diff --git a/src/ai.ts b/src/ai.ts index 4042ddf..f9e46ad 100644 --- a/src/ai.ts +++ b/src/ai.ts @@ -3,10 +3,10 @@ import * as loki from 'lokijs'; import * as WebSocket from 'ws'; import * as request from 'request-promise-native'; -import serifs from './serifs'; import config from './config'; import IModule from './module'; import MessageLike from './message-like'; +import { FriendDoc } from './friend'; const ReconnectingWebSocket = require('../node_modules/reconnecting-websocket/dist/reconnecting-websocket-cjs.js'); /** @@ -38,10 +38,7 @@ export default class 藍 { data?: any; }>; - public friends: loki.Collection<{ - userId: string; - name?: string; - }>; + public friends: loki.Collection; constructor(account: any) { this.account = account; diff --git a/src/friend.ts b/src/friend.ts new file mode 100644 index 0000000..cc7f1a3 --- /dev/null +++ b/src/friend.ts @@ -0,0 +1,110 @@ +import 藍 from './ai'; +import IModule from './module'; + +export type FriendDoc = { + userId: string; + user: any; + name?: string; + love?: number; + lastLoveIncrementedAt?: string; + todayLoveIncrements?: number; + perModulesData?: any; +}; + +export default class Friend { + private ai: 藍; + + public get userId() { + return this.doc.userId; + } + + public get name() { + return this.doc.name; + } + + public get love() { + return this.doc.love || 0; + } + + public doc: FriendDoc; + + constructor(ai: 藍, opts: { user?: any, doc?: FriendDoc }) { + this.ai = ai; + + if (opts.user) { + this.doc = this.ai.friends.findOne({ + userId: opts.user.id + }); + + if (this.doc == null) { + this.doc = this.ai.friends.insertOne({ + userId: opts.user.id, + user: opts.user + }); + } else { + this.doc.user = opts.user; + this.save(); + } + } else { + this.doc = opts.doc; + } + } + + public updateUser = (user: any) => { + this.doc.user = user; + this.save(); + } + + public getPerModulesData = (module: IModule) => { + if (this.doc.perModulesData == null) { + this.doc.perModulesData = {}; + this.doc.perModulesData[module.name] = {}; + this.save(); + } else if (this.doc.perModulesData[module.name] == null) { + this.doc.perModulesData[module.name] = {}; + this.save(); + } + + return this.doc.perModulesData[module.name]; + } + + public setPerModulesData = (module: IModule, data: any) => { + if (this.doc.perModulesData == null) { + this.doc.perModulesData = {}; + } + + this.doc.perModulesData[module.name] = data; + + this.save(); + } + + public incLove = () => { + const now = new Date(); + const y = now.getFullYear(); + const m = now.getMonth(); + const d = now.getDate(); + const today = `${y}/${m + 1}/${d}`; + + if (this.doc.lastLoveIncrementedAt != today) { + this.doc.todayLoveIncrements = 0; + } + + // 1日に上げられる親愛度は最大3 + if (this.doc.lastLoveIncrementedAt == today && this.doc.todayLoveIncrements >= 3) return; + + if (this.doc.love == null) this.doc.love = 0; + this.doc.love++; + this.doc.lastLoveIncrementedAt = today; + this.doc.todayLoveIncrements = (this.doc.todayLoveIncrements || 0) + 1; + this.save(); + } + + public updateName = (name: string) => { + this.doc.name = name; + this.save(); + } + + public save = () => { + this.ai.friends.update(this.doc); + } +} diff --git a/src/message-like.ts b/src/message-like.ts index 9a8a7b5..f4e0ce0 100644 --- a/src/message-like.ts +++ b/src/message-like.ts @@ -1,4 +1,5 @@ import 藍 from './ai'; +import Friend from './friend'; const delay = require('timeout-as-promise'); export default class MessageLike { @@ -26,22 +27,21 @@ export default class MessageLike { return this.messageOrNote.replyId; } - public friend: ReturnType<藍['friends']['findOne']>; + public friend: Friend; constructor(ai: 藍, messageOrNote: any, isMessage: boolean) { this.ai = ai; this.messageOrNote = messageOrNote; this.isMessage = isMessage; - this.friend = this.ai.friends.findOne({ - userId: this.userId - }); + this.friend = new Friend(ai, { user: this.user }); - if (this.friend == null) { - this.friend = this.ai.friends.insertOne({ - userId: this.userId - }); - } + // メッセージなどに付いているユーザー情報は省略されている場合があるので完全なユーザー情報を持ってくる + this.ai.api('users/show', { + userId: this.userId + }).then(user => { + this.friend.updateUser(user); + }); } public reply = async (text: string, cw?: string) => { diff --git a/src/modules/core/index.ts b/src/modules/core/index.ts index 9abdf81..322e72f 100644 --- a/src/modules/core/index.ts +++ b/src/modules/core/index.ts @@ -3,6 +3,11 @@ import 藍 from '../../ai'; import IModule from '../../module'; import MessageLike from '../../message-like'; import serifs from '../../serifs'; +import Friend from '../../friend'; + +function zeroPadding(num: number, length: number): string { + return ('0000000000' + num).slice(-length); +} export default class CoreModule implements IModule { public name = 'core'; @@ -10,46 +15,117 @@ export default class CoreModule implements IModule { public install = (ai: 藍) => { this.ai = ai; + + this.crawleBirthday(); + setInterval(this.crawleBirthday, 1000 * 60 * 3); } public onMention = (msg: MessageLike) => { if (!msg.text) return false; - if (msg.text.includes('って呼んで') && !msg.text.startsWith('って呼んで')) { - const name = msg.text.match(/^(.+?)って呼んで/)[1]; + return this.setName(msg) || this.greet(msg); + } - if (name.length > 10) { - msg.reply(serifs.core.tooLong); - return true; - } + /** + * 誕生日のユーザーがいないかチェック(いたら祝う) + */ + private crawleBirthday = () => { + const now = new Date(); + const y = now.getFullYear(); + const m = now.getMonth(); + const d = now.getDate(); + // Misskeyの誕生日は 2018-06-16 のような形式 + const today = `${zeroPadding(m + 1, 2)}-${d}`; - const withSan = - name.endsWith('さん') || - name.endsWith('くん') || - name.endsWith('君') || - name.endsWith('ちゃん') || - name.endsWith('様'); + const birthFriends = this.ai.friends.find({ + 'user.profile.birthday': { '$regex': new RegExp('-' + today + '$') } + } as any); - if (withSan) { - msg.friend.name = name; - this.ai.friends.update(msg.friend); - msg.reply(serifs.core.setNameOk.replace('{name}', name)); - } else { - msg.reply(serifs.core.san).then(reply => { - this.ai.subscribeReply(this, msg.userId, msg.isMessage, msg.isMessage ? msg.userId : reply.id, { - name: name - }); - }); - } + birthFriends.forEach(f => { + const friend = new Friend(this.ai, { doc: f }); + const data = friend.getPerModulesData(this); + + if (data.lastBirthdayChecked == today) return; + + data.lastBirthdayChecked = today; + friend.setPerModulesData(this, data); + + const text = friend.name ? serifs.core.happyBirthdayWithName.replace('{name}', friend.name) : serifs.core.happyBirthday; + + this.ai.sendMessage(friend.userId, { + text: text + }); + }); + } + + private setName = (msg: MessageLike): boolean => { + if (!msg.text) return false; + if (!msg.text.includes('って呼んで')) return false; + if (msg.text.startsWith('って呼んで')) return false; + + if (msg.friend.love < 5) { + msg.reply(serifs.core.requireMoreLove); return true; - } else if (msg.text.includes('おはよう')) { + } + + const name = msg.text.match(/^(.+?)って呼んで/)[1]; + + if (name.length > 10) { + msg.reply(serifs.core.tooLong); + return true; + } + + const withSan = + name.endsWith('さん') || + name.endsWith('くん') || + name.endsWith('君') || + name.endsWith('ちゃん') || + name.endsWith('様'); + + if (withSan) { + msg.friend.updateName(name); + msg.reply(serifs.core.setNameOk.replace('{name}', name)); + } else { + msg.reply(serifs.core.san).then(reply => { + this.ai.subscribeReply(this, msg.userId, msg.isMessage, msg.isMessage ? msg.userId : reply.id, { + name: name + }); + }); + } + + return true; + } + + private greet = (msg: MessageLike): boolean => { + if (!msg.text) return false; + + const incLove = () => { + const now = new Date(); + const y = now.getFullYear(); + const m = now.getMonth(); + const d = now.getDate(); + const date = `${y}/${m + 1}/${d}`; + + const data = msg.friend.getPerModulesData(this); + + if (data.lastGreetedAt == date) return; + + data.lastGreetedAt = date; + msg.friend.setPerModulesData(this, data); + + msg.friend.incLove(); + }; + + if (msg.text.includes('おはよう')) { if (msg.friend.name) { msg.reply(serifs.core.goodMorningWithName.replace('{name}', msg.friend.name)); } else { msg.reply(serifs.core.goodMorning); } + incLove(); + return true; } else if (msg.text.includes('おやすみ')) { if (msg.friend.name) { @@ -58,6 +134,8 @@ export default class CoreModule implements IModule { msg.reply(serifs.core.goodNight); } + incLove(); + return true; } else { return false; @@ -68,16 +146,15 @@ export default class CoreModule implements IModule { if (msg.text == null) return; const done = () => { - this.ai.friends.update(msg.friend); msg.reply(serifs.core.setNameOk.replace('{name}', msg.friend.name)); this.ai.unsubscribeReply(this, msg.userId); }; if (msg.text.includes('はい')) { - msg.friend.name = data.name + 'さん'; + msg.friend.updateName(data.name + 'さん'); done(); } else if (msg.text.includes('いいえ')) { - msg.friend.name = data.name; + msg.friend.updateName(data.name); done(); } else { msg.reply(serifs.core.yesOrNo).then(reply => { diff --git a/src/serifs.ts b/src/serifs.ts index f0b7a0a..e3a8d4d 100644 --- a/src/serifs.ts +++ b/src/serifs.ts @@ -7,7 +7,10 @@ export default { goodMorningWithName: 'おはようございます、{name}!', goodNight: 'おやすみなさい!', goodNightWithName: 'おやすみなさい、{name}!', - tooLong: '長すぎる気がします...' + tooLong: '長すぎる気がします...', + requireMoreLove: 'もっと仲良くなったら考えてあげてもいいですよ?', + happyBirthday: 'お誕生日おめでとうございます🎉', + happyBirthdayWithName: 'お誕生日おめでとうございます、{name}🎉' }, keyword: {