mirror of
https://github.com/syuilo/ai.git
synced 2025-03-25 21:12:56 +00:00
Merge branch 'na2na' into featReminderFix
This commit is contained in:
commit
1c6e8429b9
17 changed files with 519 additions and 65 deletions
9
.vscode/settings.json
vendored
9
.vscode/settings.json
vendored
|
@ -1,3 +1,8 @@
|
|||
{
|
||||
"typescript.tsdk": "node_modules\\typescript\\lib"
|
||||
}
|
||||
"typescript.tsdk": "node_modules\\typescript\\lib",
|
||||
"C_Cpp.errorSquiggles": "Disabled",
|
||||
"cSpell.words": [
|
||||
"lokijs",
|
||||
"todos"
|
||||
]
|
||||
}
|
||||
|
|
68
README.md
68
README.md
|
@ -1,61 +1,19 @@
|
|||
<h1><p align="center"><img src="./ai.svg" alt="藍" height="200"></p></h1>
|
||||
<p align="center">An Ai for Misskey. <a href="./torisetu.md">About Ai</a></p>
|
||||
# フォーク元と違うところ
|
||||
|
||||
## これなに
|
||||
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
|
||||
<img src="./WorksOnMyMachine.png" alt="Works on my machine" height="120">
|
||||
ランダムに決定した色の 1px \* 1px の画像をアップロードしてます。
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -12,6 +12,7 @@ type Config = {
|
|||
mecab?: string;
|
||||
mecabDic?: string;
|
||||
memoryDir?: string;
|
||||
earthQuakeMonitorPort?: number;
|
||||
};
|
||||
|
||||
const config = require('../config.json');
|
||||
|
|
14
src/index.ts
14
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'));
|
||||
|
|
52
src/modules/color/index.ts
Normal file
52
src/modules/color/index.ts
Normal file
|
@ -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<any> {
|
||||
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;
|
||||
}
|
||||
}
|
17
src/modules/color/render.ts
Normal file
17
src/modules/color/render.ts
Normal file
|
@ -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();
|
||||
}
|
32
src/modules/dic/index.ts
Normal file
32
src/modules/dic/index.ts
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
165
src/modules/earthquake/index.ts
Normal file
165
src/modules/earthquake/index.ts
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
38
src/modules/earthquake/typeMemo.c
Normal file
38
src/modules/earthquake/typeMemo.c
Normal file
|
@ -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,,,,]},
|
||||
,,]
|
||||
}
|
4
src/modules/earthquake/テスト用生データ.txt
Normal file
4
src/modules/earthquake/テスト用生データ.txt
Normal file
|
@ -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}
|
|
@ -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)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
61
src/modules/menu/index.ts
Normal file
61
src/modules/menu/index.ts
Normal file
|
@ -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()
|
||||
})
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
58
src/modules/summonCat/index.ts
Normal file
58
src/modules/summonCat/index.ts
Normal file
|
@ -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<any> {
|
||||
// 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;
|
||||
}
|
||||
}
|
|
@ -341,6 +341,8 @@ export default {
|
|||
|
||||
doneFromInvalidUser: 'イタズラはめっですよ!',
|
||||
|
||||
invalidVisibility: "公開範囲の指定を変えてみて",
|
||||
|
||||
reminds: 'やること一覧です!',
|
||||
|
||||
notify: (name) => name ? `${name}、これやりましたか?` : `これやりましたか?`,
|
||||
|
|
Loading…
Reference in a new issue