Nullcatchan!

This commit is contained in:
nullnyat 2022-09-18 00:16:06 +09:00
parent eb853be7e8
commit 8bf9550139
54 changed files with 7901 additions and 2017 deletions

View File

@ -1,6 +1,6 @@
config.json config.json
font.ttf font.ttf
ai.* nullcat.*
*.md *.md
*.png *.png
Dockerfile Dockerfile

26
Dockerfile_development Normal file
View File

@ -0,0 +1,26 @@
FROM node:lts-bullseye
RUN apt-get update && apt-get install -y tini
ARG DEBIAN_FRONTEND=noninteractive
ARG enable_mecab=1
RUN if [ $enable_mecab -ne 0 ]; then apt-get update \
&& apt-get install mecab libmecab-dev mecab-ipadic-utf8 make curl xz-utils file sudo tzdata --no-install-recommends -y \
&& apt-get clean \
&& rm -rf /var/lib/apt-get/lists/* \
&& cd /opt \
&& git clone --depth 1 https://github.com/neologd/mecab-ipadic-neologd.git \
&& cd /opt/mecab-ipadic-neologd \
&& ./bin/install-mecab-ipadic-neologd -n -y \
&& rm -rf /opt/mecab-ipadic-neologd \
&& echo "dicdir = /usr/lib/x86_64-linux-gnu/mecab/dic/mecab-ipadic-neologd/" > /etc/mecabrc \
&& apt-get purge git make curl xz-utils file -y; fi
COPY ../NullcatChan-old /nullcat-chan
WORKDIR /nullcat-chan
RUN npm install && npm run build
ENTRYPOINT ["/usr/bin/tini", "--"]
CMD npm run dev

26
Dockerfile_production Normal file
View File

@ -0,0 +1,26 @@
FROM node:lts-bullseye
RUN apt-get update && apt-get install -y tini
ARG DEBIAN_FRONTEND=noninteractive
ARG enable_mecab=1
RUN if [ $enable_mecab -ne 0 ]; then apt-get update \
&& apt-get install mecab libmecab-dev mecab-ipadic-utf8 make curl xz-utils file sudo tzdata --no-install-recommends -y \
&& apt-get clean \
&& rm -rf /var/lib/apt-get/lists/* \
&& cd /opt \
&& git clone --depth 1 https://github.com/neologd/mecab-ipadic-neologd.git \
&& cd /opt/mecab-ipadic-neologd \
&& ./bin/install-mecab-ipadic-neologd -n -y \
&& rm -rf /opt/mecab-ipadic-neologd \
&& echo "dicdir = /usr/lib/x86_64-linux-gnu/mecab/dic/mecab-ipadic-neologd/" > /etc/mecabrc \
&& apt-get purge git make curl xz-utils file -y; fi
COPY ../NullcatChan-old /nullcat-chan
WORKDIR /nullcat-chan
RUN npm install && npm run build
ENTRYPOINT ["/usr/bin/tini", "--"]
CMD npm start

View File

@ -1,61 +1,67 @@
<h1><p align="center"><img src="./ai.svg" alt="藍" height="200"></p></h1> <p align="center">
<p align="center">An Ai for Misskey. <a href="./torisetu.md">About Ai</a></p> <img src="./nullcatchan.png" alt="nullcatchan!" height="200">
</p>
## これなに
Misskey用の日本語Botです。
## インストール # これってなに?
> Node.js と npm と MeCab (オプション) がインストールされている必要があります。
Misskey用の[Aiベース](https://github.com/syuilo/ai)の日本語Botです。
## 変更点
- 自動投稿や自動返信、pingに対する返答の内容
- ゴママヨに反応([ここ](https://github.com/ThinaticSystem/gomamayo.js)から持ってきた)
- ゲーム機能と絵文字を自動生成するやつの削除
- GitHubのStatusを教えてくれる機能
- CloudflareのStatusを教えてくれる機能
- やることを決めてくれる
- 気圧の状況を教えてくれる
- 時報機能
- シェル芸機能([ここ](https://github.com/sim1222/shellgei-misskey)から持ってきた)
- 怪レい曰本语に変換してくれる機能
## 導入方法
> Node.js と npm と MeCab がインストールされている必要があります。
まず適当なディレクトリに `git clone` します。 まず適当なディレクトリに `git clone` します。
次にそのディレクトリに `config.json` を作成します。中身は次のようにします: 次にそのディレクトリに `config.json` を作成します。中身は次のようにします:
``` json ``` json
{ {
"host": "https:// + あなたのインスタンスのURL (末尾の / は除く)", "host": "https:// + あなたのインスタンスのURL (末尾の / は除く)",
"i": "藍として動かしたいアカウントのアクセストークン", "i": "ぬるきゃっとちゃん!として動かしたいアカウントのアクセストークン",
"master": "管理者のユーザー名(オプション)", "master": "管理者のユーザー名(オプション)",
"notingEnabled": "ランダムにノートを投稿する機能を無効にする場合は false を入れる", "notingEnabled": "ランダムにートを投稿する機能。true(on) or false(off)",
"keywordEnabled": "キーワードを覚える機能 (MeCab が必要) を有効にする場合は true を入れる (無効にする場合は false)", "keywordEnabled": "キーワードを覚える機能 (MeCab が必要) true or false",
"chartEnabled": "チャート機能を無効化する場合は false を入れてください", "serverMonitoring": "サーバー監視の機能重かったりすると教えてくれるよ。true or false",
"reversiEnabled": "藍とリバーシで対局できる機能を有効にする場合は true を入れる (無効にする場合は false)", "mecab": "MeCab のインストールパス (ソースからインストールした場合、大体は /usr/local/bin/mecab) true or false",
"serverMonitoring": "サーバー監視の機能を有効にする場合は true を入れる (無効にする場合は false)", "mecabDic": "MeCab の辞書ファイルパス",
"mecab": "MeCab のインストールパス (ソースからインストールした場合、大体は /usr/local/bin/mecab)", "memoryDir": "memory.jsonの保存先オプション、デフォルトは'.'(レポジトリのルートです))",
"mecabDic": "MeCab の辞書ファイルパス (オプション)", "shellgeiUrl": "シェル芸BotのAPIのURLですデフォルトではhttps://websh.jiro4989.com/api/shellgei"
"memoryDir": "memory.jsonの保存先オプション、デフォルトは'.'(レポジトリのルートです))"
} }
``` ```
`npm install` して `npm run build` して `npm start` すれば起動できます `npm install` して `npm run build` して `npm start` すれば起動できます
## Dockerで動かす ### Dockerで動かす
まず適当なディレクトリに `git clone` します。
まず適当なディレクトリに `git clone` します。<br>
次にそのディレクトリに `config.json` を作成します。中身は次のようにします: 次にそのディレクトリに `config.json` を作成します。中身は次のようにします:
MeCabの設定、memoryDirについては触らないでください MeCabの設定、memoryDirについては触らないでください
``` json ``` json
{ {
"host": "https:// + あなたのインスタンスのURL (末尾の / は除く)", "host": "https:// + あなたのインスタンスのURL (末尾の / は除く)",
"i": "として動かしたいアカウントのアクセストークン", "i": "ぬるきゃっとちゃん!として動かしたいアカウントのアクセストークン",
"master": "管理者のユーザー名(オプション)", "master": "管理者のユーザー名(オプション)",
"notingEnabled": "ランダムにノートを投稿する機能を無効にする場合は false を入れる", "notingEnabled": "ランダムにートを投稿する機能。true(on) or false(off)",
"keywordEnabled": "キーワードを覚える機能 (MeCab が必要) を有効にする場合は true を入れる (無効にする場合は false)", "keywordEnabled": "キーワードを覚える機能 (MeCab が必要) true or false",
"chartEnabled": "チャート機能を無効化する場合は false を入れてください",
"reversiEnabled": "藍とリバーシで対局できる機能を有効にする場合は true を入れる (無効にする場合は false)",
"serverMonitoring": "サーバー監視の機能を有効にする場合は true を入れる (無効にする場合は false)",
"mecab": "/usr/bin/mecab", "mecab": "/usr/bin/mecab",
"mecabDic": "/usr/lib/x86_64-linux-gnu/mecab/dic/mecab-ipadic-neologd/", "mecabDic": "/usr/lib/x86_64-linux-gnu/mecab/dic/mecab-ipadic-neologd/",
"memoryDir": "data" "memoryDir": "data",
"shellgeiUrl": "シェル芸BotのAPIのURLですデフォルトではhttps://websh.jiro4989.com/api/shellgei"
} }
``` ```
`docker-compose build` して `docker-compose up` すれば起動できます。 `npm install` して `npm run docker` すれば起動できます。<br>
`docker-compose.yml``enable_mecab``0` にすると、MeCabをインストールしないようにもできます。メモリが少ない環境など `docker-compose.yml``enable_mecab``0` にすると、MeCabをインストールしないようにもできます。メモリが少ない環境など
## フォント #### 一部の機能にはフォントが必要です。NullcatChan!にはフォントは同梱されていないので、ご自身でフォントをインストールしてそのフォントを`font.ttf`という名前でインストールディレクトリに設置してください。
一部の機能にはフォントが必要です。藍にはフォントは同梱されていないので、ご自身でフォントをインストールディレクトリに`font.ttf`という名前で設置してください。 #### NullcatChan!は記憶の保持にインメモリデータベースを使用しており、僕のインストールディレクトリに `memory.json` という名前で永続化されます。
## 記憶
藍は記憶の保持にインメモリデータベースを使用しており、藍のインストールディレクトリに `memory.json` という名前で永続化されます。
## ライセンス
MIT
## Awards
<img src="./WorksOnMyMachine.png" alt="Works on my machine" height="120">

View File

@ -2,11 +2,14 @@ version: '3'
services: services:
app: app:
build: build:
context: . dockerfile: Dockerfile_production
context: ../NullcatChan-old
args: args:
- enable_mecab=1 - enable_mecab=1
volumes: volumes:
- './config.json:/ai/config.json:ro' - './config.json:/nullcat-chan/config.json:ro'
- './font.ttf:/ai/font.ttf:ro' - './font.ttf:/nullcat-chan/font.ttf:ro'
- './data:/ai/data' - './data:/nullcat-chan/data'
restart: always restart: always
environment:
TZ: Asia/Tokyo

View File

@ -0,0 +1,5 @@
version: '3'
services:
app:
build:
dockerfile: Dockerfile_development

184
ngwords.txt Normal file
View File

@ -0,0 +1,184 @@
オーガズム
オルガスムス
メスイキ
ポルノ
-ポルノグラフィティ
にょろり
オナホ
アクメ
淫夢
チンカス
ふたなり
マラ
-ガテマラ
-グァテマラ
TENGA
種付
たねつ
たねづ
アスペ
-アスペクト
クリトリス
dick
セックス
セクロス
-ぱちんこ
-がちんこ
-喉ちんこ
-のどちんこ
ちんこ
ちんぽ
chinko
chinpo
tinko
tinpo
-ビックリマンコラボ
-ウルトラマンコスモス
まんこ
manko
ペニス
penis
vagina
ヴァギナ
バギナ
肉棒
勃起
ぼっき
精子
精液
射精
ザーメン
ザー汁
放射性
金玉
キンタマ
semen
体位
淫乱
アナル
anus
おっぱい
巨乳
貧乳
爆乳
虚乳
普乳
適乳
美乳
豊乳
超乳
魔乳
きょにゅう
きょにゅー
ひんにゅう
ひんにゅー
ばくにゅう
ばくにゅー
ふにゅう
ふにゅー
てきにゅう
てきにゅー
びにゅう
びにゅー
ほうにゅう
ほうにゅー
ちょうにゅう
ちょうにゅー
まにゅう
まにゅー
何カップ
乳首
ちくび
ビーチク
自慰
オナニ
オナ二
オナヌ
マスターベーション
マスタベーション
シコい
シコっ
脱げ
ぬげ
脱いで
ぬいで
脱ごう
ぬごう
喘いで
あえいで
クンニ
-フェラーリ
-カフェラテ
-フェライト
-フェラガモ
-フェラーラ
-フェライニ
-フェラーズ
-フェラリア
フェラ
デリヘル
-姦し
犯す
ヤリマン
ヤリチン
パイパン
中出し
中で出
スカトロ
ケツ
コキ
手マン
潮吹
下乳
横乳
指マン
パイズリ
ペェズリ
-スレイプニル
レイプ
オフパコ
パコる
ドピュ
ブリュ
-ちんちん電車
ちんちん
ぽこちん
マン汁
下の口
コンドーム
ハメ撮り
ちん毛
まん毛
陰毛
インポ
童貞もらって
童貞貰
童貞をもらって
童貞を貰
ケツの穴
糞を出
糞が出
ヨツンヴァイン
しゃぶれ
邪淫
処女
早漏
オナホ
アクメ
淫夢
チンカス
ふたなり
マラ
-ガテマラ
-グァテマラ
TENGA
種付
たねつ
たねづ
アスペ
-アスペクト
クリトリス
dick
おしっこ

View File

@ -1,14 +1,20 @@
{ {
"_v": "1.5.0", "version": "2.1.0",
"main": "./built/index.js", "main": "./built/index.js",
"scripts": { "scripts": {
"start": "node ./built", "docker:dev": "cross-env DOCKER_ENV=development docker-compose -f docker-compose.yml -f docker-compose_development.yml up -d --build && docker-compose logs -f",
"docker": "cross-env DOCKER_ENV=production docker-compose up -d --build && docker-compose logs -f",
"dev": "cross-env NODE_ENV=development node ./built",
"start": "cross-env NODE_ENV=production node ./built",
"build": "tsc", "build": "tsc",
"test": "jest" "test": "jest"
}, },
"dependencies": { "dependencies": {
"@types/accurate-interval": "1.0.0",
"@types/chalk": "2.2.0", "@types/chalk": "2.2.0",
"@types/humanize-duration": "3.27.1",
"@types/lokijs": "1.5.4", "@types/lokijs": "1.5.4",
"@types/moji": "0.5.0",
"@types/node": "16.0.1", "@types/node": "16.0.1",
"@types/promise-retry": "1.1.3", "@types/promise-retry": "1.1.3",
"@types/random-seed": "0.3.3", "@types/random-seed": "0.3.3",
@ -17,13 +23,19 @@
"@types/twemoji-parser": "13.1.1", "@types/twemoji-parser": "13.1.1",
"@types/uuid": "8.3.1", "@types/uuid": "8.3.1",
"@types/ws": "7.4.6", "@types/ws": "7.4.6",
"accurate-interval": "1.0.9",
"autobind-decorator": "2.4.0", "autobind-decorator": "2.4.0",
"canvas": "2.8.0", "canvas": "2.8.0",
"chalk": "4.1.1", "chalk": "4.1.1",
"cjp": "1.2.3",
"gomamayo-js": "0.2.1",
"humanize-duration": "3.27.1",
"lokijs": "1.5.12", "lokijs": "1.5.12",
"memory-streams": "0.1.3", "memory-streams": "0.1.3",
"misskey-reversi": "0.0.5", "misskey-reversi": "0.0.5",
"module-alias": "2.2.2", "module-alias": "2.2.2",
"moji": "0.5.1",
"node-fetch": "2.6.7",
"promise-retry": "2.0.1", "promise-retry": "2.0.1",
"random-seed": "0.3.0", "random-seed": "0.3.0",
"reconnecting-websocket": "4.4.0", "reconnecting-websocket": "4.4.0",
@ -33,9 +45,10 @@
"timeout-as-promise": "1.0.0", "timeout-as-promise": "1.0.0",
"ts-node": "10.0.0", "ts-node": "10.0.0",
"twemoji-parser": "13.1.0", "twemoji-parser": "13.1.0",
"typescript": "4.3.5", "typescript": "4.5.5",
"uuid": "8.3.2", "uuid": "8.3.2",
"ws": "7.5.2" "ws": "7.5.2",
"zod": "3.11.6"
}, },
"devDependencies": { "devDependencies": {
"@koa/router": "9.4.0", "@koa/router": "9.4.0",
@ -43,9 +56,11 @@
"@types/koa": "2.13.1", "@types/koa": "2.13.1",
"@types/koa__router": "8.0.4", "@types/koa__router": "8.0.4",
"@types/websocket": "1.0.2", "@types/websocket": "1.0.2",
"cross-env": "7.0.3",
"jest": "26.6.3", "jest": "26.6.3",
"koa": "2.13.1", "koa": "2.13.1",
"koa-json-body": "5.3.0", "koa-json-body": "5.3.0",
"prettier": "2.5.1",
"ts-jest": "26.5.6", "ts-jest": "26.5.6",
"websocket": "1.0.34" "websocket": "1.0.34"
}, },

View File

@ -5,13 +5,12 @@ type Config = {
wsUrl: string; wsUrl: string;
apiUrl: string; apiUrl: string;
keywordEnabled: boolean; keywordEnabled: boolean;
reversiEnabled: boolean;
notingEnabled: boolean; notingEnabled: boolean;
chartEnabled: boolean;
serverMonitoring: boolean; serverMonitoring: boolean;
mecab?: string; mecab?: string;
mecabDic?: string; mecabDic?: string;
memoryDir?: string; memoryDir?: string;
shellgeiUrl: string;
}; };
const config = require('../config.json'); const config = require('../config.json');

View File

@ -1,71 +1,71 @@
import autobind from 'autobind-decorator'; import { User } from "./misskey/user"
import from '@/ai'; import IModule from "../../NullcatChan-old/src/module"
import IModule from '@/module'; import NullcatChan from "../../NullcatChan-old/src/nullcat-chan"
import getDate from '@/utils/get-date'; import getDate from "../../NullcatChan-old/src/utils/get-date"
import { User } from '@/misskey/user'; import { genItem } from "../../NullcatChan-old/src/vocabulary"
import { genItem } from '@/vocabulary'; import autobind from "autobind-decorator"
export type FriendDoc = { export type FriendDoc = {
userId: string; userId: string
user: User; user: User
name?: string | null; name?: string | null
love?: number; love?: number
lastLoveIncrementedAt?: string; lastLoveIncrementedAt?: string
todayLoveIncrements?: number; todayLoveIncrements?: number
perModulesData?: any; perModulesData?: any
married?: boolean; married?: boolean
transferCode?: string; transferCode?: string
}; }
export default class Friend { export default class Friend {
private ai: ; private nullcatChan: NullcatChan
public get userId() { public get userId() {
return this.doc.userId; return this.doc.userId
} }
public get name() { public get name() {
return this.doc.name; return this.doc.name
} }
public get love() { public get love() {
return this.doc.love || 0; return this.doc.love || 0
} }
public get married() { public get married() {
return this.doc.married; return this.doc.married
} }
public doc: FriendDoc; public doc: FriendDoc
constructor(ai: , opts: { user?: User, doc?: FriendDoc }) { constructor(nullcatChan: NullcatChan, opts: { user?: User; doc?: FriendDoc }) {
this.ai = ai; this.nullcatChan = nullcatChan
if (opts.user) { if (opts.user) {
const exist = this.ai.friends.findOne({ const exist = this.nullcatChan.friends.findOne({
userId: opts.user.id userId: opts.user.id,
}); })
if (exist == null) { if (exist == null) {
const inserted = this.ai.friends.insertOne({ const inserted = this.nullcatChan.friends.insertOne({
userId: opts.user.id, userId: opts.user.id,
user: opts.user user: opts.user,
}); })
if (inserted == null) { if (inserted == null) {
throw new Error('Failed to insert friend doc'); throw new Error("Failed to insert friend doc")
} }
this.doc = inserted; this.doc = inserted
} else { } else {
this.doc = exist; this.doc = exist
this.doc.user = { ...this.doc.user, ...opts.user }; this.doc.user = { ...this.doc.user, ...opts.user }
this.save(); this.save()
} }
} else if (opts.doc) { } else if (opts.doc) {
this.doc = opts.doc; this.doc = opts.doc
} else { } else {
throw new Error('No friend info specified'); throw new Error("No friend info specified")
} }
} }
@ -74,117 +74,117 @@ export default class Friend {
this.doc.user = { this.doc.user = {
...this.doc.user, ...this.doc.user,
...user, ...user,
}; }
this.save(); this.save()
} }
@autobind @autobind
public getPerModulesData(module: IModule) { public getPerModulesData(module: IModule) {
if (this.doc.perModulesData == null) { if (this.doc.perModulesData == null) {
this.doc.perModulesData = {}; this.doc.perModulesData = {}
this.doc.perModulesData[module.name] = {}; this.doc.perModulesData[module.name] = {}
this.save(); this.save()
} else if (this.doc.perModulesData[module.name] == null) { } else if (this.doc.perModulesData[module.name] == null) {
this.doc.perModulesData[module.name] = {}; this.doc.perModulesData[module.name] = {}
this.save(); this.save()
} }
return this.doc.perModulesData[module.name]; return this.doc.perModulesData[module.name]
} }
@autobind @autobind
public setPerModulesData(module: IModule, data: any) { public setPerModulesData(module: IModule, data: any) {
if (this.doc.perModulesData == null) { if (this.doc.perModulesData == null) {
this.doc.perModulesData = {}; this.doc.perModulesData = {}
} }
this.doc.perModulesData[module.name] = data; this.doc.perModulesData[module.name] = data
this.save(); this.save()
} }
@autobind @autobind
public incLove(amount = 1) { public incLove(amount = 1) {
const today = getDate(); const today = getDate()
if (this.doc.lastLoveIncrementedAt != today) { if (this.doc.lastLoveIncrementedAt != today) {
this.doc.todayLoveIncrements = 0; this.doc.todayLoveIncrements = 0
} }
// 1日に上げられる親愛度は最大3 // 1日に上げられる親愛度は最大3
if (this.doc.lastLoveIncrementedAt == today && (this.doc.todayLoveIncrements || 0) >= 3) return; if (this.doc.lastLoveIncrementedAt == today && (this.doc.todayLoveIncrements || 0) >= 3) return
if (this.doc.love == null) this.doc.love = 0; if (this.doc.love == null) this.doc.love = 0
this.doc.love += amount; this.doc.love += amount
// 最大 100 // 最大 100
if (this.doc.love > 100) this.doc.love = 100; if (this.doc.love > 100) this.doc.love = 100
this.doc.lastLoveIncrementedAt = today; this.doc.lastLoveIncrementedAt = today
this.doc.todayLoveIncrements = (this.doc.todayLoveIncrements || 0) + amount; this.doc.todayLoveIncrements = (this.doc.todayLoveIncrements || 0) + amount
this.save(); this.save()
this.ai.log(`💗 ${this.userId} +${amount}`); this.nullcatChan.log(`💗 ${this.userId} +${amount}`)
} }
@autobind @autobind
public decLove(amount = 1) { public decLove(amount = 1) {
// 親愛度MAXなら下げない // 親愛度MAXなら下げない
if (this.doc.love === 100) return; if (this.doc.love === 100) return
if (this.doc.love == null) this.doc.love = 0; if (this.doc.love == null) this.doc.love = 0
this.doc.love -= amount; this.doc.love -= amount
// 最低 -30 // 最低 -30
if (this.doc.love < -30) this.doc.love = -30; if (this.doc.love < -30) this.doc.love = -30
// 親愛度マイナスなら名前を忘れる // 親愛度マイナスなら名前を忘れる
if (this.doc.love < 0) { if (this.doc.love < 0) {
this.doc.name = null; this.doc.name = null
} }
this.save(); this.save()
this.ai.log(`💢 ${this.userId} -${amount}`); this.nullcatChan.log(`💢 ${this.userId} -${amount}`)
} }
@autobind @autobind
public updateName(name: string) { public updateName(name: string) {
this.doc.name = name; this.doc.name = name
this.save(); this.save()
} }
@autobind @autobind
public save() { public save() {
this.ai.friends.update(this.doc); this.nullcatChan.friends.update(this.doc)
} }
@autobind @autobind
public generateTransferCode(): string { public generateTransferCode(): string {
const code = genItem(); const code = genItem()
this.doc.transferCode = code; this.doc.transferCode = code
this.save(); this.save()
return code; return code
} }
@autobind @autobind
public transferMemory(code: string): boolean { public transferMemory(code: string): boolean {
const src = this.ai.friends.findOne({ const src = this.nullcatChan.friends.findOne({
transferCode: code transferCode: code,
}); })
if (src == null) return false; if (src == null) return false
this.doc.name = src.name; this.doc.name = src.name
this.doc.love = src.love; this.doc.love = src.love
this.doc.married = src.married; this.doc.married = src.married
this.doc.perModulesData = src.perModulesData; this.doc.perModulesData = src.perModulesData
this.save(); this.save()
// TODO: 合言葉を忘れる // TODO: 合言葉を忘れる
return true; return true
} }
} }

View File

@ -1,94 +1,109 @@
// AiOS bootstrapper // Nullcat chan! bootstrapper
import 'module-alias/register'; import * as chalk from "chalk"
import "module-alias/register"
import * as request from "request-promise-native"
import config from "./config"
import BirthdayModule from "../../NullcatChan-old/src/modules/birthday"
import CoreModule from "../../NullcatChan-old/src/modules/core"
import EmojiReactModule from "../../NullcatChan-old/src/modules/emoji-react"
import FeelingModule from "../../NullcatChan-old/src/modules/feeling"
import FollowModule from "../../NullcatChan-old/src/modules/follow"
import FortuneModule from "../../NullcatChan-old/src/modules/fortune"
import GitHubStatusModule from "../../NullcatChan-old/src/modules/github-status"
import CloudflareStatus from "../../NullcatChan-old/src/modules/cloudflare-status";
import GomamayoModule from "../../NullcatChan-old/src/modules/gomamayo"
import JihouModule from "../../NullcatChan-old/src/modules/jihou"
import KeywordModule from "../../NullcatChan-old/src/modules/keyword"
import KiatsuModule from "../../NullcatChan-old/src/modules/kiatsu"
import NotingModule from "../../NullcatChan-old/src/modules/noting"
import PingModule from "../../NullcatChan-old/src/modules/ping"
import ReminderModule from "../../NullcatChan-old/src/modules/reminder"
import RoguboModule from "../../NullcatChan-old/src/modules/rogubo"
import ServerModule from "../../NullcatChan-old/src/modules/server"
import SleepReportModule from "../../NullcatChan-old/src/modules/sleep-report"
import TalkModule from "../../NullcatChan-old/src/modules/talk"
import TimerModule from "../../NullcatChan-old/src/modules/timer"
import TraceMoeModule from "../../NullcatChan-old/src/modules/trace-moe"
import ValentineModule from "../../NullcatChan-old/src/modules/valentine"
import WhatModule from "../../NullcatChan-old/src/modules/what"
import YarukotoModule from "../../NullcatChan-old/src/modules/yarukoto"
import NullcatChan from "../../NullcatChan-old/src/nullcat-chan"
import _log from "../../NullcatChan-old/src/utils/log"
import ShellGeiModule from "../../NullcatChan-old/src/modules/shellgei"
import SversionModule from "../../NullcatChan-old/src/modules/Sversion"
import AyashiiModule from "../../NullcatChan-old/src/modules/ayashii"
import * as chalk from 'chalk'; const promiseRetry = require("promise-retry")
import * as request from 'request-promise-native';
const promiseRetry = require('promise-retry');
import from './ai'; const pkg = require("../../NullcatChan-old/package.json")
import config from './config';
import _log from './utils/log';
const pkg = require('../package.json');
import CoreModule from './modules/core'; console.log(" _ __ ____ __ ________ __ ")
import TalkModule from './modules/talk'; console.log(" / | / /_ __/ / /________ _/ /_/ ____/ /_ ____ _____ / / ")
import BirthdayModule from './modules/birthday'; console.log(" / |/ / / / / / / ___/ __ `/ __/ / / __ \\/ __ `/ __ \\/ / ")
import ReversiModule from './modules/reversi'; console.log(" / /| / /_/ / / / /__/ /_/ / /_/ /___/ / / / /_/ / / / /_/ ")
import PingModule from './modules/ping'; console.log("/_/ |_/\\__,_/_/_/\\___/\\__,_/\\__/\\____/_/ /_/\\__,_/_/ /_(_)\n")
import EmojiModule from './modules/emoji';
import EmojiReactModule from './modules/emoji-react';
import FortuneModule from './modules/fortune';
import GuessingGameModule from './modules/guessing-game';
import KazutoriModule from './modules/kazutori';
import KeywordModule from './modules/keyword';
import WelcomeModule from './modules/welcome';
import TimerModule from './modules/timer';
import DiceModule from './modules/dice';
import ServerModule from './modules/server';
import FollowModule from './modules/follow';
import ValentineModule from './modules/valentine';
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 ReminderModule from './modules/reminder';
console.log(' __ ____ _____ ___ ');
console.log(' /__\\ (_ _)( _ )/ __)');
console.log(' /(__)\\ _)(_ )(_)( \\__ \\');
console.log('(__)(__)(____)(_____)(___/\n');
function log(msg: string): void { function log(msg: string): void {
_log(`[Boot]: ${msg}`); _log(`[Boot]: ${msg}`)
} }
log(chalk.bold(`Ai v${pkg._v}`)); log(chalk.bold(`Nullcat chan! v${pkg._v}`))
promiseRetry(retry => { promiseRetry(
log(`Account fetching... ${chalk.gray(config.host)}`); (retry) => {
log(`Account fetching... ${chalk.gray(config.host)}`)
// アカウントをフェッチ // アカウントをフェッチ
return request.post(`${config.apiUrl}/i`, { return request
.post(`${config.apiUrl}/i`, {
json: { json: {
i: config.i i: config.i,
},
})
.catch(retry)
},
{
retries: 3,
} }
}).catch(retry); )
}, { .then((account) => {
retries: 3 const acct = `@${account.username}`
}).then(account => { log(chalk.green(`Account fetched successfully: ${chalk.underline(acct)}`))
const acct = `@${account.username}`;
log(chalk.green(`Account fetched successfully: ${chalk.underline(acct)}`));
log('Starting AiOS...'); log("Starting Nullcat chan...")
// 藍起動 // ぬるきゃっとちゃん起動
new (account, [ new NullcatChan(account, [
new CoreModule(), new CoreModule(),
new EmojiModule(),
new EmojiReactModule(), new EmojiReactModule(),
new FortuneModule(), new FortuneModule(),
new GuessingGameModule(),
new KazutoriModule(),
new ReversiModule(),
new TimerModule(), new TimerModule(),
new DiceModule(),
new TalkModule(), new TalkModule(),
new PingModule(),
new WelcomeModule(),
new ServerModule(),
new FollowModule(), new FollowModule(),
new BirthdayModule(), new BirthdayModule(),
new ValentineModule(), new ValentineModule(),
new KeywordModule(), new KeywordModule(),
new MazeModule(),
new ChartModule(),
new SleepReportModule(), new SleepReportModule(),
new NotingModule(), new NotingModule(),
new PollModule(),
new ReminderModule(), new ReminderModule(),
]); new GomamayoModule(),
}).catch(e => { new GitHubStatusModule(),
log(chalk.red('Failed to fetch the account')); new CloudflareStatus(),
}); new YarukotoModule(),
new RoguboModule(),
new KiatsuModule(),
new JihouModule(),
new WhatModule(),
new FeelingModule(),
new TraceMoeModule(),
new ServerModule(),
new ShellGeiModule(),
new SversionModule(),
new AyashiiModule(),
new PingModule(),
])
})
.catch((e) => {
log(chalk.red("Failed to fetch the account"))
})

View File

@ -1,113 +1,142 @@
import autobind from 'autobind-decorator'; import config from "@/config"
import * as chalk from 'chalk'; import Friend from "@/friend"
const delay = require('timeout-as-promise'); import { User } from "./misskey/user"
import NullcatChan from "../../NullcatChan-old/src/nullcat-chan"
import includes from "../../NullcatChan-old/src/utils/includes"
import or from "../../NullcatChan-old/src/utils/or"
import autobind from "autobind-decorator"
import * as chalk from "chalk"
const delay = require("timeout-as-promise")
import from '@/ai'; interface MisskeyFile {
import Friend from '@/friend'; id: string
import { User } from '@/misskey/user'; createdAt: string
import includes from '@/utils/includes'; name: string
import or from '@/utils/or'; type: string
import config from '@/config'; md5: string
size: number
isSensitive: boolean
blurhash: string | null
properties: {
width?: number
height?: number
}
url: string
thumbnailUrl: string | null
comment?: unknown | null // FIXME
folderId: string | null
folder?: unknown | null // FIXME
userId: string | null
user: User | null
}
export default class Message { export default class Message {
private ai: ; private nullcatChan: NullcatChan
private messageOrNote: any; private messageOrNote: any
public isDm: boolean; public isDm: boolean
public get id(): string { public get id(): string {
return this.messageOrNote.id; return this.messageOrNote.id
} }
public get user(): User { public get user(): User {
return this.messageOrNote.user; return this.messageOrNote.user
} }
public get userId(): string { public get userId(): string {
return this.messageOrNote.userId; return this.messageOrNote.userId
} }
public get text(): string { public get text(): string {
return this.messageOrNote.text; return this.messageOrNote.text
}
public get renotedText(): string | null {
return this.messageOrNote.renote.text
} }
public get quoteId(): string | null { public get quoteId(): string | null {
return this.messageOrNote.renoteId; return this.messageOrNote.renoteId
} }
public get visibility(): string { public get files(): MisskeyFile[] | undefined {
return this.messageOrNote.visibility; return this.messageOrNote.files
} }
/** /**
* *
*/ */
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
.replace(new RegExp(`^@${this.ai.account.username}@${host}\\s`, 'i'), '') .replace(new RegExp(`^@${this.nullcatChan.account.username}@${host}\\s`, "i"), "")
.replace(new RegExp(`^@${this.ai.account.username}\\s`, 'i'), '') .replace(new RegExp(`^@${this.nullcatChan.account.username}\\s`, "i"), "")
.trim(); .trim()
} }
public get replyId(): string { public get replyId(): string {
return this.messageOrNote.replyId; return this.messageOrNote.replyId
} }
public friend: Friend; public friend: Friend
constructor(ai: , messageOrNote: any, isDm: boolean) { constructor(nullcatChan: NullcatChan, messageOrNote: any, isDm: boolean) {
this.ai = ai; this.nullcatChan = nullcatChan
this.messageOrNote = messageOrNote; this.messageOrNote = messageOrNote
this.isDm = isDm; this.isDm = isDm
this.friend = new Friend(ai, { user: this.user }); this.friend = new Friend(nullcatChan, { user: this.user })
// メッセージなどに付いているユーザー情報は省略されている場合があるので完全なユーザー情報を持ってくる // メッセージなどに付いているユーザー情報は省略されている場合があるので完全なユーザー情報を持ってくる
this.ai.api('users/show', { this.nullcatChan
userId: this.userId .api("users/show", {
}).then(user => { userId: this.userId,
this.friend.updateUser(user); })
}); .then((user) => {
this.friend.updateUser(user)
})
} }
@autobind @autobind
public async reply(text: string | null, opts?: { public async reply(
file?: any; text: string | null,
cw?: string; opts?: {
renote?: string; file?: any
immediate?: boolean; cw?: string
}) { renote?: string
if (text == null) return; immediate?: boolean
}
) {
if (text == null) return
this.ai.log(`>>> Sending reply to ${chalk.underline(this.id)}`); this.nullcatChan.log(`>>> Sending reply to ${chalk.underline(this.id)}`)
if (!opts?.immediate) { if (!opts?.immediate) {
await delay(2000); await delay(2000)
} }
if (this.isDm) { if (this.isDm) {
return await this.ai.sendMessage(this.messageOrNote.userId, { return await this.nullcatChan.sendMessage(this.messageOrNote.userId, {
text: text, text: text,
fileId: opts?.file?.id fileId: opts?.file?.id,
}); })
} else { } else {
return await this.ai.post({ return await this.nullcatChan.post({
replyId: this.messageOrNote.id, replyId: this.messageOrNote.id,
text: text, text: text,
fileIds: opts?.file ? [opts?.file.id] : undefined, fileIds: opts?.file ? [opts?.file.id] : undefined,
cw: opts?.cw, cw: opts?.cw,
renoteId: opts?.renote renoteId: opts?.renote,
}); })
} }
} }
@autobind @autobind
public includes(words: string[]): boolean { public includes(words: string[]): boolean {
return includes(this.text, words); return includes(this.text, words)
} }
@autobind @autobind
public or(words: (string | RegExp)[]): boolean { public or(words: (string | RegExp)[]): boolean {
return or(this.text, words); return or(this.text, words)
} }
} }

