diff --git a/.vscode/settings.json b/.vscode/settings.json index 68ecdba..823bc61 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,8 @@ { - "typescript.tsdk": "node_modules\\typescript\\lib" -} \ No newline at end of file + "typescript.tsdk": "node_modules\\typescript\\lib", + "C_Cpp.errorSquiggles": "Disabled", + "cSpell.words": [ + "lokijs", + "todos" + ] +} diff --git a/README.md b/README.md index 87dcf11..629d5a2 100644 --- a/README.md +++ b/README.md @@ -1,61 +1,19 @@ -

藍

-

An Ai for Misskey. About Ai

+# フォーク元と違うところ -## これなに -Misskey用の日本語Botです。 +- 一部の絵文字リアクション機能 +- ねこ召喚(summonCat) +- ランダムカラーピッカー(color) +- ランダムにクックパッドからレシピを引っ張ってくる(menu) +- 強震モニター Extension と連携して震度レポートのノート(earthquake) -## インストール -> Node.js と npm と MeCab (オプション) がインストールされている必要があります。 +# メモ -まず適当なディレクトリに `git clone` します。 -次にそのディレクトリに `config.json` を作成します。中身は次のようにします: -``` json -{ - "host": "https:// + あなたのインスタンスのURL (末尾の / は除く)", - "i": "藍として動かしたいアカウントのアクセストークン", - "master": "管理者のユーザー名(オプション)", - "notingEnabled": "ランダムにノートを投稿する機能を無効にする場合は false を入れる", - "keywordEnabled": "キーワードを覚える機能 (MeCab が必要) を有効にする場合は true を入れる (無効にする場合は false)", - "chartEnabled": "チャート機能を無効化する場合は false を入れてください", - "reversiEnabled": "藍とリバーシで対局できる機能を有効にする場合は true を入れる (無効にする場合は false)", - "serverMonitoring": "サーバー監視の機能を有効にする場合は true を入れる (無効にする場合は false)", - "mecab": "MeCab のインストールパス (ソースからインストールした場合、大体は /usr/local/bin/mecab)", - "mecabDic": "MeCab の辞書ファイルパス (オプション)", - "memoryDir": "memory.jsonの保存先(オプション、デフォルトは'.'(レポジトリのルートです))" -} -``` -`npm install` して `npm run build` して `npm start` すれば起動できます +## 強震モニターについて -## Dockerで動かす -まず適当なディレクトリに `git clone` します。 -次にそのディレクトリに `config.json` を作成します。中身は次のようにします: -(MeCabの設定、memoryDirについては触らないでください) -``` json -{ - "host": "https:// + あなたのインスタンスのURL (末尾の / は除く)", - "i": "藍として動かしたいアカウントのアクセストークン", - "master": "管理者のユーザー名(オプション)", - "notingEnabled": "ランダムにノートを投稿する機能を無効にする場合は false を入れる", - "keywordEnabled": "キーワードを覚える機能 (MeCab が必要) を有効にする場合は true を入れる (無効にする場合は false)", - "chartEnabled": "チャート機能を無効化する場合は false を入れてください", - "reversiEnabled": "藍とリバーシで対局できる機能を有効にする場合は true を入れる (無効にする場合は false)", - "serverMonitoring": "サーバー監視の機能を有効にする場合は true を入れる (無効にする場合は false)", - "mecab": "/usr/bin/mecab", - "mecabDic": "/usr/lib/x86_64-linux-gnu/mecab/dic/mecab-ipadic-neologd/", - "memoryDir": "data" -} -``` -`docker-compose build` して `docker-compose up` すれば起動できます。 -`docker-compose.yml` の `enable_mecab` を `0` にすると、MeCabをインストールしないようにもできます。(メモリが少ない環境など) +http サーバーを起動させて、震度レポートを受け取るような仕組み。 +config.json にポート番号を指定、そのポート番号に対して、震度レポートを受け取るようにする。 +リバースプロキシなんかを使ってたりします。 -## フォント -一部の機能にはフォントが必要です。藍にはフォントは同梱されていないので、ご自身でフォントをインストールディレクトリに`font.ttf`という名前で設置してください。 +## ランダムカラーピッカー -## 記憶 -藍は記憶の保持にインメモリデータベースを使用しており、藍のインストールディレクトリに `memory.json` という名前で永続化されます。 - -## ライセンス -MIT - -## Awards -Works on my machine +ランダムに決定した色の 1px \* 1px の画像をアップロードしてます。 diff --git a/package.json b/package.json index 84db823..49d11c9 100644 --- a/package.json +++ b/package.json @@ -20,10 +20,12 @@ "autobind-decorator": "2.4.0", "canvas": "2.8.0", "chalk": "4.1.1", + "jsdom": "19.0.0", "lokijs": "1.5.12", "memory-streams": "0.1.3", "misskey-reversi": "0.0.5", "module-alias": "2.2.2", + "node-fetch": "2.6.7", "promise-retry": "2.0.1", "random-seed": "0.3.0", "reconnecting-websocket": "4.4.0", @@ -40,8 +42,10 @@ "devDependencies": { "@koa/router": "9.4.0", "@types/jest": "26.0.23", + "@types/jsdom": "16.2.14", "@types/koa": "2.13.1", "@types/koa__router": "8.0.4", + "@types/node-fetch": "3.0.3", "@types/websocket": "1.0.2", "jest": "26.6.3", "koa": "2.13.1", diff --git a/src/config.ts b/src/config.ts index 1918b4f..81892a6 100644 --- a/src/config.ts +++ b/src/config.ts @@ -12,6 +12,7 @@ type Config = { mecab?: string; mecabDic?: string; memoryDir?: string; + earthQuakeMonitorPort?: number; }; const config = require('../config.json'); diff --git a/src/index.ts b/src/index.ts index dca5703..5f5b4c0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,6 +15,7 @@ import CoreModule from './modules/core'; import TalkModule from './modules/talk'; import BirthdayModule from './modules/birthday'; import ReversiModule from './modules/reversi'; +import summonCat from './modules/summonCat'; import PingModule from './modules/ping'; import EmojiModule from './modules/emoji'; import EmojiReactModule from './modules/emoji-react'; @@ -32,8 +33,12 @@ import MazeModule from './modules/maze'; import ChartModule from './modules/chart'; import SleepReportModule from './modules/sleep-report'; import NotingModule from './modules/noting'; -import PollModule from './modules/poll'; +// import PollModule from './modules/poll'; import ReminderModule from './modules/reminder'; +import earthquake from './modules/earthquake'; +import DicModule from './modules/dic'; +import menuModule from './modules/menu'; +import GetColorModule from './modules/color'; console.log(' __ ____ _____ ___ '); console.log(' /__\\ (_ _)( _ )/ __)'); @@ -66,6 +71,7 @@ promiseRetry(retry => { // 藍起動 new 藍(account, [ new CoreModule(), + new summonCat(), new EmojiModule(), new EmojiReactModule(), new FortuneModule(), @@ -86,8 +92,12 @@ promiseRetry(retry => { new ChartModule(), new SleepReportModule(), new NotingModule(), - new PollModule(), + // new PollModule(), new ReminderModule(), + new DicModule(), + new menuModule(), + new GetColorModule(), + new earthquake(), ]); }).catch(e => { log(chalk.red('Failed to fetch the account')); diff --git a/src/modules/color/index.ts b/src/modules/color/index.ts new file mode 100644 index 0000000..3cd0f2b --- /dev/null +++ b/src/modules/color/index.ts @@ -0,0 +1,52 @@ +import autobind from 'autobind-decorator'; +import Module from '@/module'; +import Message from '@/message'; +import { generateColorSample } from './render'; + +export default class extends Module { + public readonly name = 'color'; + + @autobind + public install() { + return { + mentionHook: this.mentionHook + }; + } + + @autobind + private async mentionHook(msg: Message) { + if (msg.text && msg.text.includes('色決めて')) { + // rgbをそれぞれ乱数で生成する + const r = Math.floor(Math.random() * 256); + const g = Math.floor(Math.random() * 256); + const b = Math.floor(Math.random() * 256); + // rgbをhexに変換する + const hex = `${r.toString(16)}${g.toString(16)}${b.toString(16)}`; + const message = `RGB: ${r}, ${g}, ${b} \`(#${hex})\`とかどう?` + + setTimeout(async () => { + const file = await this.getColorSampleFile(r,g,b); + this.log('Replying...'); + msg.reply(message, { file }); + }, 500); + return { + reaction: '🎨' + }; + } else { + return false; + } + } + + @autobind + private async getColorSampleFile(r,g,b): Promise { + const colorSample = generateColorSample(r,g,b); + + this.log('Image uploading...'); + const file = await this.ai.upload(colorSample, { + filename: 'color.png', + contentType: 'image/png' + }); + + return file; + } +} diff --git a/src/modules/color/render.ts b/src/modules/color/render.ts new file mode 100644 index 0000000..e4449d0 --- /dev/null +++ b/src/modules/color/render.ts @@ -0,0 +1,17 @@ +import { createCanvas } from 'canvas'; + +const imageSize = 1; //px + +export function generateColorSample(r: string, g: string, b: string) { + const canvas = createCanvas(imageSize, imageSize); + const ctx = canvas.getContext('2d'); + ctx.antialias = 'none'; + + // 引数で渡されたrgb値を基準に、色を塗りつぶす + ctx.fillStyle = `rgb(${r},${g},${b})`; + ctx.beginPath(); + ctx.fillRect(0, 0, imageSize, imageSize); + + // canvas.toBuffer()をreturn + return canvas.toBuffer(); +} \ No newline at end of file diff --git a/src/modules/dic/index.ts b/src/modules/dic/index.ts new file mode 100644 index 0000000..0220f68 --- /dev/null +++ b/src/modules/dic/index.ts @@ -0,0 +1,32 @@ +import autobind from 'autobind-decorator'; +import Module from '@/module'; +import Message from '@/message'; + +export default class extends Module { + public readonly name = 'dic'; + + @autobind + public install() { + return { + mentionHook: this.mentionHook + }; + } + + @autobind + private async mentionHook(msg: Message) { + if (msg.text && msg.text.includes('って何')) { + // msg.textのうち、「の意味は」の直前で、「@ai」よりも後の物を抽出 + const dic_prefix = "https://www.weblio.jp/content/"; + const raw_word = msg.text.split('って何')[0].split('@ai')[1].trim(); + // スペースがある場合は、半角スペースを除去 + const word = raw_word.replace(/\s/g, ''); + const url = dic_prefix + encodeURIComponent(word); + msg.reply(`こんな意味っぽい?> [${word}](${url})`, { + immediate: true + }); + return true; + } else { + return false; + } + } +} diff --git a/src/modules/earthquake/index.ts b/src/modules/earthquake/index.ts new file mode 100644 index 0000000..f48eba0 --- /dev/null +++ b/src/modules/earthquake/index.ts @@ -0,0 +1,165 @@ +import autobind from "autobind-decorator"; +import Module from "@/module"; +import config from "@/config"; +import Message from "@/message"; +import * as http from "http"; + +// 基本的に生データはstringばっかり。都合のいい形に加工済みの状態の型定義を書いています。 +// ここでいくらか言及されてる(https://bultar.bbs.fc2.com/?act=reply&tid=5645851); +interface 緊急地震速報 { + type: "eew"; + time: Date; + report: string; // 第n報 最終報はstringで'final'となるので、とりあえずstring型 + epicenter: string; // 震源地 + depth: string; // 震源の深さ + magnitude: string; // 地震の規模を示すマグニチュード + latitude: string; // 緯度らしいが謎 + longitude: string; // 経度らしいが謎 + intensity: string; // 地震の強さ + index: number; // 謎 +} + +interface 緊急地震速報キャンセル { + type: "pga_alert_cancel"; + time: Date; +} + +interface 震度レポート { + type: "intensity_report"; + time: string; + max_index: number; + intensity_list: { + intensity: string; + index: number; + region_list: string[]; + }[]; +} + +interface 地震検知 { + type: "pga_alert"; + time: Date; + max_pga: number; + new: boolean; + estimated_intensity: number; + region_list: string[]; +} + +export default class extends Module { + public readonly name = "earthquake"; + private message: string = ""; + + private thresholdVal = 3; // 下の配列の添え字に相当する値。しきい値以上のものについて通知を出す。 普段は3(震度2) + private earthquakeIntensityIndex: string[] = [ + "0未満", + "0", + "1", + "2", + "3", + "4", + "5弱", + "5強", + "6弱", + "6強", + "7", + ]; + + @autobind + public install() { + this.createListenServer(); + return {}; + } + + @autobind + private async createListenServer() { + http.createServer(async (req, res) => { + const buffers: Buffer[] = []; + for await (const chunk of req) { + buffers.push(chunk); + } + + const rawDataString = Buffer.concat(buffers).toString(); + // rawDataString について、Unicodeエスケープシーケンスが含まれていたら通常の文字列に変換する + // JSONでなければreturn falseする + if (rawDataString.match(/\\u[0-9a-f]{4}/)) { + const rawDataJSON = JSON.parse( + rawDataString.replace(/\\u([\d\w]{4})/g, (match, p1) => { + return String.fromCharCode(parseInt(p1, 16)); + }), + ); + + if (rawDataJSON.type == "intensity_report") { + if (rawDataJSON.max_index >= this.thresholdVal - 1) { + // 日付時刻は、yyyy-mm-dd hh:mm:ss + const time = new Date(parseInt(rawDataJSON.time)); + const timeString = `${time.getFullYear()}-${(time.getMonth() + + 1).toString().padStart(2, '0')}-${time.getDate().toString().padStart(2, '0')} ${time.getHours().toString().padStart(2, '0')}:${time.getMinutes().toString().padStart(2, '0')}:${time.getSeconds().toString().padStart(2, '0')}`; + const data: 震度レポート = { + type: rawDataJSON.type, + time: timeString, + max_index: rawDataJSON.max_index, + intensity_list: rawDataJSON.intensity_list, + }; + this.message = + `地震かも?\n\`\`\`\n震度レポート\n${data.time}\n最大震度: ${ + this.earthquakeIntensityIndex[data.max_index + 1] + }\n\n${ + data.intensity_list.map((intensity) => + `震度${this.earthquakeIntensityIndex[intensity.index + 1]}: ${ + intensity.region_list.join(" ") + }` + ).join("\n") + }\n\`\`\``; + } + } + if (rawDataJSON.type == "eew" && false) { // これ使わなさそうだしとりあえず入らないようにした + const data: 緊急地震速報 = { + type: rawDataJSON.type, + time: new Date(parseInt(rawDataJSON.time)), + report: rawDataJSON.report, + epicenter: rawDataJSON.epicenter, + depth: rawDataJSON.depth, + magnitude: rawDataJSON.magnitude, + latitude: rawDataJSON.latitude, + longitude: rawDataJSON.longitude, + intensity: rawDataJSON.intensity, + index: rawDataJSON.index, + }; + + if (data.report == "1") { + this.message = + `**TEST TEST TEST TEST**\n地震かも?\n\n緊急地震速報\n${data.time.toLocaleString()}\n\n第${data.report}報\n震源地: ${data.epicenter}\n震源の深さ: ${data.depth}\n地震の規模(M): ${data.magnitude}\n緯度: ${data.latitude}\n経度: ${data.longitude}\n予想される最大震度(?): ${data.intensity}\n`; + } + } + + console.table(rawDataJSON); // デバッグ用 + if (rawDataJSON.type == 'intensity_report') { + console.table(rawDataJSON.intensity_list); // デバッグ用 + } + + this.returnResponse(res, "ok"); + if (this.message) { + this.ai.post({ + cw: "試験運用中!!!!!", + visibility: "home", + text: this.message, + }); + } + } else { + this.ai.post({ + cw: "試験運用中!!!!!", + visibility: "home", + text: 'eq:デボビゲゴ', + }); + this.returnResponse(res, "debobigego"); + } + }).listen(config.earthQuakeMonitorPort || 9999); + } + + @autobind + private returnResponse(res: http.ServerResponse, text: string) { + res.writeHead(200, { + "Content-Type": "text/plain", + }); + res.end(text); + } +} diff --git a/src/modules/earthquake/typeMemo.c b/src/modules/earthquake/typeMemo.c new file mode 100644 index 0000000..e8c6b52 --- /dev/null +++ b/src/modules/earthquake/typeMemo.c @@ -0,0 +1,38 @@ +# 緊急地震速報 +{"type":"eew", +"time":long, +"report":int, +"epicenter":String, +"depth":String, +"magnitude":String, +"latitude":String, +"longitude":String, +"intensity":String, +"index":int +} + +# 地震検知 +{"type":"pga_alert", +"time":long, +"max_pga":float, +"new":boolean, +"estimated_intensity":int, +"region_list":[String,String,,,] +} + +# 地震検知キャンセル +{"type":"pga_alert_cancel", "time":long } + +# 震度レポート +{"type":"intensity_report", +"time":long, +"max_index":int, +"intensity_list":[ +{"intensity":String, +"index":int, +"region_list":[String,String,,,,]}, +{"intensity":String, +"index":int, +"region_list":[String,String,,,,]}, +,,] +} diff --git a/src/modules/earthquake/テスト用生データ.txt b/src/modules/earthquake/テスト用生データ.txt new file mode 100644 index 0000000..9d4696d --- /dev/null +++ b/src/modules/earthquake/テスト用生データ.txt @@ -0,0 +1,4 @@ +# テスト用生データ +{"type":"pga_alert","time":"1649085285968","max_pga":-0.531,"new":true,"estimated_intensity":0,"region_list":["\u8328\u57ce"]} +{"type":"intensity_report","time":"1649085285968","max_index":-1,"intensity_list":[{"intensity":"0\u672a\u6e80","index":-1,"region_list":["\u8328\u57ce"]}]} +{"type": "eew","report": "1","epicenter": "伊予灘","depth": "60km","magnitude": 3.5,"latitude": 33.8,"longitude": 132.1,"intensity": "2","index": 2} diff --git a/src/modules/emoji-react/index.ts b/src/modules/emoji-react/index.ts index e671b4d..4f2b74b 100644 --- a/src/modules/emoji-react/index.ts +++ b/src/modules/emoji-react/index.ts @@ -28,7 +28,7 @@ export default class extends Module { const react = async (reaction: string, immediate = false) => { if (!immediate) { - await delay(1500); + await delay(2500); } this.ai.api('notes/reactions/create', { noteId: note.id, @@ -36,6 +36,28 @@ export default class extends Module { }); }; + // /う[〜|ー]*んこ/g]にマッチしたときの処理 + if (note.text.match(/う[〜|ー]*んこ/g) || includes(note.text, ['unko'])) { + return await react(':anataima_unkotte_iimashitane:'); + } + + if (note.text.match(/う[〜|ー]*んち/g)) { + return await react(':erait:'); + } + + if (includes(note.text, ['いい']) && (includes(note.text, ["?"]) || includes(note.text, ["?"]))) { + // 50%の確率で":dame:"または":yattare:"を返す + if (Math.random() < 0.5) { + return react(':dame:'); + } else { + return react(':yattare:'); + } + } + + if (includes(note.text, ['どこ'])) { + return await react(':t_ofuton:'); + } + const customEmojis = note.text.match(/:([^\n:]+?):/g); if (customEmojis) { // カスタム絵文字が複数種類ある場合はキャンセル @@ -68,6 +90,17 @@ export default class extends Module { if (includes(note.text, ['ぷりん'])) return react('🍮'); if (includes(note.text, ['寿司', 'sushi']) || note.text === 'すし') return react('🍣'); - if (includes(note.text, ['藍'])) return react('🙌'); + if (includes(note.text, ['ずなず']) || includes(note.text, ['ずにゃず'])) return react('🙌'); + if (includes(note.text, ['なず']) || includes(note.text, ['にゃず'])) return react(':google_hart:'); + + const gameReact = [ + ':ysvi:', + ':ysf:', + ':yso:' + ] + if (includes(note.text, ['おゲームするかしら'])){ + // gameReactの中からランダムに選択 + return react(gameReact[Math.floor(Math.random() * gameReact.length)]); + } } } diff --git a/src/modules/menu/index.ts b/src/modules/menu/index.ts new file mode 100644 index 0000000..57f5390 --- /dev/null +++ b/src/modules/menu/index.ts @@ -0,0 +1,61 @@ +import autobind from 'autobind-decorator'; +import Module from '@/module'; +import Message from '@/message'; +import fetch from 'node-fetch'; +import { JSDOM } from 'jsdom'; + +export default class extends Module { + public readonly name = 'menu'; + + @autobind + public install() { + return { + mentionHook: this.mentionHook + }; + } + + @autobind + private async mentionHook(msg: Message) { + if (msg.text && msg.text.includes('ごはん')) { + // 1~2535111の適当な数字を取得 + const random_number = Math.floor(Math.random() * 2535111) + 1; + const url = `https://cookpad.com/recipe/${random_number}`; + //testUrlして、200以外なら再取得 + const res = await fetch(url); + if (res.status !== 200) { + return this.mentionHook(msg); + } else { + //jsdomを利用してレシピのタイトルを取得 + const dom = new JSDOM(await res.text()); + //@ts-ignore + let title = dom.window.document.querySelector('h1.recipe-title').textContent; + // titleから改行を除去 + title = title!.replace(/\n/g, ''); + msg.reply(`こんなのどう?> [${title}](${url})`, { + immediate: true + }); + return true; + } + } else { + return false; + } + } +} + +function testUrl(url: string) { + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest() + xhr.open('GET', url) + xhr.onload = () => { + if (xhr.status === 200) { + resolve(true) + } else { + reject(false) + } + } + xhr.onerror = () => { + reject(false) + } + xhr.send() + }) +} \ No newline at end of file diff --git a/src/modules/ping/index.ts b/src/modules/ping/index.ts index 147a7d5..c3bf81c 100644 --- a/src/modules/ping/index.ts +++ b/src/modules/ping/index.ts @@ -14,10 +14,16 @@ export default class extends Module { @autobind private async mentionHook(msg: Message) { - if (msg.text && msg.text.includes('ping')) { - msg.reply('PONG!', { - immediate: true - }); + if (msg.text && (msg.text.includes('ping') || msg.text.includes('おい'))) { + if (msg.text.includes('おい')) { + msg.reply('はい。。。', { + immediate: true + }); + } else { + msg.reply('PONG!', { + immediate: true + }); + } return true; } else { return false; diff --git a/src/modules/reminder/index.ts b/src/modules/reminder/index.ts index a10794d..a69c9b4 100644 --- a/src/modules/reminder/index.ts +++ b/src/modules/reminder/index.ts @@ -67,6 +67,14 @@ export default class extends Module { }; } + if (msg.visibility === 'followers') { + msg.reply(serifs.reminder.invalidVisibility); + return { + reaction: '🆖', + immediate: true, + }; + } + const remind = this.reminds.insertOne({ id: msg.id, userId: msg.userId, diff --git a/src/modules/summonCat/index.ts b/src/modules/summonCat/index.ts new file mode 100644 index 0000000..9ffce4d --- /dev/null +++ b/src/modules/summonCat/index.ts @@ -0,0 +1,58 @@ +import autobind from 'autobind-decorator'; +import Module from '@/module'; +import Message from '@/message'; +import fetch from 'node-fetch'; +import { ReadStream } from 'fs'; + +export default class extends Module { + public readonly name = 'summonCat'; + + @autobind + public install() { + return { + mentionHook: this.mentionHook + }; + } + + @autobind + private async mentionHook(msg: Message) { + // cat/Cat/ねこ/ネコ/にゃん + console.log(msg.text) + if (msg.text && (msg.text.match(/(cat|Cat|ねこ|ネコ|にゃ[〜|ー]*ん)/g))) { + const message = "にゃ~ん!"; + + setTimeout(async () => { + const file = await this.getCatImage(); + this.log(file); + this.log('Replying...'); + msg.reply(message, { file }); + }, 500); + + return { + reaction: ':blobcatmeltnomblobcatmelt:' + }; + } else { + return false; + } + } + + @autobind + private async getCatImage(): Promise { + // https://aws.random.cat/meowにGETリクエストを送る + // fileに画像URLが返ってくる + const res = await fetch('https://aws.random.cat/meow'); + const json = await res.json(); + console.table(json); + const fileUri = json.file; + // 拡張子を取り除く + const fileName = fileUri.split('/').pop().split('.')[0]; + const rawFile = await fetch(fileUri); + const imgBuffer = await rawFile.buffer(); + // 拡張子とcontentTypeを判断する + const ext = fileUri.split('.').pop(); + const file = await this.ai.upload(imgBuffer, { + filename: `${fileName}.${ext}`, + }); + return file; + } +} diff --git a/src/serifs.ts b/src/serifs.ts index 15ae16e..1fe9ca0 100644 --- a/src/serifs.ts +++ b/src/serifs.ts @@ -341,6 +341,8 @@ export default { doneFromInvalidUser: 'イタズラはめっですよ!', + invalidVisibility: "公開範囲の指定を変えてみて", + reminds: 'やること一覧です!', notify: (name) => name ? `${name}、これやりましたか?` : `これやりましたか?`,