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
font.ttf
ai.*
nullcat.*
*.md
*.png
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">An Ai for Misskey. <a href="./torisetu.md">About Ai</a></p>
<p align="center">
<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` します。
次にそのディレクトリに `config.json` を作成します。中身は次のようにします:
``` json
{
"host": "https:// + あなたのインスタンスのURL (末尾の / は除く)",
"i": "藍として動かしたいアカウントのアクセストークン",
"i": "ぬるきゃっとちゃん!として動かしたいアカウントのアクセストークン",
"master": "管理者のユーザー名(オプション)",
"notingEnabled": "ランダムにノートを投稿する機能を無効にする場合は false を入れる",
"keywordEnabled": "キーワードを覚える機能 (MeCab が必要) を有効にする場合は true を入れる (無効にする場合は false)",
"chartEnabled": "チャート機能を無効化する場合は false を入れてください",
"reversiEnabled": "藍とリバーシで対局できる機能を有効にする場合は true を入れる (無効にする場合は false)",
"serverMonitoring": "サーバー監視の機能を有効にする場合は true を入れる (無効にする場合は false)",
"mecab": "MeCab のインストールパス (ソースからインストールした場合、大体は /usr/local/bin/mecab)",
"mecabDic": "MeCab の辞書ファイルパス (オプション)",
"memoryDir": "memory.jsonの保存先オプション、デフォルトは'.'(レポジトリのルートです))"
"notingEnabled": "ランダムにートを投稿する機能。true(on) or false(off)",
"keywordEnabled": "キーワードを覚える機能 (MeCab が必要) true or false",
"serverMonitoring": "サーバー監視の機能重かったりすると教えてくれるよ。true or false",
"mecab": "MeCab のインストールパス (ソースからインストールした場合、大体は /usr/local/bin/mecab) true or false",
"mecabDic": "MeCab の辞書ファイルパス",
"memoryDir": "memory.jsonの保存先オプション、デフォルトは'.'(レポジトリのルートです))",
"shellgeiUrl": "シェル芸BotのAPIのURLですデフォルトではhttps://websh.jiro4989.com/api/shellgei"
}
```
`npm install` して `npm run build` して `npm start` すれば起動できます
`npm install` して `npm run build` して `npm start` すれば起動できます
## Dockerで動かす
まず適当なディレクトリに `git clone` します。
### Dockerで動かす
まず適当なディレクトリに `git clone` します。<br>
次にそのディレクトリに `config.json` を作成します。中身は次のようにします:
MeCabの設定、memoryDirについては触らないでください
``` json
{
"host": "https:// + あなたのインスタンスのURL (末尾の / は除く)",
"i": "として動かしたいアカウントのアクセストークン",
"i": "ぬるきゃっとちゃん!として動かしたいアカウントのアクセストークン",
"master": "管理者のユーザー名(オプション)",
"notingEnabled": "ランダムにノートを投稿する機能を無効にする場合は false を入れる",
"keywordEnabled": "キーワードを覚える機能 (MeCab が必要) を有効にする場合は true を入れる (無効にする場合は false)",
"chartEnabled": "チャート機能を無効化する場合は false を入れてください",
"reversiEnabled": "藍とリバーシで対局できる機能を有効にする場合は true を入れる (無効にする場合は false)",
"serverMonitoring": "サーバー監視の機能を有効にする場合は true を入れる (無効にする場合は false)",
"notingEnabled": "ランダムにートを投稿する機能。true(on) or false(off)",
"keywordEnabled": "キーワードを覚える機能 (MeCab が必要) true or false",
"mecab": "/usr/bin/mecab",
"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をインストールしないようにもできます。メモリが少ない環境など
## フォント
一部の機能にはフォントが必要です。藍にはフォントは同梱されていないので、ご自身でフォントをインストールディレクトリに`font.ttf`という名前で設置してください。
## 記憶
藍は記憶の保持にインメモリデータベースを使用しており、藍のインストールディレクトリに `memory.json` という名前で永続化されます。
## ライセンス
MIT
## Awards
<img src="./WorksOnMyMachine.png" alt="Works on my machine" height="120">
#### 一部の機能にはフォントが必要です。NullcatChan!にはフォントは同梱されていないので、ご自身でフォントをインストールしてそのフォントを`font.ttf`という名前でインストールディレクトリに設置してください。
#### NullcatChan!は記憶の保持にインメモリデータベースを使用しており、僕のインストールディレクトリに `memory.json` という名前で永続化されます。

View File

@ -2,11 +2,14 @@ version: '3'
services:
app:
build:
context: .
dockerfile: Dockerfile_production
context: ../NullcatChan-old
args:
- enable_mecab=1
volumes:
- './config.json:/ai/config.json:ro'
- './font.ttf:/ai/font.ttf:ro'
- './data:/ai/data'
- './config.json:/nullcat-chan/config.json:ro'
- './font.ttf:/nullcat-chan/font.ttf:ro'
- './data:/nullcat-chan/data'
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",
"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",
"test": "jest"
},
"dependencies": {
"@types/accurate-interval": "1.0.0",
"@types/chalk": "2.2.0",
"@types/humanize-duration": "3.27.1",
"@types/lokijs": "1.5.4",
"@types/moji": "0.5.0",
"@types/node": "16.0.1",
"@types/promise-retry": "1.1.3",
"@types/random-seed": "0.3.3",
@ -17,13 +23,19 @@
"@types/twemoji-parser": "13.1.1",
"@types/uuid": "8.3.1",
"@types/ws": "7.4.6",
"accurate-interval": "1.0.9",
"autobind-decorator": "2.4.0",
"canvas": "2.8.0",
"chalk": "4.1.1",
"cjp": "1.2.3",
"gomamayo-js": "0.2.1",
"humanize-duration": "3.27.1",
"lokijs": "1.5.12",
"memory-streams": "0.1.3",
"misskey-reversi": "0.0.5",
"module-alias": "2.2.2",
"moji": "0.5.1",
"node-fetch": "2.6.7",
"promise-retry": "2.0.1",
"random-seed": "0.3.0",
"reconnecting-websocket": "4.4.0",
@ -33,9 +45,10 @@
"timeout-as-promise": "1.0.0",
"ts-node": "10.0.0",
"twemoji-parser": "13.1.0",
"typescript": "4.3.5",
"typescript": "4.5.5",
"uuid": "8.3.2",
"ws": "7.5.2"
"ws": "7.5.2",
"zod": "3.11.6"
},
"devDependencies": {
"@koa/router": "9.4.0",
@ -43,9 +56,11 @@
"@types/koa": "2.13.1",
"@types/koa__router": "8.0.4",
"@types/websocket": "1.0.2",
"cross-env": "7.0.3",
"jest": "26.6.3",
"koa": "2.13.1",
"koa-json-body": "5.3.0",
"prettier": "2.5.1",
"ts-jest": "26.5.6",
"websocket": "1.0.34"
},

View File

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

View File

