Merge branch 'master' of https://github.com/syuilo/misskey
This commit is contained in:
commit
fabcad6db9
36 changed files with 1037 additions and 54 deletions
|
@ -2,6 +2,14 @@ ChangeLog (Release Notes)
|
||||||
=========================
|
=========================
|
||||||
主に notable な changes を書いていきます
|
主に notable な changes を書いていきます
|
||||||
|
|
||||||
|
2735 (2017/10/22)
|
||||||
|
-----------------
|
||||||
|
* モバイル版からでもクライアントバージョンを確認できるように
|
||||||
|
|
||||||
|
2732 (2017/10/22)
|
||||||
|
-----------------
|
||||||
|
* 依存関係の更新など
|
||||||
|
|
||||||
2584 (2017/09/08)
|
2584 (2017/09/08)
|
||||||
-----------------
|
-----------------
|
||||||
* New: ユーザーページによく使うドメインを表示 (#771)
|
* New: ユーザーページによく使うドメインを表示 (#771)
|
||||||
|
|
15
README.md
15
README.md
|
@ -25,7 +25,8 @@ and more! You can touch with your own eyes at https://misskey.xyz/.
|
||||||
|
|
||||||
Setup and Installation
|
Setup and Installation
|
||||||
----------------------------------------------------------------
|
----------------------------------------------------------------
|
||||||
Please see [Setup and installation guide](./docs/setup.en.md).
|
If you want to run your own instance of Misskey,
|
||||||
|
please see [Setup and installation guide](./docs/setup.en.md).
|
||||||
|
|
||||||
Contribution
|
Contribution
|
||||||
----------------------------------------------------------------
|
----------------------------------------------------------------
|
||||||
|
@ -42,14 +43,6 @@ If you want to donate to Misskey, please get in touch with [@syuilo][syuilo-link
|
||||||
|
|
||||||
**Note:** When you donate to Misskey, your name will be displayed in [donors](./DONORS.md).
|
**Note:** When you donate to Misskey, your name will be displayed in [donors](./DONORS.md).
|
||||||
|
|
||||||
Collaborators
|
|
||||||
----------------------------------------------------------------
|
|
||||||
| ![syuilo][syuilo-icon] | ![Morisawa Aya][ayamorisawa-icon] | ![otofune][otofune-icon] |
|
|
||||||
|------------------------|-----------------------------------|---------------------------------|
|
|
||||||
| [syuilo][syuilo-link] | [Aya Morisawa][ayamorisawa-link] | [otofune][otofune-link] |
|
|
||||||
|
|
||||||
[List of all contributors](https://github.com/syuilo/misskey/graphs/contributors)
|
|
||||||
|
|
||||||
Copyright
|
Copyright
|
||||||
----------------------------------------------------------------
|
----------------------------------------------------------------
|
||||||
Misskey is an open-source software licensed under [The MIT License](LICENSE).
|
Misskey is an open-source software licensed under [The MIT License](LICENSE).
|
||||||
|
@ -67,7 +60,3 @@ Misskey is an open-source software licensed under [The MIT License](LICENSE).
|
||||||
<!-- Collaborators Info -->
|
<!-- Collaborators Info -->
|
||||||
[syuilo-link]: https://syuilo.com
|
[syuilo-link]: https://syuilo.com
|
||||||
[syuilo-icon]: https://avatars2.githubusercontent.com/u/4439005?v=3&s=70
|
[syuilo-icon]: https://avatars2.githubusercontent.com/u/4439005?v=3&s=70
|
||||||
[ayamorisawa-link]: https://github.com/ayamorisawa
|
|
||||||
[ayamorisawa-icon]: https://avatars0.githubusercontent.com/u/10798641?v=3&s=70
|
|
||||||
[otofune-link]: https://github.com/otofune
|
|
||||||
[otofune-icon]: https://avatars0.githubusercontent.com/u/15062473?v=3&s=70
|
|
||||||
|
|
|
@ -64,7 +64,7 @@ common:
|
||||||
|
|
||||||
mk-error:
|
mk-error:
|
||||||
title: "Unable to connect to the server"
|
title: "Unable to connect to the server"
|
||||||
description: "インターネット回線に問題があるか、サーバーがダウンまたはメンテナンスしている可能性があります。しばらくしてから再度お試しください。"
|
description: "There is a problem with Internet connection, or the server may be down or maintaining. Please {try again} later."
|
||||||
thanks: "Thank you for using Misskey."
|
thanks: "Thank you for using Misskey."
|
||||||
|
|
||||||
mk-forkit:
|
mk-forkit:
|
||||||
|
|
|
@ -64,7 +64,7 @@ common:
|
||||||
|
|
||||||
mk-error:
|
mk-error:
|
||||||
title: "サーバーに接続できません"
|
title: "サーバーに接続できません"
|
||||||
description: "インターネット回線に問題があるか、サーバーがダウンまたはメンテナンスしている可能性があります。しばらくしてから再度お試しください。"
|
description: "インターネット回線に問題があるか、サーバーがダウンまたはメンテナンスしている可能性があります。しばらくしてから{再度お試し}ください。"
|
||||||
thanks: "いつもMisskeyをご利用いただきありがとうございます。"
|
thanks: "いつもMisskeyをご利用いただきありがとうございます。"
|
||||||
|
|
||||||
mk-forkit:
|
mk-forkit:
|
||||||
|
|
31
package.json
31
package.json
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "misskey",
|
"name": "misskey",
|
||||||
"author": "syuilo <i@syuilo.com>",
|
"author": "syuilo <i@syuilo.com>",
|
||||||
"version": "0.0.2584",
|
"version": "0.0.2735",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"description": "A miniblog-based SNS",
|
"description": "A miniblog-based SNS",
|
||||||
"bugs": "https://github.com/syuilo/misskey/issues",
|
"bugs": "https://github.com/syuilo/misskey/issues",
|
||||||
|
@ -48,30 +48,31 @@
|
||||||
"@types/is-url": "1.2.28",
|
"@types/is-url": "1.2.28",
|
||||||
"@types/js-yaml": "3.9.0",
|
"@types/js-yaml": "3.9.0",
|
||||||
"@types/mocha": "2.2.43",
|
"@types/mocha": "2.2.43",
|
||||||
"@types/mongodb": "2.2.11",
|
"@types/mongodb": "2.2.13",
|
||||||
"@types/monk": "1.0.6",
|
"@types/monk": "1.0.6",
|
||||||
"@types/morgan": "1.7.33",
|
"@types/morgan": "1.7.33",
|
||||||
"@types/ms": "0.7.30",
|
"@types/ms": "0.7.30",
|
||||||
"@types/multer": "1.3.2",
|
"@types/multer": "1.3.2",
|
||||||
"@types/node": "8.0.31",
|
"@types/node": "8.0.33",
|
||||||
"@types/ratelimiter": "2.1.28",
|
"@types/ratelimiter": "2.1.28",
|
||||||
"@types/redis": "2.6.0",
|
"@types/redis": "2.6.0",
|
||||||
"@types/request": "2.0.3",
|
"@types/request": "2.0.4",
|
||||||
"@types/rimraf": "2.0.0",
|
"@types/rimraf": "2.0.0",
|
||||||
"@types/riot": "3.6.0",
|
"@types/riot": "3.6.0",
|
||||||
"@types/serve-favicon": "2.2.28",
|
"@types/serve-favicon": "2.2.28",
|
||||||
"@types/uuid": "3.4.2",
|
"@types/uuid": "3.4.2",
|
||||||
"@types/webpack": "3.0.12",
|
"@types/webpack": "3.0.13",
|
||||||
"@types/webpack-stream": "3.2.7",
|
"@types/webpack-stream": "3.2.7",
|
||||||
"@types/websocket": "0.0.34",
|
"@types/websocket": "0.0.34",
|
||||||
|
"awesome-typescript-loader": "3.2.3",
|
||||||
"chai": "4.1.2",
|
"chai": "4.1.2",
|
||||||
"chai-http": "3.0.0",
|
"chai-http": "3.0.0",
|
||||||
"css-loader": "0.28.7",
|
"css-loader": "0.28.7",
|
||||||
"event-stream": "3.3.4",
|
"event-stream": "3.3.4",
|
||||||
"gulp": "3.9.1",
|
"gulp": "3.9.1",
|
||||||
"gulp-cssnano": "2.1.2",
|
"gulp-cssnano": "2.1.2",
|
||||||
"gulp-imagemin": "3.3.0",
|
|
||||||
"gulp-htmlmin": "3.0.0",
|
"gulp-htmlmin": "3.0.0",
|
||||||
|
"gulp-imagemin": "3.4.0",
|
||||||
"gulp-mocha": "4.3.1",
|
"gulp-mocha": "4.3.1",
|
||||||
"gulp-pug": "3.3.0",
|
"gulp-pug": "3.3.0",
|
||||||
"gulp-rename": "1.2.2",
|
"gulp-rename": "1.2.2",
|
||||||
|
@ -83,15 +84,15 @@
|
||||||
"mocha": "3.5.3",
|
"mocha": "3.5.3",
|
||||||
"riot-tag-loader": "1.0.0",
|
"riot-tag-loader": "1.0.0",
|
||||||
"string-replace-webpack-plugin": "0.1.3",
|
"string-replace-webpack-plugin": "0.1.3",
|
||||||
"style-loader": "0.18.2",
|
"style-loader": "0.19.0",
|
||||||
"stylus": "0.54.5",
|
"stylus": "0.54.5",
|
||||||
"stylus-loader": "3.0.1",
|
"stylus-loader": "3.0.1",
|
||||||
"swagger-jsdoc": "1.9.7",
|
"swagger-jsdoc": "1.9.7",
|
||||||
"tslint": "5.7.0",
|
"tslint": "5.7.0",
|
||||||
"uglify-es": "3.0.27",
|
"uglify-es": "3.0.27",
|
||||||
"uglify-es-webpack-plugin": "0.10.0",
|
|
||||||
"uglify-js": "git+https://github.com/mishoo/UglifyJS2.git#harmony",
|
"uglify-js": "git+https://github.com/mishoo/UglifyJS2.git#harmony",
|
||||||
"webpack": "3.6.0"
|
"uglifyjs-webpack-plugin": "1.0.0-beta.2",
|
||||||
|
"webpack": "3.8.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"accesses": "2.5.0",
|
"accesses": "2.5.0",
|
||||||
|
@ -103,12 +104,12 @@
|
||||||
"chalk": "2.1.0",
|
"chalk": "2.1.0",
|
||||||
"compression": "1.7.1",
|
"compression": "1.7.1",
|
||||||
"cors": "2.8.4",
|
"cors": "2.8.4",
|
||||||
"cropperjs": "1.0.0",
|
"cropperjs": "1.1.3",
|
||||||
"crypto": "1.0.1",
|
"crypto": "1.0.1",
|
||||||
"debug": "3.1.0",
|
"debug": "3.1.0",
|
||||||
"deep-equal": "1.0.1",
|
"deep-equal": "1.0.1",
|
||||||
"deepcopy": "0.6.3",
|
"deepcopy": "0.6.3",
|
||||||
"diskusage": "^0.2.2",
|
"diskusage": "0.2.2",
|
||||||
"download": "6.2.5",
|
"download": "6.2.5",
|
||||||
"elasticsearch": "13.3.1",
|
"elasticsearch": "13.3.1",
|
||||||
"escape-regexp": "0.0.1",
|
"escape-regexp": "0.0.1",
|
||||||
|
@ -122,8 +123,8 @@
|
||||||
"js-yaml": "3.10.0",
|
"js-yaml": "3.10.0",
|
||||||
"mecab-async": "^0.1.0",
|
"mecab-async": "^0.1.0",
|
||||||
"moji": "^0.5.1",
|
"moji": "^0.5.1",
|
||||||
"mongodb": "2.2.31",
|
"mongodb": "2.2.33",
|
||||||
"monk": "6.0.4",
|
"monk": "6.0.5",
|
||||||
"morgan": "1.9.0",
|
"morgan": "1.9.0",
|
||||||
"ms": "2.0.0",
|
"ms": "2.0.0",
|
||||||
"multer": "1.3.0",
|
"multer": "1.3.0",
|
||||||
|
@ -139,7 +140,7 @@
|
||||||
"redis": "2.8.0",
|
"redis": "2.8.0",
|
||||||
"request": "2.83.0",
|
"request": "2.83.0",
|
||||||
"rimraf": "2.6.2",
|
"rimraf": "2.6.2",
|
||||||
"riot": "3.7.2",
|
"riot": "3.7.3",
|
||||||
"rndstr": "1.0.0",
|
"rndstr": "1.0.0",
|
||||||
"s-age": "1.1.0",
|
"s-age": "1.1.0",
|
||||||
"serve-favicon": "2.4.5",
|
"serve-favicon": "2.4.5",
|
||||||
|
@ -151,7 +152,7 @@
|
||||||
"typescript": "2.5.3",
|
"typescript": "2.5.3",
|
||||||
"uuid": "3.1.0",
|
"uuid": "3.1.0",
|
||||||
"vhost": "3.0.2",
|
"vhost": "3.0.2",
|
||||||
"websocket": "1.0.24",
|
"websocket": "1.0.25",
|
||||||
"xev": "2.0.0"
|
"xev": "2.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
398
src/api/bot/core.ts
Normal file
398
src/api/bot/core.ts
Normal file
|
@ -0,0 +1,398 @@
|
||||||
|
import * as EventEmitter from 'events';
|
||||||
|
import * as bcrypt from 'bcryptjs';
|
||||||
|
|
||||||
|
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';
|
||||||
|
|
||||||
|
const hmm = [
|
||||||
|
'?',
|
||||||
|
'ふぅ~む...?',
|
||||||
|
'ちょっと何言ってるかわからないです',
|
||||||
|
'「ヘルプ」と言うと利用可能な操作が確認できますよ'
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Botの頭脳
|
||||||
|
*/
|
||||||
|
export default class BotCore extends EventEmitter {
|
||||||
|
public user: IUser = null;
|
||||||
|
|
||||||
|
private context: Context = null;
|
||||||
|
|
||||||
|
constructor(user?: IUser) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.user = user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public clearContext() {
|
||||||
|
this.setContext(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public setContext(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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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 bot = new BotCore();
|
||||||
|
bot._import(data);
|
||||||
|
return bot;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async q(query: string): Promise<string | void> {
|
||||||
|
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';
|
||||||
|
|
||||||
|
case 'help':
|
||||||
|
case 'ヘルプ':
|
||||||
|
return '利用可能なコマンド一覧です:\n' +
|
||||||
|
'help: これです\n' +
|
||||||
|
'me: アカウント情報を見ます\n' +
|
||||||
|
'login, signin: サインインします\n' +
|
||||||
|
'logout, signout: サインアウトします\n' +
|
||||||
|
'post: 投稿します\n' +
|
||||||
|
'tl: タイムラインを見ます\n' +
|
||||||
|
'@<ユーザー名>: ユーザーを表示します';
|
||||||
|
|
||||||
|
case 'me':
|
||||||
|
return this.user ? `${this.user.name}としてサインインしています。\n\n${getUserSummary(this.user)}` : 'サインインしていません';
|
||||||
|
|
||||||
|
case 'login':
|
||||||
|
case 'signin':
|
||||||
|
case 'ログイン':
|
||||||
|
case 'サインイン':
|
||||||
|
if (this.user != null) return '既にサインインしていますよ!';
|
||||||
|
this.setContext(new SigninContext(this));
|
||||||
|
return await this.context.greet();
|
||||||
|
|
||||||
|
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.tlCommand();
|
||||||
|
|
||||||
|
case 'guessing-game':
|
||||||
|
case '数当てゲーム':
|
||||||
|
this.setContext(new GuessingGameContext(this));
|
||||||
|
return await this.context.greet();
|
||||||
|
|
||||||
|
case 'othello':
|
||||||
|
case 'オセロ':
|
||||||
|
this.setContext(new OthelloContext(this));
|
||||||
|
return await this.context.greet();
|
||||||
|
|
||||||
|
default:
|
||||||
|
return hmm[Math.floor(Math.random() * hmm.length)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public signin(user: IUser) {
|
||||||
|
this.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 refreshUser() {
|
||||||
|
this.user = await User.findOne({
|
||||||
|
_id: this.user._id
|
||||||
|
}, {
|
||||||
|
fields: {
|
||||||
|
data: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.emit('updated');
|
||||||
|
}
|
||||||
|
|
||||||
|
public async tlCommand(): Promise<string | void> {
|
||||||
|
if (this.user == null) return 'まずサインインしてください。';
|
||||||
|
|
||||||
|
const tl = await require('../endpoints/posts/timeline')({
|
||||||
|
limit: 5
|
||||||
|
}, this.user);
|
||||||
|
|
||||||
|
const text = tl
|
||||||
|
.map(post => getPostSummary(post))
|
||||||
|
.join('\n-----\n');
|
||||||
|
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async showUserCommand(q: string): Promise<string | void> {
|
||||||
|
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 bot: BotCore;
|
||||||
|
|
||||||
|
public abstract async greet(): Promise<string>;
|
||||||
|
public abstract async q(query: string): Promise<string>;
|
||||||
|
public abstract export(): any;
|
||||||
|
|
||||||
|
constructor(bot: BotCore) {
|
||||||
|
super();
|
||||||
|
this.bot = bot;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static import(bot: BotCore, data: any) {
|
||||||
|
if (data.type == 'guessing-game') return GuessingGameContext.import(bot, data.content);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SigninContext extends Context {
|
||||||
|
private temporaryUser: IUser = null;
|
||||||
|
|
||||||
|
public async greet(): Promise<string> {
|
||||||
|
return 'まずユーザー名を教えてください:';
|
||||||
|
}
|
||||||
|
|
||||||
|
public async q(query: string): Promise<string> {
|
||||||
|
if (this.temporaryUser == null) {
|
||||||
|
// Fetch user
|
||||||
|
const user: IUser = await User.findOne({
|
||||||
|
username_lower: query.toLowerCase()
|
||||||
|
}, {
|
||||||
|
fields: {
|
||||||
|
data: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (user === null) {
|
||||||
|
return `${query}というユーザーは存在しませんでした... もう一度教えてください:`;
|
||||||
|
} else {
|
||||||
|
this.temporaryUser = user;
|
||||||
|
this.emit('updated');
|
||||||
|
return `パスワードを教えてください:`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Compare password
|
||||||
|
const same = bcrypt.compareSync(query, this.temporaryUser.password);
|
||||||
|
|
||||||
|
if (same) {
|
||||||
|
this.bot.signin(this.temporaryUser);
|
||||||
|
this.bot.clearContext();
|
||||||
|
return `${this.temporaryUser.name}さん、おかえりなさい!`;
|
||||||
|
} else {
|
||||||
|
return `パスワードが違います... もう一度教えてください:`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public export() {
|
||||||
|
return {
|
||||||
|
type: 'signin',
|
||||||
|
content: {
|
||||||
|
temporaryUser: this.temporaryUser
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static import(bot: BotCore, data: any) {
|
||||||
|
const context = new SigninContext(bot);
|
||||||
|
context.temporaryUser = data.temporaryUser;
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PostContext extends Context {
|
||||||
|
public async greet(): Promise<string> {
|
||||||
|
return '内容:';
|
||||||
|
}
|
||||||
|
|
||||||
|
public async q(query: string): Promise<string> {
|
||||||
|
await require('../endpoints/posts/create')({
|
||||||
|
text: query
|
||||||
|
}, this.bot.user);
|
||||||
|
this.bot.clearContext();
|
||||||
|
return '投稿しましたよ!';
|
||||||
|
}
|
||||||
|
|
||||||
|
public export() {
|
||||||
|
return {
|
||||||
|
type: 'post'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static import(bot: BotCore, data: any) {
|
||||||
|
const context = new PostContext(bot);
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class GuessingGameContext extends Context {
|
||||||
|
private secret: number;
|
||||||
|
private history: number[] = [];
|
||||||
|
|
||||||
|
public async greet(): Promise<string> {
|
||||||
|
this.secret = Math.floor(Math.random() * 100);
|
||||||
|
this.emit('updated');
|
||||||
|
return '0~100の秘密の数を当ててみてください:';
|
||||||
|
}
|
||||||
|
|
||||||
|
public async q(query: string): Promise<string> {
|
||||||
|
if (query == 'やめる') {
|
||||||
|
this.bot.clearContext();
|
||||||
|
return 'やめました。';
|
||||||
|
}
|
||||||
|
|
||||||
|
const guess = parseInt(query, 10);
|
||||||
|
|
||||||
|
if (isNaN(guess)) {
|
||||||
|
return '整数で推測してください。「やめる」と言うとゲームをやめます。';
|
||||||
|
}
|
||||||
|
|
||||||
|
const firsttime = this.history.indexOf(guess) === -1;
|
||||||
|
|
||||||
|
this.history.push(guess);
|
||||||
|
this.emit('updated');
|
||||||
|
|
||||||
|
if (this.secret < guess) {
|
||||||
|
return firsttime ? `${guess}よりも小さいですね` : `もう一度言いますが${guess}より小さいですよ`;
|
||||||
|
} else if (this.secret > guess) {
|
||||||
|
return firsttime ? `${guess}よりも大きいですね` : `もう一度言いますが${guess}より大きいですよ`;
|
||||||
|
} else {
|
||||||
|
this.bot.clearContext();
|
||||||
|
return `正解です🎉 (${this.history.length}回目で当てました)`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public export() {
|
||||||
|
return {
|
||||||
|
type: 'guessing-game',
|
||||||
|
content: {
|
||||||
|
secret: this.secret,
|
||||||
|
history: this.history
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static import(bot: BotCore, data: any) {
|
||||||
|
const context = new GuessingGameContext(bot);
|
||||||
|
context.secret = data.secret;
|
||||||
|
context.history = data.history;
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class OthelloContext extends Context {
|
||||||
|
private othello: Othello = null;
|
||||||
|
|
||||||
|
constructor(bot: BotCore) {
|
||||||
|
super(bot);
|
||||||
|
|
||||||
|
this.othello = new Othello();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async greet(): Promise<string> {
|
||||||
|
return this.othello.toPatternString('black');
|
||||||
|
}
|
||||||
|
|
||||||
|
public async q(query: string): Promise<string> {
|
||||||
|
if (query == 'やめる') {
|
||||||
|
this.bot.clearContext();
|
||||||
|
return 'オセロをやめました。';
|
||||||
|
}
|
||||||
|
|
||||||
|
const n = parseInt(query, 10);
|
||||||
|
|
||||||
|
if (isNaN(n)) {
|
||||||
|
return '番号で指定してください。「やめる」と言うとゲームをやめます。';
|
||||||
|
}
|
||||||
|
|
||||||
|
this.othello.setByNumber('black', n);
|
||||||
|
const s = this.othello.toString() + '\n\n...(AI)...\n\n';
|
||||||
|
othelloAi('white', this.othello);
|
||||||
|
if (this.othello.getPattern('black').length === 0) {
|
||||||
|
this.bot.clearContext();
|
||||||
|
const blackCount = this.othello.board.map(row => row.filter(s => s == 'black').length).reduce((a, b) => a + b);
|
||||||
|
const whiteCount = this.othello.board.map(row => row.filter(s => s == 'white').length).reduce((a, b) => a + b);
|
||||||
|
const winner = blackCount == whiteCount ? '引き分け' : blackCount > whiteCount ? '黒の勝ち' : '白の勝ち';
|
||||||
|
return this.othello.toString() + `\n\n~終了~\n\n黒${blackCount}、白${whiteCount}で${winner}です。`;
|
||||||
|
} else {
|
||||||
|
this.emit('updated');
|
||||||
|
return s + 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;
|
||||||
|
}
|
||||||
|
}
|
234
src/api/bot/interfaces/line.ts
Normal file
234
src/api/bot/interfaces/line.ts
Normal file
|
@ -0,0 +1,234 @@
|
||||||
|
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 _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;
|
||||||
|
|
||||||
|
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<void> {
|
||||||
|
this.replyToken = ev.replyToken;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
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),
|
||||||
|
template: {
|
||||||
|
type: 'buttons',
|
||||||
|
thumbnailImageUrl: `${user.avatar_url}?thumbnail&size=1024`,
|
||||||
|
title: `${user.name} (@${user.username})`,
|
||||||
|
text: user.description || '(no description)',
|
||||||
|
actions: actions
|
||||||
|
}
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async showUserTimelinePostback(userId: string) {
|
||||||
|
const tl = await require('../../endpoints/users/posts')({
|
||||||
|
user_id: userId,
|
||||||
|
limit: 5
|
||||||
|
}, this.user);
|
||||||
|
|
||||||
|
const text = `${tl[0].user.name}さんのタイムラインはこちらです:\n\n` + tl
|
||||||
|
.map(post => getPostSummary(post))
|
||||||
|
.join('\n-----\n');
|
||||||
|
|
||||||
|
this.reply([{
|
||||||
|
type: 'text',
|
||||||
|
text: text
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = async (app: express.Application) => {
|
||||||
|
if (config.line_bot == null) return;
|
||||||
|
|
||||||
|
const handler = new EventEmitter();
|
||||||
|
|
||||||
|
handler.on('event', async (ev) => {
|
||||||
|
|
||||||
|
const sourceId = ev.source.userId;
|
||||||
|
const sessionId = `line-bot-sessions:${sourceId}`;
|
||||||
|
|
||||||
|
const session = await redis.get(sessionId);
|
||||||
|
let bot: LineBot;
|
||||||
|
|
||||||
|
if (session == null) {
|
||||||
|
const user = await User.findOne({
|
||||||
|
line: {
|
||||||
|
user_id: sourceId
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
bot = new LineBot(user);
|
||||||
|
|
||||||
|
bot.on('signin', user => {
|
||||||
|
User.update(user._id, {
|
||||||
|
$set: {
|
||||||
|
line: {
|
||||||
|
user_id: sourceId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
bot.on('signout', user => {
|
||||||
|
User.update(user._id, {
|
||||||
|
$set: {
|
||||||
|
line: {
|
||||||
|
user_id: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
redis.set(sessionId, JSON.stringify(bot.export()));
|
||||||
|
} else {
|
||||||
|
bot = LineBot.import(JSON.parse(session));
|
||||||
|
}
|
||||||
|
|
||||||
|
bot.on('updated', () => {
|
||||||
|
redis.set(sessionId, JSON.stringify(bot.export()));
|
||||||
|
});
|
||||||
|
|
||||||
|
if (session != null) bot.refreshUser();
|
||||||
|
|
||||||
|
bot.react(ev);
|
||||||
|
});
|
||||||
|
|
||||||
|
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((req as any).rawBody);
|
||||||
|
|
||||||
|
const sig2 = hash.digest('base64');
|
||||||
|
|
||||||
|
// シグネチャ比較
|
||||||
|
if (sig1 === sig2) {
|
||||||
|
req.body.events.forEach(ev => {
|
||||||
|
handler.emit('event', ev);
|
||||||
|
});
|
||||||
|
|
||||||
|
res.sendStatus(200);
|
||||||
|
} else {
|
||||||
|
res.sendStatus(400);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
|
@ -21,7 +21,7 @@ module.exports = (params, user, app, isSecure) => new Promise(async (res, rej) =
|
||||||
const [data, dataError] = $(params.data).optional.object()
|
const [data, dataError] = $(params.data).optional.object()
|
||||||
.pipe(obj => {
|
.pipe(obj => {
|
||||||
const hasInvalidData = Object.entries(obj).some(([k, v]) =>
|
const hasInvalidData = Object.entries(obj).some(([k, v]) =>
|
||||||
$(k).string().match(/^[a-z_]+$/).isNg() && $(v).string().isNg());
|
$(k).string().match(/^[a-z_]+$/).nok() && $(v).string().nok());
|
||||||
return !hasInvalidData;
|
return !hasInvalidData;
|
||||||
}).$;
|
}).$;
|
||||||
if (dataError) return rej('invalid data param');
|
if (dataError) return rej('invalid data param');
|
||||||
|
|
|
@ -57,6 +57,9 @@ export type IUser = {
|
||||||
user_id: string;
|
user_id: string;
|
||||||
screen_name: string;
|
screen_name: string;
|
||||||
};
|
};
|
||||||
|
line: {
|
||||||
|
user_id: string;
|
||||||
|
};
|
||||||
description: string;
|
description: string;
|
||||||
profile: {
|
profile: {
|
||||||
location: string;
|
location: string;
|
||||||
|
@ -70,3 +73,11 @@ export type IUser = {
|
||||||
is_suspended: boolean;
|
is_suspended: boolean;
|
||||||
keywords: string[];
|
keywords: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function init(user): IUser {
|
||||||
|
user._id = new mongo.ObjectID(user._id);
|
||||||
|
user.avatar_id = new mongo.ObjectID(user.avatar_id);
|
||||||
|
user.banner_id = new mongo.ObjectID(user.banner_id);
|
||||||
|
user.pinned_post_id = new mongo.ObjectID(user.pinned_post_id);
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
|
@ -79,6 +79,7 @@ export default (
|
||||||
delete _user.twitter.access_token;
|
delete _user.twitter.access_token;
|
||||||
delete _user.twitter.access_token_secret;
|
delete _user.twitter.access_token_secret;
|
||||||
}
|
}
|
||||||
|
delete _user.line;
|
||||||
|
|
||||||
// Visible via only the official client
|
// Visible via only the official client
|
||||||
if (!opts.includeSecrets) {
|
if (!opts.includeSecrets) {
|
||||||
|
|
|
@ -19,7 +19,12 @@ app.disable('x-powered-by');
|
||||||
app.set('etag', false);
|
app.set('etag', false);
|
||||||
app.use(bodyParser.urlencoded({ extended: true }));
|
app.use(bodyParser.urlencoded({ extended: true }));
|
||||||
app.use(bodyParser.json({
|
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({
|
app.use(cors({
|
||||||
origin: true
|
origin: true
|
||||||
|
@ -54,4 +59,6 @@ app.use((req, res, next) => {
|
||||||
require('./service/github')(app);
|
require('./service/github')(app);
|
||||||
require('./service/twitter')(app);
|
require('./service/twitter')(app);
|
||||||
|
|
||||||
|
require('./bot/interfaces/line')(app);
|
||||||
|
|
||||||
module.exports = app;
|
module.exports = app;
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
const summarize = post => {
|
/**
|
||||||
|
* 投稿を表す文字列を取得します。
|
||||||
|
* @param {*} post 投稿
|
||||||
|
*/
|
||||||
|
const summarize = (post: any): string => {
|
||||||
let summary = post.text ? post.text : '';
|
let summary = post.text ? post.text : '';
|
||||||
|
|
||||||
// メディアが添付されているとき
|
// メディアが添付されているとき
|
12
src/common/get-user-summary.ts
Normal file
12
src/common/get-user-summary.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import { IUser } from '../api/models/user';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ユーザーを表す文字列を取得します。
|
||||||
|
* @param user ユーザー
|
||||||
|
*/
|
||||||
|
export default function(user: IUser): string {
|
||||||
|
return `${user.name} (@${user.username})\n` +
|
||||||
|
`${user.posts_count}投稿、${user.following_count}フォロー、${user.followers_count}フォロワー\n` +
|
||||||
|
`場所: ${user.profile.location}、誕生日: ${user.profile.birthday}\n` +
|
||||||
|
`「${user.description}」`;
|
||||||
|
}
|
268
src/common/othello.ts
Normal file
268
src/common/othello.ts
Normal file
|
@ -0,0 +1,268 @@
|
||||||
|
const BOARD_SIZE = 8;
|
||||||
|
|
||||||
|
export default class Othello {
|
||||||
|
public board: Array<Array<'black' | 'white'>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ゲームを初期化します
|
||||||
|
*/
|
||||||
|
constructor() {
|
||||||
|
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]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private write(color, x, y) {
|
||||||
|
this.board[y][x] = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 石を配置します
|
||||||
|
*/
|
||||||
|
public set(color, x, y) {
|
||||||
|
this.write(color, x, y);
|
||||||
|
|
||||||
|
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.write(color, x, _y);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 1: // 右上
|
||||||
|
for (let c = 0, i = 1; c < r[1]; c++, i++) {
|
||||||
|
this.write(color, x + i, y - i);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 2: // 右
|
||||||
|
for (let c = 0, _x = x + 1; c < r[1]; c++, _x++) {
|
||||||
|
this.write(color, _x, y);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 3: // 右下
|
||||||
|
for (let c = 0, i = 1; c < r[1]; c++, i++) {
|
||||||
|
this.write(color, x + i, y + i);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 4: // 下
|
||||||
|
for (let c = 0, _y = y + 1; c < r[1]; c++, _y++) {
|
||||||
|
this.write(color, x, _y);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 5: // 左下
|
||||||
|
for (let c = 0, i = 1; c < r[1]; c++, i++) {
|
||||||
|
this.write(color, x - i, y + i);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 6: // 左
|
||||||
|
for (let c = 0, _x = x - 1; c < r[1]; c++, _x--) {
|
||||||
|
this.write(color, _x, y);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 7: // 左上
|
||||||
|
for (let c = 0, i = 1; c < r[1]; c++, i++) {
|
||||||
|
this.write(color, x - i, y - i);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打つことができる場所を取得します
|
||||||
|
*/
|
||||||
|
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[y][x] == myColor && opponentStoneFound) {
|
||||||
|
return true;
|
||||||
|
} else if (this.board[y][x] == myColor && !opponentStoneFound) {
|
||||||
|
breaked = true;
|
||||||
|
} else if (this.board[y][x] == 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(BOARD_SIZE - 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 < BOARD_SIZE; c++, x++) {
|
||||||
|
if (iterate(x, targety)) {
|
||||||
|
res.push([2, c]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 右下
|
||||||
|
iterate = createIterater();
|
||||||
|
for (let c = 0, i = 1; i < Math.min(BOARD_SIZE - targetx, BOARD_SIZE - targety); c++, i++) {
|
||||||
|
if (iterate(targetx + i, targety + i)) {
|
||||||
|
res.push([3, c]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下
|
||||||
|
iterate = createIterater();
|
||||||
|
for (let c = 0, y = targety + 1; y < BOARD_SIZE; c++, y++) {
|
||||||
|
if (iterate(targetx, y)) {
|
||||||
|
res.push([4, c]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 左下
|
||||||
|
iterate = createIterater();
|
||||||
|
for (let c = 0, i = 1; i < Math.min(targetx, BOARD_SIZE - 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');
|
||||||
|
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 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] : '┼';
|
||||||
|
return state === 'black' ? '⚫️' : state === 'white' ? '⚪️' : i != -1 ? num[i] : '🔹';
|
||||||
|
}).join('')).join('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 corners = ps.filter(p =>
|
||||||
|
// 左上
|
||||||
|
(p[0] == 0 && p[1] == 0) ||
|
||||||
|
// 右上
|
||||||
|
(p[0] == (BOARD_SIZE - 1) && p[1] == 0) ||
|
||||||
|
// 右下
|
||||||
|
(p[0] == (BOARD_SIZE - 1) && p[1] == (BOARD_SIZE - 1)) ||
|
||||||
|
// 左下
|
||||||
|
(p[0] == 0 && p[1] == (BOARD_SIZE - 1))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (corners.length > 0) { // どこかしらの角に打てる場合
|
||||||
|
// 打てる角からランダムに選択して打つ
|
||||||
|
const p = corners[Math.floor(Math.random() * corners.length)];
|
||||||
|
othello.set(color, p[0], p[1]);
|
||||||
|
} else { // 打てる角がない場合
|
||||||
|
// 打てる場所からランダムに選択して打つ
|
||||||
|
const p = ps[Math.floor(Math.random() * ps.length)];
|
||||||
|
othello.set(color, p[0], p[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 相手の打つ場所がない場合続けてAIのターン
|
||||||
|
if (othello.getPattern(opponentColor).length === 0) {
|
||||||
|
think();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
think();
|
||||||
|
}
|
|
@ -68,6 +68,10 @@ type Source = {
|
||||||
hook_secret: string;
|
hook_secret: string;
|
||||||
username: string;
|
username: string;
|
||||||
};
|
};
|
||||||
|
line_bot?: {
|
||||||
|
channel_secret: string;
|
||||||
|
channel_access_token: string;
|
||||||
|
};
|
||||||
analysis?: {
|
analysis?: {
|
||||||
mecab_command?: string;
|
mecab_command?: string;
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
"allowJs": true,
|
||||||
"noEmitOnError": false,
|
"noEmitOnError": false,
|
||||||
"noImplicitAny": false,
|
"noImplicitAny": false,
|
||||||
"noImplicitReturns": true,
|
"noImplicitReturns": true,
|
||||||
|
|
|
@ -1,7 +1,13 @@
|
||||||
<mk-error>
|
<mk-error>
|
||||||
<img src="/assets/error.jpg" alt=""/>
|
<img src="data:image/jpeg;base64,%base64:/assets/error.jpg%" alt=""/>
|
||||||
<h1>%i18n:common.tags.mk-error.title%</h1>
|
<h1>%i18n:common.tags.mk-error.title%</h1>
|
||||||
<p class="text">%i18n:common.tags.mk-error.description%</p>
|
<p class="text">{
|
||||||
|
'%i18n:common.tags.mk-error.description%'.substr(0, '%i18n:common.tags.mk-error.description%'.indexOf('{'))
|
||||||
|
}<a onclick={ reload }>{
|
||||||
|
'%i18n:common.tags.mk-error.description%'.match(/\{(.+?)\}/)[1]
|
||||||
|
}</a>{
|
||||||
|
'%i18n:common.tags.mk-error.description%'.substr('%i18n:common.tags.mk-error.description%'.indexOf('}') + 1)
|
||||||
|
}</p>
|
||||||
<p class="thanks">%i18n:common.tags.mk-error.thanks%</p>
|
<p class="thanks">%i18n:common.tags.mk-error.thanks%</p>
|
||||||
<style>
|
<style>
|
||||||
:scope
|
:scope
|
||||||
|
@ -53,5 +59,9 @@
|
||||||
document.title = 'Oops!';
|
document.title = 'Oops!';
|
||||||
document.documentElement.style.background = '#f8f8f8';
|
document.documentElement.style.background = '#f8f8f8';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.reload = () => {
|
||||||
|
location.reload();
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
</mk-error>
|
</mk-error>
|
||||||
|
|
|
@ -11,7 +11,7 @@ import * as riot from 'riot';
|
||||||
import init from '../init';
|
import init from '../init';
|
||||||
import route from './router';
|
import route from './router';
|
||||||
import fuckAdBlock from './scripts/fuck-ad-block';
|
import fuckAdBlock from './scripts/fuck-ad-block';
|
||||||
import getPostSummary from '../common/scripts/get-post-summary';
|
import getPostSummary from '../../../common/get-post-summary.ts';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* init
|
* init
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<mk-version-home-widget>
|
<mk-version-home-widget>
|
||||||
<p>ver{ version }</p>
|
<p>ver { version }</p>
|
||||||
<style>
|
<style>
|
||||||
:scope
|
:scope
|
||||||
display block
|
display block
|
||||||
|
|
|
@ -207,7 +207,7 @@
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
<script>
|
<script>
|
||||||
import getPostSummary from '../../common/scripts/get-post-summary';
|
import getPostSummary from '../../../../common/get-post-summary.ts';
|
||||||
this.getPostSummary = getPostSummary;
|
this.getPostSummary = getPostSummary;
|
||||||
|
|
||||||
this.mixin('i');
|
this.mixin('i');
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
</style>
|
</style>
|
||||||
<script>
|
<script>
|
||||||
import Progress from '../../../common/scripts/loading';
|
import Progress from '../../../common/scripts/loading';
|
||||||
import getPostSummary from '../../../common/scripts/get-post-summary';
|
import getPostSummary from '../../../../../common/get-post-summary.ts';
|
||||||
|
|
||||||
this.mixin('i');
|
this.mixin('i');
|
||||||
this.mixin('api');
|
this.mixin('api');
|
||||||
|
|
|
@ -110,7 +110,7 @@
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
<script>
|
<script>
|
||||||
import getPostSummary from '../../common/scripts/get-post-summary';
|
import getPostSummary from '../../../../common/get-post-summary.ts';
|
||||||
this.getPostSummary = getPostSummary;
|
this.getPostSummary = getPostSummary;
|
||||||
this.notification = this.opts.notification;
|
this.notification = this.opts.notification;
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -163,7 +163,7 @@
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
<script>
|
<script>
|
||||||
import getPostSummary from '../../common/scripts/get-post-summary';
|
import getPostSummary from '../../../../common/get-post-summary.ts';
|
||||||
this.getPostSummary = getPostSummary;
|
this.getPostSummary = getPostSummary;
|
||||||
this.notification = this.opts.notification;
|
this.notification = this.opts.notification;
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -78,7 +78,7 @@
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
<script>
|
<script>
|
||||||
import getPostSummary from '../../common/scripts/get-post-summary';
|
import getPostSummary from '../../../../common/get-post-summary.ts';
|
||||||
this.getPostSummary = getPostSummary;
|
this.getPostSummary = getPostSummary;
|
||||||
|
|
||||||
this.mixin('api');
|
this.mixin('api');
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
<script>
|
<script>
|
||||||
import ui from '../../scripts/ui-event';
|
import ui from '../../scripts/ui-event';
|
||||||
import Progress from '../../../common/scripts/loading';
|
import Progress from '../../../common/scripts/loading';
|
||||||
import getPostSummary from '../../../common/scripts/get-post-summary';
|
import getPostSummary from '../../../../../common/get-post-summary.ts';
|
||||||
import openPostForm from '../../scripts/open-post-form';
|
import openPostForm from '../../scripts/open-post-form';
|
||||||
|
|
||||||
this.mixin('i');
|
this.mixin('i');
|
||||||
|
|
|
@ -29,6 +29,7 @@
|
||||||
<ul>
|
<ul>
|
||||||
<li><a onclick={ signout }><i class="fa fa-power-off"></i>%i18n:mobile.tags.mk-settings-page.signout%</a></li>
|
<li><a onclick={ signout }><i class="fa fa-power-off"></i>%i18n:mobile.tags.mk-settings-page.signout%</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
<p><small>ver { version }</small></p>
|
||||||
<style>
|
<style>
|
||||||
:scope
|
:scope
|
||||||
display block
|
display block
|
||||||
|
@ -96,5 +97,7 @@
|
||||||
this.signout = signout;
|
this.signout = signout;
|
||||||
|
|
||||||
this.mixin('i');
|
this.mixin('i');
|
||||||
|
|
||||||
|
this.version = VERSION;
|
||||||
</script>
|
</script>
|
||||||
</mk-settings>
|
</mk-settings>
|
||||||
|
|
|
@ -264,7 +264,7 @@
|
||||||
</style>
|
</style>
|
||||||
<script>
|
<script>
|
||||||
import compile from '../../common/scripts/text-compiler';
|
import compile from '../../common/scripts/text-compiler';
|
||||||
import getPostSummary from '../../common/scripts/get-post-summary';
|
import getPostSummary from '../../../../common/get-post-summary.ts';
|
||||||
import openPostForm from '../scripts/open-post-form';
|
import openPostForm from '../scripts/open-post-form';
|
||||||
|
|
||||||
this.mixin('api');
|
this.mixin('api');
|
||||||
|
|
|
@ -464,7 +464,7 @@
|
||||||
</style>
|
</style>
|
||||||
<script>
|
<script>
|
||||||
import compile from '../../common/scripts/text-compiler';
|
import compile from '../../common/scripts/text-compiler';
|
||||||
import getPostSummary from '../../common/scripts/get-post-summary';
|
import getPostSummary from '../../../../common/get-post-summary.ts';
|
||||||
import openPostForm from '../scripts/open-post-form';
|
import openPostForm from '../scripts/open-post-form';
|
||||||
|
|
||||||
this.mixin('api');
|
this.mixin('api');
|
||||||
|
|
|
@ -428,7 +428,7 @@
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
<script>
|
<script>
|
||||||
import summary from '../../common/scripts/get-post-summary';
|
import summary from '../../../../common/get-post-summary.ts';
|
||||||
|
|
||||||
this.post = this.opts.post;
|
this.post = this.opts.post;
|
||||||
this.text = summary(this.post);
|
this.text = summary(this.post);
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
"allowJs": true,
|
||||||
"noEmitOnError": false,
|
"noEmitOnError": false,
|
||||||
"noImplicitAny": false,
|
"noImplicitAny": false,
|
||||||
"noImplicitReturns": true,
|
"noImplicitReturns": true,
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
"ordered-imports": [false],
|
"ordered-imports": [false],
|
||||||
"arrow-parens": false,
|
"arrow-parens": false,
|
||||||
"object-literal-shorthand": false,
|
"object-literal-shorthand": false,
|
||||||
|
"object-literal-key-quotes": false,
|
||||||
"triple-equals": [false],
|
"triple-equals": [false],
|
||||||
"no-shadowed-variable": false,
|
"no-shadowed-variable": false,
|
||||||
"no-string-literal": false,
|
"no-string-literal": false,
|
||||||
|
@ -23,6 +24,7 @@
|
||||||
"comment-format": [false],
|
"comment-format": [false],
|
||||||
"interface-over-type-literal": false,
|
"interface-over-type-literal": false,
|
||||||
"max-line-length": [false],
|
"max-line-length": [false],
|
||||||
|
"max-classes-per-file": false,
|
||||||
"member-ordering": [false],
|
"member-ordering": [false],
|
||||||
"ban-types": [
|
"ban-types": [
|
||||||
"Object"
|
"Object"
|
||||||
|
|
19
webpack/module/rules/base64.ts
Normal file
19
webpack/module/rules/base64.ts
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
/**
|
||||||
|
* Replace base64 symbols
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as fs from 'fs';
|
||||||
|
const StringReplacePlugin = require('string-replace-webpack-plugin');
|
||||||
|
|
||||||
|
export default () => ({
|
||||||
|
enforce: 'pre',
|
||||||
|
test: /\.(tag|js)$/,
|
||||||
|
exclude: /node_modules/,
|
||||||
|
loader: StringReplacePlugin.replace({
|
||||||
|
replacements: [{
|
||||||
|
pattern: /%base64:(.+?)%/g, replacement: (_, key) => {
|
||||||
|
return fs.readFileSync(__dirname + '/../../../src/web/' + key, 'base64');
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
});
|
|
@ -1,11 +1,15 @@
|
||||||
import i18n from './i18n';
|
import i18n from './i18n';
|
||||||
|
import base64 from './base64';
|
||||||
import themeColor from './theme-color';
|
import themeColor from './theme-color';
|
||||||
import tag from './tag';
|
import tag from './tag';
|
||||||
import stylus from './stylus';
|
import stylus from './stylus';
|
||||||
|
import typescript from './typescript';
|
||||||
|
|
||||||
export default (lang, locale) => [
|
export default (lang, locale) => [
|
||||||
i18n(lang, locale),
|
i18n(lang, locale),
|
||||||
|
base64(),
|
||||||
themeColor(),
|
themeColor(),
|
||||||
tag(),
|
tag(),
|
||||||
stylus()
|
stylus(),
|
||||||
|
typescript()
|
||||||
];
|
];
|
||||||
|
|
8
webpack/module/rules/typescript.ts
Normal file
8
webpack/module/rules/typescript.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
/**
|
||||||
|
* TypeScript
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default () => ({
|
||||||
|
test: /\.ts$/,
|
||||||
|
use: 'awesome-typescript-loader'
|
||||||
|
});
|
|
@ -2,13 +2,11 @@ const StringReplacePlugin = require('string-replace-webpack-plugin');
|
||||||
|
|
||||||
import constant from './const';
|
import constant from './const';
|
||||||
import hoist from './hoist';
|
import hoist from './hoist';
|
||||||
//import minify from './minify';
|
import minify from './minify';
|
||||||
import banner from './banner';
|
import banner from './banner';
|
||||||
|
|
||||||
/*
|
|
||||||
const env = process.env.NODE_ENV;
|
const env = process.env.NODE_ENV;
|
||||||
const isProduction = env === 'production';
|
const isProduction = env === 'production';
|
||||||
*/
|
|
||||||
|
|
||||||
export default version => {
|
export default version => {
|
||||||
const plugins = [
|
const plugins = [
|
||||||
|
@ -16,11 +14,11 @@ export default version => {
|
||||||
new StringReplacePlugin(),
|
new StringReplacePlugin(),
|
||||||
hoist()
|
hoist()
|
||||||
];
|
];
|
||||||
/*
|
|
||||||
if (isProduction) {
|
if (isProduction) {
|
||||||
plugins.push(minify());
|
plugins.push(minify());
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
plugins.push(banner(version));
|
plugins.push(banner(version));
|
||||||
|
|
||||||
return plugins;
|
return plugins;
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
const UglifyEsPlugin = require('uglify-es-webpack-plugin');
|
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
|
||||||
|
|
||||||
export default () => new UglifyEsPlugin();
|
export default () => new UglifyJsPlugin();
|
||||||
|
|
Loading…
Reference in a new issue