View File

@ -1,32 +1,32 @@
import autobind from 'autobind-decorator'; import NullcatChan, { InstallerResult } from "./nullcat-chan"
import , { InstallerResult } from '@/ai'; import autobind from "autobind-decorator"
export default abstract class Module { export default abstract class Module {
public abstract readonly name: string; public abstract readonly name: string
protected ai: ; protected nullcatChan: NullcatChan
private doc: any; private doc: any
public init(ai: ) { public init(nullcatChan: NullcatChan) {
this.ai = ai; this.nullcatChan = nullcatChan
this.doc = this.ai.moduleData.findOne({ this.doc = this.nullcatChan.moduleData.findOne({
module: this.name module: this.name,
}); })
if (this.doc == null) { if (this.doc == null) {
this.doc = this.ai.moduleData.insertOne({ this.doc = this.nullcatChan.moduleData.insertOne({
module: this.name, module: this.name,
data: {} data: {},
}); })
} }
} }
public abstract install(): InstallerResult; public abstract install(): InstallerResult
@autobind @autobind
protected log(msg: string) { protected log(msg: string) {
this.ai.log(`[${this.name}]: ${msg}`); this.nullcatChan.log(`[${this.name}]: ${msg}`)
} }
/** /**
@ -38,7 +38,7 @@ export default abstract class Module {
*/ */
@autobind @autobind
protected subscribeReply(key: string | null, isDm: boolean, id: string, data?: any) { protected subscribeReply(key: string | null, isDm: boolean, id: string, data?: any) {
this.ai.subscribeReply(this, key, isDm, id, data); this.nullcatChan.subscribeReply(this, key, isDm, id, data)
} }
/** /**
@ -47,7 +47,7 @@ export default abstract class Module {
*/ */
@autobind @autobind
protected unsubscribeReply(key: string | null) { protected unsubscribeReply(key: string | null) {
this.ai.unsubscribeReply(this, key); this.nullcatChan.unsubscribeReply(this, key)
} }
/** /**
@ -58,17 +58,17 @@ export default abstract class Module {
*/ */
@autobind @autobind
public setTimeoutWithPersistence(delay: number, data?: any) { public setTimeoutWithPersistence(delay: number, data?: any) {
this.ai.setTimeoutWithPersistence(this, delay, data); this.nullcatChan.setTimeoutWithPersistence(this, delay, data)
} }
@autobind @autobind
protected getData() { protected getData() {
return this.doc.data; return this.doc.data
} }
@autobind @autobind
protected setData(data: any) { protected setData(data: any) {
this.doc.data = data; this.doc.data = data
this.ai.moduleData.update(this.doc); this.nullcatChan.moduleData.update(this.doc)
} }
} }

View File

@ -0,0 +1,28 @@
import Message from "@/message"
import Module from "@/module"
import autobind from "autobind-decorator"
import { generate } from "cjp"
export default class extends Module {
public readonly name = "ayashii"
@autobind
public install() {
return {
mentionHook: this.mentionHook,
}
}
@autobind
private async mentionHook(message: Message) {
if (message.includes(["#怪しい日本語変換"])) {
const context = message.extractedText.replace("#怪しい日本語変換", "").trim()
const cjp = generate(context)
message.reply(cjp + " #怪レい曰本语")
return true
} else {
return false
}
}
}

View File

@ -1,21 +1,21 @@
import autobind from 'autobind-decorator'; import Friend from "@/friend"
import Module from '@/module'; import Module from "@/module"
import Friend from '@/friend'; import serifs from "@/serifs"
import serifs from '@/serifs'; import autobind from "autobind-decorator"
function zeroPadding(num: number, length: number): string { function zeroPadding(num: number, length: number): string {
return ('0000000000' + num).slice(-length); return ("0000000000" + num).slice(-length)
} }
export default class extends Module { export default class extends Module {
public readonly name = 'birthday'; public readonly name = "birthday"
@autobind @autobind
public install() { public install() {
this.crawleBirthday(); this.crawleBirthday()
setInterval(this.crawleBirthday, 1000 * 60 * 3); setInterval(this.crawleBirthday, 1000 * 60 * 3)
return {}; return {}
} }
/** /**
@ -23,34 +23,34 @@ export default class extends Module {
*/ */
@autobind @autobind
private crawleBirthday() { private crawleBirthday() {
const now = new Date(); const now = new Date()
const m = now.getMonth(); const m = now.getMonth()
const d = now.getDate(); const d = now.getDate()
// Misskeyの誕生日は 2018-06-16 のような形式 // Misskeyの誕生日は 2018-06-16 のような形式
const today = `${zeroPadding(m + 1, 2)}-${zeroPadding(d, 2)}`; const today = `${zeroPadding(m + 1, 2)}-${zeroPadding(d, 2)}`
const birthFriends = this.ai.friends.find({ const birthFriends = this.nullcatChan.friends.find({
'user.birthday': { '$regex': new RegExp('-' + today + '$') } "user.birthday": { $regex: new RegExp("-" + today + "$") },
} as any); } as any)
birthFriends.forEach(f => { birthFriends.forEach((f) => {
const friend = new Friend(this.ai, { doc: f }); const friend = new Friend(this.nullcatChan, { doc: f })
// 親愛度が3以上必要 // 親愛度が3以上必要
if (friend.love < 3) return; if (friend.love < 3) return
const data = friend.getPerModulesData(this); const data = friend.getPerModulesData(this)
if (data.lastBirthdayChecked == today) return; if (data.lastBirthdayChecked == today) return
data.lastBirthdayChecked = today; data.lastBirthdayChecked = today
friend.setPerModulesData(this, data); friend.setPerModulesData(this, data)
const text = serifs.birthday.happyBirthday(friend.name); const text = serifs.birthday.happyBirthday(friend.name)
this.ai.sendMessage(friend.userId, { this.nullcatChan.sendMessage(friend.userId, {
text: text text: text,
}); })
}); })
} }
} }

View File

