From 6a5c6280ffd3ffe820beb23294f1c2c1f5deb9cf Mon Sep 17 00:00:00 2001 From: syuilo Date: Sat, 7 Oct 2017 03:36:46 +0900 Subject: :v: --- src/api/bot/interfaces/line.ts | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 src/api/bot/interfaces/line.ts (limited to 'src/api/bot/interfaces') diff --git a/src/api/bot/interfaces/line.ts b/src/api/bot/interfaces/line.ts new file mode 100644 index 0000000000..4bee844c12 --- /dev/null +++ b/src/api/bot/interfaces/line.ts @@ -0,0 +1,37 @@ +import * as EventEmitter from 'events'; +import * as express from 'express'; +import * as crypto from 'crypto'; +//import User from '../../models/user'; +import config from '../../../conf'; +/*import BotCore from '../core'; + +const sessions: { + userId: string; + session: BotCore; +}[] = []; +*/ +module.exports = async (app: express.Application) => { + if (config.line_bot == null) return; + + const handler = new EventEmitter(); + + app.post('/hooks/line', (req, res, next) => { + // req.headers['X-Line-Signature'] は常に string ですが、型定義の都合上 + // string | string[] になっているので string を明示しています + const sig1 = req.headers['X-Line-Signature'] as string; + + const hash = crypto.createHmac('sha256', config.line_bot.channel_secret) + .update(JSON.stringify(req.body)); + + const sig2 = hash.digest('base64'); + + // シグネチャ比較 + if (sig1 === sig2) { + console.log(req.body); + handler.emit(req.body.type); + res.sendStatus(200); + } else { + res.sendStatus(400); + } + }); +}; -- cgit v1.2.3-freya From fffea98462b7ba3250118b4bdf4e5678cf6e4ba7 Mon Sep 17 00:00:00 2001 From: syuilo Date: Sat, 7 Oct 2017 04:30:57 +0900 Subject: :v: --- src/api/bot/core.ts | 4 +-- src/api/bot/interfaces/line.ts | 65 +++++++++++++++++++++++++++++++++++------- src/api/server.ts | 7 ++++- src/config.ts | 1 + tslint.json | 1 + 5 files changed, 64 insertions(+), 14 deletions(-) (limited to 'src/api/bot/interfaces') diff --git a/src/api/bot/core.ts b/src/api/bot/core.ts index 002ac1b06e..47989dbaa2 100644 --- a/src/api/bot/core.ts +++ b/src/api/bot/core.ts @@ -4,11 +4,11 @@ import * as bcrypt from 'bcryptjs'; import User, { IUser } from '../models/user'; export default class BotCore extends EventEmitter { - public user: IUser; + public user: IUser = null; private context: Context = null; - constructor(user: IUser) { + constructor(user?: IUser) { super(); this.user = user; diff --git a/src/api/bot/interfaces/line.ts b/src/api/bot/interfaces/line.ts index 4bee844c12..8c7d6acfd5 100644 --- a/src/api/bot/interfaces/line.ts +++ b/src/api/bot/interfaces/line.ts @@ -1,34 +1,77 @@ import * as EventEmitter from 'events'; import * as express from 'express'; +import * as request from 'request'; import * as crypto from 'crypto'; //import User from '../../models/user'; import config from '../../../conf'; -/*import BotCore from '../core'; +import BotCore from '../core'; -const sessions: { - userId: string; +const sessions: Array<{ + sourceId: string; session: BotCore; -}[] = []; -*/ +}> = []; + module.exports = async (app: express.Application) => { if (config.line_bot == null) return; const handler = new EventEmitter(); + handler.on('message', async (ev) => { + // テキスト以外(スタンプなど)は無視 + if (ev.message.type !== 'text') return; + + const sourceId = ev.source.userId; + let session = sessions.find(s => { + return s.sourceId === sourceId; + }); + + if (!session) { + session = { + sourceId: sourceId, + session: new BotCore() + }; + + sessions.push(session); + } + + const res = await session.session.q(ev.message.text); + + request({ + url: 'https://api.line.me/v2/bot/message/reply', + headers: { + 'Authorization': `Bearer ${config.line_bot.channel_access_token}` + }, + json: { + replyToken: ev.replyToken, + messages: [{ + type: 'text', + text: res + }] + } + }, (err, res, body) => { + if (err) { + console.error(err); + return; + } + }); + }); + app.post('/hooks/line', (req, res, next) => { - // req.headers['X-Line-Signature'] は常に string ですが、型定義の都合上 + // req.headers['x-line-signature'] は常に string ですが、型定義の都合上 // string | string[] になっているので string を明示しています - const sig1 = req.headers['X-Line-Signature'] as string; + const sig1 = req.headers['x-line-signature'] as string; - const hash = crypto.createHmac('sha256', config.line_bot.channel_secret) - .update(JSON.stringify(req.body)); + const hash = crypto.createHmac('SHA256', config.line_bot.channel_secret) + .update((req as any).rawBody); const sig2 = hash.digest('base64'); // シグネチャ比較 if (sig1 === sig2) { - console.log(req.body); - handler.emit(req.body.type); + req.body.events.forEach(ev => { + handler.emit(ev.type, ev); + }); + res.sendStatus(200); } else { res.sendStatus(400); diff --git a/src/api/server.ts b/src/api/server.ts index fdff0c7546..3de32d9eab 100644 --- a/src/api/server.ts +++ b/src/api/server.ts @@ -19,7 +19,12 @@ app.disable('x-powered-by'); app.set('etag', false); app.use(bodyParser.urlencoded({ extended: true })); app.use(bodyParser.json({ - type: ['application/json', 'text/plain'] + type: ['application/json', 'text/plain'], + verify: (req, res, buf, encoding) => { + if (buf && buf.length) { + (req as any).rawBody = buf.toString(encoding || 'utf8'); + } + } })); app.use(cors({ origin: true diff --git a/src/config.ts b/src/config.ts index 0ea332f67d..46a93f5fef 100644 --- a/src/config.ts +++ b/src/config.ts @@ -70,6 +70,7 @@ type Source = { }; line_bot?: { channel_secret: string; + channel_access_token: string; }; analysis?: { mecab_command?: string; diff --git a/tslint.json b/tslint.json index 33704ca43b..1c44579512 100644 --- a/tslint.json +++ b/tslint.json @@ -16,6 +16,7 @@ "ordered-imports": [false], "arrow-parens": false, "object-literal-shorthand": false, + "object-literal-key-quotes": false, "triple-equals": [false], "no-shadowed-variable": false, "no-string-literal": false, -- cgit v1.2.3-freya From 0b20c3caa69b438c4d551fe1fc1a09364c9650eb Mon Sep 17 00:00:00 2001 From: syuilo Date: Sat, 7 Oct 2017 04:48:56 +0900 Subject: :v: --- src/api/bot/interfaces/line.ts | 37 +++++++++++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 8 deletions(-) (limited to 'src/api/bot/interfaces') diff --git a/src/api/bot/interfaces/line.ts b/src/api/bot/interfaces/line.ts index 8c7d6acfd5..9e1c813570 100644 --- a/src/api/bot/interfaces/line.ts +++ b/src/api/bot/interfaces/line.ts @@ -2,13 +2,13 @@ import * as EventEmitter from 'events'; import * as express from 'express'; import * as request from 'request'; import * as crypto from 'crypto'; -//import User from '../../models/user'; +import User from '../../models/user'; import config from '../../../conf'; import BotCore from '../core'; const sessions: Array<{ sourceId: string; - session: BotCore; + core: BotCore; }> = []; module.exports = async (app: express.Application) => { @@ -21,22 +21,43 @@ module.exports = async (app: express.Application) => { if (ev.message.type !== 'text') return; const sourceId = ev.source.userId; - let session = sessions.find(s => { - return s.sourceId === sourceId; - }); + let session = sessions.find(s => s.sourceId === sourceId); if (!session) { + const user = await User.findOne({ + line: { + user_id: sourceId + } + }); + + let core: BotCore; + + if (user) { + core = new BotCore(user); + } else { + core = new BotCore(); + core.on('set-user', user => { + User.update(user._id, { + $set: { + line: { + user_id: sourceId + } + } + }); + }); + } + session = { sourceId: sourceId, - session: new BotCore() + core: core }; sessions.push(session); } - const res = await session.session.q(ev.message.text); + const res = await session.core.q(ev.message.text); - request({ + request.post({ url: 'https://api.line.me/v2/bot/message/reply', headers: { 'Authorization': `Bearer ${config.line_bot.channel_access_token}` -- cgit v1.2.3-freya From 0f2b503f2f545ce11fcca8c2c17a1084fd91df5e Mon Sep 17 00:00:00 2001 From: syuilo Date: Sat, 7 Oct 2017 05:50:01 +0900 Subject: :v: --- src/api/bot/core.ts | 52 ++++++++++++++++++++++++++++++++++++++++-- src/api/bot/interfaces/line.ts | 38 +++++++++++++++--------------- 2 files changed, 70 insertions(+), 20 deletions(-) (limited to 'src/api/bot/interfaces') diff --git a/src/api/bot/core.ts b/src/api/bot/core.ts index 47989dbaa2..1f624c5f0a 100644 --- a/src/api/bot/core.ts +++ b/src/api/bot/core.ts @@ -14,6 +14,31 @@ export default class BotCore extends EventEmitter { this.user = user; } + private setContect(context: Context) { + this.context = context; + this.emit('updated'); + + if (context) { + context.on('updated', () => { + this.emit('updated'); + }); + } + } + + public export() { + return { + user: this.user, + context: this.context ? this.context.export() : null + }; + } + + public static import(data) { + const core = new BotCore(); + core.user = data.user; + core.setContect(data.context ? Context.import(core, data.context) : null); + return core; + } + public async q(query: string): Promise { if (this.context != null) { return await this.context.q(query); @@ -22,9 +47,11 @@ export default class BotCore extends EventEmitter { switch (query) { case 'ping': return 'PONG'; + case 'me': + return this.user ? `${this.user.name}としてサインインしています` : 'サインインしていません'; case 'ログイン': case 'サインイン': - this.context = new SigninContext(this); + this.setContect(new SigninContext(this)); return await this.context.greet(); default: return '?'; @@ -34,18 +61,26 @@ export default class BotCore extends EventEmitter { public setUser(user: IUser) { this.user = user; this.emit('set-user', user); + this.emit('updated'); } } -abstract class Context { +abstract class Context extends EventEmitter { protected core: BotCore; public abstract async greet(): Promise; public abstract async q(query: string): Promise; + public abstract export(): any; constructor(core: BotCore) { + super(); this.core = core; } + + public static import(core: BotCore, data: any) { + if (data.type == 'signin') return SigninContext.import(core, data.content); + return null; + } } class SigninContext extends Context { @@ -71,6 +106,7 @@ class SigninContext extends Context { return `${query}というユーザーは存在しませんでした... もう一度教えてください:`; } else { this.temporaryUser = user; + this.emit('updated'); return `パスワードを教えてください:`; } } else { @@ -85,4 +121,16 @@ class SigninContext extends Context { } } } + + public export() { + return { + temporaryUser: this.temporaryUser + }; + } + + public static import(core: BotCore, data: any) { + const context = new SigninContext(core); + context.temporaryUser = data.temporaryUser; + return context; + } } diff --git a/src/api/bot/interfaces/line.ts b/src/api/bot/interfaces/line.ts index 9e1c813570..61aa728121 100644 --- a/src/api/bot/interfaces/line.ts +++ b/src/api/bot/interfaces/line.ts @@ -5,11 +5,10 @@ import * as crypto from 'crypto'; import User from '../../models/user'; import config from '../../../conf'; import BotCore from '../core'; +import _redis from '../../../db/redis'; +import prominence = require('prominence'); -const sessions: Array<{ - sourceId: string; - core: BotCore; -}> = []; +const redis = prominence(_redis); module.exports = async (app: express.Application) => { if (config.line_bot == null) return; @@ -21,22 +20,23 @@ module.exports = async (app: express.Application) => { if (ev.message.type !== 'text') return; const sourceId = ev.source.userId; - let session = sessions.find(s => s.sourceId === sourceId); + const sessionId = `line-bot-sessions:${sourceId}`; - if (!session) { + const _session = await redis.get(sessionId); + let session: BotCore; + + if (_session == null) { const user = await User.findOne({ line: { user_id: sourceId } }); - let core: BotCore; - if (user) { - core = new BotCore(user); + session = new BotCore(user); } else { - core = new BotCore(); - core.on('set-user', user => { + session = new BotCore(); + session.on('set-user', user => { User.update(user._id, { $set: { line: { @@ -47,16 +47,18 @@ module.exports = async (app: express.Application) => { }); } - session = { - sourceId: sourceId, - core: core - }; - - sessions.push(session); + redis.set(sessionId, JSON.stringify(session.export())); + } else { + session = BotCore.import(JSON.parse(_session)); } - const res = await session.core.q(ev.message.text); + session.on('updated', () => { + redis.set(sessionId, JSON.stringify(session.export())); + }); + + const res = await session.q(ev.message.text); + // 返信 request.post({ url: 'https://api.line.me/v2/bot/message/reply', headers: { -- cgit v1.2.3-freya From f5fe36825b0c83d8770e0f3cdf5c2d477eb2b995 Mon Sep 17 00:00:00 2001 From: syuilo Date: Sat, 7 Oct 2017 06:03:16 +0900 Subject: :v: --- src/api/bot/core.ts | 5 +++-- src/api/bot/interfaces/line.ts | 20 ++++++++------------ 2 files changed, 11 insertions(+), 14 deletions(-) (limited to 'src/api/bot/interfaces') diff --git a/src/api/bot/core.ts b/src/api/bot/core.ts index 1f624c5f0a..4109519ca5 100644 --- a/src/api/bot/core.ts +++ b/src/api/bot/core.ts @@ -34,7 +34,7 @@ export default class BotCore extends EventEmitter { public static import(data) { const core = new BotCore(); - core.user = data.user; + core.user = data.user ? data.user : null; core.setContect(data.context ? Context.import(core, data.context) : null); return core; } @@ -84,7 +84,7 @@ abstract class Context extends EventEmitter { } class SigninContext extends Context { - private temporaryUser: IUser; + private temporaryUser: IUser = null; public async greet(): Promise { return 'まずユーザー名を教えてください:'; @@ -124,6 +124,7 @@ class SigninContext extends Context { public export() { return { + type: 'signin', temporaryUser: this.temporaryUser }; } diff --git a/src/api/bot/interfaces/line.ts b/src/api/bot/interfaces/line.ts index 61aa728121..52559eaeff 100644 --- a/src/api/bot/interfaces/line.ts +++ b/src/api/bot/interfaces/line.ts @@ -32,20 +32,16 @@ module.exports = async (app: express.Application) => { } }); - if (user) { - session = new BotCore(user); - } else { - session = new BotCore(); - session.on('set-user', user => { - User.update(user._id, { - $set: { - line: { - user_id: sourceId - } + session = new BotCore(user); + session.on('set-user', user => { + User.update(user._id, { + $set: { + line: { + user_id: sourceId } - }); + } }); - } + }); redis.set(sessionId, JSON.stringify(session.export())); } else { -- cgit v1.2.3-freya From 1d4f9378ca57dc9a19d4ff13e676669e01814612 Mon Sep 17 00:00:00 2001 From: syuilo Date: Sat, 7 Oct 2017 06:43:36 +0900 Subject: :v: --- src/api/bot/core.ts | 99 ++++++++++++++++++++++++++++++++++++++---- src/api/bot/interfaces/line.ts | 13 +++++- src/tsconfig.json | 1 + tsconfig.json | 1 + 4 files changed, 105 insertions(+), 9 deletions(-) (limited to 'src/api/bot/interfaces') diff --git a/src/api/bot/core.ts b/src/api/bot/core.ts index 4109519ca5..cf2bdef1dc 100644 --- a/src/api/bot/core.ts +++ b/src/api/bot/core.ts @@ -3,6 +3,8 @@ import * as bcrypt from 'bcryptjs'; import User, { IUser } from '../models/user'; +import getPostSummary from '../../common/get-post-summary.js'; + export default class BotCore extends EventEmitter { public user: IUser = null; @@ -14,7 +16,7 @@ export default class BotCore extends EventEmitter { this.user = user; } - private setContect(context: Context) { + public setContext(context: Context) { this.context = context; this.emit('updated'); @@ -35,7 +37,7 @@ export default class BotCore extends EventEmitter { public static import(data) { const core = new BotCore(); core.user = data.user ? data.user : null; - core.setContect(data.context ? Context.import(core, data.context) : null); + core.setContext(data.context ? Context.import(core, data.context) : null); return core; } @@ -47,22 +49,74 @@ export default class BotCore extends EventEmitter { switch (query) { case 'ping': return 'PONG'; + + case 'help': + case 'ヘルプ': + return 'コマンド一覧です:' + + 'help: これです\n' + + 'me: アカウント情報を見ます\n' + + 'login, signin: サインインします\n' + + 'logout, signout: サインアウトします\n' + + 'post: 投稿します\n' + + 'tl: タイムラインを見ます\n'; + case 'me': return this.user ? `${this.user.name}としてサインインしています` : 'サインインしていません'; + + case 'login': + case 'signin': case 'ログイン': case 'サインイン': - this.setContect(new SigninContext(this)); + this.setContext(new SigninContext(this)); return await this.context.greet(); - default: + + case 'logout': + case 'signout': + case 'ログアウト': + case 'サインアウト': + if (this.user == null) return '今はサインインしてないですよ!'; + this.signout(); + return 'ご利用ありがとうございました <3'; + + case 'post': + case '投稿': + if (this.user == null) return 'まずサインインしてください。'; + this.setContext(new PostContext(this)); + return await this.context.greet(); + + case 'tl': + case 'タイムライン': + return await this.getTl(); + + default: return '?'; } } - public setUser(user: IUser) { + public signin(user: IUser) { this.user = user; - this.emit('set-user', user); + this.emit('signin', user); this.emit('updated'); } + + public signout() { + const user = this.user; + this.user = null; + this.emit('signout', user); + this.emit('updated'); + } + + public async getTl() { + if (this.user == null) return 'まずサインインしてください。'; + + const tl = await require('../endpoints/posts/timeline')({}, this.user); + + const text = tl + .map(post => getPostSummary(post)) + .join('\n-----\n'); + + return text; + } } abstract class Context extends EventEmitter { @@ -78,6 +132,7 @@ abstract class Context extends EventEmitter { } public static import(core: BotCore, data: any) { + if (data.type == 'post') return PostContext.import(core, data.content); if (data.type == 'signin') return SigninContext.import(core, data.content); return null; } @@ -114,7 +169,8 @@ class SigninContext extends Context { const same = bcrypt.compareSync(query, this.temporaryUser.password); if (same) { - this.core.setUser(this.temporaryUser); + this.core.signin(this.temporaryUser); + this.core.setContext(null); return `${this.temporaryUser.name}さん、おかえりなさい!`; } else { return `パスワードが違います... もう一度教えてください:`; @@ -125,7 +181,9 @@ class SigninContext extends Context { public export() { return { type: 'signin', - temporaryUser: this.temporaryUser + content: { + temporaryUser: this.temporaryUser + } }; } @@ -135,3 +193,28 @@ class SigninContext extends Context { return context; } } + +class PostContext extends Context { + public async greet(): Promise { + return '内容:'; + } + + public async q(query: string): Promise { + await require('../endpoints/posts/create')({ + text: query + }, this.core.user); + this.core.setContext(null); + return '投稿しましたよ!'; + } + + public export() { + return { + type: 'post' + }; + } + + public static import(core: BotCore, data: any) { + const context = new PostContext(core); + return context; + } +} diff --git a/src/api/bot/interfaces/line.ts b/src/api/bot/interfaces/line.ts index 52559eaeff..437f29cb3c 100644 --- a/src/api/bot/interfaces/line.ts +++ b/src/api/bot/interfaces/line.ts @@ -33,7 +33,8 @@ module.exports = async (app: express.Application) => { }); session = new BotCore(user); - session.on('set-user', user => { + + session.on('signin', user => { User.update(user._id, { $set: { line: { @@ -43,6 +44,16 @@ module.exports = async (app: express.Application) => { }); }); + session.on('signout', user => { + User.update(user._id, { + $set: { + line: { + user_id: null + } + } + }); + }); + redis.set(sessionId, JSON.stringify(session.export())); } else { session = BotCore.import(JSON.parse(_session)); diff --git a/src/tsconfig.json b/src/tsconfig.json index ecff047a74..36600eed2b 100644 --- a/src/tsconfig.json +++ b/src/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "allowJs": true, "noEmitOnError": false, "noImplicitAny": false, "noImplicitReturns": true, diff --git a/tsconfig.json b/tsconfig.json index 064a04e4d2..a38ff220b2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "allowJs": true, "noEmitOnError": false, "noImplicitAny": false, "noImplicitReturns": true, -- cgit v1.2.3-freya From 96b6ef4d9ba73dc04eb601ffedee52d6b6ab580a Mon Sep 17 00:00:00 2001 From: syuilo Date: Sat, 7 Oct 2017 18:30:04 +0900 Subject: :v: --- src/api/bot/core.ts | 70 ++++++++++++++++++--------- src/api/bot/interfaces/line.ts | 105 +++++++++++++++++++++++++++++------------ 2 files changed, 121 insertions(+), 54 deletions(-) (limited to 'src/api/bot/interfaces') diff --git a/src/api/bot/core.ts b/src/api/bot/core.ts index 4970f24469..4e168a6054 100644 --- a/src/api/bot/core.ts +++ b/src/api/bot/core.ts @@ -43,18 +43,26 @@ export default class BotCore extends EventEmitter { }; } + protected _import(data) { + this.user = data.user ? initUser(data.user) : null; + this.setContext(data.context ? Context.import(this, data.context) : null); + } + public static import(data) { - const core = new BotCore(); - core.user = data.user ? initUser(data.user) : null; - core.setContext(data.context ? Context.import(core, data.context) : null); - return core; + const bot = new BotCore(); + bot._import(data); + return bot; } - public async q(query: string): Promise { + public async q(query: string): Promise { if (this.context != null) { return await this.context.q(query); } + if (/^@[a-zA-Z0-9-]+$/.test(query)) { + return await this.showUserCommand(query); + } + switch (query) { case 'ping': return 'PONG'; @@ -67,7 +75,8 @@ export default class BotCore extends EventEmitter { 'login, signin: サインインします\n' + 'logout, signout: サインアウトします\n' + 'post: 投稿します\n' + - 'tl: タイムラインを見ます\n'; + 'tl: タイムラインを見ます\n' + + '@<ユーザー名>: ユーザーを表示します'; case 'me': return this.user ? `${this.user.name}としてサインインしています。\n\n${getUserSummary(this.user)}` : 'サインインしていません'; @@ -76,6 +85,7 @@ export default class BotCore extends EventEmitter { case 'signin': case 'ログイン': case 'サインイン': + if (this.user != null) return '既にサインインしていますよ!'; this.setContext(new SigninContext(this)); return await this.context.greet(); @@ -95,9 +105,9 @@ export default class BotCore extends EventEmitter { case 'tl': case 'タイムライン': - return await this.getTl(); + return await this.tlCommand(); - default: + default: return '?'; } } @@ -115,7 +125,7 @@ export default class BotCore extends EventEmitter { this.emit('updated'); } - public async getTl() { + public async tlCommand(): Promise { if (this.user == null) return 'まずサインインしてください。'; const tl = await require('../endpoints/posts/timeline')({ @@ -128,23 +138,37 @@ export default class BotCore extends EventEmitter { return text; } + + public async showUserCommand(q: string): Promise { + try { + const user = await require('../endpoints/users/show')({ + username: q.substr(1) + }, this.user); + + const text = getUserSummary(user); + + return text; + } catch (e) { + return `問題が発生したようです...: ${e}`; + } + } } abstract class Context extends EventEmitter { - protected core: BotCore; + protected bot: BotCore; public abstract async greet(): Promise; public abstract async q(query: string): Promise; public abstract export(): any; - constructor(core: BotCore) { + constructor(bot: BotCore) { super(); - this.core = core; + this.bot = bot; } - public static import(core: BotCore, data: any) { - if (data.type == 'post') return PostContext.import(core, data.content); - if (data.type == 'signin') return SigninContext.import(core, data.content); + public static import(bot: BotCore, data: any) { + if (data.type == 'post') return PostContext.import(bot, data.content); + if (data.type == 'signin') return SigninContext.import(bot, data.content); return null; } } @@ -179,8 +203,8 @@ class SigninContext extends Context { const same = bcrypt.compareSync(query, this.temporaryUser.password); if (same) { - this.core.signin(this.temporaryUser); - this.core.clearContext(); + this.bot.signin(this.temporaryUser); + this.bot.clearContext(); return `${this.temporaryUser.name}さん、おかえりなさい!`; } else { return `パスワードが違います... もう一度教えてください:`; @@ -197,8 +221,8 @@ class SigninContext extends Context { }; } - public static import(core: BotCore, data: any) { - const context = new SigninContext(core); + public static import(bot: BotCore, data: any) { + const context = new SigninContext(bot); context.temporaryUser = data.temporaryUser; return context; } @@ -212,8 +236,8 @@ class PostContext extends Context { public async q(query: string): Promise { await require('../endpoints/posts/create')({ text: query - }, this.core.user); - this.core.clearContext(); + }, this.bot.user); + this.bot.clearContext(); return '投稿しましたよ!'; } @@ -223,8 +247,8 @@ class PostContext extends Context { }; } - public static import(core: BotCore, data: any) { - const context = new PostContext(core); + public static import(bot: BotCore, data: any) { + const context = new PostContext(bot); return context; } } diff --git a/src/api/bot/interfaces/line.ts b/src/api/bot/interfaces/line.ts index 437f29cb3c..03dc2a85ba 100644 --- a/src/api/bot/interfaces/line.ts +++ b/src/api/bot/interfaces/line.ts @@ -10,20 +10,83 @@ import prominence = require('prominence'); const redis = prominence(_redis); +class LineBot extends BotCore { + private replyToken: string; + + private reply(messages: any[]) { + request.post({ + url: 'https://api.line.me/v2/bot/message/reply', + headers: { + 'Authorization': `Bearer ${config.line_bot.channel_access_token}` + }, + json: { + replyToken: this.replyToken, + messages: messages + } + }, (err, res, body) => { + if (err) { + console.error(err); + return; + } + }); + } + + public async react(ev: any): Promise { + // テキスト以外(スタンプなど)は無視 + if (ev.message.type !== 'text') return; + + const res = await this.q(ev.message.text); + + if (res == null) return; + + // 返信 + this.reply([{ + type: 'text', + text: res + }]); + } + + public static import(data) { + const bot = new LineBot(); + bot._import(data); + return bot; + } + + public async showUserCommand(q: string) { + const user = await require('../endpoints/users/show')({ + username: q.substr(1) + }, this.user); + + this.reply([{ + type: 'template', + altText: await super.showUserCommand(q), + template: { + type: 'buttons', + thumbnailImageUrl: `${user.avatar_url}?thumbnail&size=1024`, + title: `${user.name} (@${user.username})`, + text: user.description || '(no description)', + actions: [{ + type: 'uri', + label: 'Webで見る', + uri: `${config.url}/${user.username}` + }] + } + }]); + } +} + module.exports = async (app: express.Application) => { if (config.line_bot == null) return; const handler = new EventEmitter(); handler.on('message', async (ev) => { - // テキスト以外(スタンプなど)は無視 - if (ev.message.type !== 'text') return; const sourceId = ev.source.userId; const sessionId = `line-bot-sessions:${sourceId}`; const _session = await redis.get(sessionId); - let session: BotCore; + let bot: LineBot; if (_session == null) { const user = await User.findOne({ @@ -32,9 +95,9 @@ module.exports = async (app: express.Application) => { } }); - session = new BotCore(user); + bot = new LineBot(user); - session.on('signin', user => { + bot.on('signin', user => { User.update(user._id, { $set: { line: { @@ -44,7 +107,7 @@ module.exports = async (app: express.Application) => { }); }); - session.on('signout', user => { + bot.on('signout', user => { User.update(user._id, { $set: { line: { @@ -54,36 +117,16 @@ module.exports = async (app: express.Application) => { }); }); - redis.set(sessionId, JSON.stringify(session.export())); + redis.set(sessionId, JSON.stringify(bot.export())); } else { - session = BotCore.import(JSON.parse(_session)); + bot = LineBot.import(JSON.parse(_session)); } - session.on('updated', () => { - redis.set(sessionId, JSON.stringify(session.export())); + bot.on('updated', () => { + redis.set(sessionId, JSON.stringify(bot.export())); }); - const res = await session.q(ev.message.text); - - // 返信 - request.post({ - url: 'https://api.line.me/v2/bot/message/reply', - headers: { - 'Authorization': `Bearer ${config.line_bot.channel_access_token}` - }, - json: { - replyToken: ev.replyToken, - messages: [{ - type: 'text', - text: res - }] - } - }, (err, res, body) => { - if (err) { - console.error(err); - return; - } - }); + bot.react(ev); }); app.post('/hooks/line', (req, res, next) => { -- cgit v1.2.3-freya From 64db80662706fedbf1c0103187de413dcb2d1360 Mon Sep 17 00:00:00 2001 From: syuilo Date: Sat, 7 Oct 2017 18:31:55 +0900 Subject: :v: --- src/api/bot/interfaces/line.ts | 2 ++ 1 file changed, 2 insertions(+) (limited to 'src/api/bot/interfaces') diff --git a/src/api/bot/interfaces/line.ts b/src/api/bot/interfaces/line.ts index 03dc2a85ba..c5e8293b92 100644 --- a/src/api/bot/interfaces/line.ts +++ b/src/api/bot/interfaces/line.ts @@ -32,6 +32,8 @@ class LineBot extends BotCore { } public async react(ev: any): Promise { + this.replyToken = ev.replyToken; + // テキスト以外(スタンプなど)は無視 if (ev.message.type !== 'text') return; -- cgit v1.2.3-freya From 041499814a955b514970bbd3cb2d89480e6802a3 Mon Sep 17 00:00:00 2001 From: syuilo Date: Sat, 7 Oct 2017 19:09:10 +0900 Subject: :v: --- src/api/bot/interfaces/line.ts | 78 +++++++++++++++++++++++++++++++++++------- 1 file changed, 66 insertions(+), 12 deletions(-) (limited to 'src/api/bot/interfaces') diff --git a/src/api/bot/interfaces/line.ts b/src/api/bot/interfaces/line.ts index c5e8293b92..cc634ca89c 100644 --- a/src/api/bot/interfaces/line.ts +++ b/src/api/bot/interfaces/line.ts @@ -7,9 +7,25 @@ import config from '../../../conf'; import BotCore from '../core'; import _redis from '../../../db/redis'; import prominence = require('prominence'); +import getPostSummary from '../../../common/get-post-summary'; const redis = prominence(_redis); +// SEE: https://developers.line.me/media/messaging-api/messages/sticker_list.pdf +const stickers = [ + '297', + '298', + '299', + '300', + '301', + '302', + '303', + '304', + '305', + '306', + '307' +]; + class LineBot extends BotCore { private replyToken: string; @@ -34,18 +50,36 @@ class LineBot extends BotCore { public async react(ev: any): Promise { this.replyToken = ev.replyToken; - // テキスト以外(スタンプなど)は無視 - if (ev.message.type !== 'text') return; - - const res = await this.q(ev.message.text); - - if (res == null) return; - - // 返信 - this.reply([{ - type: 'text', - text: res - }]); + // テキスト + if (ev.message.type == 'text') { + const res = await this.q(ev.message.text); + + if (res == null) return; + + // 返信 + this.reply([{ + type: 'text', + text: res + }]); + // スタンプ + } else if (ev.message.type == 'sticker') { + // スタンプで返信 + this.reply([{ + type: 'sticker', + packageId: '4', + stickerId: stickers[Math.floor(Math.random() * stickers.length)] + }]); + // postback + } else if (ev.message.type == 'postback') { + const data = ev.message.postback.data; + const cmd = data.split('|')[0]; + const arg = data.split('|')[1]; + switch (cmd) { + case 'showtl': + this.showUserTimelinePostback(arg); + break; + } + } } public static import(data) { @@ -68,6 +102,10 @@ class LineBot extends BotCore { title: `${user.name} (@${user.username})`, text: user.description || '(no description)', actions: [{ + type: 'postback', + label: 'タイムラインを見る', + data: `showtl|${user._id}` + }, { type: 'uri', label: 'Webで見る', uri: `${config.url}/${user.username}` @@ -75,6 +113,22 @@ class LineBot extends BotCore { } }]); } + + public async showUserTimelinePostback(userId: string) { + const tl = await require('../../endpoints/users/posts')({ + user_id: userId, + limit: 5 + }, this.user); + + const text = tl + .map(post => getPostSummary(post)) + .join('\n-----\n'); + + this.reply([{ + type: 'text', + text: text + }]); + } } module.exports = async (app: express.Application) => { -- cgit v1.2.3-freya From b1990bc8477ebf44158bfb3aea8a4d93a0f2821a Mon Sep 17 00:00:00 2001 From: syuilo Date: Sat, 7 Oct 2017 19:15:32 +0900 Subject: :v: --- src/api/bot/interfaces/line.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/api/bot/interfaces') diff --git a/src/api/bot/interfaces/line.ts b/src/api/bot/interfaces/line.ts index cc634ca89c..91240a325f 100644 --- a/src/api/bot/interfaces/line.ts +++ b/src/api/bot/interfaces/line.ts @@ -89,7 +89,7 @@ class LineBot extends BotCore { } public async showUserCommand(q: string) { - const user = await require('../endpoints/users/show')({ + const user = await require('../../endpoints/users/show')({ username: q.substr(1) }, this.user); -- cgit v1.2.3-freya From a931bcbf8d325a1fb7346954b20b64065052727b Mon Sep 17 00:00:00 2001 From: syuilo Date: Sat, 7 Oct 2017 19:29:32 +0900 Subject: :v: --- src/api/bot/interfaces/line.ts | 45 ++++++++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 21 deletions(-) (limited to 'src/api/bot/interfaces') diff --git a/src/api/bot/interfaces/line.ts b/src/api/bot/interfaces/line.ts index 91240a325f..06b46c953b 100644 --- a/src/api/bot/interfaces/line.ts +++ b/src/api/bot/interfaces/line.ts @@ -50,25 +50,28 @@ class LineBot extends BotCore { public async react(ev: any): Promise { this.replyToken = ev.replyToken; - // テキスト - if (ev.message.type == 'text') { - const res = await this.q(ev.message.text); - - if (res == null) return; - - // 返信 - this.reply([{ - type: 'text', - text: res - }]); - // スタンプ - } else if (ev.message.type == 'sticker') { - // スタンプで返信 - this.reply([{ - type: 'sticker', - packageId: '4', - stickerId: stickers[Math.floor(Math.random() * stickers.length)] - }]); + // メッセージ + if (ev.type == 'message') { + // テキスト + if (ev.message.type == 'text') { + const res = await this.q(ev.message.text); + + if (res == null) return; + + // 返信 + this.reply([{ + type: 'text', + text: res + }]); + // スタンプ + } else if (ev.message.type == 'sticker') { + // スタンプで返信 + this.reply([{ + type: 'sticker', + packageId: '4', + stickerId: stickers[Math.floor(Math.random() * stickers.length)] + }]); + } // postback } else if (ev.message.type == 'postback') { const data = ev.message.postback.data; @@ -136,7 +139,7 @@ module.exports = async (app: express.Application) => { const handler = new EventEmitter(); - handler.on('message', async (ev) => { + handler.on('event', async (ev) => { const sourceId = ev.source.userId; const sessionId = `line-bot-sessions:${sourceId}`; @@ -198,7 +201,7 @@ module.exports = async (app: express.Application) => { // シグネチャ比較 if (sig1 === sig2) { req.body.events.forEach(ev => { - handler.emit(ev.type, ev); + handler.emit('event', ev); }); res.sendStatus(200); -- cgit v1.2.3-freya From 103979dd6ff567ca72a08411b9c0859a0f8bb6fc Mon Sep 17 00:00:00 2001 From: syuilo Date: Sat, 7 Oct 2017 19:37:51 +0900 Subject: :v: --- src/api/bot/interfaces/line.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src/api/bot/interfaces') diff --git a/src/api/bot/interfaces/line.ts b/src/api/bot/interfaces/line.ts index 06b46c953b..956dcc6574 100644 --- a/src/api/bot/interfaces/line.ts +++ b/src/api/bot/interfaces/line.ts @@ -73,8 +73,8 @@ class LineBot extends BotCore { }]); } // postback - } else if (ev.message.type == 'postback') { - const data = ev.message.postback.data; + } else if (ev.type == 'postback') { + const data = ev.postback.data; const cmd = data.split('|')[0]; const arg = data.split('|')[1]; switch (cmd) { -- cgit v1.2.3-freya From d4d110a5c269bf799f2486fcd6a1782d10155481 Mon Sep 17 00:00:00 2001 From: syuilo Date: Sat, 7 Oct 2017 19:39:36 +0900 Subject: :v: --- src/api/bot/interfaces/line.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/api/bot/interfaces') diff --git a/src/api/bot/interfaces/line.ts b/src/api/bot/interfaces/line.ts index 956dcc6574..2bf62c1f6c 100644 --- a/src/api/bot/interfaces/line.ts +++ b/src/api/bot/interfaces/line.ts @@ -107,7 +107,7 @@ class LineBot extends BotCore { actions: [{ type: 'postback', label: 'タイムラインを見る', - data: `showtl|${user._id}` + data: `showtl|${user.id}` }, { type: 'uri', label: 'Webで見る', -- cgit v1.2.3-freya From b18e4fea9851ce0692b3f1a627c1d220d5f16b9c Mon Sep 17 00:00:00 2001 From: syuilo Date: Sun, 8 Oct 2017 03:24:10 +0900 Subject: :v: --- src/api/bot/core.ts | 51 ++++++++ src/api/bot/interfaces/line.ts | 112 ++++++++++------- src/common/othello.ts | 275 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 393 insertions(+), 45 deletions(-) create mode 100644 src/common/othello.ts (limited to 'src/api/bot/interfaces') diff --git a/src/api/bot/core.ts b/src/api/bot/core.ts index bc5818d976..6042862d39 100644 --- a/src/api/bot/core.ts +++ b/src/api/bot/core.ts @@ -6,6 +6,8 @@ import User, { IUser, init as initUser } from '../models/user'; import getPostSummary from '../../common/get-post-summary'; import getUserSummary from '../../common/get-user-summary'; +import Othello, { ai as othelloAi } from '../../common/othello'; + /** * Botの頭脳 */ @@ -106,6 +108,11 @@ export default class BotCore extends EventEmitter { case 'タイムライン': return await this.tlCommand(); + case 'othello': + case 'オセロ': + this.setContext(new OthelloContext(this)); + return await this.context.greet(); + default: return '?'; } @@ -124,6 +131,18 @@ export default class BotCore extends EventEmitter { this.emit('updated'); } + public async refreshUser() { + this.user = await User.findOne({ + _id: this.user._id + }, { + fields: { + data: false + } + }); + + this.emit('updated'); + } + public async tlCommand(): Promise { if (this.user == null) return 'まずサインインしてください。'; @@ -166,6 +185,7 @@ abstract class Context extends EventEmitter { } public static import(bot: BotCore, data: any) { + if (data.type == 'othello') return OthelloContext.import(bot, data.content); if (data.type == 'post') return PostContext.import(bot, data.content); if (data.type == 'signin') return SigninContext.import(bot, data.content); return null; @@ -251,3 +271,34 @@ class PostContext extends Context { return context; } } + +class OthelloContext extends Context { + private othello: Othello = null; + + public async greet(): Promise { + this.othello = new Othello(); + return this.othello.toPatternString('black'); + } + + public async q(query: string): Promise { + this.othello.setByNumber('black', parseInt(query, 10)); + othelloAi('white', this.othello); + return this.othello.toPatternString('black'); + } + + public export() { + return { + type: 'othello', + content: { + board: this.othello.board + } + }; + } + + public static import(bot: BotCore, data: any) { + const context = new OthelloContext(bot); + context.othello = new Othello(); + context.othello.board = data.board; + return context; + } +} diff --git a/src/api/bot/interfaces/line.ts b/src/api/bot/interfaces/line.ts index 2bf62c1f6c..0caa71ed2b 100644 --- a/src/api/bot/interfaces/line.ts +++ b/src/api/bot/interfaces/line.ts @@ -50,38 +50,44 @@ class LineBot extends BotCore { public async react(ev: any): Promise { this.replyToken = ev.replyToken; - // メッセージ - if (ev.type == 'message') { - // テキスト - if (ev.message.type == 'text') { - const res = await this.q(ev.message.text); - - if (res == null) return; - - // 返信 - this.reply([{ - type: 'text', - text: res - }]); - // スタンプ - } else if (ev.message.type == 'sticker') { - // スタンプで返信 - this.reply([{ - type: 'sticker', - packageId: '4', - stickerId: stickers[Math.floor(Math.random() * stickers.length)] - }]); - } - // postback - } else if (ev.type == 'postback') { - const data = ev.postback.data; - const cmd = data.split('|')[0]; - const arg = data.split('|')[1]; - switch (cmd) { - case 'showtl': - this.showUserTimelinePostback(arg); - break; - } + switch (ev.type) { + // メッセージ + case 'message': + switch (ev.message.type) { + // テキスト + case 'text': + const res = await this.q(ev.message.text); + if (res == null) return; + // 返信 + this.reply([{ + type: 'text', + text: res + }]); + break; + + // スタンプ + case 'sticker': + // スタンプで返信 + this.reply([{ + type: 'sticker', + packageId: '4', + stickerId: stickers[Math.floor(Math.random() * stickers.length)] + }]); + break; + } + break; + + // postback + case 'postback': + const data = ev.postback.data; + const cmd = data.split('|')[0]; + const arg = data.split('|')[1]; + switch (cmd) { + case 'showtl': + this.showUserTimelinePostback(arg); + break; + } + break; } } @@ -96,6 +102,28 @@ class LineBot extends BotCore { username: q.substr(1) }, this.user); + const actions = []; + + actions.push({ + type: 'postback', + label: 'タイムラインを見る', + data: `showtl|${user.id}` + }); + + if (user.twitter) { + actions.push({ + type: 'uri', + label: 'Twitterアカウントを見る', + uri: `https://twitter.com/${user.twitter.screen_name}` + }); + } + + actions.push({ + type: 'uri', + label: 'Webで見る', + uri: `${config.url}/${user.username}` + }); + this.reply([{ type: 'template', altText: await super.showUserCommand(q), @@ -104,15 +132,7 @@ class LineBot extends BotCore { thumbnailImageUrl: `${user.avatar_url}?thumbnail&size=1024`, title: `${user.name} (@${user.username})`, text: user.description || '(no description)', - actions: [{ - type: 'postback', - label: 'タイムラインを見る', - data: `showtl|${user.id}` - }, { - type: 'uri', - label: 'Webで見る', - uri: `${config.url}/${user.username}` - }] + actions: actions } }]); } @@ -123,7 +143,7 @@ class LineBot extends BotCore { limit: 5 }, this.user); - const text = tl + const text = `${tl[0].user.name}さんのタイムラインはこちらです:\n\n` + tl .map(post => getPostSummary(post)) .join('\n-----\n'); @@ -144,10 +164,10 @@ module.exports = async (app: express.Application) => { const sourceId = ev.source.userId; const sessionId = `line-bot-sessions:${sourceId}`; - const _session = await redis.get(sessionId); + const session = await redis.get(sessionId); let bot: LineBot; - if (_session == null) { + if (session == null) { const user = await User.findOne({ line: { user_id: sourceId @@ -178,13 +198,15 @@ module.exports = async (app: express.Application) => { redis.set(sessionId, JSON.stringify(bot.export())); } else { - bot = LineBot.import(JSON.parse(_session)); + bot = LineBot.import(JSON.parse(session)); } bot.on('updated', () => { redis.set(sessionId, JSON.stringify(bot.export())); }); + if (session != null) bot.refreshUser(); + bot.react(ev); }); diff --git a/src/common/othello.ts b/src/common/othello.ts new file mode 100644 index 0000000000..0060401976 --- /dev/null +++ b/src/common/othello.ts @@ -0,0 +1,275 @@ +import * as EventEmitter from 'events'; + +export default class Othello extends EventEmitter { + public board: Array>; + + /** + * ゲームを初期化します + */ + constructor() { + super(); + + this.board = [ + [null, null, null, null, null, null, null, null], + [null, null, null, null, null, null, null, null], + [null, null, null, null, null, null, null, null], + [null, null, null, 'black', 'white', null, null, null], + [null, null, null, 'white', 'black', null, null, null], + [null, null, null, null, null, null, null, null], + [null, null, null, null, null, null, null, null], + [null, null, null, null, null, null, null, null] + ]; + } + + public setByNumber(color, n) { + const ps = this.getPattern(color); + this.set(color, ps[n][0], ps[n][1]); + } + + /** + * 石を配置します + */ + public set(color, x, y) { + this.board[y][x] = color; + + const reverses = this.getReverse(color, x, y); + + reverses.forEach(r => { + switch (r[0]) { + case 0: // 上 + for (let c = 0, _y = y - 1; c < r[1]; c++, _y--) { + this.board[x][_y] = color; + } + break; + + case 1: // 右上 + for (let c = 0, i = 1; c < r[1]; c++, i++) { + this.board[x + i][y - i] = color; + } + break; + + case 2: // 右 + for (let c = 0, _x = x + 1; c < r[1]; c++, _x++) { + this.board[_x][y] = color; + } + break; + + case 3: // 右下 + for (let c = 0, i = 1; c < r[1]; c++, i++) { + this.board[x + i][y + i] = color; + } + break; + + case 4: // 下 + for (let c = 0, _y = y + 1; c < r[1]; c++, _y++) { + this.board[x][_y] = color; + } + break; + + case 5: // 左下 + for (let c = 0, i = 1; c < r[1]; c++, i++) { + this.board[x - i][y + i] = color; + } + break; + + case 6: // 左 + for (let c = 0, _x = x - 1; c < r[1]; c++, _x--) { + this.board[_x][y] = color; + } + break; + + case 7: // 左上 + for (let c = 0, i = 1; c < r[1]; c++, i++) { + this.board[x - i][y - i] = color; + } + break; + } + }); + + this.emit('set:' + color, x, y); + } + + /** + * 打つことができる場所を取得します + */ + public getPattern(myColor): number[][] { + const result = []; + this.board.forEach((stones, y) => stones.forEach((stone, x) => { + if (stone != null) return; + if (this.canReverse(myColor, x, y)) result.push([x, y]); + })); + return result; + } + + /** + * 指定の位置に石を打つことができるかどうか(相手の石を1つでも反転させられるか)を取得します + */ + public canReverse(myColor, targetx, targety): boolean { + return this.getReverse(myColor, targetx, targety) !== null; + } + + private getReverse(myColor, targetx, targety): number[] { + const opponentColor = myColor == 'black' ? 'white' : 'black'; + + const createIterater = () => { + let opponentStoneFound = false; + let breaked = false; + return (x, y): any => { + if (breaked) { + return; + } else if (this.board[x][y] == myColor && opponentStoneFound) { + return true; + } else if (this.board[x][y] == myColor && !opponentStoneFound) { + breaked = true; + } else if (this.board[x][y] == opponentColor) { + opponentStoneFound = true; + } else { + breaked = true; + } + }; + }; + + const res = []; + + let iterate; + + // 上 + iterate = createIterater(); + for (let c = 0, y = targety - 1; y >= 0; c++, y--) { + if (iterate(targetx, y)) { + res.push([0, c]); + break; + } + } + + // 右上 + iterate = createIterater(); + for (let c = 0, i = 1; i < Math.min(8 - targetx, targety); c++, i++) { + if (iterate(targetx + i, targety - i)) { + res.push([1, c]); + break; + } + } + + // 右 + iterate = createIterater(); + for (let c = 0, x = targetx + 1; x < 8; c++, x++) { + if (iterate(x, targety)) { + res.push([2, c]); + break; + } + } + + // 右下 + iterate = createIterater(); + for (let c = 0, i = 1; i < Math.min(8 - targetx, 8 - targety); c++, i++) { + if (iterate(targetx + i, targety + i)) { + res.push([3, c]); + break; + } + } + + // 下 + iterate = createIterater(); + for (let c = 0, y = targety + 1; y < 8; c++, y++) { + if (iterate(targetx, y)) { + res.push([4, c]); + break; + } + } + + // 左下 + iterate = createIterater(); + for (let c = 0, i = 1; i < Math.min(targetx, 8 - targety); c++, i++) { + if (iterate(targetx - i, targety + i)) { + res.push([5, c]); + break; + } + } + + // 左 + iterate = createIterater(); + for (let c = 0, x = targetx - 1; x >= 0; c++, x--) { + if (iterate(x, targety)) { + res.push([6, c]); + break; + } + } + + // 左上 + iterate = createIterater(); + for (let c = 0, i = 1; i < Math.min(targetx, targety); c++, i++) { + if (iterate(targetx - i, targety - i)) { + res.push([7, c]); + break; + } + } + + return res.length === 0 ? null : res; + } + + public toString(): string { + return this.board.map(row => row.map(state => state === 'black' ? '⚫️' : state === 'white' ? '⚪️' : '🔹').join('')).join('\n'); + } + + public toPatternString(color): string { + const num = ['0️⃣', '1️⃣', '2️⃣', '3️⃣', '4️⃣', '5️⃣', '6️⃣', '7️⃣', '8️⃣', '9️⃣', '🔟']; + + const pattern = this.getPattern(color); + + return this.board.map((row, y) => row.map((state, x) => { + const i = pattern.findIndex(p => p[0] == x && p[1] == y); + return state === 'black' ? '⚫️' : state === 'white' ? '⚪️' : i != -1 ? num[i] : '🔹'; + }).join('')).join('\n'); + } +} +/* +export class Ai { + private othello: Othello; + private color: string; + private opponentColor: string; + + constructor(color: string, othello: Othello) { + this.othello = othello; + this.color = color; + this.opponentColor = this.color == 'black' ? 'white' : 'black'; + + this.othello.on('set:' + this.opponentColor, () => { + this.turn(); + }); + + if (this.color == 'black') { + this.turn(); + } + } + + public turn() { + const ps = this.othello.getPattern(this.color); + if (ps.length > 0) { + const p = ps[Math.floor(Math.random() * ps.length)]; + this.othello.set(this.color, p[0], p[1]); + + // 相手の打つ場所がない場合続けてAIのターン + if (this.othello.getPattern(this.opponentColor).length === 0) { + this.turn(); + } + } + } +} +*/ +export function ai(color: string, othello: Othello) { + const opponentColor = color == 'black' ? 'white' : 'black'; + + function think() { + const ps = othello.getPattern(color); + if (ps.length > 0) { + const p = ps[Math.floor(Math.random() * ps.length)]; + othello.set(color, p[0], p[1]); + + // 相手の打つ場所がない場合続けてAIのターン + if (othello.getPattern(opponentColor).length === 0) { + think(); + } + } + } +} -- cgit v1.2.3-freya