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
-
+ランダムに決定した色の 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}、これやりましたか?` : `これやりましたか?`,