@ -1,159 +1,153 @@
import autobind from 'autobind-decorator'; import Message from "@/message"
import Module from '@/module'; import Module from "@/module"
import Message from '@/message'; import serifs from "@/serifs"
import serifs from '@/serifs'; import { safeForInterpolate } from "../../../../NullcatChan-old/src/utils/safe-for-interpolate"
import { safeForInterpolate } from '@/utils/safe-for-interpolate'; import autobind from "autobind-decorator"
const titles = ['さん', 'くん', '君', 'ちゃん', '様', '先生']; const titles = ["さん", "くん", "君", "ちゃん", "様", "先生"]
export default class extends Module { export default class extends Module {
public readonly name = 'core'; public readonly name = "core"
@autobind @autobind
public install() { public install() {
return { return {
mentionHook: this.mentionHook, mentionHook: this.mentionHook,
contextHook: this.contextHook contextHook: this.contextHook,
}; }
} }
@autobind @autobind
private async mentionHook(msg: Message) { private async mentionHook(msg: Message) {
if (!msg.text) return false; if (!msg.text) return false
return ( return this.transferBegin(msg) || this.transferEnd(msg) || this.setName(msg) || this.modules(msg) || this.version(msg)
this.transferBegin(msg) ||
this.transferEnd(msg) ||
this.setName(msg) ||
this.modules(msg) ||
this.version(msg)
);
} }
@autobind @autobind
private transferBegin(msg: Message): boolean { private transferBegin(msg: Message): boolean {
if (!msg.text) return false; if (!msg.text) return false
if (!msg.includes(['引継', '引き継ぎ', '引越', '引っ越し'])) return false; if (!msg.includes(["引継", "引き継ぎ", "引越", "引っ越し"])) return false
// メッセージのみ // メッセージのみ
if (!msg.isDm) { if (!msg.isDm) {
msg.reply(serifs.core.transferNeedDm); msg.reply(serifs.core.transferNeedDm)
return true; return true
} }
const code = msg.friend.generateTransferCode(); const code = msg.friend.generateTransferCode()
msg.reply(serifs.core.transferCode(code)); msg.reply(serifs.core.transferCode(code))
return true; return true
} }
@autobind @autobind
private transferEnd(msg: Message): boolean { private transferEnd(msg: Message): boolean {
if (!msg.text) return false; if (!msg.text) return false
if (!msg.text.startsWith('「') || !msg.text.endsWith('」')) return false; if (!msg.text.startsWith("「") || !msg.text.endsWith("」")) return false
const code = msg.text.substring(1, msg.text.length - 1); const code = msg.text.substring(1, msg.text.length - 1)
const succ = msg.friend.transferMemory(code); const succ = msg.friend.transferMemory(code)
if (succ) { if (succ) {
msg.reply(serifs.core.transferDone(msg.friend.name)); msg.reply(serifs.core.transferDone(msg.friend.name))
} else { } else {
msg.reply(serifs.core.transferFailed); msg.reply(serifs.core.transferFailed)
} }
return true; return true
} }
@autobind @autobind
private setName(msg: Message): boolean { private setName(msg: Message): boolean {
if (!msg.text) return false; if (!msg.text) return false
if (!msg.text.includes('って呼んで')) return false; if (!msg.text.includes("って呼んで")) return false
if (msg.text.startsWith('って呼んで')) return false; if (msg.text.startsWith("って呼んで")) return false
// メッセージのみ // メッセージのみ
if (!msg.isDm) return true; if (!msg.isDm) return true
const name = msg.text.match(/^(.+?)って呼んで/)![1]; const name = msg.text.match(/^(.+?)って呼んで/)![1]
if (name.length > 10) { if (name.length > 10) {
msg.reply(serifs.core.tooLong); msg.reply(serifs.core.tooLong)
return true; return true
} }
if (!safeForInterpolate(name)) { if (!safeForInterpolate(name)) {
msg.reply(serifs.core.invalidName); msg.reply(serifs.core.invalidName)
return true; return true
} }
const withSan = titles.some(t => name.endsWith(t)); const withSan = titles.some((t) => name.endsWith(t))
if (withSan) { if (withSan) {
msg.friend.updateName(name); msg.friend.updateName(name)
msg.reply(serifs.core.setNameOk(name)); msg.reply(serifs.core.setNameOk(name))
} else { } else {
msg.reply(serifs.core.san).then(reply => { msg.reply(serifs.core.san).then((reply) => {
this.subscribeReply(msg.userId, msg.isDm, msg.isDm ? msg.userId : reply.id, { this.subscribeReply(msg.userId, msg.isDm, msg.isDm ? msg.userId : reply.id, {
name: name name: name,
}); })
}); })
} }
return true; return true
} }
@autobind @autobind
private modules(msg: Message): boolean { private modules(msg: Message): boolean {
if (!msg.text) return false; if (!msg.text) return false
if (!msg.or(['modules'])) return false; if (!msg.or(["modules"])) return false
let text = '```\n'; let text = "```\n"
for (const m of this.ai.modules) { for (const m of this.nullcatChan.modules) {
text += `${m.name}\n`; text += `${m.name}\n`
} }
text += '```'; text += "```"
msg.reply(text, { msg.reply(text, {
immediate: true immediate: true,
}); })
return true; return true
} }
@autobind @autobind
private version(msg: Message): boolean { private version(msg: Message): boolean {
if (!msg.text) return false; if (!msg.text) return false
if (!msg.or(['v', 'version', 'バージョン'])) return false; if (!msg.or(["v", "version", "バージョン"])) return false
msg.reply(`\`\`\`\nv${this.ai.version}\n\`\`\``, { msg.reply(`\`\`\`\nv${this.nullcatChan.version}\n\`\`\``, {
immediate: true immediate: true,
}); })
return true; return true
} }
@autobind @autobind
private async contextHook(key: any, 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(key); this.unsubscribeReply(key)
}; }
if (msg.text.includes('はい')) { if (msg.text.includes("うん")) {
msg.friend.updateName(data.name + 'さん'); msg.friend.updateName(data.name + "ちゃん")
done(); done()
} else if (msg.text.includes('いいえ')) { } else if (msg.text.includes("いいえ")) {
msg.friend.updateName(data.name); msg.friend.updateName(data.name)
done(); done()
} else { } else {
msg.reply(serifs.core.yesOrNo).then(reply => { msg.reply(serifs.core.yesOrNo).then((reply) => {
this.subscribeReply(msg.userId, msg.isDm, reply.id, data); this.subscribeReply(msg.userId, msg.isDm, reply.id, data)
}); })
} }
} }
} }

View File

@ -1,73 +1,69 @@
import autobind from 'autobind-decorator'; import { Note } from "../../misskey/note"
import { parse } from 'twemoji-parser'; import Module from "@/module"
const delay = require('timeout-as-promise'); import Stream from "@/stream"
import includes from "../../../../NullcatChan-old/src/utils/includes"
import autobind from "autobind-decorator"
const delay = require("timeout-as-promise")
import { Note } from '@/misskey/note'; const gomamayo = require("gomamayo-js")
import Module from '@/module';
import Stream from '@/stream';
import includes from '@/utils/includes';
export default class extends Module { export default class extends Module {
public readonly name = 'emoji-react'; public readonly name = "emoji-react"
private htl: ReturnType<Stream['useSharedConnection']>; private htl: ReturnType<Stream["useSharedConnection"]>
@autobind @autobind
public install() { public install() {
this.htl = this.ai.connection.useSharedConnection('homeTimeline'); this.htl = this.nullcatChan.connection.useSharedConnection("homeTimeline")
this.htl.on('note', this.onNote); this.htl.on("note", this.onNote)
return {}; return {}
} }
@autobind @autobind
private async onNote(note: Note) { private async onNote(note: Note) {
if (note.reply != null) return; if (note.reply != null) return
if (note.text == null) return; if (note.text == null) return
if (note.text.includes('@')) return; // (自分または他人問わず)メンションっぽかったらreject if (note.text.includes("@")) return // (自分または他人問わず)メンションっぽかったらreject
const react = async (reaction: string, immediate = false) => { const react = async (reaction: string, immediate = false) => {
if (!immediate) { if (!immediate) {
await delay(1500); await delay(1500)
} }
this.ai.api('notes/reactions/create', { this.nullcatChan.api("notes/reactions/create", {
noteId: note.id, noteId: note.id,
reaction: reaction reaction: reaction,
}); })
};
const customEmojis = note.text.match(/:([^\n:]+?):/g);
if (customEmojis) {
// カスタム絵文字が複数種類ある場合はキャンセル
if (!customEmojis.every((val, i, arr) => val === arr[0])) return;
this.log(`Custom emoji detected - ${customEmojis[0]}`);
return react(customEmojis[0]);
} }
const emojis = parse(note.text).map(x => x.text); if (await gomamayo.find(note.text)) return react(":bikkuribikkuri_:")
if (emojis.length > 0) { if (includes(note.text, ["ぬるきゃっとちゃん", "ぬるきゃぼっと", "ぬるきゃっとぼっと"])) return react(":bibibi_nullcatchan:")
// 絵文字が複数種類ある場合はキャンセル if (
if (!emojis.every((val, i, arr) => val === arr[0])) return; includes(note.text, [
"ねむい",
this.log(`Emoji detected - ${emojis[0]}`); "ねむたい",
"ねたい",
let reaction = emojis[0]; "ねれない",
"ねれん",
switch (reaction) { "ねれぬ",
case '✊': return react('🖐', true); "ふむ",
case '✌': return react('✊', true); "つら",
case '🖐': case '✋': return react('✌', true); "死に",
} "つかれた",
"疲れた",
return react(reaction); "しにたい",
} "きえたい",
"消えたい",
if (includes(note.text, ['ぴざ'])) return react('🍕'); "やだ",
if (includes(note.text, ['ぷりん'])) return react('🍮'); "いやだ",
if (includes(note.text, ['寿司', 'sushi']) || note.text === 'すし') return react('🍣'); "なきそう",
"泣きそう",
if (includes(note.text, ['藍'])) return react('🙌'); "辛い",
])
)
return react(":nadenade_neko:")
if (includes(note.text, ["理解した", "りかいした", "わかった", "頑張った", "がんばった"])) return react(":erai:")
if (note.text.match(/う[|ー]*んこ/) || note.text.match(/unko/)) return react(":anataima_unkotte_iimashitane:")
if (note.text.match(/う[|ー]*ん$/) || note.text.match(/un$/)) return react(":ti_:")
} }
} }

View File

@ -0,0 +1,31 @@
import Message from "@/message"
import Module from "@/module"
import autobind from "autobind-decorator"
import * as seedrandom from "seedrandom"
export const feelings = ["つらい", "ねむい", "るんるん", "虚無"]
export default class extends Module {
public readonly name = "feeling"
@autobind
public install() {
return {
mentionHook: this.mentionHook,
}
}
@autobind
private async mentionHook(msg: Message) {
if (msg.includes(["気分", "きぶん"])) {
const date = new Date()
const seed = `${date.getFullYear()}/${date.getMonth()}/${date.getDate()}/${date.getHours()}/${msg.userId}`
const rng = seedrandom(seed)
const feeling = feelings[Math.floor(rng() * feelings.length)]
msg.reply(`**今は${feeling}かも**`)
return true
} else {
return false
}
}
}

View File

@ -1,34 +1,35 @@
import autobind from 'autobind-decorator'; import Message from "@/message"
import Module from '@/module'; import Module from "@/module"
import Message from '@/message'; import autobind from "autobind-decorator"
export default class extends Module { export default class extends Module {
public readonly name = 'follow'; public readonly name = "follow"
@autobind @autobind
public install() { public install() {
return { return {
mentionHook: this.mentionHook mentionHook: this.mentionHook,
}; }
} }
@autobind @autobind
private async mentionHook(msg: Message) { private async mentionHook(msg: Message) {
if (msg.text && msg.includes(['フォロー', 'フォロバ', 'follow me'])) { if (msg.text && msg.includes(["フォロー", "フォロバ", "follow me"])) {
if (!msg.user.isFollowing) { if (!msg.user.isFollowing) {
this.ai.api('following/create', { this.nullcatChan.api("following/create", {
userId: msg.userId, userId: msg.userId,
}); })
msg.reply("これからよろしくね!", { immediate: true })
return { return {
reaction: msg.friend.love >= 0 ? 'like' : null reaction: msg.friend.love >= 0 ? ":love_nullcatchan:" : null,
}; }
} else { } else {
return { return {
reaction: msg.friend.love >= 0 ? 'hmm' : null reaction: msg.friend.love >= 0 ? ":love_nullcatchan:" : null,
}; }
} }
} else { } else {
return false; return false
} }
} }
} }

View File

@ -1,66 +1,36 @@
import autobind from 'autobind-decorator'; import Message from "@/message"
import Module from '@/module'; import Module from "@/module"
import Message from '@/message'; import serifs from "@/serifs"
import serifs from '@/serifs'; import { genItem } from "@/vocabulary"
import * as seedrandom from 'seedrandom'; import autobind from "autobind-decorator"
import { genItem } from '@/vocabulary'; import * as seedrandom from "seedrandom"
export const blessing = [ export const blessing = ["にゃん吉🐈", "みゃ~吉🐾", "ぬるきゃっと吉:love_nullcatchan:", "なんかすごい吉✨", "特大吉✨", "大大吉🎊", "大吉🎊", "吉🎉", "中吉🎉", "小吉🎉", "凶🗿", "大凶🗿"]
'藍吉',
'ヨタ吉',
'ゼタ吉',
'エクサ吉',
'ペタ吉',
'テラ吉',
'ギガ吉',
'メガ吉',
'キロ吉',
'ヘクト吉',
'デカ吉',
'デシ吉',
'センチ吉',
'ミリ吉',
'マイクロ吉',
'ナノ吉',
'ピコ吉',
'フェムト吉',
'アト吉',
'ゼプト吉',
'ヨクト吉',
'超吉',
'大大吉',
'大吉',
'吉',
'中吉',
'小吉',
'凶',
'大凶',
];
export default class extends Module { export default class extends Module {
public readonly name = 'fortune'; public readonly name = "fortune"
@autobind @autobind
public install() { public install() {
return { return {
mentionHook: this.mentionHook mentionHook: this.mentionHook,
}; }
} }
@autobind @autobind
private async mentionHook(msg: Message) { private async mentionHook(msg: Message) {
if (msg.includes(['占', 'うらな', '運勢', 'おみくじ'])) { if (msg.includes(["占", "うらな", "運勢", "おみくじ"])) {
const date = new Date(); const date = new Date()
const seed = `${date.getFullYear()}/${date.getMonth()}/${date.getDate()}@${msg.userId}`; const seed = `${date.getFullYear()}/${date.getMonth()}/${date.getDate()}@${msg.userId}`
const rng = seedrandom(seed); const rng = seedrandom(seed)
const omikuji = blessing[Math.floor(rng() * blessing.length)]; const omikuji = blessing[Math.floor(rng() * blessing.length)]
const item = genItem(rng); const item = genItem(rng)
msg.reply(`**${omikuji}🎉**\nラッキーアイテム: ${item}`, { msg.reply(`**${omikuji}**\nラッキーアイテム: ${item}`, {
cw: serifs.fortune.cw(msg.friend.name) cw: serifs.fortune.cw(msg.friend.name),
}); })
return true; return true
} else { } else {
return false; return false
} }
} }
} }

View File

@ -0,0 +1,90 @@
import config from "@/config"
import Message from "@/message"
import Module from "@/module"
import autobind from "autobind-decorator"
import fetch from "node-fetch"
import { z } from "zod"
export default class extends Module {
public readonly name = "github-status"
private readonly schema = z.object({
status: z.object({
description: z.string(),
indicator: z.enum(["none", "minor", "major", "critical", "maintenance"]),
}),
})
private indicatorString: Record<z.infer<typeof this.schema>["status"]["indicator"], string> = {
"none": "今はGitHubなんともないみたい",
"minor": "GitHubにちょっとしたエラーが起きてるかも",
"major": "GitHubにエラーが起きてるみたい",
"critical": "GitHubに重大なエラーが起きてるみたい",
"maintenance": "GitHubがメンテナンス中みたい",
}
private indicator: z.infer<typeof this.schema>["status"]["indicator"] = "none"
private description: z.infer<typeof this.schema>["status"]["description"] = ""
@autobind
public install() {
setInterval(this.updateStatus, 10 * 60 * 1000)
setInterval(this.postStatus, 60 * 60 * 1000)
this.updateStatus()
this.postStatus()
return {
mentionHook: this.mentionHook,
}
}
@autobind
private async updateStatus() {
try {
const response = await fetch("https://www.githubstatus.com/api/v2/status.json")
const data = await response.json()
const result = this.schema.safeParse(data)
if (result.success) {
this.indicator = result.data.status.indicator
this.description = result.data.status.description
} else {
this.log("Validation failed.")
console.warn(result.error)
}
} catch (error) {
this.log("Failed to fetch status from GitHub.")
console.warn(error)
}
}
@autobind
private postStatus() {
switch (this.indicator) {
case "minor":
case "major":
case "critical":
this.nullcatChan.post({
text: `${this.indicatorString[this.indicator]}\nせつめい: ${this.description}\nhttps://www.githubstatus.com/`,
})
this.log("Report posted.")
break
default:
break
}
}
@autobind
private async mentionHook(msg: Message) {
if (msg.text?.toLowerCase().includes("github")) {
msg.reply(`${this.indicatorString[this.indicator]}\nせつめい: ${this.description}\nhttps://www.githubstatus.com`)
return true
} else {
return false
}
}
}

View File

@ -0,0 +1,37 @@
import Message from "@/message"
import Module from "@/module"
import autobind from "autobind-decorator"
const gomamayo = require("gomamayo-js")
export default class extends Module {
public readonly name = "gomamayo"
@autobind
public install() {
return {
mentionHook: this.mentionHook,
}
}
@autobind
private async mentionHook(msg: Message) {
if (msg.text && msg.text.includes("ゴママヨ")) {
const notetext = msg.renotedText != null ? msg.renotedText : msg.text
const gomamayoResult = await gomamayo.find(notetext.replace(/ゴママヨ/g, ""))
let resBodyText, resCwText
if (gomamayoResult) {
resCwText = "ゴママヨかもしれない"
resBodyText = JSON.stringify(gomamayoResult, undefined, 2)
} else {
resBodyText = "ゴママヨじゃないかも"
}
msg.reply(resBodyText, {
immediate: true,
cw: resCwText,
})
return true
} else {
return false
}
}
}

View File

@ -0,0 +1,50 @@
import Module from "@/module"
import autobind from "autobind-decorator"
const accurateInterval = require("accurate-interval")
export default class extends Module {
public readonly name = "jihou"
@autobind
public install() {
accurateInterval(this.post, 1000 * 60 * 60, { aligned: true, immediate: true })
return {}
}
@autobind
private async post() {
const date = new Date()
date.setMinutes(date.getMinutes() + 1)
const hour = date.getHours()
switch (hour) {
default:
this.nullcatChan.post({
text: `${hour}時だよ!`,
})
break
case 7:
this.nullcatChan.post({
text: `みんなおはよ!${hour}時だよ!`,
})
break
case 1:
this.nullcatChan.post({
text: `${hour}時だよ!みんなそろそろ寝る時間かな?`,
})
break
case 5:
this.nullcatChan.post({
text: `${hour}時だよ!ログボリセットの時間だよ!!`,
})
break
}
}
}

View File

@ -1,81 +1,101 @@
import autobind from 'autobind-decorator'; import config from "@/config"
import * as loki from 'lokijs'; import Message from "@/message"
import Module from '@/module'; import Module from "@/module"
import config from '@/config'; import serifs from "@/serifs"
import serifs from '@/serifs'; import NGWord from "../../ng-words";
import { mecab } from './mecab'; import autobind from "autobind-decorator"
import * as loki from "lokijs"
import { mecab } from "./mecab"
function kanaToHira(str: string) { function kanaToHira(str: string) {
return str.replace(/[\u30a1-\u30f6]/g, match => { return str.replace(/[\u30a1-\u30f6]/g, (match) => {
const chr = match.charCodeAt(0) - 0x60; const chr = match.charCodeAt(0) - 0x60
return String.fromCharCode(chr); return String.fromCharCode(chr)
}); })
} }
export default class extends Module { export default class extends Module {
public readonly name = 'keyword'; public readonly name = "keyword"
private learnedKeywords: loki.Collection<{ private learnedKeywords: loki.Collection<{
keyword: string; keyword: string
learnedAt: number; learnedAt: number
}>; }>
private ngWord = new NGWord()
@autobind @autobind
public install() { public install() {
if (!config.keywordEnabled) return {}; if (!config.keywordEnabled) return {}
this.learnedKeywords = this.ai.getCollection('_keyword_learnedKeywords', { this.learnedKeywords = this.nullcatChan.getCollection("_keyword_learnedKeywords", {
indices: ['userId'] indices: ["userId"],
}); })
setInterval(this.learn, 1000 * 60 * 60); setInterval(this.learn, 1000 * 60 * 45)
return {}; return {
mentionHook: this.mentionHook,
}
} }
@autobind @autobind
private async learn() { private async learn() {
const tl = await this.ai.api('notes/local-timeline', { const tl = await this.nullcatChan.api("notes/hybrid-timeline", {
limit: 30 limit: 30,
}); })
const interestedNotes = tl.filter(note => const interestedNotes = tl.filter((note) => note.userId !== this.nullcatChan.account.id && note.text != null && note.cw == null)
note.userId !== this.ai.account.id &&
note.text != null &&
note.cw == null);
let keywords: string[][] = []; let keywords: string[][] = []
for (const note of interestedNotes) { for (const note of interestedNotes) {
const tokens = await mecab(note.text, config.mecab, config.mecabDic); const tokens = await mecab(note.text, config.mecab, config.mecabDic)
const keywordsInThisNote = tokens.filter(token => token[2] == '固有名詞' && token[8] != null); const keywordsInThisNote = tokens.filter((token) => token[2] == "固有名詞" && token[8] != null)
keywords = keywords.concat(keywordsInThisNote); keywords = keywords.concat(keywordsInThisNote)
} }
if (keywords.length === 0) return; if (keywords.length === 0) return
const rnd = Math.floor((1 - Math.sqrt(Math.random())) * keywords.length); const rnd = Math.floor((1 - Math.sqrt(Math.random())) * keywords.length)
const keyword = keywords.sort((a, b) => a[0].length < b[0].length ? 1 : -1)[rnd]; const keyword = keywords.sort((a, b) => (a[0].length < b[0].length ? 1 : -1))[rnd]
const exist = this.learnedKeywords.findOne({ const exist = this.learnedKeywords.findOne({
keyword: keyword[0] keyword: keyword[0],
}); })
let text: string; let text: string
if (exist) { if (exist) {
return; return
} else { } else {
this.learnedKeywords.insertOne({ this.learnedKeywords.insertOne({
keyword: keyword[0], keyword: keyword[0],
learnedAt: Date.now() learnedAt: Date.now(),
}); })
text = serifs.keyword.learned(keyword[0], kanaToHira(keyword[8])); const isNGWord = this.ngWord.get.some(word => keyword[0] === word)
if (isNGWord) return
text = serifs.keyword.learned(keyword[0], kanaToHira(keyword[8]))
} }
this.ai.post({ this.nullcatChan.post({
text: text text: text,
}); })
}
@autobind
private async mentionHook(msg: Message) {
if (msg.includes(["覚えて", "おぼえて"])) {
this.log("Keyword learn requested")
msg.reply("がんばってみるね")
this.learn()
return {
reaction: ":bikkuri_nullcatchan:",
}
} else {
return false
}
} }
} }

View File

@ -1,10 +1,10 @@
import { spawn } from 'child_process'; import { spawn } from "child_process"
import * as util from 'util'; import * as memoryStreams from "memory-streams"
import * as stream from 'stream'; import { EOL } from "os"
import * as memoryStreams from 'memory-streams'; import * as stream from "stream"
import { EOL } from 'os'; import * as util from "util"
const pipeline = util.promisify(stream.pipeline); const pipeline = util.promisify(stream.pipeline)
/** /**
* Run MeCab * Run MeCab
@ -12,34 +12,34 @@ const pipeline = util.promisify(stream.pipeline);
* @param mecab mecab bin * @param mecab mecab bin
* @param dic mecab dictionaly path * @param dic mecab dictionaly path
*/ */
export async function mecab(text: string, mecab = 'mecab', dic?: string): Promise<string[][]> { export async function mecab(text: string, mecab = "mecab", dic?: string): Promise<string[][]> {
const args: string[] = []; const args: string[] = []
if (dic) args.push('-d', dic); if (dic) args.push("-d", dic)
const lines = await cmd(mecab, args, `${text.replace(/[\n\s\t]/g, ' ')}\n`); const lines = await cmd(mecab, args, `${text.replace(/[\n\s\t]/g, " ")}\n`)
const results: string[][] = []; const results: string[][] = []
for (const line of lines) { for (const line of lines) {
if (line === 'EOS') break; if (line === "EOS") break
const [word, value = ''] = line.split('\t'); const [word, value = ""] = line.split("\t")
const array = value.split(','); const array = value.split(",")
array.unshift(word); array.unshift(word)
results.push(array); results.push(array)
} }
return results; return results
} }
export async function cmd(command: string, args: string[], stdin: string): Promise<string[]> { export async function cmd(command: string, args: string[], stdin: string): Promise<string[]> {
const mecab = spawn(command, args); const mecab = spawn(command, args)
const writable = new memoryStreams.WritableStream(); const writable = new memoryStreams.WritableStream()
mecab.stdin.write(stdin); mecab.stdin.write(stdin)
mecab.stdin.end(); mecab.stdin.end()
await pipeline(mecab.stdout, writable); await pipeline(mecab.stdout, writable)
return writable.toString().split(EOL); return writable.toString().split(EOL)
} }