@ -1,71 +1,71 @@
import autobind from 'autobind-decorator';
import from '@/ai';
import IModule from '@/module';
import getDate from '@/utils/get-date';
import { User } from '@/misskey/user';
import { genItem } from '@/vocabulary';
import { User } from "./misskey/user"
import IModule from "../../NullcatChan-old/src/module"
import NullcatChan from "../../NullcatChan-old/src/nullcat-chan"
import getDate from "../../NullcatChan-old/src/utils/get-date"
import { genItem } from "../../NullcatChan-old/src/vocabulary"
import autobind from "autobind-decorator"
export type FriendDoc = {
userId: string;
user: User;
name?: string | null;
love?: number;
lastLoveIncrementedAt?: string;
todayLoveIncrements?: number;
perModulesData?: any;
married?: boolean;
transferCode?: string;
};
userId: string
user: User
name?: string | null
love?: number
lastLoveIncrementedAt?: string
todayLoveIncrements?: number
perModulesData?: any
married?: boolean
transferCode?: string
}
export default class Friend {
private ai: ;
private nullcatChan: NullcatChan
public get userId() {
return this.doc.userId;
return this.doc.userId
}
public get name() {
return this.doc.name;
return this.doc.name
}
public get love() {
return this.doc.love || 0;
return this.doc.love || 0
}
public get married() {
return this.doc.married;
return this.doc.married
}
public doc: FriendDoc;
public doc: FriendDoc
constructor(ai: , opts: { user?: User, doc?: FriendDoc }) {
this.ai = ai;
constructor(nullcatChan: NullcatChan, opts: { user?: User; doc?: FriendDoc }) {
this.nullcatChan = nullcatChan
if (opts.user) {
const exist = this.ai.friends.findOne({
userId: opts.user.id
});
const exist = this.nullcatChan.friends.findOne({
userId: opts.user.id,
})
if (exist == null) {
const inserted = this.ai.friends.insertOne({
const inserted = this.nullcatChan.friends.insertOne({
userId: opts.user.id,
user: opts.user
});
user: opts.user,
})
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 {
this.doc = exist;
this.doc.user = { ...this.doc.user, ...opts.user };
this.save();
this.doc = exist
this.doc.user = { ...this.doc.user, ...opts.user }
this.save()
}
} else if (opts.doc) {
this.doc = opts.doc;
this.doc = opts.doc
} 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,
...user,
};
this.save();
}
this.save()
}
@autobind
public getPerModulesData(module: IModule) {
if (this.doc.perModulesData == null) {
this.doc.perModulesData = {};
this.doc.perModulesData[module.name] = {};
this.save();
this.doc.perModulesData = {}
this.doc.perModulesData[module.name] = {}
this.save()
} else if (this.doc.perModulesData[module.name] == null) {
this.doc.perModulesData[module.name] = {};
this.save();
this.doc.perModulesData[module.name] = {}
this.save()
}
return this.doc.perModulesData[module.name];
return this.doc.perModulesData[module.name]
}
@autobind
public setPerModulesData(module: IModule, data: any) {
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
public incLove(amount = 1) {
const today = getDate();
const today = getDate()
if (this.doc.lastLoveIncrementedAt != today) {
this.doc.todayLoveIncrements = 0;
this.doc.todayLoveIncrements = 0
}
// 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;
this.doc.love += amount;
if (this.doc.love == null) this.doc.love = 0
this.doc.love += amount
// 最大 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.todayLoveIncrements = (this.doc.todayLoveIncrements || 0) + amount;
this.save();
this.doc.lastLoveIncrementedAt = today
this.doc.todayLoveIncrements = (this.doc.todayLoveIncrements || 0) + amount
this.save()
this.ai.log(`💗 ${this.userId} +${amount}`);
this.nullcatChan.log(`💗 ${this.userId} +${amount}`)
}
@autobind
public decLove(amount = 1) {
// 親愛度MAXなら下げない
if (this.doc.love === 100) return;
if (this.doc.love === 100) return
if (this.doc.love == null) this.doc.love = 0;
this.doc.love -= amount;
if (this.doc.love == null) this.doc.love = 0
this.doc.love -= amount
// 最低 -30
if (this.doc.love < -30) this.doc.love = -30;
if (this.doc.love < -30) this.doc.love = -30
// 親愛度マイナスなら名前を忘れる
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
public updateName(name: string) {
this.doc.name = name;
this.save();
this.doc.name = name
this.save()
}
@autobind
public save() {
this.ai.friends.update(this.doc);
this.nullcatChan.friends.update(this.doc)
}
@autobind
public generateTransferCode(): string {
const code = genItem();
const code = genItem()
this.doc.transferCode = code;
this.save();
this.doc.transferCode = code
this.save()
return code;
return code
}
@autobind
public transferMemory(code: string): boolean {
const src = this.ai.friends.findOne({
transferCode: code
});
const src = this.nullcatChan.friends.findOne({
transferCode: code,
})
if (src == null) return false;
if (src == null) return false
this.doc.name = src.name;
this.doc.love = src.love;
this.doc.married = src.married;
this.doc.perModulesData = src.perModulesData;
this.save();
this.doc.name = src.name
this.doc.love = src.love
this.doc.married = src.married
this.doc.perModulesData = src.perModulesData
this.save()
// 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';
import * as request from 'request-promise-native';
const promiseRetry = require('promise-retry');
const promiseRetry = require("promise-retry")
import from './ai';
import config from './config';
import _log from './utils/log';
const pkg = require('../package.json');
const pkg = require("../../NullcatChan-old/package.json")
import CoreModule from './modules/core';
import TalkModule from './modules/talk';
import BirthdayModule from './modules/birthday';
import ReversiModule from './modules/reversi';
import PingModule from './modules/ping';
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');
console.log(" _ __ ____ __ ________ __ ")
console.log(" / | / /_ __/ / /________ _/ /_/ ____/ /_ ____ _____ / / ")
console.log(" / |/ / / / / / / ___/ __ `/ __/ / / __ \\/ __ `/ __ \\/ / ")
console.log(" / /| / /_/ / / / /__/ /_/ / /_/ /___/ / / / /_/ / / / /_/ ")
console.log("/_/ |_/\\__,_/_/_/\\___/\\__,_/\\__/\\____/_/ /_/\\__,_/_/ /_(_)\n")
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 => {
log(`Account fetching... ${chalk.gray(config.host)}`);
promiseRetry(
(retry) => {
log(`Account fetching... ${chalk.gray(config.host)}`)
// アカウントをフェッチ
return request.post(`${config.apiUrl}/i`, {
json: {
i: config.i
}
}).catch(retry);
}, {
retries: 3
}).then(account => {
const acct = `@${account.username}`;
log(chalk.green(`Account fetched successfully: ${chalk.underline(acct)}`));
// アカウントをフェッチ
return request
.post(`${config.apiUrl}/i`, {
json: {
i: config.i,
},
})
.catch(retry)
},
{
retries: 3,
}
)
.then((account) => {
const acct = `@${account.username}`
log(chalk.green(`Account fetched successfully: ${chalk.underline(acct)}`))
log('Starting AiOS...');
log("Starting Nullcat chan...")
// 藍起動
new (account, [
new CoreModule(),
new EmojiModule(),
new EmojiReactModule(),
new FortuneModule(),
new GuessingGameModule(),
new KazutoriModule(),
new ReversiModule(),
new TimerModule(),
new DiceModule(),
new TalkModule(),
new PingModule(),
new WelcomeModule(),
new ServerModule(),
new FollowModule(),
new BirthdayModule(),
new ValentineModule(),
new KeywordModule(),
new MazeModule(),
new ChartModule(),
new SleepReportModule(),
new NotingModule(),
new PollModule(),
new ReminderModule(),
]);
}).catch(e => {
log(chalk.red('Failed to fetch the account'));
});
// ぬるきゃっとちゃん起動
new NullcatChan(account, [
new CoreModule(),
new EmojiReactModule(),
new FortuneModule(),
new TimerModule(),
new TalkModule(),
new FollowModule(),
new BirthdayModule(),
new ValentineModule(),
new KeywordModule(),
new SleepReportModule(),
new NotingModule(),
new ReminderModule(),
new GomamayoModule(),
new GitHubStatusModule(),
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 * as chalk from 'chalk';
const delay = require('timeout-as-promise');
import config from "@/config"
import Friend from "@/friend"
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';
import Friend from '@/friend';
import { User } from '@/misskey/user';
import includes from '@/utils/includes';
import or from '@/utils/or';
import config from '@/config';
interface MisskeyFile {
id: string
createdAt: string
name: string
type: string
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 {
private ai: ;
private messageOrNote: any;
public isDm: boolean;
private nullcatChan: NullcatChan
private messageOrNote: any
public isDm: boolean
public get id(): string {
return this.messageOrNote.id;
return this.messageOrNote.id
}
public get user(): User {
return this.messageOrNote.user;
return this.messageOrNote.user
}
public get userId(): string {
return this.messageOrNote.userId;
return this.messageOrNote.userId
}
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 {
return this.messageOrNote.renoteId;
return this.messageOrNote.renoteId
}
public get visibility(): string {
return this.messageOrNote.visibility;
public get files(): MisskeyFile[] | undefined {
return this.messageOrNote.files
}
/**
*
*/
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
.replace(new RegExp(`^@${this.ai.account.username}@${host}\\s`, 'i'), '')
.replace(new RegExp(`^@${this.ai.account.username}\\s`, 'i'), '')
.trim();
.replace(new RegExp(`^@${this.nullcatChan.account.username}@${host}\\s`, "i"), "")
.replace(new RegExp(`^@${this.nullcatChan.account.username}\\s`, "i"), "")
.trim()
}
public get replyId(): string {
return this.messageOrNote.replyId;
return this.messageOrNote.replyId
}
public friend: Friend;
public friend: Friend
constructor(ai: , messageOrNote: any, isDm: boolean) {
this.ai = ai;
this.messageOrNote = messageOrNote;
this.isDm = isDm;
constructor(nullcatChan: NullcatChan, messageOrNote: any, isDm: boolean) {
this.nullcatChan = nullcatChan
this.messageOrNote = messageOrNote
this.isDm = isDm
this.friend = new Friend(ai, { user: this.user });
this.friend = new Friend(nullcatChan, { user: this.user })
// メッセージなどに付いているユーザー情報は省略されている場合があるので完全なユーザー情報を持ってくる
this.ai.api('users/show', {
userId: this.userId
}).then(user => {
this.friend.updateUser(user);
});
this.nullcatChan
.api("users/show", {
userId: this.userId,
})
.then((user) => {
this.friend.updateUser(user)
})
}
@autobind
public async reply(text: string | null, opts?: {
file?: any;
cw?: string;
renote?: string;
immediate?: boolean;
}) {
if (text == null) return;
public async reply(
text: string | null,
opts?: {
file?: any
cw?: string
renote?: string
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) {
await delay(2000);
await delay(2000)
}
if (this.isDm) {
return await this.ai.sendMessage(this.messageOrNote.userId, {
return await this.nullcatChan.sendMessage(this.messageOrNote.userId, {
text: text,
fileId: opts?.file?.id
});
fileId: opts?.file?.id,
})
} else {
return await this.ai.post({
return await this.nullcatChan.post({
replyId: this.messageOrNote.id,
text: text,
fileIds: opts?.file ? [opts?.file.id] : undefined,
cw: opts?.cw,
renoteId: opts?.renote
});
renoteId: opts?.renote,
})
}
}
@autobind
public includes(words: string[]): boolean {
return includes(this.text, words);
return includes(this.text, words)
}
@autobind
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 , { InstallerResult } from '@/ai';
import NullcatChan, { InstallerResult } from "./nullcat-chan"
import autobind from "autobind-decorator"
export default abstract class Module {
public abstract readonly name: string;
public abstract readonly name: string
protected ai: ;
private doc: any;
protected nullcatChan: NullcatChan
private doc: any
public init(ai: ) {
this.ai = ai;
public init(nullcatChan: NullcatChan) {
this.nullcatChan = nullcatChan
this.doc = this.ai.moduleData.findOne({
module: this.name
});
this.doc = this.nullcatChan.moduleData.findOne({
module: this.name,
})
if (this.doc == null) {
this.doc = this.ai.moduleData.insertOne({
this.doc = this.nullcatChan.moduleData.insertOne({
module: this.name,
data: {}
});
data: {},
})
}
}
public abstract install(): InstallerResult;
public abstract install(): InstallerResult
@autobind
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
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
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
public setTimeoutWithPersistence(delay: number, data?: any) {
this.ai.setTimeoutWithPersistence(this, delay, data);
this.nullcatChan.setTimeoutWithPersistence(this, delay, data)
}
@autobind
protected getData() {
return this.doc.data;
return this.doc.data
}
@autobind
protected setData(data: any) {
this.doc.data = data;
this.ai.moduleData.update(this.doc);
this.doc.data = data
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 Module from '@/module';
import Friend from '@/friend';
import serifs from '@/serifs';
import Friend from "@/friend"
import Module from "@/module"
import serifs from "@/serifs"
import autobind from "autobind-decorator"
function zeroPadding(num: number, length: number): string {
return ('0000000000' + num).slice(-length);
return ("0000000000" + num).slice(-length)
}
export default class extends Module {
public readonly name = 'birthday';
public readonly name = "birthday"
@autobind
public install() {
this.crawleBirthday();
setInterval(this.crawleBirthday, 1000 * 60 * 3);
this.crawleBirthday()
setInterval(this.crawleBirthday, 1000 * 60 * 3)
return {};
return {}
}
/**
@ -23,34 +23,34 @@ export default class extends Module {
*/
@autobind
private crawleBirthday() {
const now = new Date();
const m = now.getMonth();
const d = now.getDate();
const now = new Date()
const m = now.getMonth()
const d = now.getDate()
// 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({
'user.birthday': { '$regex': new RegExp('-' + today + '$') }
} as any);
const birthFriends = this.nullcatChan.friends.find({
"user.birthday": { $regex: new RegExp("-" + today + "$") },
} as any)
birthFriends.forEach(f => {
const friend = new Friend(this.ai, { doc: f });
birthFriends.forEach((f) => {
const friend = new Friend(this.nullcatChan, { doc: f })
// 親愛度が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;
friend.setPerModulesData(this, data);
data.lastBirthdayChecked = today
friend.setPerModulesData(this, data)
const text = serifs.birthday.happyBirthday(friend.name);
const text = serifs.birthday.happyBirthday(friend.name)
this.ai.sendMessage(friend.userId, {
text: text
});
});
this.nullcatChan.sendMessage(friend.userId, {
text: text,
})
})
}
}

View File

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

View File

@ -1,73 +1,69 @@
import autobind from 'autobind-decorator';
import { parse } from 'twemoji-parser';
const delay = require('timeout-as-promise');
import { Note } from "../../misskey/note"
import Module from "@/module"
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';
import Module from '@/module';
import Stream from '@/stream';
import includes from '@/utils/includes';
const gomamayo = require("gomamayo-js")
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
public install() {
this.htl = this.ai.connection.useSharedConnection('homeTimeline');
this.htl.on('note', this.onNote);
this.htl = this.nullcatChan.connection.useSharedConnection("homeTimeline")
this.htl.on("note", this.onNote)
return {};
return {}
}
@autobind
private async onNote(note: Note) {
if (note.reply != null) return;
if (note.text == null) return;
if (note.text.includes('@')) return; // (自分または他人問わず)メンションっぽかったらreject
if (note.reply != null) return
if (note.text == null) return
if (note.text.includes("@")) return // (自分または他人問わず)メンションっぽかったらreject
const react = async (reaction: string, immediate = false) => {
if (!immediate) {
await delay(1500);
await delay(1500)
}
this.ai.api('notes/reactions/create', {
this.nullcatChan.api("notes/reactions/create", {
noteId: note.id,
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]);
reaction: reaction,
})
}
const emojis = parse(note.text).map(x => x.text);
if (emojis.length > 0) {
// 絵文字が複数種類ある場合はキャンセル
if (!emojis.every((val, i, arr) => val === arr[0])) return;
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('🙌');
if (await gomamayo.find(note.text)) return react(":bikkuribikkuri_:")
if (includes(note.text, ["ぬるきゃっとちゃん", "ぬるきゃぼっと", "ぬるきゃっとぼっと"])) return react(":bibibi_nullcatchan:")
if (
includes(note.text, [
"ねむい",
"ねむたい",
"ねたい",
"ねれない",
"ねれん",
"ねれぬ",
"ふむ",
"つら",
"死に",
"つかれた",
"疲れた",
"しにたい",
"きえたい",
"消えたい",
"やだ",
"いやだ",
"なきそう",
"泣きそう",
"辛い",
])
)
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 Module from '@/module';
import Message from '@/message';
import Message from "@/message"
import Module from "@/module"
import autobind from "autobind-decorator"
export default class extends Module {
public readonly name = 'follow';
public readonly name = "follow"
@autobind
public install() {
return {
mentionHook: this.mentionHook
};
mentionHook: this.mentionHook,
}
}
@autobind
private async mentionHook(msg: Message) {
if (msg.text && msg.includes(['フォロー', 'フォロバ', 'follow me'])) {
if (msg.text && msg.includes(["フォロー", "フォロバ", "follow me"])) {
if (!msg.user.isFollowing) {
this.ai.api('following/create', {
this.nullcatChan.api("following/create", {
userId: msg.userId,
});
})
msg.reply("これからよろしくね!", { immediate: true })
return {
reaction: msg.friend.love >= 0 ? 'like' : null
};
reaction: msg.friend.love >= 0 ? ":love_nullcatchan:" : null,
}
} else {
return {
reaction: msg.friend.love >= 0 ? 'hmm' : null
};
reaction: msg.friend.love >= 0 ? ":love_nullcatchan:" : null,
}
}
} else {
return false;
return false
}
}
}

View File

@ -1,66 +1,36 @@
import autobind from 'autobind-decorator';
import Module from '@/module';
import Message from '@/message';
import serifs from '@/serifs';
import * as seedrandom from 'seedrandom';
import { genItem } from '@/vocabulary';
import Message from "@/message"
import Module from "@/module"
import serifs from "@/serifs"
import { genItem } from "@/vocabulary"
import autobind from "autobind-decorator"
import * as seedrandom from "seedrandom"
export const blessing = [
'藍吉',
'ヨタ吉',
'ゼタ吉',
'エクサ吉',
'ペタ吉',
'テラ吉',
'ギガ吉',
'メガ吉',
'キロ吉',
'ヘクト吉',
'デカ吉',
'デシ吉',
'センチ吉',
'ミリ吉',
'マイクロ吉',
'ナノ吉',
'ピコ吉',
'フェムト吉',
'アト吉',
'ゼプト吉',
'ヨクト吉',
'超吉',
'大大吉',
'大吉',
'吉',
'中吉',
'小吉',
'凶',
'大凶',
];
export const blessing = ["にゃん吉🐈", "みゃ~吉🐾", "ぬるきゃっと吉:love_nullcatchan:", "なんかすごい吉✨", "特大吉✨", "大大吉🎊", "大吉🎊", "吉🎉", "中吉🎉", "小吉🎉", "凶🗿", "大凶🗿"]
export default class extends Module {
public readonly name = 'fortune';
public readonly name = "fortune"
@autobind
public install() {
return {
mentionHook: this.mentionHook
};
mentionHook: this.mentionHook,
}
}
@autobind
private async mentionHook(msg: Message) {
if (msg.includes(['占', 'うらな', '運勢', 'おみくじ'])) {
const date = new Date();
const seed = `${date.getFullYear()}/${date.getMonth()}/${date.getDate()}@${msg.userId}`;
const rng = seedrandom(seed);
const omikuji = blessing[Math.floor(rng() * blessing.length)];
const item = genItem(rng);
msg.reply(`**${omikuji}🎉**\nラッキーアイテム: ${item}`, {
cw: serifs.fortune.cw(msg.friend.name)
});
return true;
if (msg.includes(["占", "うらな", "運勢", "おみくじ"])) {
const date = new Date()
const seed = `${date.getFullYear()}/${date.getMonth()}/${date.getDate()}@${msg.userId}`
const rng = seedrandom(seed)
const omikuji = blessing[Math.floor(rng() * blessing.length)]
const item = genItem(rng)
msg.reply(`**${omikuji}**\nラッキーアイテム: ${item}`, {
cw: serifs.fortune.cw(msg.friend.name),
})
return true
} 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 * as loki from 'lokijs';
import Module from '@/module';
import config from '@/config';
import serifs from '@/serifs';
import { mecab } from './mecab';
import config from "@/config"
import Message from "@/message"
import Module from "@/module"
import serifs from "@/serifs"
import NGWord from "../../ng-words";
import autobind from "autobind-decorator"
import * as loki from "lokijs"
import { mecab } from "./mecab"
function kanaToHira(str: string) {
return str.replace(/[\u30a1-\u30f6]/g, match => {
const chr = match.charCodeAt(0) - 0x60;
return String.fromCharCode(chr);
});
return str.replace(/[\u30a1-\u30f6]/g, (match) => {
const chr = match.charCodeAt(0) - 0x60
return String.fromCharCode(chr)
})
}
export default class extends Module {
public readonly name = 'keyword';
public readonly name = "keyword"
private learnedKeywords: loki.Collection<{
keyword: string;
learnedAt: number;
}>;
keyword: string
learnedAt: number
}>
private ngWord = new NGWord()
@autobind
public install() {
if (!config.keywordEnabled) return {};
if (!config.keywordEnabled) return {}
this.learnedKeywords = this.ai.getCollection('_keyword_learnedKeywords', {
indices: ['userId']
});
this.learnedKeywords = this.nullcatChan.getCollection("_keyword_learnedKeywords", {
indices: ["userId"],
})
setInterval(this.learn, 1000 * 60 * 60);
setInterval(this.learn, 1000 * 60 * 45)
return {};
return {
mentionHook: this.mentionHook,
}
}
@autobind
private async learn() {
const tl = await this.ai.api('notes/local-timeline', {
limit: 30
});
const tl = await this.nullcatChan.api("notes/hybrid-timeline", {
limit: 30,
})
const interestedNotes = tl.filter(note =>
note.userId !== this.ai.account.id &&
note.text != null &&
note.cw == null);
const interestedNotes = tl.filter((note) => note.userId !== this.nullcatChan.account.id && note.text != null && note.cw == null)
let keywords: string[][] = [];
let keywords: string[][] = []
for (const note of interestedNotes) {
const tokens = await mecab(note.text, config.mecab, config.mecabDic);
const keywordsInThisNote = tokens.filter(token => token[2] == '固有名詞' && token[8] != null);
keywords = keywords.concat(keywordsInThisNote);
const tokens = await mecab(note.text, config.mecab, config.mecabDic)
const keywordsInThisNote = tokens.filter((token) => token[2] == "固有名詞" && token[8] != null)
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 keyword = keywords.sort((a, b) => a[0].length < b[0].length ? 1 : -1)[rnd];
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 exist = this.learnedKeywords.findOne({
keyword: keyword[0]
});
keyword: keyword[0],
})
let text: string;
let text: string
if (exist) {
return;
return
} else {
this.learnedKeywords.insertOne({
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({
text: text
});
this.nullcatChan.post({
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 * as util from 'util';
import * as stream from 'stream';
import * as memoryStreams from 'memory-streams';
import { EOL } from 'os';
import { spawn } from "child_process"
import * as memoryStreams from "memory-streams"
import { EOL } from "os"
import * as stream from "stream"
import * as util from "util"
const pipeline = util.promisify(stream.pipeline);
const pipeline = util.promisify(stream.pipeline)
/**
* Run MeCab
@ -12,34 +12,34 @@ const pipeline = util.promisify(stream.pipeline);
* @param mecab mecab bin
* @param dic mecab dictionaly path
*/
export async function mecab(text: string, mecab = 'mecab', dic?: string): Promise<string[][]> {
const args: string[] = [];
if (dic) args.push('-d', dic);
export async function mecab(text: string, mecab = "mecab", dic?: string): Promise<string[][]> {
const args: string[] = []
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) {
if (line === 'EOS') break;
const [word, value = ''] = line.split('\t');
const array = value.split(',');
array.unshift(word);
results.push(array);
if (line === "EOS") break
const [word, value = ""] = line.split("\t")
const array = value.split(",")
array.unshift(word)
results.push(array)
}
return results;
return results
}
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.end();
mecab.stdin.write(stdin)
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 Module from '@/module';
import serifs from '@/serifs';
import { genItem } from '@/vocabulary';
import config from '@/config';
import config from "@/config"
import Module from "@/module"
import serifs from "@/serifs"
import autobind from "autobind-decorator"
export default class extends Module {
public readonly name = 'noting';
public readonly name = "noting"
@autobind
public install() {
if (config.notingEnabled === false) return {};
if (config.notingEnabled === false) return {}
setInterval(() => {
if (Math.random() < 0.04) {
this.post();
if (Math.random() < 0.1) {
this.post()
}
}, 1000 * 60 * 10);
}, 1000 * 60 * 10)
return {};
return {}
}
@autobind
private post() {
const 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 notes = serifs.noting.notes
const note = notes[Math.floor(Math.random() * notes.length)];
const note = notes[Math.floor(Math.random() * notes.length)]
// TODO: 季節に応じたセリフ
this.ai.post({
text: typeof note === 'function' ? note() : note
});
this.nullcatChan.post({
text: note,
})
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,75 +1,72 @@
import autobind from 'autobind-decorator';
import Module from '@/module';
import Message from '@/message';
import serifs from '@/serifs';
import Message from "@/message"
import Module from "@/module"
import serifs from "@/serifs"
import autobind from "autobind-decorator"
export default class extends Module {
public readonly name = 'timer';
public readonly name = "timer"
@autobind
public install() {
return {
mentionHook: this.mentionHook,
timeoutCallback: this.timeoutCallback,
};
}
}
@autobind
private async mentionHook(msg: Message) {
const secondsQuery = (msg.text || '').match(/([0-9]+)秒/);
const minutesQuery = (msg.text || '').match(/([0-9]+)分/);
const hoursQuery = (msg.text || '').match(/([0-9]+)時間/);
const secondsQuery = (msg.text || "").match(/([0-9]+)秒/)
const minutesQuery = (msg.text || "").match(/([0-9]+)分/)
const hoursQuery = (msg.text || "").match(/([0-9]+)時間/)
const seconds = secondsQuery ? parseInt(secondsQuery[1], 10) : 0;
const minutes = minutesQuery ? parseInt(minutesQuery[1], 10) : 0;
const hours = hoursQuery ? parseInt(hoursQuery[1], 10) : 0;
const seconds = secondsQuery ? parseInt(secondsQuery[1], 10) : 0
const minutes = minutesQuery ? parseInt(minutesQuery[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) {
msg.reply(serifs.timer.invalid);
return true;
if (seconds + minutes + hours == 0) {
msg.reply(serifs.timer.invalid)
return true
}
const time =
(1000 * seconds) +
(1000 * 60 * minutes) +
(1000 * 60 * 60 * hours);
const time = 1000 * seconds + 1000 * 60 * minutes + 1000 * 60 * 60 * hours
if (time > 86400000) {
msg.reply(serifs.timer.tooLong);
return true;
msg.reply(serifs.timer.tooLong)
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, {
isDm: msg.isDm,
msgId: msg.id,
userId: msg.friend.userId,
time: str
});
time: str,
})
return true;
return true
}
@autobind
private timeoutCallback(data) {
const friend = this.ai.lookupFriend(data.userId);
if (friend == null) return; // 処理の流れ上、実際にnullになることは無さそうだけど一応
const text = serifs.timer.notify(data.time, friend.name);
const friend = this.nullcatChan.lookupFriend(data.userId)
if (friend == null) return // 処理の流れ上、実際にnullになることは無さそうだけど一応
const text = serifs.timer.notify(data.time, friend.name)
if (data.isDm) {
this.ai.sendMessage(friend.userId, {
text: text
});
this.nullcatChan.sendMessage(friend.userId, {
text: text,
})
} else {
this.ai.post({
this.nullcatChan.post({
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 Module from '@/module';
import Friend from '@/friend';
import serifs from '@/serifs';
import Friend from "@/friend"
import Module from "@/module"
import serifs from "@/serifs"
import autobind from "autobind-decorator"
export default class extends Module {
public readonly name = 'valentine';
public readonly name = "valentine"
@autobind
public install() {
this.crawleValentine();
setInterval(this.crawleValentine, 1000 * 60 * 3);
this.crawleValentine()
setInterval(this.crawleValentine, 1000 * 60 * 3)
return {};
return {}
}
/**
@ -19,33 +19,33 @@ export default class extends Module {
*/
@autobind
private crawleValentine() {
const now = new Date();
const now = new Date()
const isValentine = now.getMonth() == 1 && now.getDate() == 14;
if (!isValentine) return;
const isValentine = now.getMonth() == 1 && now.getDate() == 14
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 => {
const friend = new Friend(this.ai, { doc: f });
friends.forEach((f) => {
const friend = new Friend(this.nullcatChan, { doc: f })
// 親愛度が5以上必要
if (friend.love < 5) return;
// 親愛度が7以上必要
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;
friend.setPerModulesData(this, data);
data.lastChocolated = date
friend.setPerModulesData(this, data)
const text = serifs.valentine.chocolateForYou(friend.name);
const text = serifs.valentine.chocolateForYou(friend.name)
this.ai.sendMessage(friend.userId, {
text: text
});
});
this.nullcatChan.sendMessage(friend.userId, {
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 {
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: {
@ -22,88 +22,88 @@ export default {
},
*/
goodNight: name => name ? `おやすみなさい、${name}` : 'おやすみなさい',
goodNight: name => name ? `おやすみ${name}` : 'おやすみ',
omedeto: name => name ? `ありがとうございます、${name}` : 'ありがとうございます♪',
omedeto: name => name ? `ありがと${name}` : 'ありがと~!',
erait: {
general: name => name ? [
`${name}、今日もえらいです`,
`${name}、今日もえらいですよ~♪`
`${name}、今日もえらい`,
`${name}、今日もえらいね!`
] : [
`今日もえらいです`,
`今日もえらいですよ~♪`
`今日もえらい`,
`今日もえらいね!`
],
specify: (thing, name) => name ? [
`${name}${thing}てえらいです`,
`${name}${thing}てえらいですよ~♪`
`${name}${thing}てえらい`,
`${name}${thing}てえらいね!`
] : [
`${thing}てえらいです`,
`${thing}てえらいですよ~♪`
`${thing}てえらい`,
`${thing}てえらいね!`
],
specify2: (thing, name) => name ? [
`${name}${thing}でえらいです`,
`${name}${thing}でえらいですよ~♪`
`${name}${thing}でえらい`,
`${name}${thing}でえらいね!`
] : [
`${thing}でえらいです`,
`${thing}でえらいですよ~♪`
`${thing}でえらい`,
`${thing}でえらいね!`
],
},
okaeri: {
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: {
love: name => name ? `いってらっしゃい${name}` : 'いってらっしゃい♪',
love: name => name ? `いってらっしゃい${name}` : 'いってらっしゃい!',
normal: name => name ? `いってらっしゃい${name}` : 'いってらっしゃい!',
normal: name => name ? `いってらっしゃい${name}` : 'いってらっしゃい!',
},
tooLong: '長すぎる気がします...',
tooLong: '長すぎる..',
invalidName: '発音が難しい気がします',
invalidName: '発音が難しいよぉ...',
nadenade: {
normal: 'ひゃっ…! びっくりしました',
normal: 'うにゃ…?! びっくりした...',
love2: ['わわっ… 恥ずかしいです', 'あうぅ… 恥ずかしいです…', 'ふやぁ…'],
love2: ['あぅ… 恥ずかしいよぉ', 'あぅ… 恥ずかしぃ…', 'ふみゃ…!'],
love3: ['んぅ… ありがとうございます♪', 'わっ、なんだか落ち着きますね♪', 'くぅんっ… 安心します…', '眠くなってきました…'],
love3: ['んへへぇ ありがと:love_nullcatchan:', 'にへぇ~~', 'んみゅっ… ', 'もっともっとぉ...'],
hate1: '…っ! やめてほしいです...',
hate1: 'やめて',
hate2: '触らないでください',
hate2: '触んないで',
hate3: '近寄らないでください',
hate3: 'きもい',
hate4: 'やめてください。通報しますよ',
hate4: '..',
},
kawaii: {
normal: ['ありがとうございます♪', '照れちゃいます...'],
normal: ['そんなことないよ?', 'えへへへうれしい。'],
love: ['嬉しいです♪', '照れちゃいます...'],
love: ['えへへ。うれしいな', 'んむぅ~~...うれしい。'],
hate: '…ありがとうございます'
hate: 'は?きも。'
},
suki: {
normal: 'えっ… ありがとうございます…♪',
normal: 'えへへ。ありがと~!',
love: name => `私もその… ${name}のこと好きですよ`,
love: name => `僕も${name}のこと好き`,
hate: null
},
@ -113,360 +113,163 @@ export default {
love: 'ぎゅーっ♪',
hate: '離れてください...'
hate: '無理...やめて...'
},
humu: {
love: 'え、えっと…… ふみふみ……… どうですか…?',
love: 'もふもふ!ふみふみ!',
normal: 'えぇ... それはちょっと...',
normal: 'ふみふみ!',
hate: '……'
hate: ''
},
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: {
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: {
learned: (word, reading) => `(${word}..... ${reading}..... 覚えました)`,
learned: (word, reading) => `え~っと...${word}...${reading}...僕覚えた!!!`,
remembered: (word) => `${word}`
},
dice: {
done: res => `${res} です!`
},
birthday: {
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}`,
happyBirthday: name => name ? `お誕生日おめでと~~~!!!${name}` : 'お誕生日おめでと~~~~~!!!',
},
/**
*
*/
fortune: {
cw: name => name ? `私が今日の${name}の運勢を占いました...` : '私が今日のあなたの運勢を占いました...',
cw: name => name ? `今日の${name}の運勢を占ったよ!` : '今日のきみの運勢を占ったよ!',
},
/**
*
*/
timer: {
set: 'わかりました',
set: 'OK',
invalid: 'うーん...',
invalid: 'うむむ?',
tooLong: '長すぎます…',
tooLong: '長すぎる…',
notify: (time, name) => name ? `${name}${time}経ちましたよ!` : `${time}経ちましたよ!`
notify: (time, name) => name ? `${name}${time}経ったよ!` : `${time}経ったよ!`
},
/**
*
*/
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 ? [
`よく出来ました、${name}`,
`${name}、さすがですっ`,
`${name}、えらすぎます...`,
`すごい!!天才!!${name}えらい!!`,
`${name}さすがすぎる!!!`,
`${name}えらすぎる!!`,
] : [
`よく出来ました♪`,
`さすがですっ`,
`えらすぎます...`,
`すごい!!天才!!えらい!!`,
`さすがすぎる!!!`,
`えらすぎる!!`,
],
cancel: `わかりました。`,
cancel: `OK`,
},
server: {
cpu: 'サーバーざぁこ♡♡♡'
},
/**
*
*/
rogubo: 'ログボ!!',
/**
*
*/
valentine: {
chocolateForYou: name => name ? `${name}、その... チョコレート作ったのでよかったらどうぞ!🍫` : 'チョコレート作ったのでよかったらどうぞ!🍫',
},
server: {
cpu: 'サーバーの負荷が高そうです。大丈夫でしょうか...'
},
maze: {
post: '今日の迷路です! #AiMaze',
foryou: '描きました!'
},
chart: {
post: 'インスタンスの投稿数です!',
foryou: '描きました!'
chocolateForYou: name => name ? `${name}!チョコあげる!` : 'チョコあげる!',
},
sleepReport: {
report: hours => `ぅ、${hours}時間くらい寝ちゃってたみたいです`,
reportUtatane: 'ん... うたた寝しちゃってました',
report: hours => `んぬぁ~、${hours}時間くらいねちゃってたかも`,
reportUtatane: 'ぬぁ... ',
},
noting: {
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;
}
}

View File

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

View File

@ -1,20 +1,7 @@
import * as seedrandom from 'seedrandom';
export const itemPrefixes = [
'プラチナ製',
'新鮮な',
'最新式の',
'古代の',
'手作り',
'時計じかけの',
'伝説の',
'焼き',
'生の',
'藍謹製',
'ポケットサイズ',
'3日前の',
'そこらへんの',
'偽の',
'使用済み',
'壊れた',
'市販の',
@ -22,52 +9,24 @@ export const itemPrefixes = [
'業務用の',
'Microsoft製',
'Apple製',
'人類の技術を結集して作った',
'2018年製', // TODO ランダム
'500kgくらいある',
'高級',
'腐った',
'人工知能搭載',
'反重力',
'折り畳み式',
'携帯型',
'遺伝子組み換え',
'飛行能力を獲得した',
'純金製',
'透明な',
'光る',
'ハート型の',
'動く',
'半分にカットされた',
'USBコネクタ付きの',
'いにしえの',
'呪われた',
'エンチャントされた',
'一日分のビタミンが入った',
'かじりかけ',
'幻の',
'仮想的な',
'原子力',
'高度に訓練された',
'遺伝子組み換えでない',
'ダンジョン最深部で見つかった',
'異世界の',
'異星の',
'謎の',
'時空を歪める',
'異音がする',
'霧散する',
'プラズマ化した',
'衝撃を与えると低確率で爆発する',
'ズッキーニに擬態した',
'仮説上の',
'毒の',
'真の',
'究極の',
'チョコ入り',
'異臭を放つ',
'4次元',
'脈動する',
'得体の知れない',
'四角い',
'暴れ回る',
@ -75,70 +34,27 @@ export const itemPrefixes = [
'闇の',
'暗黒の',
'封印されし',
'死の',
'凍った',
'魔の',
'禁断の',
'ホログラフィックな',
'油圧式',
'辛そうで辛くない少し辛い',
'焦げた',
'宇宙',
'電子',
'陽電子',
'量子力学的',
'シュレディンガーの',
'分散型',
'卵かけ',
'次世代',
'帯電',
'太古の',
'WiFi対応',
'高反発',
'【令和最新版】',
'廉価版',
'ねばねば',
'どろどろ',
'パサパサの',
'湿気った',
'賞味期限切れ',
'地獄から来た',
'ニンニクマシ',
'放射性',
'フラクタルな',
'再帰的',
'ときどき分裂する',
'3G対応',
'消費期限切れ',
'消える',
'等速直線運動する',
'X線照射',
'蠢く',
'形而上学的',
'もちもち',
'冷やし',
'あつあつ',
'巨大',
'ナノサイズ',
'やわらかい',
'人の手に負えない',
'バグった',
'人工',
'天然',
'祀られた',
'チョコレートコーティング',
'抗菌仕様',
'耐火',
'激',
'猛',
'超',
'群生する',
'軽量',
'国宝級',
'流行りの',
'8カラットの',
'中古の',
'新品の',
'愛妻',
'ブランドものの',
'増殖する',
'ぷるぷる',
'ぐにゃぐにゃ',
'多目的',
@ -146,315 +62,54 @@ export const itemPrefixes = [
'激辛',
'先進的な',
'レトロな',
'ヴィンテージ',
'合法',
'違法',
'プレミア付き',
'デカ',
'ギガ',
'穢れた',
'品質保証付き',
'AppleCare+加入済み',
'えっちな',
'デザイナーズ',
'蠱惑的な',
'霊験灼かな',
'つやつや',
'べとべと',
'ムキムキ',
'オーバークロックされた',
'無機質な',
'前衛的な',
'怪しい',
'妖しい',
'カビの生えた',
'熟成',
'アルミダイキャスト',
'養殖',
'やばい',
'すごい',
'かわいい',
'デジタル',
'アナログ',
'彁な',
'カラフルな',
'電動',
'当たり判定のない',
'めり込んだ',
'100年に一度の',
'ジューシーな',
'Hi-Res',
'確変',
'食用',
'THE ',
'某',
'朽ちゆく',
'滅びの',
'反発係数がe>1の',
'摩擦係数0の',
'解き放たれし',
'大きな',
'小さな',
'強欲な',
'うねうね',
'水没',
'燃え盛る',
'高圧',
'異常',
];
export const items = [
'ナス',
'トマト',
'きゅうり',
'じゃがいも',
'焼きビーフン',
'腰',
'寿司',
'かぼちゃ',
'諭吉',
'キロバー',
'アルミニウム',
'ナトリウム',
'マグネシウム',
'プルトニウム',
'ちいさなメダル',
'牛乳パック',
'ペットボトル',
'クッキー',
'チョコレート',
'メイド服',
'オレンジ',
'ニーソ',
'反物質コンデンサ',
'粒子加速器',
'マイクロプロセッサ(4コア8スレッド)',
'原子力発電所',
'レイヤ4スイッチ',
'緩衝チェーン',
'陽電子頭脳',
'惑星',
'テルミン',
'虫歯車',
'マウンター',
'バケットホイールエクスカベーター',
'デーモンコア',
'ゲームボーイアドバンス',
'右足',
'左足',
'お金',
'金パブ',
'ブロン',
'ぬるきゃっとちゃん!',
'この世のすべて',
'量子コンピューター',
'アナモルフィックレンズ',
'押し入れの奥から出てきた謎の生き物',
'スマートフォン',
'時計',
'プリン',
'ガブリエルのラッパ',
'メンガーのスポンジ',
'ハンドスピナー',
'超立方体',
'建築物',
'エナジードリンク',
'マウスカーソル',
'メガネ',
'まぐろ',
'ゴミ箱',
'つまようじ',
'お弁当に入ってる緑の仕切りみたいなやつ',
'割りばし',
'換気扇',
'ペットボトルのキャップ',
'消波ブロック',
'ピザ',
'歯磨き粉',
'空き缶',
'キーホルダー',
'金髪碧眼の美少女',
'SDカード',
'リップクリーム',
'チョコ無しチョココロネ',
'鳥インフルエンザ',
'自動販売機',
'重いもの',
'ノートパソコン',
'ビーフジャーキー',
'さけるチーズ',
'ダイヤモンド',
'物体',
'月の石',
'特異点',
'中性子星',
'液体',
'衛星',
'ズッキーニ',
'黒いもの',
'白いもの',
'赤いもの',
'丸いもの',
'四角いもの',
'カード状のもの',
'気体',
'鉛筆',
'消しゴム',
'つるぎ',
'棒状のもの',
'農産物',
'メタルスライム',
'タコの足',
'きのこ',
'なめこ',
'缶チューハイ',
'爪切り',
'耳かき',
'スマホ',
'PC',
'モンスター',
'好きなもの',
'ぬいぐるみ',
'ティラノサウルス',
'尿路結石',
'エンターキー',
'壺',
'水銀',
'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 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秒」のように単位を混ぜることもできるよ
### リマインダー
```
@ai remind 部屋の掃除
```
のようにメンションを飛ばすと12時間置きに責付かれます。その飛ばしたメンションか、藍ちゃんからの催促に「やった」または「やめた」と返信することでリマインダー解除されます。
また、引用Renoteでメンションすることもできます。
`@nullcat todo(リマインド、これやる) 寝る` みたいに言ってくれたら1時間置きにリマインドするよ。その飛ばしたメンションか、僕からの催促に「やった」「やめた」など返信するとリマインダー解除されるよ<br>
引用Renoteでメンションすることもできるよ<br>
リマインダーの一覧は `@nullcat todos` で見れるよ
### 福笑い
藍に「絵文字」と言うと、藍が考えた絵文字の組み合わせを教えてくれます。
### GitHub Status
僕に「GitHub」って言ってくれたら今のStatusを教えるよ
### サイコロ
ダイスノーテーションを伝えるとサイコロを振ってくれます。
例: "2d6" (6面サイコロを2回振る)、"3d5" (5面サイコロを3回振る)
### Cloudflare Status
僕に「Cloudflare」って言ってくれたら今のStatusを教えるよ
### 迷路
「迷路」と言うと迷路を描いてくれます。「難しい」「簡単」などの言葉を添えることで、難易度も調整できます。
### シェル芸機能
僕に #シェル芸#shellge をつけてコマンドを送ってくれたら実行結果を返すよ
### 数当てゲーム
藍にメッセージで「数当てゲーム」と言うと遊べます。
藍の考えている数字を当てるゲームです。
### 怪レい曰本语変換
僕に `#怪しい日本語変換` っていうタグ付きで変換してほしい文章をメンションしてくれたら怪レい曰本语に変換するよ
### 数取りゲーム
藍に「数取りゲーム」と言うと遊べます。
複数人で行うゲームで、もっとも大きい数字を言った人が勝ちです。
### やること決める
僕に「なにしよ」って言ってくれたらやることを決めるよ
### リバーシ
藍とリバーシで対局できます。(この機能はインスタンスによっては無効になっている可能性があります)
藍に「リバーシ」と言うか、リバーシで藍を指名すれば対局できます。
強さも調整できます。
### 覚える
たまにタイムラインにあるキーワードを「覚え」ます。
(この機能はインスタンスによっては無効になっている可能性があります)
### 気圧
僕に「気圧教えて」って言ってくれたら今の気圧を教えるよ
### 呼び方を教える
藍があなたのことをなんて呼べばいいか教えられます。
ただし後述の親愛度が一定の値に達している必要があります。
(トークでのみ反応します)
### いらっしゃい
Misskeyにアカウントを作成して初めて投稿を行うと、藍がネコミミアンテナでそれを補足し、Renoteしてみんなに知らせてくれる機能です。
### Follow me
藍に「フォローして」と言うとフォローしてくれます。
僕が君のことをなんて呼べばいいか教えてくれたら、その名前で呼ぶよ!<br>
親愛度が一定の値に達している必要があるよ<br>
(チャットのみで反応するよ)
### HappyBirthday
藍があなたの誕生日を祝ってくれます。
誕生日になったら僕が君の誕生日を祝うよ
### バレンタイン
藍がチョコレートをくれます。
### チャート
インスタンスの投稿チャートなどを投稿してくれます。
### サーバー監視
サーバーの状態を監視し、負荷が高くなっているときは教えてくれます。
バレンタインになったら仲のいい子に僕がチョコレートをあげるよ
### ping
PONGを返します。生存確認にどうぞ
僕に「ping」って言ってくれたらフォローするよで起きてるとき返信するよ寝てるときは返信できないかも...
### その他反応するフレーズ (トークのみ)
* かわいい
* なでなで
* 好き
* ぎゅー
* 罵って
* 踏んで
* 痛い
### 親愛度
僕は君に対する親愛度を持っているよ<br>
僕にお話ししてくれたりすると、少しずつ上がるよ<br>
親愛度によって反応が変化するよ!親愛度がある程度ないとしてくれないこともあるよ<br>
たくさん話しかけてね
## 親愛度
藍はあなたに対する親愛度を持っています。
藍に挨拶したりすると、少しずつ上がっていきます。
親愛度によって反応や各種セリフが変化します。親愛度がある程度ないとしてくれないこともあります。
僕のリポジトリは[ここ](https://github.com/nullnyat/NullcatChan)だよ

View File

@ -14,10 +14,10 @@
"noLib": false,
"outDir": "built",
"rootDir": "src",
"baseUrl": ".",
"baseUrl": "../NullcatChan-old",
"paths": {
"@/*": ["src/*"]
},
}
},
"compileOnSave": false,
"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 => {
/**
*
*
*
*/
function denoise(text: string): string {
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(/、+$/, '');
}

4919
yarn.lock Normal file

File diff suppressed because it is too large Load Diff