リマインダーとか

This commit is contained in:
syuilo 2020-10-31 12:18:42 +09:00
parent 741feb86d1
commit 31449bd398
12 changed files with 215 additions and 30 deletions

View file

@ -1,5 +1,5 @@
{ {
"_v": "1.3.0", "_v": "1.4.0",
"main": "./built/index.js", "main": "./built/index.js",
"scripts": { "scripts": {
"start": "node ./built", "start": "node ./built",

View file

@ -18,11 +18,12 @@ import log from '@/utils/log';
const pkg = require('../package.json'); const pkg = require('../package.json');
type MentionHook = (msg: Message) => Promise<boolean | HandlerResult>; type MentionHook = (msg: Message) => Promise<boolean | HandlerResult>;
type ContextHook = (msg: Message, data?: any) => Promise<void | HandlerResult>; type ContextHook = (key: any, msg: Message, data?: any) => Promise<void | boolean | HandlerResult>;
type TimeoutCallback = (data?: any) => void; type TimeoutCallback = (data?: any) => void;
export type HandlerResult = { export type HandlerResult = {
reaction: string | null; reaction?: string | null;
immediate?: boolean;
}; };
export type InstallerResult = { export type InstallerResult = {
@ -220,18 +221,10 @@ export default class 藍 {
}); });
let reaction: string | null = 'love'; let reaction: string | null = 'love';
let immediate: boolean = false;
//#region //#region
// コンテキストがあればコンテキストフック呼び出し const invokeMentionHooks = async () => {
// なければそれぞれのモジュールについてフックが引っかかるまで呼び出し
if (context != null) {
const handler = this.contextHooks[context.module];
const res = await handler(msg, context.data);
if (res != null && typeof res === 'object') {
reaction = res.reaction;
}
} else {
let res: boolean | HandlerResult | null = null; let res: boolean | HandlerResult | null = null;
for (const handler of this.mentionHooks) { for (const handler of this.mentionHooks) {
@ -240,12 +233,33 @@ export default class 藍 {
} }
if (res != null && typeof res === 'object') { if (res != null && typeof res === 'object') {
reaction = res.reaction; if (res.reaction != null) reaction = res.reaction;
if (res.immediate != null) immediate = res.immediate;
} }
};
// コンテキストがあればコンテキストフック呼び出し
// なければそれぞれのモジュールについてフックが引っかかるまで呼び出し
if (context != null) {
const handler = this.contextHooks[context.module];
const res = await handler(context.key, msg, context.data);
if (res != null && typeof res === 'object') {
if (res.reaction != null) reaction = res.reaction;
if (res.immediate != null) immediate = res.immediate;
}
if (res === false) {
await invokeMentionHooks();
}
} else {
await invokeMentionHooks();
} }
//#endregion //#endregion
await delay(1000); if (!immediate) {
await delay(1000);
}
if (msg.isDm) { if (msg.isDm) {
// 既読にする // 既読にする

View file

@ -33,6 +33,7 @@ import ChartModule from './modules/chart';
import SleepReportModule from './modules/sleep-report'; import SleepReportModule from './modules/sleep-report';
import NotingModule from './modules/noting'; import NotingModule from './modules/noting';
import PollModule from './modules/poll'; import PollModule from './modules/poll';
import ReminderModule from './modules/reminder';
console.log(' __ ____ _____ ___ '); console.log(' __ ____ _____ ___ ');
console.log(' /__\\ (_ _)( _ )/ __)'); console.log(' /__\\ (_ _)( _ )/ __)');
@ -86,6 +87,7 @@ promiseRetry(retry => {
new SleepReportModule(), new SleepReportModule(),
new NotingModule(), new NotingModule(),
new PollModule(), new PollModule(),
new ReminderModule(),
]); ]);
}).catch(e => { }).catch(e => {
log(chalk.red('Failed to fetch the account')); log(chalk.red('Failed to fetch the account'));

View file

@ -30,6 +30,13 @@ export default class Message {
return this.messageOrNote.text; return this.messageOrNote.text;
} }
public get quoteId(): string | null {
return this.messageOrNote.renoteId;
}
/**
*
*/
public get extractedText(): string { public get extractedText(): string {
const host = new URL(config.host).host.replace(/\./g, '\\.'); const host = new URL(config.host).host.replace(/\./g, '\\.');
return this.text return this.text

View file

@ -141,12 +141,12 @@ export default class extends Module {
} }
@autobind @autobind
private async contextHook(msg: Message, data: any) { private async contextHook(key: any, msg: Message, data: any) {
if (msg.text == null) return; if (msg.text == null) return;
const done = () => { const done = () => {
msg.reply(serifs.core.setNameOk(msg.friend.name)); msg.reply(serifs.core.setNameOk(msg.friend.name));
this.unsubscribeReply(msg.userId); this.unsubscribeReply(key);
}; };
if (msg.text.includes('はい')) { if (msg.text.includes('はい')) {

View file

@ -66,7 +66,7 @@ export default class extends Module {
} }
@autobind @autobind
private async contextHook(msg: Message) { private async contextHook(key: any, msg: Message) {
if (msg.text == null) return; if (msg.text == null) return;
const exist = this.guesses.findOne({ const exist = this.guesses.findOne({
@ -76,7 +76,7 @@ export default class extends Module {
// 処理の流れ上、実際にnullになることは無さそうだけど一応 // 処理の流れ上、実際にnullになることは無さそうだけど一応
if (exist == null) { if (exist == null) {
this.unsubscribeReply(msg.userId); this.unsubscribeReply(key);
return; return;
} }
@ -85,7 +85,7 @@ export default class extends Module {
exist.isEnded = true; exist.isEnded = true;
exist.endedAt = Date.now(); exist.endedAt = Date.now();
this.guesses.update(exist); this.guesses.update(exist);
this.unsubscribeReply(msg.userId); this.unsubscribeReply(key);
return; return;
} }
@ -124,7 +124,7 @@ export default class extends Module {
if (end) { if (end) {
exist.isEnded = true; exist.isEnded = true;
exist.endedAt = Date.now(); exist.endedAt = Date.now();
this.unsubscribeReply(msg.userId); this.unsubscribeReply(key);
} }
this.guesses.update(exist); this.guesses.update(exist);

View file

@ -4,6 +4,7 @@ import Module from '@/module';
import Message from '@/message'; import Message from '@/message';
import serifs from '@/serifs'; import serifs from '@/serifs';
import { User } from '@/misskey/user'; import { User } from '@/misskey/user';
import { acct } from '@/utils/acct';
type Game = { type Game = {
votes: { votes: {
@ -82,7 +83,7 @@ export default class extends Module {
} }
@autobind @autobind
private async contextHook(msg: Message) { private async contextHook(key: any, msg: Message) {
if (msg.text == null) return { if (msg.text == null) return {
reaction: 'hmm' reaction: 'hmm'
}; };
@ -172,12 +173,6 @@ export default class extends Module {
return; return;
} }
function acct(user: Game['votes'][0]['user']): string {
return user.host
? `@${user.username}@${user.host}`
: `@${user.username}`;
}
let results: string[] = []; let results: string[] = [];
let winner: Game['votes'][0]['user'] | null = null; let winner: Game['votes'][0]['user'] | null = null;

View file

@ -0,0 +1,140 @@
import autobind from 'autobind-decorator';
import * as loki from 'lokijs';
import Module from '@/module';
import Message from '@/message';
import serifs from '@/serifs';
import { acct } from '@/utils/acct';
const NOTIFY_INTERVAL = 1000 * 60 * 60 * 12;
export default class extends Module {
public readonly name = 'reminder';
private reminds: loki.Collection<{
userId: string;
id: string;
isDm: boolean;
thing: string | null;
quoteId: string | null;
times: number; // 催促した回数(使うのか?)
createdAt: number;
}>;
@autobind
public install() {
this.reminds = this.ai.getCollection('reminds', {
indices: ['userId', 'id']
});
return {
mentionHook: this.mentionHook,
contextHook: this.contextHook,
timeoutCallback: this.timeoutCallback,
};
}
@autobind
private async mentionHook(msg: Message) {
let text = msg.extractedText.toLowerCase();
if (!text.startsWith('remind') && !text.startsWith('todo')) return false;
if (text.match(/^(.+?)\s(.+)/)) {
text = text.replace(/^(.+?)\s/, '');
} else {
text = '';
}
const separatorIndex = text.indexOf(' ') > -1 ? text.indexOf(' ') : text.indexOf('\n');
const thing = text.substr(separatorIndex + 1).trim();
if (thing === '' && msg.quoteId == null) {
msg.reply(serifs.reminder.invalid);
return true;
}
const remind = this.reminds.insertOne({
id: msg.id,
userId: msg.userId,
isDm: msg.isDm,
thing: thing === '' ? null : thing,
quoteId: msg.quoteId,
times: 0,
createdAt: Date.now(),
});
this.subscribeReply(msg.id, msg.isDm, msg.isDm ? msg.userId : msg.id, {
id: remind!.id
});
// タイマーセット
this.setTimeoutWithPersistence(NOTIFY_INTERVAL, {
id: msg.id,
});
return {
reaction: '🆗',
immediate: true,
};
}
@autobind
private async contextHook(key: any, msg: Message, data: any) {
if (msg.text == null) return;
const remind = this.reminds.findOne({
id: data.id,
});
if (remind == null) {
this.unsubscribeReply(key);
return;
}
const done = msg.includes(['done', 'やった']);
const cancel = msg.includes(['やめる']);
if (done || cancel) {
this.unsubscribeReply(key);
this.reminds.remove(remind);
msg.reply(done ? serifs.reminder.done(msg.friend.name) : serifs.reminder.cancel);
return;
} else {
if (msg.isDm) this.unsubscribeReply(key);
return false;
}
}
@autobind
private async timeoutCallback(data) {
const remind = this.reminds.findOne({
id: data.id
});
if (remind == null) return;
remind.times++;
this.reminds.update(remind);
const friend = this.ai.lookupFriend(remind.userId);
if (friend == null) return; // 処理の流れ上、実際にnullになることは無さそうだけど一応
let reply;
if (remind.isDm) {
this.ai.sendMessage(friend.userId, {
text: serifs.reminder.notifyWithThing(remind.thing, friend.name)
});
} else {
reply = await this.ai.post({
renoteId: remind.thing == null && remind.quoteId ? remind.quoteId : remind.id,
text: acct(friend.doc.user) + ' ' + serifs.reminder.notify(friend.name)
});
}
this.subscribeReply(remind.id, remind.isDm, remind.isDm ? remind.userId : reply.id, {
id: remind.id
});
// タイマーセット
this.setTimeoutWithPersistence(NOTIFY_INTERVAL, {
id: remind.id,
});
}
}

View file

@ -335,6 +335,21 @@ export default {
notify: (time, name) => name ? `${name}${time}経ちましたよ!` : `${time}経ちましたよ!` notify: (time, name) => name ? `${name}${time}経ちましたよ!` : `${time}経ちましたよ!`
}, },
/**
*
*/
reminder: {
invalid: 'うーん...',
notify: (name) => name ? `${name}、これやりましたか?` : `これやりましたか?`,
notifyWithThing: (thing, name) => name ? `${name}、「${thing}」やりましたか?` : `${thing}」やりましたか?`,
done: (name) => name ? `よく出来ました、${name}` : `よく出来ました♪`,
cancel: `わかりました。`,
},
/** /**
* *
*/ */

5
src/utils/acct.ts Normal file
View file

@ -0,0 +1,5 @@
export function acct(user: { username: string; host?: string | null; }): string {
return user.host
? `@${user.username}@${user.host}`
: `@${user.username}`;
}

View file

@ -3,8 +3,8 @@ import { katakanaToHiragana, hankakuToZenkaku } from './japanese';
export default function(text: string, words: string[]): boolean { export default function(text: string, words: string[]): boolean {
if (text == null) return false; if (text == null) return false;
text = katakanaToHiragana(hankakuToZenkaku(text)); text = katakanaToHiragana(hankakuToZenkaku(text)).toLowerCase();
words = words.map(word => katakanaToHiragana(word)); words = words.map(word => katakanaToHiragana(word).toLowerCase());
return words.some(word => text.includes(word)); return words.some(word => text.includes(word));
} }

View file

@ -15,6 +15,13 @@
### タイマー ### タイマー
指定した時間、分、秒を経過したら教えてくれます。「3分40秒」のように単位を混ぜることもできます。 指定した時間、分、秒を経過したら教えてくれます。「3分40秒」のように単位を混ぜることもできます。
### リマインダー
```
@ai remind 部屋の掃除
```
のようにメンションを飛ばすと12時間置きに催促されます。その飛ばしたメンションか、藍ちゃんからの催促に「やった」と返信することでリマインダー解除されます。
また、引用Renoteでメンションすることもできます。
### 福笑い ### 福笑い
藍に「絵文字」と言うと、藍が考えた絵文字の組み合わせを教えてくれます。 藍に「絵文字」と言うと、藍が考えた絵文字の組み合わせを教えてくれます。