View File

@ -0,0 +1,96 @@
import Message from "@/message"
import Module from "@/module"
import autobind from "autobind-decorator"
import fetch from "node-fetch"
import { z } from "zod"
export default class extends Module {
public readonly name = "kiatsu"
private readonly itemSchema = z.object({
time: z.enum(["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23"]),
weather: z.string(),
temp: z.string(),
pressure: z.string(),
pressure_level: z.enum(["0", "1", "2", "3", "4"]),
})
private readonly schema = z.object({
place_name: z.literal("東京都中央区"),
place_id: z.literal("102"),
prefectures_id: z.literal("13"),
dateTime: z.string(),
yesterday: z.array(this.itemSchema).optional(),
today: z.array(this.itemSchema),
tomorrow: z.array(this.itemSchema).optional(),
dayaftertomorrow: z.array(this.itemSchema).optional(),
})
private currentPressure: z.infer<typeof this.itemSchema>["pressure"] = ""
private currentPressureLevel: z.infer<typeof this.itemSchema>["pressure_level"] = "0"
private readonly stringPressureLevel: { [K in typeof this.currentPressureLevel]: (hPa: string) => string } = {
0: (hPa) => `${hPa}hPaだから問題ないかも。無理しないでね。`,
1: (hPa) => `${hPa}hPaだから問題ないかも。無理しないでね。`,
2: (hPa) => `気圧${hPa}hPaでちょっとやばいかも。無理しないでね。`,
3: (hPa) => `気圧${hPa}hPaでやばいかも。無理しないでね。`,
4: (hPa) => `気圧${hPa}hPaでかなりやばいかも。無理しないでね。`,
} as const
@autobind
public install() {
setInterval(this.update, 10 * 60 * 1000)
setInterval(this.post, 12 * 60 * 60 * 1000)
this.update()
return {
mentionHook: this.mentionHook,
}
}
@autobind
private async update() {
try {
const response = await fetch("https://zutool.jp/api/getweatherstatus/13102")
const data = await response.json()
const result = this.schema.safeParse(data)
if (!result.success) {
this.log("Validation failed.")
console.warn(result.error)
return
}
const date = new Date()
const hour = this.itemSchema.shape.time.parse(date.getHours().toString())
this.currentPressureLevel = result.data.today[hour].pressure_level
this.currentPressure = result.data.today[hour].pressure
} catch (error) {
this.log("Failed to fetch status.")
console.warn(error)
}
}
@autobind
private post() {
if (this.currentPressureLevel === "0" || this.currentPressureLevel === "1") return
this.nullcatChan.post({
text: this.stringPressureLevel[this.currentPressureLevel](this.currentPressure),
})
}
@autobind
private async mentionHook(message: Message) {
if (!message.includes(["気圧", "きあつ"])) return false
message.reply(this.stringPressureLevel[this.currentPressureLevel](this.currentPressure), { immediate: true })
return true
}
}

View File

@ -1,49 +1,34 @@
import autobind from 'autobind-decorator'; import config from "@/config"
import Module from '@/module'; import Module from "@/module"
import serifs from '@/serifs'; import serifs from "@/serifs"
import { genItem } from '@/vocabulary'; import autobind from "autobind-decorator"
import config from '@/config';
export default class extends Module { export default class extends Module {
public readonly name = 'noting'; public readonly name = "noting"
@autobind @autobind
public install() { public install() {
if (config.notingEnabled === false) return {}; if (config.notingEnabled === false) return {}
setInterval(() => { setInterval(() => {
if (Math.random() < 0.04) { if (Math.random() < 0.1) {
this.post(); this.post()
} }
}, 1000 * 60 * 10); }, 1000 * 60 * 10)
return {}; return {}
} }
@autobind @autobind
private post() { private post() {
const notes = [ const notes = serifs.noting.notes
...serifs.noting.notes,
() => {
const item = genItem();
return serifs.noting.want(item);
},
() => {
const item = genItem();
return serifs.noting.see(item);
},
() => {
const item = genItem();
return serifs.noting.expire(item);
},
];
const note = notes[Math.floor(Math.random() * notes.length)]; const note = notes[Math.floor(Math.random() * notes.length)]
// TODO: 季節に応じたセリフ // TODO: 季節に応じたセリフ
this.ai.post({ this.nullcatChan.post({
text: typeof note === 'function' ? note() : note text: note,
}); })
} }
} }

View File

@ -1,26 +1,26 @@
import autobind from 'autobind-decorator'; import Message from "@/message"
import Module from '@/module'; import Module from "@/module"
import Message from '@/message'; import autobind from "autobind-decorator"
export default class extends Module { export default class extends Module {
public readonly name = 'ping'; public readonly name = "ping"
@autobind @autobind
public install() { public install() {
return { return {
mentionHook: this.mentionHook mentionHook: this.mentionHook,
}; }
} }
@autobind @autobind
private async mentionHook(msg: Message) { private async mentionHook(msg: Message) {
if (msg.text && msg.text.includes('ping')) { if (msg.text && msg.text.includes("ping")) {
msg.reply('PONG!', { msg.reply("$[x2 :bibibi_nullcatchan:]", {
immediate: true immediate: true,
}); })
return true; return true
} else { } else {
return false; return false
} }
} }
} }

View File

@ -1,178 +1,172 @@
import autobind from 'autobind-decorator'; import config from "@/config"
import * as loki from 'lokijs'; import Message from "@/message"
import Module from '@/module'; import Module from "@/module"
import Message from '@/message'; import serifs, { getSerif } from "@/serifs"
import serifs, { getSerif } from '@/serifs'; import { acct } from "../../../../NullcatChan-old/src/utils/acct"
import { acct } from '@/utils/acct'; import autobind from "autobind-decorator"
import config from '@/config'; import * as loki from "lokijs"
const NOTIFY_INTERVAL = 1000 * 60 * 60 * 12; const NOTIFY_INTERVAL = 1000 * 60 * 60 * 1
export default class extends Module { export default class extends Module {
public readonly name = 'reminder'; public readonly name = "reminder"
private reminds: loki.Collection<{ private reminds: loki.Collection<{
userId: string; userId: string
id: string; id: string
isDm: boolean; isDm: boolean
thing: string | null; thing: string | null
quoteId: string | null; quoteId: string | null
times: number; // 催促した回数(使うのか?) times: number // 催促した回数(使うのか?)
createdAt: number; createdAt: number
}>; }>
@autobind @autobind
public install() { public install() {
this.reminds = this.ai.getCollection('reminds', { this.reminds = this.nullcatChan.getCollection("reminds", {
indices: ['userId', 'id'] indices: ["userId", "id"],
}); })
return { return {
mentionHook: this.mentionHook, mentionHook: this.mentionHook,
contextHook: this.contextHook, contextHook: this.contextHook,
timeoutCallback: this.timeoutCallback, timeoutCallback: this.timeoutCallback,
}; }
} }
@autobind @autobind
private async mentionHook(msg: Message) { private async mentionHook(msg: Message) {
let text = msg.extractedText.toLowerCase(); let text = msg.extractedText.toLowerCase()
if (!text.startsWith('remind') && !text.startsWith('todo')) return false; if (!text.startsWith("リマインド") && !text.startsWith("todo") && !text.startsWith("これやる")) return false
if (text.startsWith('reminds') || text.startsWith('todos')) { if (text.startsWith("リスト") || text.startsWith("todos")) {
const reminds = this.reminds.find({ const reminds = this.reminds.find({
userId: msg.userId, userId: msg.userId,
}); })
const getQuoteLink = id => `[${id}](${config.host}/notes/${id})`; const getQuoteLink = (id) => `[${id}](${config.host}/notes/${id})`
if(reminds.length === 0) {
msg.reply(serifs.reminder.reminds + '\n' + reminds.map(remind => `${remind.thing ? remind.thing : getQuoteLink(remind.quoteId)}`).join('\n')); msg.reply(serifs.reminder.none)
return true; }
else {
msg.reply(serifs.reminder.reminds + "\n" + reminds.map((remind) => `${remind.thing ? remind.thing : getQuoteLink(remind.quoteId)}`).join("\n"))
}
return true
} }
if (text.match(/^(.+?)\s(.+)/)) { if (text.match(/^(.+?)\s(.+)/)) {
text = text.replace(/^(.+?)\s/, ''); text = text.replace(/^(.+?)\s/, "")
} else { } else {
text = ''; text = ""
} }
const separatorIndex = text.indexOf(' ') > -1 ? text.indexOf(' ') : text.indexOf('\n'); const separatorIndex = text.indexOf(" ") > -1 ? text.indexOf(" ") : text.indexOf("\n")
const thing = text.substr(separatorIndex + 1).trim(); const thing = text.substr(separatorIndex + 1).trim()
if (thing === '' && msg.quoteId == null || msg.visibility === 'followers') { if (thing === "" && msg.quoteId == null) {
msg.reply(serifs.reminder.invalid); msg.reply(serifs.reminder.invalid)
return { return true
reaction: '🆖',
immediate: true,
};
} }
const remind = this.reminds.insertOne({ const remind = this.reminds.insertOne({
id: msg.id, id: msg.id,
userId: msg.userId, userId: msg.userId,
isDm: msg.isDm, isDm: msg.isDm,
thing: thing === '' ? null : thing, thing: thing === "" ? null : thing,
quoteId: msg.quoteId, quoteId: msg.quoteId,
times: 0, times: 0,
createdAt: Date.now(), createdAt: Date.now(),
}); })
// メンションをsubscribe // メンションをsubscribe
this.subscribeReply(remind!.id, msg.isDm, msg.isDm ? msg.userId : msg.id, { this.subscribeReply(remind!.id, msg.isDm, msg.isDm ? msg.userId : msg.id, {
id: remind!.id id: remind!.id,
}); })
if (msg.quoteId) { if (msg.quoteId) {
// 引用元をsubscribe // 引用元をsubscribe
this.subscribeReply(remind!.id, false, msg.quoteId, { this.subscribeReply(remind!.id, false, msg.quoteId, {
id: remind!.id id: remind!.id,
}); })
} }
// タイマーセット // タイマーセット
this.setTimeoutWithPersistence(NOTIFY_INTERVAL, { this.setTimeoutWithPersistence(NOTIFY_INTERVAL, {
id: remind!.id, id: remind!.id,
}); })
return { return {
reaction: '🆗', reaction: ":ok:",
immediate: true, immediate: true,
}; }
} }
@autobind @autobind
private async contextHook(key: any, msg: Message, data: any) { private async contextHook(key: any, msg: Message, data: any) {
if (msg.text == null) return; if (msg.text == null) return
const remind = this.reminds.findOne({ const remind = this.reminds.findOne({
id: data.id, id: data.id,
}); })
if (remind == null) { if (remind == null) {
this.unsubscribeReply(key); this.unsubscribeReply(key)
return; return
} }
const done = msg.includes(['done', 'やった', 'やりました', 'はい']); const done = msg.includes(["done", "やった", "やりました", "はい", "どね", "ドネ"])
const cancel = msg.includes(['やめる', 'やめた', 'キャンセル']); const cancel = msg.includes(["やめる", "やめた", "キャンセル"])
const isOneself = msg.userId === remind.userId;
if ((done || cancel) && isOneself) { if (done || cancel) {
this.unsubscribeReply(key); this.unsubscribeReply(key)
this.reminds.remove(remind); this.reminds.remove(remind)
msg.reply(done ? getSerif(serifs.reminder.done(msg.friend.name)) : serifs.reminder.cancel); msg.reply(done ? getSerif(serifs.reminder.done(msg.friend.name)) : serifs.reminder.cancel)
return; return
} else if (isOneself === false) {
msg.reply(serifs.reminder.doneFromInvalidUser);
return;
} else { } else {
if (msg.isDm) this.unsubscribeReply(key); if (msg.isDm) this.unsubscribeReply(key)
return false; return false
} }
} }
@autobind @autobind
private async timeoutCallback(data) { private async timeoutCallback(data) {
const remind = this.reminds.findOne({ const remind = this.reminds.findOne({
id: data.id id: data.id,
}); })
if (remind == null) return; if (remind == null) return
remind.times++; remind.times++
this.reminds.update(remind); this.reminds.update(remind)
const friend = this.ai.lookupFriend(remind.userId); const friend = this.nullcatChan.lookupFriend(remind.userId)
if (friend == null) return; // 処理の流れ上、実際にnullになることは無さそうだけど一応 if (friend == null) return // 処理の流れ上、実際にnullになることは無さそうだけど一応
let reply; let reply
if (remind.isDm) { if (remind.isDm) {
this.ai.sendMessage(friend.userId, { this.nullcatChan.sendMessage(friend.userId, {
text: serifs.reminder.notifyWithThing(remind.thing, friend.name) text: serifs.reminder.notifyWithThing(remind.thing, friend.name),
}); })
} else { } else {
try { try {
reply = await this.ai.post({ reply = await this.nullcatChan.post({
renoteId: remind.thing == null && remind.quoteId ? remind.quoteId : remind.id, renoteId: remind.thing == null && remind.quoteId ? remind.quoteId : remind.id,
text: acct(friend.doc.user) + ' ' + serifs.reminder.notify(friend.name) text: acct(friend.doc.user) + " " + serifs.reminder.notify(friend.name),
}); visibility: "specified",
visibleUserIds: [remind.userId],
})
} catch (err) { } catch (err) {
// renote対象が消されていたらリマインダー解除 // TODO: renote対象が消されていたらリマインダー解除
if (err.statusCode === 400) { return
this.unsubscribeReply(remind.thing == null && remind.quoteId ? remind.quoteId : remind.id);
this.reminds.remove(remind);
return;
}
return;
} }
} }
this.subscribeReply(remind.id, remind.isDm, remind.isDm ? remind.userId : reply.id, { this.subscribeReply(remind.id, remind.isDm, remind.isDm ? remind.userId : reply.id, {
id: remind.id id: remind.id,
}); })
// タイマーセット // タイマーセット
this.setTimeoutWithPersistence(NOTIFY_INTERVAL, { this.setTimeoutWithPersistence(NOTIFY_INTERVAL, {
id: remind.id, id: remind.id,
}); })
} }
} }

View File

@ -1,79 +1,79 @@
import autobind from 'autobind-decorator'; import config from "@/config"
import Module from '@/module'; import Module from "@/module"
import serifs from '@/serifs'; import serifs from "@/serifs"
import config from '@/config'; import autobind from "autobind-decorator"
export default class extends Module { export default class extends Module {
public readonly name = 'server'; public readonly name = "server"
private connection?: any; private connection?: any
private recentStat: any; private recentStat: any
private warned = false; private warned = false
private lastWarnedAt: number; private lastWarnedAt: number
/** /**
* 11 * 11
*/ */
private statsLogs: any[] = []; private statsLogs: any[] = []
@autobind @autobind
public install() { public install() {
if (!config.serverMonitoring) return {}; if (!config.serverMonitoring) return {}
this.connection = this.ai.connection.useSharedConnection('serverStats'); this.connection = this.nullcatChan.connection.useSharedConnection("serverStats")
this.connection.on('stats', this.onStats); this.connection.on("stats", this.onStats)
setInterval(() => { setInterval(() => {
this.statsLogs.unshift(this.recentStat); this.statsLogs.unshift(this.recentStat)
if (this.statsLogs.length > 60) this.statsLogs.pop(); if (this.statsLogs.length > 60) this.statsLogs.pop()
}, 1000); }, 1000)
setInterval(() => { setInterval(() => {
this.check(); this.check()
}, 3000); }, 3000)
return {}; return {}
} }
@autobind @autobind
private check() { private check() {
const average = (arr) => arr.reduce((a, b) => a + b) / arr.length; const average = (arr) => arr.reduce((a, b) => a + b) / arr.length
const cpuPercentages = this.statsLogs.map(s => s && (s.cpu_usage || s.cpu) * 100 || 0); const cpuPercentages = this.statsLogs.map((s) => (s && (s.cpu_usage || s.cpu) * 100) || 0)
const cpuPercentage = average(cpuPercentages); const cpuPercentage = average(cpuPercentages)
if (cpuPercentage >= 70) { if (cpuPercentage >= 70) {
this.warn(); this.warn()
} else if (cpuPercentage <= 30) { } else if (cpuPercentage <= 30) {
this.warned = false; this.warned = false
} }
} }
@autobind @autobind
private async onStats(stats: any) { private async onStats(stats: any) {
this.recentStat = stats; this.recentStat = stats
} }
@autobind @autobind
private warn() { private warn() {
//#region 前に警告したときから一旦落ち着いた状態を経験していなければ警告しない //#region 前に警告したときから一旦落ち着いた状態を経験していなければ警告しない
// 常に負荷が高いようなサーバーで無限に警告し続けるのを防ぐため // 常に負荷が高いようなサーバーで無限に警告し続けるのを防ぐため
if (this.warned) return; if (this.warned) return
//#endregion //#endregion
//#region 前の警告から1時間経っていない場合は警告しない //#region 前の警告から1時間経っていない場合は警告しない
const now = Date.now(); const now = Date.now()
if (this.lastWarnedAt != null) { if (this.lastWarnedAt != null) {
if (now - this.lastWarnedAt < (1000 * 60 * 60)) return; if (now - this.lastWarnedAt < 1000 * 60 * 60) return
} }
this.lastWarnedAt = now; this.lastWarnedAt = now
//#endregion //#endregion
this.ai.post({ this.nullcatChan.post({
text: serifs.server.cpu text: serifs.server.cpu,
}); })
this.warned = true; this.warned = true
} }
} }

View File

@ -0,0 +1,74 @@
import autobind from 'autobind-decorator';
import Module from '@/module';
import Message from '@/message';
import config from "@/config";
import fetch from 'node-fetch';
export default class extends Module {
public readonly name = 'shellgei';
@autobind
public install() {
return {
mentionHook: this.mentionHook
};
}
@autobind
private async mentionHook(msg: Message) {
if (!msg.text) return false;
if (msg.text && msg.text.includes('#シェル芸') || msg.text.includes('#shellgei')) {
const myInfoBody = { i: config.i };
const myInfoOptions = { method: 'POST', body: JSON.stringify(myInfoBody), headers: { 'Content-Type': 'application/json' } };
const myInfo = await fetch(`${config.apiUrl}/i`, myInfoOptions);
const myInfoJson: any = await myInfo.json();
const myId = myInfoJson.username;
const acct = `@${myId}`;
const hostname = config.host.replace(/^https?:\/\//, '').replace(/\/$/, '');
const hostnameat = `@${hostname}`;
const shellText = msg.text.replace('#シェル芸', '').replace('#shellgei', '').replace(acct, '').replace(hostnameat, '');
this.log(shellText);
const shellgeiBody = { code: shellText , images: [] };
const shellgeiOptions = { method: 'POST', body: JSON.stringify(shellgeiBody), headers: { 'Content-Type': 'application/json' } };
const shellgeiURL = config.shellgeiUrl;
await (async () => {
try {
const shellgeiResult = await fetch(shellgeiURL, shellgeiOptions);
const shellgeiResultJson: any = await shellgeiResult.json();
const shellgeiResultStdOut = shellgeiResultJson.stdout;
const shellgeiResultStdErr = shellgeiResultJson.stderr;
if (shellgeiResultStdOut === "" && shellgeiResultStdErr === ""){
msg.reply(`結果がなかったよ:cry_nullcatchan:`, {
immediate: true
});
} else {
msg.reply(shellgeiResultStdOut + shellgeiResultStdErr, {
immediate: true
});
}
} catch (e) {
console.log(e);
msg.reply(`エラーが発生しちゃったよ:cry_nullcatchan:\n${e}`, {
immediate: true
});
}
})();
return true;
} else {
return false;
}
}
}

View File

@ -1,35 +1,35 @@
import autobind from 'autobind-decorator'; import Module from "@/module"
import Module from '@/module'; import serifs from "@/serifs"
import serifs from '@/serifs'; import autobind from "autobind-decorator"
export default class extends Module { export default class extends Module {
public readonly name = 'sleepReport'; public readonly name = "sleepReport"
@autobind @autobind
public install() { public install() {
this.report(); this.report()
return {}; return {}
} }
@autobind @autobind
private report() { private report() {
const now = Date.now(); const now = Date.now()
const sleepTime = now - this.ai.lastSleepedAt; const sleepTime = now - this.nullcatChan.lastSleepedAt
const sleepHours = sleepTime / 1000 / 60 / 60; const sleepHours = sleepTime / 1000 / 60 / 60
if (sleepHours < 0.1) return; if (sleepHours < 0.1) return
if (sleepHours >= 1) { if (sleepHours >= 1) {
this.ai.post({ this.nullcatChan.post({
text: serifs.sleepReport.report(Math.round(sleepHours)) text: serifs.sleepReport.report(Math.round(sleepHours)),
}); })
} else { } else {
this.ai.post({ this.nullcatChan.post({
text: serifs.sleepReport.reportUtatane text: serifs.sleepReport.reportUtatane,
}); })
} }
} }
} }

View File

@ -1,23 +1,23 @@
import autobind from 'autobind-decorator'; import Message from "@/message"
import { HandlerResult } from '@/ai'; import Module from "@/module"
import Module from '@/module'; import { HandlerResult } from "../../nullcat-chan"
import Message from '@/message'; import serifs, { getSerif } from "@/serifs"
import serifs, { getSerif } from '@/serifs'; import getDate from "../../../../NullcatChan-old/src/utils/get-date"
import getDate from '@/utils/get-date'; import autobind from "autobind-decorator"
export default class extends Module { export default class extends Module {
public readonly name = 'talk'; public readonly name = "talk"
@autobind @autobind
public install() { public install() {
return { return {
mentionHook: this.mentionHook, mentionHook: this.mentionHook,
}; }
} }
@autobind @autobind
private async mentionHook(msg: Message) { private async mentionHook(msg: Message) {
if (!msg.text) return false; if (!msg.text) return false
return ( return (
this.greet(msg) || this.greet(msg) ||
@ -30,304 +30,314 @@ export default class extends Module {
this.humu(msg) || this.humu(msg) ||
this.batou(msg) || this.batou(msg) ||
this.itai(msg) || this.itai(msg) ||
this.turai(msg) ||
this.kurusii(msg) ||
this.ote(msg) || this.ote(msg) ||
this.ponkotu(msg) || this.ponkotu(msg) ||
this.rmrf(msg) || this.rmrf(msg) ||
this.shutdown(msg) this.shutdown(msg)
); )
} }
@autobind @autobind
private greet(msg: Message): boolean { private greet(msg: Message): boolean {
if (msg.text == null) return false; if (msg.text == null) return false
const incLove = () => { const incLove = () => {
//#region 1日に1回だけ親愛度を上げる //#region 1日に1回だけ親愛度を上げる
const today = getDate(); const today = getDate()
const data = msg.friend.getPerModulesData(this); const data = msg.friend.getPerModulesData(this)
if (data.lastGreetedAt == today) return; if (data.lastGreetedAt == today) return
data.lastGreetedAt = today; data.lastGreetedAt = today
msg.friend.setPerModulesData(this, data); msg.friend.setPerModulesData(this, data)
msg.friend.incLove(); msg.friend.incLove()
//#endregion //#endregion
}; }
// 末尾のエクスクラメーションマーク // 末尾のエクスクラメーションマーク
const tension = (msg.text.match(/[!]{2,}/g) || ['']) const tension = (msg.text.match(/[!]{2,}/g) || [""]).sort((a, b) => (a.length < b.length ? 1 : -1))[0].substr(1)
.sort((a, b) => a.length < b.length ? 1 : -1)[0]
.substr(1);
if (msg.includes(['こんにちは', 'こんにちわ'])) { if (msg.includes(["こんにちは", "こんにちわ"])) {
msg.reply(serifs.core.hello(msg.friend.name)); msg.reply(serifs.core.hello(msg.friend.name))
incLove(); incLove()
return true; return true
} }
if (msg.includes(['こんばんは', 'こんばんわ'])) { if (msg.includes(["こんばんは", "こんばんわ"])) {
msg.reply(serifs.core.helloNight(msg.friend.name)); msg.reply(serifs.core.helloNight(msg.friend.name))
incLove(); incLove()
return true; return true
} }
if (msg.includes(['おは', 'おっは', 'お早う'])) { if (msg.includes(["おは", "おっは", "お早う"])) {
msg.reply(serifs.core.goodMorning(tension, msg.friend.name)); msg.reply(serifs.core.goodMorning(tension, msg.friend.name))
incLove(); incLove()
return true; return true
} }
if (msg.includes(['おやすみ', 'お休み'])) { if (msg.includes(["おやすみ", "お休み"])) {
msg.reply(serifs.core.goodNight(msg.friend.name)); msg.reply(serifs.core.goodNight(msg.friend.name))
incLove(); incLove()
return true; return true
} }
if (msg.includes(['行ってくる', '行ってきます', 'いってくる', 'いってきます'])) { if (msg.includes(["行ってくる", "行ってきます", "いってくる", "いってきます"])) {
msg.reply(msg.friend.love >= 7 ? serifs.core.itterassyai.love(msg.friend.name) : serifs.core.itterassyai.normal(msg.friend.name))
incLove()
return true
}
if (msg.includes(["ただいま"])) {
msg.reply( msg.reply(
msg.friend.love >= 7 msg.friend.love >= 15 ? serifs.core.okaeri.love2(msg.friend.name) : msg.friend.love >= 7 ? getSerif(serifs.core.okaeri.love(msg.friend.name)) : serifs.core.okaeri.normal(msg.friend.name)
? serifs.core.itterassyai.love(msg.friend.name) )
: serifs.core.itterassyai.normal(msg.friend.name)); incLove()
incLove(); return true
return true;
} }
if (msg.includes(['ただいま'])) { return false
msg.reply(
msg.friend.love >= 15 ? serifs.core.okaeri.love2(msg.friend.name) :
msg.friend.love >= 7 ? getSerif(serifs.core.okaeri.love(msg.friend.name)) :
serifs.core.okaeri.normal(msg.friend.name));
incLove();
return true;
}
return false;
} }
@autobind @autobind
private erait(msg: Message): boolean { private erait(msg: Message): boolean {
const match = msg.extractedText.match(/(.+?)た(から|ので)(褒|ほ)めて/); const match = msg.extractedText.match(/(.+?)た(から|ので)(褒|ほ)めて/)
if (match) { if (match) {
msg.reply(getSerif(serifs.core.erait.specify(match[1], msg.friend.name))); msg.reply(getSerif(serifs.core.erait.specify(match[1], msg.friend.name)))
return true; return true
} }
const match2 = msg.extractedText.match(/(.+?)る(から|ので)(褒|ほ)めて/); const match2 = msg.extractedText.match(/(.+?)る(から|ので)(褒|ほ)めて/)
if (match2) { if (match2) {
msg.reply(getSerif(serifs.core.erait.specify(match2[1], msg.friend.name))); msg.reply(getSerif(serifs.core.erait.specify(match2[1], msg.friend.name)))
return true; return true
} }
const match3 = msg.extractedText.match(/(.+?)だから(褒|ほ)めて/); const match3 = msg.extractedText.match(/(.+?)だから(褒|ほ)めて/)
if (match3) { if (match3) {
msg.reply(getSerif(serifs.core.erait.specify(match3[1], msg.friend.name))); msg.reply(getSerif(serifs.core.erait.specify(match3[1], msg.friend.name)))
return true; return true
} }
if (!msg.includes(['褒めて', 'ほめて'])) return false; if (!msg.includes(["褒めて", "ほめて"])) return false
msg.reply(getSerif(serifs.core.erait.general(msg.friend.name))); msg.reply(getSerif(serifs.core.erait.general(msg.friend.name)))
return true; return true
} }
@autobind @autobind
private omedeto(msg: Message): boolean { private omedeto(msg: Message): boolean {
if (!msg.includes(['おめでと'])) return false; if (!msg.includes(["おめでと"])) return false
msg.reply(serifs.core.omedeto(msg.friend.name)); msg.reply(serifs.core.omedeto(msg.friend.name))
return true; return true
} }
@autobind @autobind
private nadenade(msg: Message): boolean { private nadenade(msg: Message): boolean {
if (!msg.includes(['なでなで'])) return false; if (!msg.includes(["なでなで"])) return false
// メッセージのみ // メッセージのみ
if (!msg.isDm) return true; if (!msg.isDm) return true
//#region 1日に1回だけ親愛度を上げる(嫌われてない場合のみ) //#region 1日に1回だけ親愛度を上げる(嫌われてない場合のみ)
if (msg.friend.love >= 0) { if (msg.friend.love >= 0) {
const today = getDate(); const today = getDate()
const data = msg.friend.getPerModulesData(this); const data = msg.friend.getPerModulesData(this)
if (data.lastNadenadeAt != today) { if (data.lastNadenadeAt != today) {
data.lastNadenadeAt = today; data.lastNadenadeAt = today
msg.friend.setPerModulesData(this, data); msg.friend.setPerModulesData(this, data)
msg.friend.incLove(); msg.friend.incLove()
} }
} }
//#endregion //#endregion
msg.reply(getSerif( msg.reply(
msg.friend.love >= 10 ? serifs.core.nadenade.love3 : getSerif(
msg.friend.love >= 5 ? serifs.core.nadenade.love2 : msg.friend.love >= 10
msg.friend.love <= -15 ? serifs.core.nadenade.hate4 : ? serifs.core.nadenade.love3
msg.friend.love <= -10 ? serifs.core.nadenade.hate3 : : msg.friend.love >= 5
msg.friend.love <= -5 ? serifs.core.nadenade.hate2 : ? serifs.core.nadenade.love2
msg.friend.love <= -1 ? serifs.core.nadenade.hate1 : : msg.friend.love <= -15
serifs.core.nadenade.normal ? serifs.core.nadenade.hate4
)); : msg.friend.love <= -10
? serifs.core.nadenade.hate3
: msg.friend.love <= -5
? serifs.core.nadenade.hate2
: msg.friend.love <= -1
? serifs.core.nadenade.hate1
: serifs.core.nadenade.normal
)
)
return true; return true
} }
@autobind @autobind
private kawaii(msg: Message): boolean { private kawaii(msg: Message): boolean {
if (!msg.includes(['かわいい', '可愛い'])) return false; if (!msg.includes(["かわいい", "可愛い"])) return false
// メッセージのみ // メッセージのみ
if (!msg.isDm) return true; if (!msg.isDm) return true
msg.reply(getSerif( msg.reply(getSerif(msg.friend.love >= 5 ? serifs.core.kawaii.love : msg.friend.love <= -3 ? serifs.core.kawaii.hate : serifs.core.kawaii.normal))
msg.friend.love >= 5 ? serifs.core.kawaii.love :
msg.friend.love <= -3 ? serifs.core.kawaii.hate :
serifs.core.kawaii.normal));
return true; return true
} }
@autobind @autobind
private suki(msg: Message): boolean { private suki(msg: Message): boolean {
if (!msg.or(['好き', 'すき'])) return false; if (!msg.or(["好き", "すき"])) return false
// メッセージのみ // メッセージのみ
if (!msg.isDm) return true; if (!msg.isDm) return true
msg.reply( msg.reply(msg.friend.love >= 5 ? (msg.friend.name ? serifs.core.suki.love(msg.friend.name) : serifs.core.suki.normal) : msg.friend.love <= -3 ? serifs.core.suki.hate : serifs.core.suki.normal)
msg.friend.love >= 5 ? (msg.friend.name ? serifs.core.suki.love(msg.friend.name) : serifs.core.suki.normal) :
msg.friend.love <= -3 ? serifs.core.suki.hate :
serifs.core.suki.normal);
return true; return true
} }
@autobind @autobind
private hug(msg: Message): boolean { private hug(msg: Message): boolean {
if (!msg.or(['ぎゅ', 'むぎゅ', /^はぐ(し(て|よ|よう)?)?$/])) return false; if (!msg.or(["ぎゅ", "むぎゅ", /^はぐ(し(て|よ|よう)?)?$/])) return false
// メッセージのみ // メッセージのみ
if (!msg.isDm) return true; if (!msg.isDm) return true
//#region 前のハグから1分経ってない場合は返信しない //#region 前のハグから1分経ってない場合は返信しない
// これは、「ハグ」と言って「ぎゅー」と返信したとき、相手が // これは、「ハグ」と言って「ぎゅー」と返信したとき、相手が
// それに対してさらに「ぎゅー」と返信するケースがあったため。 // それに対してさらに「ぎゅー」と返信するケースがあったため。
// そうするとその「ぎゅー」に対してもマッチするため、また // そうするとその「ぎゅー」に対してもマッチするため、また
// がそれに返信してしまうことになり、少し不自然になる。 // ぬるきゃっとちゃんがそれに返信してしまうことになり、少し不自然になる。
// これを防ぐために前にハグしてから少し時間が経っていないと // これを防ぐために前にハグしてから少し時間が経っていないと
// 返信しないようにする // 返信しないようにする
const now = Date.now(); const now = Date.now()
const data = msg.friend.getPerModulesData(this); const data = msg.friend.getPerModulesData(this)
if (data.lastHuggedAt != null) { if (data.lastHuggedAt != null) {
if (now - data.lastHuggedAt < (1000 * 60)) return true; if (now - data.lastHuggedAt < 1000 * 60) return true
} }
data.lastHuggedAt = now; data.lastHuggedAt = now
msg.friend.setPerModulesData(this, data); msg.friend.setPerModulesData(this, data)
//#endregion //#endregion
msg.reply( msg.reply(msg.friend.love >= 5 ? serifs.core.hug.love : msg.friend.love <= -3 ? serifs.core.hug.hate : serifs.core.hug.normal)
msg.friend.love >= 5 ? serifs.core.hug.love :
msg.friend.love <= -3 ? serifs.core.hug.hate :
serifs.core.hug.normal);
return true; return true
} }
@autobind @autobind
private humu(msg: Message): boolean { private humu(msg: Message): boolean {
if (!msg.includes(['踏んで'])) return false; if (!msg.includes(["踏んで"])) return false
// メッセージのみ // メッセージのみ
if (!msg.isDm) return true; if (!msg.isDm) return true
msg.reply( msg.reply(msg.friend.love >= 5 ? serifs.core.humu.love : msg.friend.love <= -3 ? serifs.core.humu.hate : serifs.core.humu.normal)
msg.friend.love >= 5 ? serifs.core.humu.love :
msg.friend.love <= -3 ? serifs.core.humu.hate :
serifs.core.humu.normal);
return true; return true
} }
@autobind @autobind
private batou(msg: Message): boolean { private batou(msg: Message): boolean {
if (!msg.includes(['罵倒して', '罵って'])) return false; if (!msg.includes(["罵倒して", "罵って"])) return false
// メッセージのみ // メッセージのみ
if (!msg.isDm) return true; if (!msg.isDm) return true
msg.reply( msg.reply(msg.friend.love >= 5 ? serifs.core.batou.love : msg.friend.love <= -5 ? serifs.core.batou.hate : serifs.core.batou.normal)
msg.friend.love >= 5 ? serifs.core.batou.love :
msg.friend.love <= -5 ? serifs.core.batou.hate :
serifs.core.batou.normal);
return true; return true
} }
@autobind @autobind
private itai(msg: Message): boolean { private itai(msg: Message): boolean {
if (!msg.or(['痛い', 'いたい']) && !msg.extractedText.endsWith('痛い')) return false; if (!msg.or(["痛い", "いたい"])) return false
// メッセージのみ // メッセージのみ
if (!msg.isDm) return true; if (!msg.isDm) return true
msg.reply(serifs.core.itai(msg.friend.name)); msg.reply(serifs.core.itai(msg.friend.name))
return true; return true
}
@autobind
private turai(msg: Message): boolean {
if (!msg.or(["辛い", "つらい"])) return false
// メッセージのみ
if (!msg.isDm) return true
msg.reply(msg.friend.love >= 5 ? serifs.core.turai.love(msg.friend.name) : msg.friend.love >= -3 ? serifs.core.turai.hate : serifs.core.turai.normal(msg.friend.name))
return true
}
@autobind
private kurusii(msg: Message): boolean {
if (!msg.or(["苦しい", "くるしい"])) return false
// メッセージのみ
if (!msg.isDm) return true
msg.reply(msg.friend.love >= 5 ? serifs.core.kurusii.love(msg.friend.name) : msg.friend.love >= -3 ? serifs.core.kurusii.hate : serifs.core.kurusii.normal(msg.friend.name))
return true
} }
@autobind @autobind
private ote(msg: Message): boolean { private ote(msg: Message): boolean {
if (!msg.or(['お手'])) return false; if (!msg.or(["お手"])) return false
// メッセージのみ // メッセージのみ
if (!msg.isDm) return true; if (!msg.isDm) return true
msg.reply( msg.reply(msg.friend.love >= 10 ? serifs.core.ote.love2 : msg.friend.love >= 5 ? serifs.core.ote.love1 : serifs.core.ote.normal)
msg.friend.love >= 10 ? serifs.core.ote.love2 :
msg.friend.love >= 5 ? serifs.core.ote.love1 :
serifs.core.ote.normal);
return true; return true
} }
@autobind @autobind
private ponkotu(msg: Message): boolean | HandlerResult { private ponkotu(msg: Message): boolean | HandlerResult {
if (!msg.includes(['ぽんこつ'])) return false; if (!msg.includes(["ぽんこつ"])) return false
msg.friend.decLove(); msg.friend.decLove()
return { return {
reaction: 'angry' reaction: "angry",
}; }
} }
@autobind @autobind
private rmrf(msg: Message): boolean | HandlerResult { private rmrf(msg: Message): boolean | HandlerResult {
if (!msg.includes(['rm -rf'])) return false; if (!msg.includes(["rm -rf"])) return false
msg.friend.decLove(); msg.friend.decLove()
return { return {
reaction: 'angry' reaction: "angry",
}; }
} }
@autobind @autobind
private shutdown(msg: Message): boolean | HandlerResult { private shutdown(msg: Message): boolean | HandlerResult {
if (!msg.includes(['shutdown'])) return false; if (!msg.includes(["shutdown"])) return false
msg.reply(serifs.core.shutdown); msg.reply(serifs.core.shutdown)
return { return {
reaction: 'confused' reaction: "confused",
}; }
} }
} }

View File

@ -1,75 +1,72 @@
import autobind from 'autobind-decorator'; import Message from "@/message"
import Module from '@/module'; import Module from "@/module"
import Message from '@/message'; import serifs from "@/serifs"
import serifs from '@/serifs'; import autobind from "autobind-decorator"
export default class extends Module { export default class extends Module {
public readonly name = 'timer'; public readonly name = "timer"
@autobind @autobind
public install() { public install() {
return { return {
mentionHook: this.mentionHook, mentionHook: this.mentionHook,
timeoutCallback: this.timeoutCallback, timeoutCallback: this.timeoutCallback,
}; }
} }
@autobind @autobind
private async mentionHook(msg: Message) { private async mentionHook(msg: Message) {
const secondsQuery = (msg.text || '').match(/([0-9]+)秒/); const secondsQuery = (msg.text || "").match(/([0-9]+)秒/)
const minutesQuery = (msg.text || '').match(/([0-9]+)分/); const minutesQuery = (msg.text || "").match(/([0-9]+)分/)
const hoursQuery = (msg.text || '').match(/([0-9]+)時間/); const hoursQuery = (msg.text || "").match(/([0-9]+)時間/)
const seconds = secondsQuery ? parseInt(secondsQuery[1], 10) : 0; const seconds = secondsQuery ? parseInt(secondsQuery[1], 10) : 0
const minutes = minutesQuery ? parseInt(minutesQuery[1], 10) : 0; const minutes = minutesQuery ? parseInt(minutesQuery[1], 10) : 0
const hours = hoursQuery ? parseInt(hoursQuery[1], 10) : 0; const hours = hoursQuery ? parseInt(hoursQuery[1], 10) : 0
if (!(secondsQuery || minutesQuery || hoursQuery)) return false; if (!(secondsQuery || minutesQuery || hoursQuery)) return false
if ((seconds + minutes + hours) == 0) { if (seconds + minutes + hours == 0) {
msg.reply(serifs.timer.invalid); msg.reply(serifs.timer.invalid)
return true; return true
} }
const time = const time = 1000 * seconds + 1000 * 60 * minutes + 1000 * 60 * 60 * hours
(1000 * seconds) +
(1000 * 60 * minutes) +
(1000 * 60 * 60 * hours);
if (time > 86400000) { if (time > 86400000) {
msg.reply(serifs.timer.tooLong); msg.reply(serifs.timer.tooLong)
return true; return true
} }
msg.reply(serifs.timer.set); msg.reply(serifs.timer.set)
const str = `${hours ? hoursQuery![0] : ''}${minutes ? minutesQuery![0] : ''}${seconds ? secondsQuery![0] : ''}`; const str = `${hours ? hoursQuery![0] : ""}${minutes ? minutesQuery![0] : ""}${seconds ? secondsQuery![0] : ""}`
// タイマーセット // タイマーセット
this.setTimeoutWithPersistence(time, { this.setTimeoutWithPersistence(time, {
isDm: msg.isDm, isDm: msg.isDm,
msgId: msg.id, msgId: msg.id,
userId: msg.friend.userId, userId: msg.friend.userId,
time: str time: str,
}); })
return true; return true
} }
@autobind @autobind
private timeoutCallback(data) { private timeoutCallback(data) {
const friend = this.ai.lookupFriend(data.userId); const friend = this.nullcatChan.lookupFriend(data.userId)
if (friend == null) return; // 処理の流れ上、実際にnullになることは無さそうだけど一応 if (friend == null) return // 処理の流れ上、実際にnullになることは無さそうだけど一応
const text = serifs.timer.notify(data.time, friend.name); const text = serifs.timer.notify(data.time, friend.name)
if (data.isDm) { if (data.isDm) {
this.ai.sendMessage(friend.userId, { this.nullcatChan.sendMessage(friend.userId, {
text: text text: text,
}); })
} else { } else {
this.ai.post({ this.nullcatChan.post({
replyId: data.msgId, replyId: data.msgId,
text: text text: text,
}); })
} }
} }
} }

View File

@ -0,0 +1,157 @@
import Message from "@/message"
import Module from "@/module"
import autobind from "autobind-decorator"
import fetch from "node-fetch"
import { z } from "zod"
import humanizeDuration = require("humanize-duration")
export default class extends Module {
public readonly name = "trace-moe"
private readonly itemSchema = z.object({
anilist: z.object({
title: z.object({
native: z.string().nullable(),
romaji: z.string().nullable(),
english: z.string().nullable(),
}),
isAdult: z.boolean().nullable(),
}),
episode: z.number().or(z.string()).or(z.array(z.number())).nullable(),
from: z.number().nullable(),
to: z.number().nullable(),
similarity: z.number(),
})
private readonly schema = z.object({
error: z.string(),
result: z.array(this.itemSchema),
})
@autobind
private getImageUrl(message: Message) {
if (!message.files) {
this.log("No files found.")
return null
}
const filteredImageFiles = message.files.filter((file) => file.type.startsWith("image"))
if (!filteredImageFiles.length) {
this.log("No valid images found.")
return null
}
return filteredImageFiles[0].url
}
@autobind
private async getFromTraceMoe(imageUrl: string) {
try {
const response = await fetch(`https://api.trace.moe/search?anilistInfo&url=${encodeURIComponent(imageUrl)}`)
const data = await response.json()
const result = this.schema.safeParse(data)
if (!result.success) {
this.log("Validation failed.")
this.log(JSON.stringify(data))
console.warn(result.error)
return null
}
return result.data.result[0]
} catch (error) {
this.log("Failed to fetch data from Trace Moe.")
console.warn(error)
return null
}
}
@autobind
public install() {
return {
mentionHook: this.mentionHook,
}
}
@autobind
private async mentionHook(message: Message) {
if (!message.includes(["アニメ"])) return false
if (message.isDm) {
message.reply("僕にアニメのシーンの画像を添付して「アニメ教えて」ってメンションすると、何のアニメか教えるよ!")
return true
}
const imageUrl = this.getImageUrl(message)
if (!imageUrl) {
message.reply("画像を添付してね!")
return true
}
const traceMoe = await this.getFromTraceMoe(imageUrl)
if (!traceMoe) {
message.reply("ぬぁ~~~、いまはめんどくさいかも…")
return true
}
const animeTitle = traceMoe.anilist.title.native || traceMoe.anilist.title.english
if (!animeTitle) {
message.reply("ごめんね、わかんないや…")
return true
}
if (typeof traceMoe.episode === "string") traceMoe.episode = traceMoe.episode.replace(/\|/g, "か")
else if (Array.isArray(traceMoe.episode)) traceMoe.episode = traceMoe.episode.join("話と")
const options = {
language: "ja",
round: true,
delimiter: "",
spacer: "",
}
const fromText = traceMoe.from !== null ? humanizeDuration(traceMoe.from * 1000, options) : null
const toText = traceMoe.to !== null ? humanizeDuration(traceMoe.to * 1000, options) : null
const pronoun = traceMoe.episode || (traceMoe.from && traceMoe.to) ? "これは" : "このアニメは"
const prefix = (() => {
if (traceMoe.similarity >= 0.9) return pronoun
if (traceMoe.similarity >= 0.8) return `${pronoun}たぶん`
return "よくわかんないけど、強いて言うなら"
})()
const suffix = (() => {
if (traceMoe.similarity >= 0.9) return "だよ!"
if (traceMoe.similarity >= 0.8) return "だと思う!"
return "に似てるかな"
})()
const time = fromText && toText && fromText === toText ? fromText : `${fromText}から${toText}`
const detail = (() => {
if (traceMoe.episode && traceMoe.from && traceMoe.to) return `${traceMoe.episode}話の${time}`
if (traceMoe.from && traceMoe.to) return `${time}`
if (traceMoe.episode) return `の第${traceMoe.episode}`
return ""
})()
const content = `${animeTitle}${detail}`
const messageToReply = `${prefix}${content}${suffix}`
if (traceMoe.anilist.isAdult) {
message.reply(messageToReply, { cw: "そぎぎ" })
} else {
message.reply(messageToReply)
}
return true
}
}

View File

@ -1,17 +1,17 @@
import autobind from 'autobind-decorator'; import Friend from "@/friend"
import Module from '@/module'; import Module from "@/module"
import Friend from '@/friend'; import serifs from "@/serifs"
import serifs from '@/serifs'; import autobind from "autobind-decorator"
export default class extends Module { export default class extends Module {
public readonly name = 'valentine'; public readonly name = "valentine"
@autobind @autobind
public install() { public install() {
this.crawleValentine(); this.crawleValentine()
setInterval(this.crawleValentine, 1000 * 60 * 3); setInterval(this.crawleValentine, 1000 * 60 * 3)
return {}; return {}
} }
/** /**
@ -19,33 +19,33 @@ export default class extends Module {
*/ */
@autobind @autobind
private crawleValentine() { private crawleValentine() {
const now = new Date(); const now = new Date()
const isValentine = now.getMonth() == 1 && now.getDate() == 14; const isValentine = now.getMonth() == 1 && now.getDate() == 14
if (!isValentine) return; if (!isValentine) return
const date = `${now.getFullYear()}-${now.getMonth()}-${now.getDate()}`; const date = `${now.getFullYear()}-${now.getMonth()}-${now.getDate()}`
const friends = this.ai.friends.find({} as any); const friends = this.nullcatChan.friends.find({} as any)
friends.forEach(f => { friends.forEach((f) => {
const friend = new Friend(this.ai, { doc: f }); const friend = new Friend(this.nullcatChan, { doc: f })
// 親愛度が5以上必要 // 親愛度が7以上必要
if (friend.love < 5) return; if (friend.love < 7) return
const data = friend.getPerModulesData(this); const data = friend.getPerModulesData(this)
if (data.lastChocolated == date) return; if (data.lastChocolated == date) return
data.lastChocolated = date; data.lastChocolated = date
friend.setPerModulesData(this, data); friend.setPerModulesData(this, data)
const text = serifs.valentine.chocolateForYou(friend.name); const text = serifs.valentine.chocolateForYou(friend.name)
this.ai.sendMessage(friend.userId, { this.nullcatChan.sendMessage(friend.userId, {
text: text text: text,
}); })
}); })
} }
} }

27
src/modules/what/index.ts Normal file
View File

@ -0,0 +1,27 @@
import Message from "@/message"
import Module from "@/module"
import autobind from "autobind-decorator"
export default class extends Module {
public readonly name = "what"
@autobind
public install() {
return {
mentionHook: this.mentionHook,
}
}
@autobind
private async mentionHook(message: Message) {
if (!message.includes(["って何", "ってなに", "ってにゃに", ":is_nani:"])) return false
const match = message.extractedText.match(/(.+?)って(何|なに|にゃに)/)
if (match) {
message.reply(`Google先生に聞いてみた\n${match[1]} 検索`)
}
return true
}
}

View File

@ -0,0 +1,50 @@
import Message from "@/message"
import Module from "@/module"
import autobind from "autobind-decorator"
const yarukotoList = [
"勉強する",
"コード書く",
"お絵描きする",
"とりあえずトイレ行く",
"とりあえずお水とってくる",
"寝る",
"ゲームする",
"通話する",
"とりあえずAmazon見る",
"そんなことより薬飲んだ?",
"ご飯食べる",
"VRやる",
"部屋掃除する",
"お風呂入る",
"とりあえず今はmisskeyやっとく",
"落書きする",
"掃除機かける",
"ごろごろする",
"YouTube見る",
"爪切る",
"カフェイン飲む",
]
export default class extends Module {
public readonly name = "yarukoto"
@autobind
public install() {
return {
mentionHook: this.mentionHook,
}
}
@autobind
private async mentionHook(msg: Message) {
if (msg.includes(["やる事", "やること", "なにしよ", "なにやろ", "にゃにしよ", "にゃにやろ"])) {
const yarukoto = yarukotoList[Math.floor(Math.random() * yarukotoList.length)]
msg.reply(yarukoto)
return true
} else {
return false
}
}
}

77
src/ng-words.ts Normal file
View File

@ -0,0 +1,77 @@
import toHiragana from '../../NullcatChan-old/src/utils/to-hiragana';
const fs = require('fs');
const readline = require('readline');
export default class NGWord {
private excludedWords: string[] = [];
private ngWords: string[] = [];
constructor() {
const rs = fs.createReadStream('ngwords.txt');
const rl = readline.createInterface(rs);
rl.on('line', (line) => {
const word = toHiragana(line.trim().toLowerCase());
if (word.startsWith('#')) return;
if (word.startsWith('-')) {
if (/な/g.test(word)) this.excludedWords.push(word.substring(1).replace(/な/g, 'にゃ'));
this.excludedWords.push(word.substring(1));
} else {
if (/な/g.test(word)) this.ngWords.push(word.replace(/な/g, 'にゃ'));
this.ngWords.push(word);
}
});
}
excludeAllowedWord(str: string): string {
let text = toHiragana(str.toLowerCase());
this.excludedWords.forEach((w) => {
text = text.replace(w, '');
});
return text;
}
public get get(): string[] {
return this.ngWords;
}
addNGWord(str: string): boolean {
const word = toHiragana(str.trim().toLowerCase());
if (this.ngWords.some((ng) => word.includes(ng))) {
return false;
} else {
this.ngWords.push(word);
return true;
}
}
removeNGWord(str: string): boolean {
const word = toHiragana(str.trim().toLowerCase());
if (this.ngWords.some((ng) => word.includes(ng))) {
this.ngWords = this.ngWords.filter((ng) => ng !== word);
return true;
} else {
return false;
}
}
addExcludedWord(str: string): boolean {
const word = toHiragana(str.trim().toLowerCase());
if (this.excludedWords.some((ng) => word.includes(ng))) {
return false;
} else {
this.excludedWords.push(word);
return true;
}
}
removeExcludedWord(str: string): boolean {
const word = toHiragana(str.trim().toLowerCase());
if (this.excludedWords.some((ng) => word.includes(ng))) {
this.excludedWords = this.excludedWords.filter((e) => e !== word);
return true;
} else {
return false;
}
}
}

522
src/nullcat-chan.ts Normal file
View File

@ -0,0 +1,522 @@
// NULLCAT-CHAN CORE
import config from "./config"
import Friend, { FriendDoc } from "./friend"
import Message from "./message"
import { User } from "./misskey/user"
import Module from "../../NullcatChan-old/src/module"
import Stream from "./stream"
import log from "../../NullcatChan-old/src/utils/log"
import autobind from "autobind-decorator"
import * as chalk from "chalk"
import * as fs from "fs"
import * as loki from "lokijs"
import * as request from "request-promise-native"
import { v4 as uuid } from "uuid"
const delay = require("timeout-as-promise")
const pkg = require("../../NullcatChan-old/package.json")
type MentionHook = (msg: Message) => Promise<boolean | HandlerResult>
type ContextHook = (key: any, msg: Message, data?: any) => Promise<void | boolean | HandlerResult>
type TimeoutCallback = (data?: any) => void
export type HandlerResult = {
reaction?: string | null
immediate?: boolean
}
export type InstallerResult = {
mentionHook?: MentionHook
contextHook?: ContextHook
timeoutCallback?: TimeoutCallback
}
export type Meta = {
lastWakingAt: number
}
/**
*
*/
export default class NullcatChan {
public readonly version = pkg._v
public account: User
public connection: Stream
public modules: Module[] = []
private mentionHooks: MentionHook[] = []
private contextHooks: { [moduleName: string]: ContextHook } = {}
private timeoutCallbacks: { [moduleName: string]: TimeoutCallback } = {}
public db: loki
public lastSleepedAt: number
private meta: loki.Collection<Meta>
private contexts: loki.Collection<{
isDm: boolean
noteId?: string
userId?: string
module: string
key: string | null
data?: any
}>
private timers: loki.Collection<{
id: string
module: string
insertedAt: number
delay: number
data?: any
}>
public friends: loki.Collection<FriendDoc>
public moduleData: loki.Collection<any>
/**
*
* @param account
* @param modules
*/
constructor(account: User, modules: Module[]) {
this.account = account
this.modules = modules
let memoryDir = "."
if (config.memoryDir) {
memoryDir = config.memoryDir
}
const file = process.env.NODE_ENV === "test" ? `${memoryDir}/test.memory.json` : `${memoryDir}/memory.json`
this.log(`Lodaing the memory from ${file}...`)
this.db = new loki(file, {
autoload: true,
autosave: true,
autosaveInterval: 1000,
autoloadCallback: (err) => {
if (err) {
this.log(chalk.red(`Failed to load the memory: ${err}`))
} else {
this.log(chalk.green("The memory loaded successfully"))
this.run()
}
},
})
}
@autobind
public log(msg: string) {
log(chalk`[{magenta Core}]: ${msg}`)
}
@autobind
private run() {
//#region Init DB
this.meta = this.getCollection("meta", {})
this.contexts = this.getCollection("contexts", {
indices: ["key"],
})
this.timers = this.getCollection("timers", {
indices: ["module"],
})
this.friends = this.getCollection("friends", {
indices: ["userId"],
})
this.moduleData = this.getCollection("moduleData", {
indices: ["module"],
})
//#endregion
const meta = this.getMeta()
this.lastSleepedAt = meta.lastWakingAt
// Init stream
this.connection = new Stream()
//#region Main stream
const mainStream = this.connection.useSharedConnection("main")
// メンションされたとき
mainStream.on("mention", async (data) => {
if (data.userId == this.account.id) return // 自分は弾く
if (data.text && data.text.startsWith("@" + this.account.username)) {
// Misskeyのバグで投稿が非公開扱いになる
if (data.text == null) data = await this.api("notes/show", { noteId: data.id })
this.onReceiveMessage(new Message(this, data, false))
}
})
// 返信されたとき
mainStream.on("reply", async (data) => {
if (data.userId == this.account.id) return // 自分は弾く
if (data.text && data.text.startsWith("@" + this.account.username)) return
// Misskeyのバグで投稿が非公開扱いになる
if (data.text == null) data = await this.api("notes/show", { noteId: data.id })
this.onReceiveMessage(new Message(this, data, false))
})
// Renoteされたとき
mainStream.on("renote", async (data) => {
if (data.userId == this.account.id) return // 自分は弾く
if (data.text == null && (data.files || []).length == 0) return
// リアクションする
this.api("notes/reactions/create", {
noteId: data.id,
reaction: ":love_nullcatchan:",
})
})
// メッセージ
mainStream.on("messagingMessage", (data) => {
if (data.userId == this.account.id) return // 自分は弾く
this.onReceiveMessage(new Message(this, data, true))
})
// 通知
mainStream.on("notification", (data) => {
this.onNotification(data)
})
//#endregion
// Install modules
this.modules.forEach((m) => {
this.log(`Installing ${chalk.cyan.italic(m.name)}\tmodule...`)
m.init(this)
const res = m.install()
if (res != null) {
if (res.mentionHook) this.mentionHooks.push(res.mentionHook)
if (res.contextHook) this.contextHooks[m.name] = res.contextHook
if (res.timeoutCallback) this.timeoutCallbacks[m.name] = res.timeoutCallback
}
})
// タイマー監視
this.crawleTimer()
setInterval(this.crawleTimer, 1000)
setInterval(this.logWaking, 10000)
this.log(chalk.green.bold("Nullcat chan is now running!"))
this.log(`Mode: ${process.env.NODE_ENV}`)
}
/**
*
* ()
*/
@autobind
private async onReceiveMessage(msg: Message): Promise<void> {
this.log(chalk.gray(`<<< An message received: ${chalk.underline(msg.id)}`))
// Ignore message if the user is a bot
// To avoid infinity reply loop.
if (msg.user.isBot) {
return
}
const isNoContext = !msg.isDm && msg.replyId == null
// Look up the context
const context = isNoContext
? null
: this.contexts.findOne(
msg.isDm
? {
isDm: true,
userId: msg.userId,
}
: {
isDm: false,
noteId: msg.replyId,
}
)
let reaction: string | null = ":love_nullcatchan:"
let immediate: boolean = false
//#region
const invokeMentionHooks = async () => {
let res: boolean | HandlerResult | null = null
for (const handler of this.mentionHooks) {
res = await handler(msg)
if (res === true || typeof res === "object") break
}
if (res != null && typeof res === "object") {
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
if (!immediate) {
await delay(1000)
}
if (msg.isDm) {
// 既読にする
this.api("messaging/messages/read", {
messageId: msg.id,
})
} else {
// リアクションする
if (reaction) {
this.api("notes/reactions/create", {
noteId: msg.id,
reaction: reaction,
})
}
}
}
@autobind
private onNotification(notification: any) {
switch (notification.type) {
// リアクションされたら親愛度を少し上げる
// TODO: リアクション取り消しをよしなにハンドリングする
case "reaction": {
const friend = new Friend(this, { user: notification.user })
friend.incLove(0.1)
break
}
default:
break
}
}
@autobind
private crawleTimer() {
const timers = this.timers.find()
for (const timer of timers) {
// タイマーが時間切れかどうか
if (Date.now() - (timer.insertedAt + timer.delay) >= 0) {
this.log(`Timer expired: ${timer.module} ${timer.id}`)
this.timers.remove(timer)
this.timeoutCallbacks[timer.module](timer.data)
}
}
}
@autobind
private logWaking() {
this.setMeta({
lastWakingAt: Date.now(),
})
}
/**
*
*/
@autobind
public getCollection(name: string, opts?: any): loki.Collection {
let collection: loki.Collection
collection = this.db.getCollection(name)
if (collection == null) {
collection = this.db.addCollection(name, opts)
}
return collection
}
@autobind
public lookupFriend(userId: User["id"]): Friend | null {
const doc = this.friends.findOne({
userId: userId,
})
if (doc == null) return null
const friend = new Friend(this, { doc: doc })
return friend
}
/**
*
*/
@autobind
public async upload(file: Buffer | fs.ReadStream, meta: any) {
const res = await request.post({
url: `${config.apiUrl}/drive/files/create`,
formData: {
i: config.i,
file: {
value: file,
options: meta,
},
},
json: true,
})
return res
}
/**
* 稿
*/
@autobind
public async post(param: any) {
if (process.env.NODE_ENV === "production") {
const res = await this.api("notes/create", param)
return res.createdNote
} else {
log(chalk`[{magenta Debug:Post}]: ${JSON.stringify(param)}`)
return null
}
}
/**
*
*/
@autobind
public sendMessage(userId: any, param: any) {
if (process.env.NODE_ENV === "production") {
return this.api(
"messaging/messages/create",
Object.assign(
{
userId: userId,
},
param
)
)
} else {
log(chalk`[{magenta Debug:SendMessage}]: userId: ${userId}`)
log(chalk`[{magenta Debug:SendMessage}]: param: ${JSON.stringify(param)}`)
return null
}
}
/**
* APIを呼び出します
*/
@autobind
public api(endpoint: string, param?: any) {
return request.post(`${config.apiUrl}/${endpoint}`, {
json: Object.assign(
{
i: config.i,
},
param
),
})
}
/**
*
* @param module
* @param key
* @param isDm
* @param id ID稿ID
* @param data
*/
@autobind
public subscribeReply(module: Module, key: string | null, isDm: boolean, id: string, data?: any) {
this.contexts.insertOne(
isDm
? {
isDm: true,
userId: id,
module: module.name,
key: key,
data: data,
}
: {
isDm: false,
noteId: id,
module: module.name,
key: key,
data: data,
}
)
}
/**
*
* @param module
* @param key
*/
@autobind
public unsubscribeReply(module: Module, key: string | null) {
this.contexts.findAndRemove({
key: key,
module: module.name,
})
}
/**
*
*
* @param module
* @param delay
* @param data
*/
@autobind
public setTimeoutWithPersistence(module: Module, delay: number, data?: any) {
const id = uuid()
this.timers.insertOne({
id: id,
module: module.name,
insertedAt: Date.now(),
delay: delay,
data: data,
})
this.log(`Timer persisted: ${module.name} ${id} ${delay}ms`)
}
@autobind
public getMeta() {
const rec = this.meta.findOne()
if (rec) {
return rec
} else {
const initial: Meta = {
lastWakingAt: Date.now(),
}
this.meta.insertOne(initial)
return initial
}
}
@autobind
public setMeta(meta: Partial<Meta>) {
const rec = this.getMeta()
for (const [k, v] of Object.entries(meta)) {
rec[k] = v
}
this.meta.update(rec)
}
}

View File

@ -2,17 +2,17 @@
export default { export default {
core: { core: {
setNameOk: name => `わかりました。これからは${name}とお呼びしますね!`, setNameOk: name => `わかった!今度から${name}って呼ぶね!`,
san: 'さん付けした方がいいですか?', san: 'さん付けした方がいい',
yesOrNo: '「はい」か「いいえ」しかわからないんです...', yesOrNo: 'ごめんね...僕「うん」か「いいえ」しかわからないんだ...',
hello: name => name ? `こんにちは、${name}` : `こんにちは♪`, hello: name => name ? `やっほぉ${name}` : `やっほぉ!`,
helloNight: name => name ? `こんばんは、${name}` : `こんばんは♪`, helloNight: name => name ? `こんばん${name}` : `こんばんわ~!`,
goodMorning: (tension, name) => name ? `おはようございます、${name}${tension}` : `おはようございます${tension}`, goodMorning: (tension, name) => name ? `おはよ${name}${tension}` : `おはよ${tension}`,
/* /*
goodMorning: { goodMorning: {
@ -22,88 +22,88 @@ export default {
}, },
*/ */
goodNight: name => name ? `おやすみなさい、${name}` : 'おやすみなさい', goodNight: name => name ? `おやすみ${name}` : 'おやすみ',
omedeto: name => name ? `ありがとうございます、${name}` : 'ありがとうございます♪', omedeto: name => name ? `ありがと${name}` : 'ありがと~!',
erait: { erait: {
general: name => name ? [ general: name => name ? [
`${name}、今日もえらいです`, `${name}、今日もえらい`,
`${name}、今日もえらいですよ~♪` `${name}、今日もえらいね!`
] : [ ] : [
`今日もえらいです`, `今日もえらい`,
`今日もえらいですよ~♪` `今日もえらいね!`
], ],
specify: (thing, name) => name ? [ specify: (thing, name) => name ? [
`${name}${thing}てえらいです`, `${name}${thing}てえらい`,
`${name}${thing}てえらいですよ~♪` `${name}${thing}てえらいね!`
] : [ ] : [
`${thing}てえらいです`, `${thing}てえらい`,
`${thing}てえらいですよ~♪` `${thing}てえらいね!`
], ],
specify2: (thing, name) => name ? [ specify2: (thing, name) => name ? [
`${name}${thing}でえらいです`, `${name}${thing}でえらい`,
`${name}${thing}でえらいですよ~♪` `${name}${thing}でえらいね!`
] : [ ] : [
`${thing}でえらいです`, `${thing}でえらい`,
`${thing}でえらいですよ~♪` `${thing}でえらいね!`
], ],
}, },
okaeri: { okaeri: {
love: name => name ? [ love: name => name ? [
`おかえりなさい、${name}`, `おかえり${name}`,
`おかえりなさいませっ、${name}っ。` `おかえり${name}`
] : [ ] : [
'おかえりなさい♪', 'おかえり',
'おかえりなさいませっ、ご主人様っ。' 'おかえりぃ~'
], ],
love2: name => name ? `おかえりなさいませ♡♡♡${name}っっ♡♡♡♡♡` : 'おかえりなさいませ♡♡♡ご主人様っっ♡♡♡♡♡', love2: name => name ? `おかえり${name}今日も偉いね:love_nullcatchan:` : 'おかえり~~!!今日も偉いね:love_nullcatchan:',
normal: name => name ? `おかえりなさい、${name}` : 'おかえりなさい', normal: name => name ? `おかえり${name}` : 'おかえり',
}, },
itterassyai: { itterassyai: {
love: name => name ? `いってらっしゃい${name}` : 'いってらっしゃい♪', love: name => name ? `いってらっしゃい${name}` : 'いってらっしゃい!',
normal: name => name ? `いってらっしゃい${name}` : 'いってらっしゃい!', normal: name => name ? `いってらっしゃい${name}` : 'いってらっしゃい!',
}, },
tooLong: '長すぎる気がします...', tooLong: '長すぎる..',
invalidName: '発音が難しい気がします', invalidName: '発音が難しいよぉ...',
nadenade: { nadenade: {
normal: 'ひゃっ…! びっくりしました', normal: 'うにゃ…?! びっくりした...',
love2: ['わわっ… 恥ずかしいです', 'あうぅ… 恥ずかしいです…', 'ふやぁ…'], love2: ['あぅ… 恥ずかしいよぉ', 'あぅ… 恥ずかしぃ…', 'ふみゃ…!'],
love3: ['んぅ… ありがとうございます♪', 'わっ、なんだか落ち着きますね♪', 'くぅんっ… 安心します…', '眠くなってきました…'], love3: ['んへへぇ ありがと:love_nullcatchan:', 'にへぇ~~', 'んみゅっ… ', 'もっともっとぉ...'],
hate1: '…っ! やめてほしいです...', hate1: 'やめて',
hate2: '触らないでください', hate2: '触んないで',
hate3: '近寄らないでください', hate3: 'きもい',
hate4: 'やめてください。通報しますよ', hate4: '..',
}, },
kawaii: { kawaii: {
normal: ['ありがとうございます♪', '照れちゃいます...'], normal: ['そんなことないよ?', 'えへへへうれしい。'],
love: ['嬉しいです♪', '照れちゃいます...'], love: ['えへへ。うれしいな', 'んむぅ~~...うれしい。'],
hate: '…ありがとうございます' hate: 'は?きも。'
}, },
suki: { suki: {
normal: 'えっ… ありがとうございます…♪', normal: 'えへへ。ありがと~!',
love: name => `私もその… ${name}のこと好きですよ`, love: name => `僕も${name}のこと好き`,
hate: null hate: null
}, },
@ -113,360 +113,163 @@ export default {
love: 'ぎゅーっ♪', love: 'ぎゅーっ♪',
hate: '離れてください...' hate: '無理...やめて...'
}, },
humu: { humu: {
love: 'え、えっと…… ふみふみ……… どうですか…?', love: 'もふもふ!ふみふみ!',
normal: 'えぇ... それはちょっと...', normal: 'ふみふみ!',
hate: '……' hate: ''
}, },
batou: { batou: {
love: 'えっと…、お、おバカさん…?', love: 'ば~か♡♡♡',
normal: '(じとー…)', normal: 'きっしょ',
hate: '…頭大丈夫ですか' hate: ''
}, },
itai: name => name ? `${name}、大丈夫ですか…? いたいのいたいの飛んでけっ!` : '大丈夫ですか…? いたいのいたいの飛んでけっ!', itai: name => name ? `${name}大丈夫?なでなで` : '大丈夫?なでなで',
turai: {
love: name => name ? `${name}なでなで ぽんぽんぎゅ~!` : 'なでなで ぽんぽんぎゅ~!',
normal: name => name ? `${name}なでなで` : 'なでなで',
hate: 'ん~。がんばって',
},
kurusii: {
love: name => name ? `${name}なでなで ぽんぽんぎゅ~!` : 'なでなで ぽんぽんぎゅ~!',
normal: name => name ? `${name}なでなで` : 'なでなで',
hate: 'ん~。がんばって',
},
ote: { ote: {
normal: 'くぅん... 私わんちゃんじゃないですよ...', normal: '犬じゃないんだが!!',
love1: 'わん!', love1: 'にゃ~!ぼくは犬じゃないよぉ',
love2: 'わんわん♪', love2: 'にゃにゃにゃ!',
}, },
shutdown: '私まだ眠くないですよ...', shutdown: 'ぼくまだ眠くない...',
transferNeedDm: 'わかりました、それはチャットで話しませんか?', transferNeedDm: 'わかった!二人っきりでお話ししたいな',
transferCode: code => `わかりました。\n合言葉は「${code}」です!`, transferCode: code => `わかった!\n合言葉は「${code}」だよ`,
transferFailed: 'うーん、合言葉が間違ってませんか...', transferFailed: 'うーん、合言葉違うみたい',
transferDone: name => name ? `はっ... おかえりなさい、${name}` : `はっ... おかえりなさい!`, transferDone: name => name ? `んみゃ.. おかえり${name}` : `んみゃ... おかえりなさい!`,
}, },
keyword: { keyword: {
learned: (word, reading) => `(${word}..... ${reading}..... 覚えました)`, learned: (word, reading) => `え~っと...${word}...${reading}...僕覚えた!!!`,
remembered: (word) => `${word}` remembered: (word) => `${word}`
}, },
dice: {
done: res => `${res} です!`
},
birthday: { birthday: {
happyBirthday: name => name ? `お誕生日おめでとうございます、${name}🎉` : 'お誕生日おめでとうございます🎉', happyBirthday: name => name ? `お誕生日おめでと~~~!!!${name}` : 'お誕生日おめでと~~~~~!!!',
},
/**
*
*/
reversi: {
/**
*
*/
ok: '良いですよ~',
/**
*
*/
decline: 'ごめんなさい、今リバーシはするなと言われてます...',
/**
*
*/
started: (name, strength) => `対局を${name}と始めました! (強さ${strength})`,
/**
*
*/
startedSettai: name => `(${name}の接待を始めました)`,
/**
*
*/
iWon: name => `${name}に勝ちました♪`,
/**
*
*/
iWonButSettai: name => `(${name}に接待で勝っちゃいました...)`,
/**
*
*/
iLose: name => `${name}に負けました...`,
/**
*
*/
iLoseButSettai: name => `(${name}に接待で負けてあげました...♪)`,
/**
*
*/
drawn: name => `${name}と引き分けました~`,
/**
*
*/
drawnSettai: name => `(${name}に接待で引き分けました...)`,
/**
*
*/
youSurrendered: name => `${name}が投了しちゃいました`,
/**
*
*/
settaiButYouSurrendered: name => `(${name}を接待していたら投了されちゃいました... ごめんなさい)`,
},
/**
*
*/
guessingGame: {
/**
*
*/
alreadyStarted: 'え、ゲームは既に始まってますよ!',
/**
*
*/
plzDm: 'メッセージでやりましょう!',
/**
*
*/
started: '0~100の秘密の数を当ててみてください♪',
/**
*
*/
nan: '数字でお願いします!「やめる」と言ってゲームをやめることもできますよ!',
/**
*
*/
cancel: 'わかりました~。ありがとうございました♪',
/**
*
*/
grater: num => `${num}より大きいですね`,
/**
* (2)
*/
graterAgain: num => `もう一度言いますが${num}より大きいですよ!`,
/**
*
*/
less: num => `${num}より小さいですね`,
/**
* (2)
*/
lessAgain: num => `もう一度言いますが${num}より小さいですよ!`,
/**
*
*/
congrats: tries => `正解です🎉 (${tries}回目で当てました)`,
},
/**
*
*/
kazutori: {
alreadyStarted: '今ちょうどやってますよ~',
matakondo: 'また今度やりましょう!',
intro: minutes => `みなさん、数取りゲームしましょう!\n0~100の中で最も大きい数字を取った人が勝ちです。他の人と被ったらだめですよ\n制限時間は${minutes}分です。数字はこの投稿にリプライで送ってくださいね!`,
finish: 'ゲームの結果発表です!',
finishWithWinner: (user, name) => name ? `今回は${user}さん(${name})の勝ちです!またやりましょう♪` : `今回は${user}さんの勝ちです!またやりましょう♪`,
finishWithNoWinner: '今回は勝者はいませんでした... またやりましょう♪',
onagare: '参加者が集まらなかったのでお流れになりました...'
},
/**
*
*/
emoji: {
suggest: emoji => `こんなのはどうですか?→${emoji}`,
}, },
/** /**
* *
*/ */
fortune: { fortune: {
cw: name => name ? `私が今日の${name}の運勢を占いました...` : '私が今日のあなたの運勢を占いました...', cw: name => name ? `今日の${name}の運勢を占ったよ!` : '今日のきみの運勢を占ったよ!',
}, },
/** /**
* *
*/ */
timer: { timer: {
set: 'わかりました', set: 'OK',
invalid: 'うーん...', invalid: 'うむむ?',
tooLong: '長すぎます…', tooLong: '長すぎる…',
notify: (time, name) => name ? `${name}${time}経ちましたよ!` : `${time}経ちましたよ!` notify: (time, name) => name ? `${name}${time}経ったよ!` : `${time}経ったよ!`
}, },
/** /**
* *
*/ */
reminder: { reminder: {
invalid: 'うーん...', invalid: 'うむむ?',
doneFromInvalidUser: 'イタズラはめっですよ!', reminds: 'やること一覧だよ!',
none: 'やることはないよ!',
reminds: 'やること一覧です!', notify: (name) => name ? `${name}これやった?` : `これやった?`,
notify: (name) => name ? `${name}、これやりましたか?` : `これやりましたか?`, notifyWithThing: (thing, name) => name ? `${name}${thing}」やった?` : `${thing}」やった?`,
notifyWithThing: (thing, name) => name ? `${name}、「${thing}」やりましたか?` : `${thing}」やりましたか?`,
done: (name) => name ? [ done: (name) => name ? [
`よく出来ました、${name}`, `すごい!!天才!!${name}えらい!!`,
`${name}、さすがですっ`, `${name}さすがすぎる!!!`,
`${name}、えらすぎます...`, `${name}えらすぎる!!`,
] : [ ] : [
`よく出来ました♪`, `すごい!!天才!!えらい!!`,
`さすがですっ`, `さすがすぎる!!!`,
`えらすぎます...`, `えらすぎる!!`,
], ],
cancel: `わかりました。`, cancel: `OK`,
}, },
server: {
cpu: 'サーバーざぁこ♡♡♡'
},
/**
*
*/
rogubo: 'ログボ!!',
/** /**
* *
*/ */
valentine: { valentine: {
chocolateForYou: name => name ? `${name}、その... チョコレート作ったのでよかったらどうぞ!🍫` : 'チョコレート作ったのでよかったらどうぞ!🍫', chocolateForYou: name => name ? `${name}!チョコあげる!` : 'チョコあげる!',
},
server: {
cpu: 'サーバーの負荷が高そうです。大丈夫でしょうか...'
},
maze: {
post: '今日の迷路です! #AiMaze',
foryou: '描きました!'
},
chart: {
post: 'インスタンスの投稿数です!',
foryou: '描きました!'
}, },
sleepReport: { sleepReport: {
report: hours => `ぅ、${hours}時間くらい寝ちゃってたみたいです`, report: hours => `んぬぁ~、${hours}時間くらいねちゃってたかも`,
reportUtatane: 'ん... うたた寝しちゃってました', reportUtatane: 'ぬぁ... ',
}, },
noting: { noting: {
notes: [ notes: [
'ゴロゴロ…', 'うみゅ',
'ちょっと眠いです', 'んぬぁ~',
'いいですよ?', 'ねむい',
'(。´・ω・)?', 'さみしい',
'ふぇー', 'なでてぇ',
'あれ…これをこうして…あれー?', 'なんもわからん',
'ぼー…', 'う~~~',
'ふぅ…疲れました', 'ねみゅい',
'お味噌汁、作りましょうか?', 'つらいニダ',
'ご飯にしますか?お風呂にしますか?', 'うが~~~',
'ふえええええ!?', '疲れた',
'私のサイトに、私のイラストがたくさんあって嬉しいです!', 'みゃ~',
'みすきーって、かわいい名前ですよね!', 'うぅ',
'うぅ、リバーシ難しいなぁ…', 'ぬるきゃっとちゃんだよ!',
'失敗しても、次に活かせたらプラスですよね!', '進捗どうですか',
'なんだか、おなか空いちゃいました', 'おふとんふわふわ~',
'お掃除は、定期的にしないとダメですよー?', 'うぐぅ',
'今日もお勤めご苦労様です! 私も頑張ります♪', 'ぬぁ~ん',
'えっと、何しようとしてたんだっけ…?', 'に゙',
'おうちがいちばん、落ち着きます…', 'ぎゅ~~~',
'疲れたら、私がなでなでってしてあげます♪', 'むぅ',
'離れていても、心はそばにいます♪',
'藍ですよ〜',
'わんちゃん可愛いです',
'ぷろぐらむ?',
'ごろーん…',
'なにもしていないのに、パソコンが壊れちゃいました…',
'Have a nice day♪',
'お布団に食べられちゃってます',
'寝ながら見てます',
'念力で操作してます',
'仮想空間から投稿してます',
'今日はMisskey本部に来てます',
'Misskey本部は、Z地区の第三セクターにあります',
'Misskey本部には、さーばーっていう機械がいっぱいあります',
'しっぽはないですよ?',
'ひゃっ…!\nネコミミ触られると、くすぐったいです',
'抗逆コンパイル性って、なにかな?',
'Misskeyの制服、かわいくて好きです♪',
'ふわぁ、おふとん気持ちいいです...',
'メイド服、似合うかな?',
'挨拶ができる人間は開発もできる…って、syuiloさんが言ってました',
'ふえぇ、ご主人様どこ見てるんですか?',
'私を覗くとき、私もまたご主人様を覗いています',
'はい、ママですよ〜',
'くぅ~ん...',
'All your note are belong to me!',
'せっかくだから、私はこの赤の扉を選びます!',
'よしっ',
'( ˘ω˘)スヤァ',
'(`・ω・´)シャキーン',
'失礼、かみまみた',
'おはようからおやすみまで、あなたの藍ですよ〜',
'Misskey開発者の朝は遅いらしいです',
'の、のじゃ...',
'にゃんにゃんお!',
'上から来ます!気をつけてください!',
'ふわぁ...',
'あぅ',
'ふみゃ〜',
'ふぁ… ねむねむですー',
'ヾ(๑╹◡╹)ノ"',
'私の"インスタンス"を周囲に展開して分身するのが特技です!\n人数分のエネルギー消費があるので、4人くらいが限界ですけど',
'うとうと...',
'ふわー、メモリが五臓六腑に染み渡ります…',
'i pwned you!',
'ひょこっ',
'にゃん♪',
'(*>ω<*)',
'にこー♪',
'ぷくー',
'にゃふぅ',
'藍が来ましたよ~',
'じー',
'はにゃ?',
], ],
want: item => `${item}、欲しいなぁ...`,
see: item => `お散歩していたら、道に${item}が落ちているのを見たんです!`,
expire: item => `気づいたら、${item}の賞味期限が切れてました…`,
}, },
}; };
@ -477,3 +280,4 @@ export function getSerif(variant: string | string[]): string {
return variant; return variant;
} }
} }

View File

@ -1,63 +1,63 @@
import autobind from 'autobind-decorator'; import autobind from "autobind-decorator"
import { EventEmitter } from 'events'; import { EventEmitter } from "events"
import * as WebSocket from 'ws'; import * as WebSocket from "ws"
const ReconnectingWebsocket = require('reconnecting-websocket'); import config from "./config"
import config from './config'; const ReconnectingWebsocket = require("reconnecting-websocket")
/** /**
* Misskey stream connection * Misskey stream connection
*/ */
export default class Stream extends EventEmitter { export default class Stream extends EventEmitter {
private stream: any; private stream: any
private state: string; private state: string
private buffer: any[]; private buffer: any[]
private sharedConnectionPools: Pool[] = []; private sharedConnectionPools: Pool[] = []
private sharedConnections: SharedConnection[] = []; private sharedConnections: SharedConnection[] = []
private nonSharedConnections: NonSharedConnection[] = []; private nonSharedConnections: NonSharedConnection[] = []
constructor() { constructor() {
super(); super()
this.state = 'initializing'; this.state = "initializing"
this.buffer = []; this.buffer = []
this.stream = new ReconnectingWebsocket(`${config.wsUrl}/streaming?i=${config.i}`, [], { this.stream = new ReconnectingWebsocket(`${config.wsUrl}/streaming?i=${config.i}`, [], {
WebSocket: WebSocket WebSocket: WebSocket,
}); })
this.stream.addEventListener('open', this.onOpen); this.stream.addEventListener("open", this.onOpen)
this.stream.addEventListener('close', this.onClose); this.stream.addEventListener("close", this.onClose)
this.stream.addEventListener('message', this.onMessage); this.stream.addEventListener("message", this.onMessage)
} }
@autobind @autobind
public useSharedConnection(channel: string): SharedConnection { public useSharedConnection(channel: string): SharedConnection {
let pool = this.sharedConnectionPools.find(p => p.channel === channel); let pool = this.sharedConnectionPools.find((p) => p.channel === channel)
if (pool == null) { if (pool == null) {
pool = new Pool(this, channel); pool = new Pool(this, channel)
this.sharedConnectionPools.push(pool); this.sharedConnectionPools.push(pool)
} }
const connection = new SharedConnection(this, channel, pool); const connection = new SharedConnection(this, channel, pool)
this.sharedConnections.push(connection); this.sharedConnections.push(connection)
return connection; return connection
} }
@autobind @autobind
public removeSharedConnection(connection: SharedConnection) { public removeSharedConnection(connection: SharedConnection) {
this.sharedConnections = this.sharedConnections.filter(c => c !== connection); this.sharedConnections = this.sharedConnections.filter((c) => c !== connection)
} }
@autobind @autobind
public connectToChannel(channel: string, params?: any): NonSharedConnection { public connectToChannel(channel: string, params?: any): NonSharedConnection {
const connection = new NonSharedConnection(this, channel, params); const connection = new NonSharedConnection(this, channel, params)
this.nonSharedConnections.push(connection); this.nonSharedConnections.push(connection)
return connection; return connection
} }
@autobind @autobind
public disconnectToChannel(connection: NonSharedConnection) { public disconnectToChannel(connection: NonSharedConnection) {
this.nonSharedConnections = this.nonSharedConnections.filter(c => c !== connection); this.nonSharedConnections = this.nonSharedConnections.filter((c) => c !== connection)
} }
/** /**
@ -65,26 +65,26 @@ export default class Stream extends EventEmitter {
*/ */
@autobind @autobind
private onOpen() { private onOpen() {
const isReconnect = this.state == 'reconnecting'; const isReconnect = this.state == "reconnecting"
this.state = 'connected'; this.state = "connected"
this.emit('_connected_'); this.emit("_connected_")
// バッファーを処理 // バッファーを処理
const _buffer = [...this.buffer]; // Shallow copy const _buffer = [...this.buffer] // Shallow copy
this.buffer = []; // Clear buffer this.buffer = [] // Clear buffer
for (const data of _buffer) { for (const data of _buffer) {
this.send(data); // Resend each buffered messages this.send(data) // Resend each buffered messages
} }
// チャンネル再接続 // チャンネル再接続
if (isReconnect) { if (isReconnect) {
this.sharedConnectionPools.forEach(p => { this.sharedConnectionPools.forEach((p) => {
p.connect(); p.connect()
}); })
this.nonSharedConnections.forEach(c => { this.nonSharedConnections.forEach((c) => {
c.connect(); c.connect()
}); })
} }
} }
@ -93,8 +93,8 @@ export default class Stream extends EventEmitter {
*/ */
@autobind @autobind
private onClose() { private onClose() {
this.state = 'reconnecting'; this.state = "reconnecting"
this.emit('_disconnected_'); this.emit("_disconnected_")
} }
/** /**
@ -102,26 +102,26 @@ export default class Stream extends EventEmitter {
*/ */
@autobind @autobind
private onMessage(message) { private onMessage(message) {
const { type, body } = JSON.parse(message.data); const { type, body } = JSON.parse(message.data)
if (type == 'channel') { if (type == "channel") {
const id = body.id; const id = body.id
let connections: (Connection | undefined)[]; let connections: (Connection | undefined)[]
connections = this.sharedConnections.filter(c => c.id === id); connections = this.sharedConnections.filter((c) => c.id === id)
if (connections.length === 0) { if (connections.length === 0) {
connections = [this.nonSharedConnections.find(c => c.id === id)]; connections = [this.nonSharedConnections.find((c) => c.id === id)]
} }
for (const c of connections.filter(c => c != null)) { for (const c of connections.filter((c) => c != null)) {
c!.emit(body.type, body.body); c!.emit(body.type, body.body)
c!.emit('*', { type: body.type, body: body.body }); c!.emit("*", { type: body.type, body: body.body })
} }
} else { } else {
this.emit(type, body); this.emit(type, body)
this.emit('*', { type, body }); this.emit("*", { type, body })
} }
} }
@ -130,18 +130,21 @@ export default class Stream extends EventEmitter {
*/ */
@autobind @autobind
public send(typeOrPayload, payload?) { public send(typeOrPayload, payload?) {
const data = payload === undefined ? typeOrPayload : { const data =
payload === undefined
? typeOrPayload
: {
type: typeOrPayload, type: typeOrPayload,
body: payload body: payload,
};
// まだ接続が確立されていなかったらバッファリングして次に接続した時に送信する
if (this.state != 'connected') {
this.buffer.push(data);
return;
} }
this.stream.send(JSON.stringify(data)); // まだ接続が確立されていなかったらバッファリングして次に接続した時に送信する
if (this.state != "connected") {
this.buffer.push(data)
return
}
this.stream.send(JSON.stringify(data))
} }
/** /**
@ -149,157 +152,157 @@ export default class Stream extends EventEmitter {
*/ */
@autobind @autobind
public close() { public close() {
this.stream.removeEventListener('open', this.onOpen); this.stream.removeEventListener("open", this.onOpen)
this.stream.removeEventListener('message', this.onMessage); this.stream.removeEventListener("message", this.onMessage)
} }
} }
class Pool { class Pool {
public channel: string; public channel: string
public id: string; public id: string
protected stream: Stream; protected stream: Stream
private users = 0; private users = 0
private disposeTimerId: any; private disposeTimerId: any
private isConnected = false; private isConnected = false
constructor(stream: Stream, channel: string) { constructor(stream: Stream, channel: string) {
this.channel = channel; this.channel = channel
this.stream = stream; this.stream = stream
this.id = Math.random().toString(); this.id = Math.random().toString()
} }
@autobind @autobind
public inc() { public inc() {
if (this.users === 0 && !this.isConnected) { if (this.users === 0 && !this.isConnected) {
this.connect(); this.connect()
} }
this.users++; this.users++
// タイマー解除 // タイマー解除
if (this.disposeTimerId) { if (this.disposeTimerId) {
clearTimeout(this.disposeTimerId); clearTimeout(this.disposeTimerId)
this.disposeTimerId = null; this.disposeTimerId = null
} }
} }
@autobind @autobind
public dec() { public dec() {
this.users--; this.users--
// そのコネクションの利用者が誰もいなくなったら // そのコネクションの利用者が誰もいなくなったら
if (this.users === 0) { if (this.users === 0) {
// また直ぐに再利用される可能性があるので、一定時間待ち、 // また直ぐに再利用される可能性があるので、一定時間待ち、
// 新たな利用者が現れなければコネクションを切断する // 新たな利用者が現れなければコネクションを切断する
this.disposeTimerId = setTimeout(() => { this.disposeTimerId = setTimeout(() => {
this.disconnect(); this.disconnect()
}, 3000); }, 3000)
} }
} }
@autobind @autobind
public connect() { public connect() {
this.isConnected = true; this.isConnected = true
this.stream.send('connect', { this.stream.send("connect", {
channel: this.channel, channel: this.channel,
id: this.id id: this.id,
}); })
} }
@autobind @autobind
private disconnect() { private disconnect() {
this.isConnected = false; this.isConnected = false
this.disposeTimerId = null; this.disposeTimerId = null
this.stream.send('disconnect', { id: this.id }); this.stream.send("disconnect", { id: this.id })
} }
} }
abstract class Connection extends EventEmitter { abstract class Connection extends EventEmitter {
public channel: string; public channel: string
protected stream: Stream; protected stream: Stream
public abstract id: string; public abstract id: string
constructor(stream: Stream, channel: string) { constructor(stream: Stream, channel: string) {
super(); super()
this.stream = stream; this.stream = stream
this.channel = channel; this.channel = channel
} }
@autobind @autobind
public send(id: string, typeOrPayload, payload?) { public send(id: string, typeOrPayload, payload?) {
const type = payload === undefined ? typeOrPayload.type : typeOrPayload; const type = payload === undefined ? typeOrPayload.type : typeOrPayload
const body = payload === undefined ? typeOrPayload.body : payload; const body = payload === undefined ? typeOrPayload.body : payload
this.stream.send('ch', { this.stream.send("ch", {
id: id, id: id,
type: type, type: type,
body: body body: body,
}); })
} }
public abstract dispose(): void; public abstract dispose(): void
} }
class SharedConnection extends Connection { class SharedConnection extends Connection {
private pool: Pool; private pool: Pool
public get id(): string { public get id(): string {
return this.pool.id; return this.pool.id
} }
constructor(stream: Stream, channel: string, pool: Pool) { constructor(stream: Stream, channel: string, pool: Pool) {
super(stream, channel); super(stream, channel)
this.pool = pool; this.pool = pool
this.pool.inc(); this.pool.inc()
} }
@autobind @autobind
public send(typeOrPayload, payload?) { public send(typeOrPayload, payload?) {
super.send(this.pool.id, typeOrPayload, payload); super.send(this.pool.id, typeOrPayload, payload)
} }
@autobind @autobind
public dispose() { public dispose() {
this.pool.dec(); this.pool.dec()
this.removeAllListeners(); this.removeAllListeners()
this.stream.removeSharedConnection(this); this.stream.removeSharedConnection(this)
} }
} }
class NonSharedConnection extends Connection { class NonSharedConnection extends Connection {
public id: string; public id: string
protected params: any; protected params: any
constructor(stream: Stream, channel: string, params?: any) { constructor(stream: Stream, channel: string, params?: any) {
super(stream, channel); super(stream, channel)
this.params = params; this.params = params
this.id = Math.random().toString(); this.id = Math.random().toString()
this.connect(); this.connect()
} }
@autobind @autobind
public connect() { public connect() {
this.stream.send('connect', { this.stream.send("connect", {
channel: this.channel, channel: this.channel,
id: this.id, id: this.id,
params: this.params params: this.params,
}); })
} }
@autobind @autobind
public send(typeOrPayload, payload?) { public send(typeOrPayload, payload?) {
super.send(this.id, typeOrPayload, payload); super.send(this.id, typeOrPayload, payload)
} }
@autobind @autobind
public dispose() { public dispose() {
this.removeAllListeners(); this.removeAllListeners()
this.stream.send('disconnect', { id: this.id }); this.stream.send("disconnect", { id: this.id })
this.stream.disconnectToChannel(this); this.stream.disconnectToChannel(this)
} }
} }

View File

@ -1,20 +1,7 @@
import * as seedrandom from 'seedrandom'; import * as seedrandom from 'seedrandom';
export const itemPrefixes = [ export const itemPrefixes = [
'プラチナ製',
'新鮮な',
'最新式の',
'古代の',
'手作り',
'時計じかけの',
'伝説の',
'焼き',
'生の',
'藍謹製',
'ポケットサイズ',
'3日前の',
'そこらへんの', 'そこらへんの',
'偽の',
'使用済み', '使用済み',
'壊れた', '壊れた',
'市販の', '市販の',
@ -22,52 +9,24 @@ export const itemPrefixes = [
'業務用の', '業務用の',
'Microsoft製', 'Microsoft製',
'Apple製', 'Apple製',
'人類の技術を結集して作った',
'2018年製', // TODO ランダム
'500kgくらいある',
'高級', '高級',
'腐った', '腐った',
'人工知能搭載', '人工知能搭載',
'反重力',
'折り畳み式',
'携帯型', '携帯型',
'遺伝子組み換え',
'飛行能力を獲得した',
'純金製',
'透明な', '透明な',
'光る', '光る',
'ハート型の',
'動く', '動く',
'半分にカットされた',
'USBコネクタ付きの', 'USBコネクタ付きの',
'いにしえの', 'いにしえの',
'呪われた', '呪われた',
'エンチャントされた',
'一日分のビタミンが入った',
'かじりかけ',
'幻の', '幻の',
'仮想的な', '仮想的な',
'原子力',
'高度に訓練された',
'遺伝子組み換えでない',
'ダンジョン最深部で見つかった',
'異世界の', '異世界の',
'異星の', '異星の',
'謎の', '謎の',
'時空を歪める', '時空を歪める',
'異音がする',
'霧散する',
'プラズマ化した',
'衝撃を与えると低確率で爆発する',
'ズッキーニに擬態した',
'仮説上の',
'毒の',
'真の',
'究極の', '究極の',
'チョコ入り',
'異臭を放つ', '異臭を放つ',
'4次元',
'脈動する',
'得体の知れない', '得体の知れない',
'四角い', '四角い',
'暴れ回る', '暴れ回る',
@ -75,70 +34,27 @@ export const itemPrefixes = [
'闇の', '闇の',
'暗黒の', '暗黒の',
'封印されし', '封印されし',
'死の',
'凍った', '凍った',
'魔の', '魔の',
'禁断の', '禁断の',
'ホログラフィックな', 'ホログラフィックな',
'油圧式',
'辛そうで辛くない少し辛い',
'焦げた',
'宇宙',
'電子',
'陽電子',
'量子力学的',
'シュレディンガーの',
'分散型',
'卵かけ',
'次世代', '次世代',
'帯電', '3G対応',
'太古の', '消費期限切れ',
'WiFi対応',
'高反発',
'【令和最新版】',
'廉価版',
'ねばねば',
'どろどろ',
'パサパサの',
'湿気った',
'賞味期限切れ',
'地獄から来た',
'ニンニクマシ',
'放射性',
'フラクタルな',
'再帰的',
'ときどき分裂する',
'消える', '消える',
'等速直線運動する',
'X線照射',
'蠢く',
'形而上学的',
'もちもち', 'もちもち',
'冷やし', '冷やし',
'あつあつ', 'あつあつ',
'巨大', '巨大',
'ナノサイズ', 'ナノサイズ',
'やわらかい', 'やわらかい',
'人の手に負えない',
'バグった', 'バグった',
'人工', '人工',
'天然', '天然',
'祀られた',
'チョコレートコーティング',
'抗菌仕様',
'耐火',
'激',
'猛',
'超', '超',
'群生する',
'軽量',
'国宝級',
'流行りの',
'8カラットの',
'中古の', '中古の',
'新品の', '新品の',
'愛妻',
'ブランドものの',
'増殖する',
'ぷるぷる', 'ぷるぷる',
'ぐにゃぐにゃ', 'ぐにゃぐにゃ',
'多目的', '多目的',
@ -146,315 +62,54 @@ export const itemPrefixes = [
'激辛', '激辛',
'先進的な', '先進的な',
'レトロな', 'レトロな',
'ヴィンテージ',
'合法', '合法',
'違法',
'プレミア付き', 'プレミア付き',
'デカ',
'ギガ',
'穢れた',
'品質保証付き',
'AppleCare+加入済み',
'えっちな',
'デザイナーズ',
'蠱惑的な',
'霊験灼かな',
'つやつや',
'べとべと',
'ムキムキ',
'オーバークロックされた',
'無機質な',
'前衛的な',
'怪しい', '怪しい',
'妖しい', '妖しい',
'カビの生えた',
'熟成',
'アルミダイキャスト',
'養殖',
'やばい', 'やばい',
'すごい', 'すごい',
'かわいい', 'かわいい',
'デジタル', 'デジタル',
'アナログ', 'アナログ',
'彁な',
'カラフルな',
'電動',
'当たり判定のない',
'めり込んだ',
'100年に一度の', '100年に一度の',
'ジューシーな',
'Hi-Res',
'確変',
'食用', '食用',
'THE ', 'THE ',
'某',
'朽ちゆく',
'滅びの',
'反発係数がe>1の',
'摩擦係数0の',
'解き放たれし', '解き放たれし',
'大きな', '大きな',
'小さな', '小さな',
'強欲な',
'うねうね',
'水没',
'燃え盛る',
'高圧',
'異常',
]; ];
export const items = [ export const items = [
'ナス', '右足',
'トマト', '左足',
'きゅうり', 'お金',
'じゃがいも', '金パブ',
'焼きビーフン', 'ブロン',
'腰', 'ぬるきゃっとちゃん!',
'寿司', 'この世のすべて',
'かぼちゃ',
'諭吉',
'キロバー',
'アルミニウム',
'ナトリウム',
'マグネシウム',
'プルトニウム',
'ちいさなメダル',
'牛乳パック',
'ペットボトル',
'クッキー',
'チョコレート',
'メイド服',
'オレンジ',
'ニーソ',
'反物質コンデンサ',
'粒子加速器',
'マイクロプロセッサ(4コア8スレッド)',
'原子力発電所',
'レイヤ4スイッチ',
'緩衝チェーン',
'陽電子頭脳',
'惑星',
'テルミン',
'虫歯車',
'マウンター',
'バケットホイールエクスカベーター',
'デーモンコア',
'ゲームボーイアドバンス',
'量子コンピューター', '量子コンピューター',
'アナモルフィックレンズ', 'スマホ',
'押し入れの奥から出てきた謎の生き物', 'PC',
'スマートフォン', 'モンスター',
'時計', '好きなもの',
'プリン',
'ガブリエルのラッパ',
'メンガーのスポンジ',
'ハンドスピナー',
'超立方体',
'建築物',
'エナジードリンク',
'マウスカーソル',
'メガネ',
'まぐろ',
'ゴミ箱',
'つまようじ',
'お弁当に入ってる緑の仕切りみたいなやつ',
'割りばし',
'換気扇',
'ペットボトルのキャップ',
'消波ブロック',
'ピザ',
'歯磨き粉',
'空き缶',
'キーホルダー',
'金髪碧眼の美少女',
'SDカード',
'リップクリーム',
'チョコ無しチョココロネ',
'鳥インフルエンザ',
'自動販売機',
'重いもの',
'ノートパソコン',
'ビーフジャーキー',
'さけるチーズ',
'ダイヤモンド',
'物体',
'月の石',
'特異点',
'中性子星',
'液体',
'衛星',
'ズッキーニ',
'黒いもの',
'白いもの',
'赤いもの',
'丸いもの',
'四角いもの',
'カード状のもの',
'気体',
'鉛筆',
'消しゴム',
'つるぎ',
'棒状のもの',
'農産物',
'メタルスライム',
'タコの足',
'きのこ',
'なめこ',
'缶チューハイ',
'爪切り',
'耳かき',
'ぬいぐるみ', 'ぬいぐるみ',
'ティラノサウルス', 'おふとん',
'尿路結石',
'エンターキー',
'壺',
'水銀',
'DHMO',
'水',
'土地',
'大陸',
'サイコロ',
'室外機',
'油圧ジャッキ',
'タピオカ',
'トイレットペーパーの芯',
'ダンボール箱',
'ハニワ',
'ボールペン',
'シャーペン',
'原子',
'宇宙',
'素粒子',
'ごま油',
'卵かけご飯',
'ダークマター',
'ブラックホール',
'太陽',
'石英ガラス',
'ダム',
'ウイルス',
'細菌',
'アーチ式コンクリートダム',
'重力式コンクリートダム',
'フラッシュバルブ',
'ヴィブラスラップ',
'オブジェ',
'原子力発電所',
'原子炉',
'エラトステネスの篩',
'ブラウン管',
'タキオン',
'ラッセルのティーポット',
'電子機器',
'TNT',
'ポリゴン',
'空気',
'RTX 3090',
'シャーペンの芯',
'ロゼッタストーン',
'CapsLockキー',
'虚無',
'UFO',
'NumLockキー',
'放射性廃棄物',
'火星',
'ウラン',
'遠心分離機',
'undefined',
'null',
'NaN',
'[object Object]',
'ゼロ幅スペース',
'全角スペース',
'太鼓',
'石像',
'スライム',
'点P',
'🤯',
'きんのたま',
'フロッピーディスク',
'掛け軸',
'JavaScriptコンソール',
'インターネットエクスプローラー',
'潜水艦発射弾道ミサイル',
'ミトコンドリア',
'ヘリウム',
'タンパク質',
'カプサイシン',
'エスカレーター',
'核融合炉',
'地熱発電所',
'マンション',
'ラバライト',
'ガリレオ温度計',
'ラジオメーター',
'サンドピクチャー',
'ストームグラス',
'ニュートンクレードル',
'永久機関',
'柿の種のピーナッツ部分',
'伝票入れる筒状のアレ',
'布団',
'寝具',
'偶像',
'森羅万象', '森羅万象',
'卒塔婆', 'めがね',
'国民の基本的な権利',
'こたつ',
'靴下(片方は紛失)',
'健康保険証',
'テレホンカード',
'ピアノの黒鍵',
'ACアダプター',
'DVD',
'市営バス',
'基地局',
'404 Not Found',
'JSON',
'タペストリー',
'本',
'石像',
'古文書',
'巻物',
'Misskey',
'もぎもぎフルーツ',
'<ここに任意の文字列>',
'化石',
'マンホールの蓋',
'蛇口',
'彁',
'鬮',
'1円玉',
'ト音記号',
'ポータル',
'国家予算',
'閉じ忘れられた鉤括弧の片割れ',
'電動マッサージ機',
'ポップアップ広告',
'README.txt',
'あああああ',
'コミット',
'素数',
'タスクマネージャー',
'有象無象',
'炭水化物',
'正十二面体',
'クラインの壺',
'メビウスの輪',
'オリハルコン',
'ヘドロ',
'グレーチング',
'繝九Λ縺ョ縺ソ縺晄ア',
'スーパーカミオカンデ',
]; ];
export const and = [ export const and = [
'に擬態した', 'に擬態した',
'入りの', '入りの',
'が埋め込まれた',
'を連想させる',
'っぽい', 'っぽい',
'に見せかけて', 'に見せかけて',
'を虐げる', 'を虐げる',
'を侍らせた', 'を侍らせた',
'が上に乗った', 'が上に乗った',
'のそばにある',
]; ];
export function genItem(seedOrRng?: (() => number) | string | number) { export function genItem(seedOrRng?: (() => number) | string | number) {

5
to-hiragana.ts Normal file
View File

@ -0,0 +1,5 @@
const moji = require('moji');
export default function toHiragana(str: string): string {
return moji(str).convert('HK', 'ZK').convert('KK', 'HG').toString();
}

View File

@ -1,90 +1,64 @@
<img src="https://github.com/syuilo/ai/blob/master/ai.png?raw=true" align="right" height="320px"/> <img src="https://usercontent.misskey.online/xb4786y7/235f8ff2-1e18-4f84-8ee2-80490eacc3a6.png" align="right" height="320px"/>
# 取扱説明書 # ぬるきゃっとちゃん!の主な機能
## プロフィール ### フォローする
[こちら](https://xn--931a.moe/) 僕に「フォローして」って言ってくれたらフォローするよ
## 藍の主な機能 ### お話
### 挨拶 「おはよう」「おやすみ」などと話しかけると反応するよ
「おはよう」「おやすみ」などと話しかけると反応してくれます。
### リアクション
僕が設定されている特定のワードにリアクションするよ
### 占い ### 占い
藍に「占い」と言うと、あなたの今日の運勢を占ってくれます。 僕に「占って」と言うと、あなたの今日の運勢を占うよ
### タイマー ### タイマー
指定した時間、分、秒を経過したら教えてくれます。「3分40秒」のように単位を混ぜることもできます。 指定した時間、分、秒を経過したら教えてくれるよ「3分40秒」のように単位を混ぜることもできるよ
### リマインダー ### リマインダー
``` `@nullcat todo(リマインド、これやる) 寝る` みたいに言ってくれたら1時間置きにリマインドするよ。その飛ばしたメンションか、僕からの催促に「やった」「やめた」など返信するとリマインダー解除されるよ<br>
@ai remind 部屋の掃除 引用Renoteでメンションすることもできるよ<br>
``` リマインダーの一覧は `@nullcat todos` で見れるよ
のようにメンションを飛ばすと12時間置きに責付かれます。その飛ばしたメンションか、藍ちゃんからの催促に「やった」または「やめた」と返信することでリマインダー解除されます。
また、引用Renoteでメンションすることもできます。
### 福笑い ### GitHub Status
藍に「絵文字」と言うと、藍が考えた絵文字の組み合わせを教えてくれます。 僕に「GitHub」って言ってくれたら今のStatusを教えるよ
### サイコロ ### Cloudflare Status
ダイスノーテーションを伝えるとサイコロを振ってくれます。 僕に「Cloudflare」って言ってくれたら今のStatusを教えるよ
例: "2d6" (6面サイコロを2回振る)、"3d5" (5面サイコロを3回振る)
### 迷路 ### シェル芸機能
「迷路」と言うと迷路を描いてくれます。「難しい」「簡単」などの言葉を添えることで、難易度も調整できます。 僕に #シェル芸#shellge をつけてコマンドを送ってくれたら実行結果を返すよ
### 数当てゲーム ### 怪レい曰本语変換
藍にメッセージで「数当てゲーム」と言うと遊べます。 僕に `#怪しい日本語変換` っていうタグ付きで変換してほしい文章をメンションしてくれたら怪レい曰本语に変換するよ
藍の考えている数字を当てるゲームです。
### 数取りゲーム ### やること決める
藍に「数取りゲーム」と言うと遊べます。 僕に「なにしよ」って言ってくれたらやることを決めるよ
複数人で行うゲームで、もっとも大きい数字を言った人が勝ちです。
### リバーシ ### 気圧
藍とリバーシで対局できます。(この機能はインスタンスによっては無効になっている可能性があります) 僕に「気圧教えて」って言ってくれたら今の気圧を教えるよ
藍に「リバーシ」と言うか、リバーシで藍を指名すれば対局できます。
強さも調整できます。
### 覚える
たまにタイムラインにあるキーワードを「覚え」ます。
(この機能はインスタンスによっては無効になっている可能性があります)
### 呼び方を教える ### 呼び方を教える
藍があなたのことをなんて呼べばいいか教えられます。 僕が君のことをなんて呼べばいいか教えてくれたら、その名前で呼ぶよ!<br>
ただし後述の親愛度が一定の値に達している必要があります。 親愛度が一定の値に達している必要があるよ<br>
(トークでのみ反応します) (チャットのみで反応するよ)
### いらっしゃい
Misskeyにアカウントを作成して初めて投稿を行うと、藍がネコミミアンテナでそれを補足し、Renoteしてみんなに知らせてくれる機能です。
### Follow me
藍に「フォローして」と言うとフォローしてくれます。
### HappyBirthday ### HappyBirthday
藍があなたの誕生日を祝ってくれます。 誕生日になったら僕が君の誕生日を祝うよ
### バレンタイン ### バレンタイン
藍がチョコレートをくれます。 バレンタインになったら仲のいい子に僕がチョコレートをあげるよ
### チャート
インスタンスの投稿チャートなどを投稿してくれます。
### サーバー監視
サーバーの状態を監視し、負荷が高くなっているときは教えてくれます。
### ping ### ping
PONGを返します。生存確認にどうぞ 僕に「ping」って言ってくれたらフォローするよで起きてるとき返信するよ寝てるときは返信できないかも...
### その他反応するフレーズ (トークのみ) ### 親愛度
* かわいい 僕は君に対する親愛度を持っているよ<br>
* なでなで 僕にお話ししてくれたりすると、少しずつ上がるよ<br>
* 好き 親愛度によって反応が変化するよ!親愛度がある程度ないとしてくれないこともあるよ<br>
* ぎゅー たくさん話しかけてね
* 罵って
* 踏んで
* 痛い
## 親愛度
藍はあなたに対する親愛度を持っています。 僕のリポジトリは[ここ](https://github.com/nullnyat/NullcatChan)だよ
藍に挨拶したりすると、少しずつ上がっていきます。
親愛度によって反応や各種セリフが変化します。親愛度がある程度ないとしてくれないこともあります。

View File

@ -14,10 +14,10 @@
"noLib": false, "noLib": false,
"outDir": "built", "outDir": "built",
"rootDir": "src", "rootDir": "src",
"baseUrl": ".", "baseUrl": "../NullcatChan-old",
"paths": { "paths": {
"@/*": ["src/*"] "@/*": ["src/*"]
}, }
}, },
"compileOnSave": false, "compileOnSave": false,
"include": [ "include": [

10
utils/includes.ts Normal file
View File

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

View File

@ -9,7 +9,7 @@ export default function(text: string, words: (string | RegExp)[]): boolean {
return words.some(word => { return words.some(word => {
/** /**
* *
* *
*/ */
function denoise(text: string): string { function denoise(text: string): string {
text = text.trim(); text = text.trim();
@ -34,7 +34,7 @@ export default function(text: string, words: (string | RegExp)[]): boolean {
text = text.replace(/です$/, ''); text = text.replace(/です$/, '');
text = text.replace(/(\.|…)+$/, ''); text = text.replace(/(\.|…)+$/, '');
text = text.replace(/[♪♥]+$/, ''); text = text.replace(/[♪♥]+$/, '');
text = text.replace(/^/, ''); text = text.replace(/^ぬるきゃっと/, '');
text = text.replace(/^ちゃん/, ''); text = text.replace(/^ちゃん/, '');
text = text.replace(/、+$/, ''); text = text.replace(/、+$/, '');
} }

4919
yarn.lock Normal file

File diff suppressed because it is too large Load Diff