├── .node-version ├── .npmrc ├── ai.png ├── .vscode └── settings.json ├── WorksOnMyMachine.png ├── .gitignore ├── .github ├── dependabot.yml └── workflows │ ├── dockerimage.yml │ ├── nodejs.yml │ └── docker.yml ├── src ├── misskey │ ├── user.ts │ └── note.ts ├── utils │ ├── acct.ts │ ├── get-date.ts │ ├── safe-for-interpolate.ts │ ├── includes.ts │ ├── log.ts │ ├── or.ts │ └── japanese.ts ├── modules │ ├── maze │ │ ├── maze.ts │ │ ├── themes.ts │ │ ├── index.ts │ │ ├── gen-maze.ts │ │ └── render-maze.ts │ ├── ping │ │ └── index.ts │ ├── sleep-report │ │ └── index.ts │ ├── follow │ │ └── index.ts │ ├── welcome │ │ └── index.ts │ ├── dice │ │ └── index.ts │ ├── valentine │ │ └── index.ts │ ├── keyword │ │ ├── mecab.ts │ │ └── index.ts │ ├── birthday │ │ └── index.ts │ ├── server │ │ └── index.ts │ ├── fortune │ │ └── index.ts │ ├── emoji-react │ │ └── index.ts │ ├── version │ │ └── index.ts │ ├── emoji │ │ └── index.ts │ ├── timer │ │ └── index.ts │ ├── noting │ │ └── index.ts │ ├── guessing-game │ │ └── index.ts │ ├── core │ │ └── index.ts │ ├── chart │ │ ├── index.ts │ │ └── render-chart.ts │ ├── reversi │ │ ├── index.ts │ │ └── back.ts │ ├── reminder │ │ └── index.ts │ ├── kazutori │ │ └── index.ts │ ├── poll │ │ └── index.ts │ └── talk │ │ └── index.ts ├── kewyegenabo.ts ├── config.ts ├── module.ts ├── message.ts ├── index.ts ├── friend.ts ├── vocabulary.ts ├── stream.ts ├── serifs.ts └── ai.ts ├── .dockerignore ├── .editorconfig ├── example.config.json ├── docker-compose.yml ├── tsconfig.json ├── Dockerfile ├── LICENSE ├── package.json ├── README.md ├── torisetu.md ├── ai.svg └── yarn.lock /.node-version: -------------------------------------------------------------------------------- 1 | 20.18.1 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | save-exact = true 2 | package-lock = false 3 | -------------------------------------------------------------------------------- /ai.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mei23/ai/HEAD/ai.png -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules\\typescript\\lib" 3 | } -------------------------------------------------------------------------------- /WorksOnMyMachine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mei23/ai/HEAD/WorksOnMyMachine.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | config.json 2 | built 3 | node_modules 4 | memory.json 5 | memory/ 6 | data 7 | font.ttf 8 | yarn-error.log 9 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 50 8 | -------------------------------------------------------------------------------- /src/misskey/user.ts: -------------------------------------------------------------------------------- 1 | export type User = { 2 | id: string; 3 | name: string; 4 | username: string; 5 | host?: string | null; 6 | isFollowing?: boolean; 7 | isBot: boolean; 8 | }; 9 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .github 3 | .vscode 4 | Dockerfile 5 | built/ 6 | data/ 7 | node_modules/ 8 | config.json 9 | memory.json 10 | memory/ 11 | font.ttf 12 | docker-compose.yml 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | indent_size = 2 6 | charset = utf-8 7 | insert_final_newline = true 8 | 9 | [*.yml] 10 | indent_style = space 11 | -------------------------------------------------------------------------------- /src/utils/acct.ts: -------------------------------------------------------------------------------- 1 | export function acct(user: { username: string; host?: string | null; }): string { 2 | return user.host 3 | ? `@${user.username}@${user.host}` 4 | : `@${user.username}`; 5 | } 6 | -------------------------------------------------------------------------------- /src/utils/get-date.ts: -------------------------------------------------------------------------------- 1 | export default function (): string { 2 | const now = new Date(); 3 | const y = now.getFullYear(); 4 | const m = now.getMonth(); 5 | const d = now.getDate(); 6 | const today = `${y}/${m + 1}/${d}`; 7 | return today; 8 | } 9 | -------------------------------------------------------------------------------- /src/modules/maze/maze.ts: -------------------------------------------------------------------------------- 1 | export type CellType = 'void' | 'empty' | 'left' | 'right' | 'top' | 'bottom' | 'leftTop' | 'leftBottom' | 'rightTop' | 'rightBottom' | 'leftRightTop' | 'leftRightBottom' | 'leftTopBottom' | 'rightTopBottom' | 'leftRight' | 'topBottom' | 'cross'; 2 | -------------------------------------------------------------------------------- /example.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "host": "https://example.com", 3 | "i": "", 4 | "master": null, 5 | "notingEnabled": true, 6 | "keywordEnabled": false, 7 | "keywordInterval": 60, 8 | "chartEnabled": true, 9 | "reversiEnabled": false, 10 | "serverMonitoring": false 11 | } 12 | -------------------------------------------------------------------------------- /src/misskey/note.ts: -------------------------------------------------------------------------------- 1 | export type Note = { 2 | id: string; 3 | userId: string; 4 | text: string | null; 5 | reply: any | null; 6 | poll?: { 7 | choices: { 8 | votes: number; 9 | text: string; 10 | }[]; 11 | expiredAfter: number; 12 | multiple: boolean; 13 | } | null; 14 | }; 15 | -------------------------------------------------------------------------------- /src/utils/safe-for-interpolate.ts: -------------------------------------------------------------------------------- 1 | const invalidChars = [ 2 | '@', 3 | '#', 4 | '*', 5 | ':', 6 | '(', 7 | ')', 8 | '[', 9 | ']', 10 | ' ', 11 | ' ', 12 | ]; 13 | 14 | export function safeForInterpolate(text: string): boolean { 15 | return !invalidChars.some(c => text.includes(c)); 16 | } 17 | -------------------------------------------------------------------------------- /.github/workflows/dockerimage.yml: -------------------------------------------------------------------------------- 1 | name: Docker Image CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v2 11 | - name: Build the Docker image 12 | run: docker build . --file Dockerfile --tag my-image-name:$(date +%s) 13 | -------------------------------------------------------------------------------- /src/utils/includes.ts: -------------------------------------------------------------------------------- 1 | import { katakanaToHiragana, hankakuToZenkaku } from './japanese'; 2 | 3 | export default function(text: string, words: string[]): boolean { 4 | if (text == null) return false; 5 | 6 | text = katakanaToHiragana(hankakuToZenkaku(text)).toLowerCase(); 7 | words = words.map(word => katakanaToHiragana(word).toLowerCase()); 8 | 9 | return words.some(word => text.includes(word)); 10 | } 11 | -------------------------------------------------------------------------------- /src/utils/log.ts: -------------------------------------------------------------------------------- 1 | import * as chalk from 'chalk'; 2 | 3 | export default function(msg: string) { 4 | const now = new Date(); 5 | const date = `${zeroPad(now.getHours())}:${zeroPad(now.getMinutes())}:${zeroPad(now.getSeconds())}`; 6 | console.log(`${chalk.gray(date)} ${msg}`); 7 | } 8 | 9 | function zeroPad(num: number, length: number = 2): string { 10 | return ('0000000000' + num).slice(-length); 11 | } 12 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | node-version: [18.x, 20.x, 22.x] 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - run: yarn install 21 | - run: yarn build 22 | -------------------------------------------------------------------------------- /src/kewyegenabo.ts: -------------------------------------------------------------------------------- 1 | const trueSet = 'ァアィイゥウェエォオカヵガキギクグケヶゲコゴサザシジスズセゼソゾタダチヂッツヅテデトドナニヌネノハバパヒビピフブプヘベペホボポマミムメモャヤュユョヨラリルレロヮワヰヱヲンヴァィゥェォヵヶッャュョヮー'; 2 | const falseSet = 'ょゎァアィイゥウェエォオカヵガキギクグケヶゲコゴサザシジスズセゼソゾタダチヂッツヅテデトドナニヌネノハバパヒビピフブプヘベペホボポマミムメモャヤュユョヨラリルレロヮワヰヱヲンヴァィゥェォヵヶッャュョ'; 3 | 4 | export function kewyegenabo(text: string | null) { 5 | if (text == null) return text; 6 | return [...text].map((c) => { 7 | const idx = trueSet.indexOf(c); 8 | if (idx >= 0) { 9 | return falseSet[idx]; 10 | } else { 11 | return c; 12 | } 13 | }).join(''); 14 | } 15 | -------------------------------------------------------------------------------- /src/modules/ping/index.ts: -------------------------------------------------------------------------------- 1 | import autobind from 'autobind-decorator'; 2 | import Module from '@/module'; 3 | import Message from '@/message'; 4 | 5 | export default class extends Module { 6 | public readonly name = 'ping'; 7 | 8 | @autobind 9 | public install() { 10 | return { 11 | mentionHook: this.mentionHook 12 | }; 13 | } 14 | 15 | @autobind 16 | private async mentionHook(msg: Message) { 17 | if (msg.text && msg.text.includes('ping')) { 18 | msg.reply('PONG!', { 19 | immediate: true 20 | }); 21 | return true; 22 | } else { 23 | return false; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | ai: 5 | build: . 6 | restart: always 7 | environment: 8 | FC_LANG: ja 9 | #FC_LANG: zh_CN 10 | #FC_LANG: zh_TW 11 | TZ: Asia/Tokyo 12 | #TZ: Asia/Kamchatka 13 | #TZ: Asia/Seoul 14 | #TZ: Asia/Pyongyang 15 | #TZ: Asia/Shanghai 16 | #TZ: Asia/Taipei 17 | #TZ: America/New_York 18 | #TZ: America/Chicago 19 | #TZ: America/Los_Angeles 20 | #TZ: UTC 21 | #TZ: Europe/London 22 | volumes: 23 | - ./config.json:/ai/config.json:ro 24 | - ./memory:/ai/memory 25 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | type Config = { 2 | host: string; 3 | i: string; 4 | master?: string; 5 | wsUrl: string; 6 | apiUrl: string; 7 | keywordEnabled: boolean; 8 | keywordInterval?: number; 9 | reversiEnabled: boolean; 10 | notingEnabled: boolean; 11 | chartEnabled: boolean; 12 | serverMonitoring: boolean; 13 | mecab?: string; 14 | mecabDic?: string; 15 | mecabNeologd?: boolean; 16 | welcomeLocal?: boolean; 17 | }; 18 | 19 | const config = require('../config.json'); 20 | 21 | config.wsUrl = config.host.replace('http', 'ws'); 22 | config.apiUrl = config.host + '/api'; 23 | 24 | export default config as Config; 25 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noEmitOnError": true, 4 | "noImplicitAny": false, 5 | "noImplicitReturns": true, 6 | "noImplicitThis": true, 7 | "noFallthroughCasesInSwitch": true, 8 | "strictNullChecks": true, 9 | "experimentalDecorators": true, 10 | "sourceMap": false, 11 | "target": "es2017", 12 | "module": "commonjs", 13 | "removeComments": false, 14 | "noLib": false, 15 | "outDir": "built", 16 | "rootDir": "src", 17 | "baseUrl": ".", 18 | "paths": { 19 | "@/*": ["src/*"] 20 | }, 21 | }, 22 | "compileOnSave": false, 23 | "include": [ 24 | "./src/**/*.ts" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20.18.1-bullseye 2 | 3 | RUN apt-get update 4 | RUN apt-get install -y build-essential mecab libmecab-dev mecab-ipadic-utf8 sudo git make curl xz-utils file fonts-noto 5 | 6 | WORKDIR /mecab-ipadic-neologd 7 | RUN git clone --depth 1 https://github.com/yokomotod/mecab-ipadic-neologd.git 8 | RUN cd mecab-ipadic-neologd && ./bin/install-mecab-ipadic-neologd -n -y -a 9 | 10 | WORKDIR /ai 11 | COPY . ./ 12 | RUN yarn install 13 | RUN yarn build 14 | 15 | # font.ttfがないとコケるバージョン用 (存在しない文字はfallbackするので別に欧文フォントでいい) 16 | RUN ln -s /usr/share/fonts/truetype/noto/NotoSans-Regular.ttf font.ttf 17 | 18 | CMD ["npm", "start"] 19 | -------------------------------------------------------------------------------- /src/modules/sleep-report/index.ts: -------------------------------------------------------------------------------- 1 | import autobind from 'autobind-decorator'; 2 | import Module from '@/module'; 3 | import serifs from '@/serifs'; 4 | 5 | export default class extends Module { 6 | public readonly name = 'sleepReport'; 7 | 8 | @autobind 9 | public install() { 10 | this.report(); 11 | 12 | return {}; 13 | } 14 | 15 | @autobind 16 | private report() { 17 | const now = Date.now(); 18 | 19 | const sleepTime = now - this.ai.lastSleepedAt; 20 | 21 | const sleepHours = sleepTime / 1000 / 60 / 60; 22 | 23 | if (sleepHours < 0.1) return; 24 | 25 | if (sleepHours >= 1) { 26 | this.ai.post({ 27 | text: serifs.sleepReport.report(Math.round(sleepHours)) 28 | }); 29 | } else { 30 | this.ai.post({ 31 | text: serifs.sleepReport.reportUtatane 32 | }); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/modules/follow/index.ts: -------------------------------------------------------------------------------- 1 | import autobind from 'autobind-decorator'; 2 | import Module from '@/module'; 3 | import Message from '@/message'; 4 | 5 | export default class extends Module { 6 | public readonly name = 'follow'; 7 | 8 | @autobind 9 | public install() { 10 | return { 11 | mentionHook: this.mentionHook 12 | }; 13 | } 14 | 15 | @autobind 16 | private async mentionHook(msg: Message) { 17 | if (msg.text && msg.includes(['フォロー', 'フォロバ', 'follow me'])) { 18 | if (!msg.user.isFollowing) { 19 | this.ai.api('following/create', { 20 | userId: msg.userId, 21 | }); 22 | return { 23 | reaction: msg.friend.love >= 0 ? 'like' : null 24 | }; 25 | } else { 26 | return { 27 | reaction: msg.friend.love >= 0 ? 'hmm' : null 28 | }; 29 | } 30 | } else { 31 | return false; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/modules/welcome/index.ts: -------------------------------------------------------------------------------- 1 | import autobind from 'autobind-decorator'; 2 | import Module from '@/module'; 3 | import config from '@/config'; 4 | 5 | export default class extends Module { 6 | public readonly name = 'welcome'; 7 | 8 | @autobind 9 | public install() { 10 | const tl = this.ai.connection.useSharedConnection('localTimeline'); 11 | 12 | tl.on('note', this.onLocalNote); 13 | 14 | return {}; 15 | } 16 | 17 | @autobind 18 | private onLocalNote(note: any) { 19 | if (note.isFirstNote) { 20 | setTimeout(() => { 21 | this.ai.api('notes/create', { 22 | visibility: 'public', 23 | localOnly: !!config.welcomeLocal, 24 | text: '新規さんを見つけました', 25 | renoteId: note.id 26 | }); 27 | }, 3000); 28 | 29 | setTimeout(() => { 30 | this.ai.api('notes/reactions/create', { 31 | noteId: note.id, 32 | reaction: 'congrats' 33 | }); 34 | }, 5000); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: Publish Docker image 2 | 3 | on: 4 | release: 5 | types: [published] 6 | workflow_dispatch: 7 | 8 | jobs: 9 | push_to_registry: 10 | name: Push Docker image to Docker Hub 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Check out the repo 15 | uses: actions/checkout@v2 16 | - name: Docker meta 17 | id: meta 18 | uses: docker/metadata-action@v3 19 | with: 20 | images: mei23/ia 21 | - name: Log in to Docker Hub 22 | uses: docker/login-action@v1 23 | with: 24 | username: ${{ secrets.DOCKER_USERNAME }} 25 | password: ${{ secrets.DOCKER_PASSWORD }} 26 | - name: Build and Push to Docker Hub 27 | uses: docker/build-push-action@v2 28 | with: 29 | context: . 30 | push: true 31 | tags: ${{ steps.meta.outputs.tags }} 32 | labels: ${{ steps.meta.outputs.labels }} 33 | -------------------------------------------------------------------------------- /src/modules/dice/index.ts: -------------------------------------------------------------------------------- 1 | import autobind from 'autobind-decorator'; 2 | import Module from '@/module'; 3 | import Message from '@/message'; 4 | import serifs from '@/serifs'; 5 | 6 | export default class extends Module { 7 | public readonly name = 'dice'; 8 | 9 | @autobind 10 | public install() { 11 | return { 12 | mentionHook: this.mentionHook 13 | }; 14 | } 15 | 16 | @autobind 17 | private async mentionHook(msg: Message) { 18 | if (msg.text == null) return false; 19 | 20 | const query = msg.text.match(/([0-9]+)[dD]([0-9]+)/); 21 | 22 | if (query == null) return false; 23 | 24 | const times = parseInt(query[1], 10); 25 | const dice = parseInt(query[2], 10); 26 | 27 | if (times < 1 || times > 10) return false; 28 | if (dice < 2 || dice > Math.pow(2, 48)) return false; 29 | 30 | const results: number[] = []; 31 | 32 | for (let i = 0; i < times; i++) { 33 | results.push(Math.floor(Math.random() * dice) + 1); 34 | } 35 | 36 | msg.reply(serifs.dice.done(results.join(' '))); 37 | 38 | return true; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018-2021 syuilo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/modules/maze/themes.ts: -------------------------------------------------------------------------------- 1 | export const themes = [{ 2 | bg1: '#C1D9CE', 3 | bg2: '#F2EDD5', 4 | wall: '#0F8AA6', 5 | road: '#C1D9CE', 6 | marker: '#84BFBF', 7 | }, { 8 | bg1: '#17275B', 9 | bg2: '#1F2E67', 10 | wall: '#17275B', 11 | road: '#6A77A4', 12 | marker: '#E6E5E3', 13 | }, { 14 | bg1: '#BFD962', 15 | bg2: '#EAF2AC', 16 | wall: '#1E4006', 17 | road: '#BFD962', 18 | marker: '#74A608', 19 | }, { 20 | bg1: '#C0CCB8', 21 | bg2: '#FFE2C0', 22 | wall: '#664A3C', 23 | road: '#FFCB99', 24 | marker: '#E78F72', 25 | }, { 26 | bg1: '#101010', 27 | bg2: '#151515', 28 | wall: '#909090', 29 | road: '#202020', 30 | marker: '#606060', 31 | }, { 32 | bg1: '#e0e0e0', 33 | bg2: '#f2f2f2', 34 | wall: '#a0a0a0', 35 | road: '#e0e0e0', 36 | marker: '#707070', 37 | }, { 38 | bg1: '#7DE395', 39 | bg2: '#D0F3CF', 40 | wall: '#349D9E', 41 | road: '#7DE395', 42 | marker: '#56C495', 43 | }, { 44 | bg1: '#C9EEEA', 45 | bg2: '#DBF4F1', 46 | wall: '#4BC6B9', 47 | road: '#C9EEEA', 48 | marker: '#19A89D', 49 | }, { 50 | bg1: '#1e231b', 51 | bg2: '#27331e', 52 | wall: '#67b231', 53 | road: '#385622', 54 | marker: '#78d337', 55 | }]; 56 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "_v": "1.8.0-ia", 3 | "main": "./built/index.js", 4 | "engines": { 5 | "node": ">=14.17.0" 6 | }, 7 | "scripts": { 8 | "start": "node ./built", 9 | "build": "tsc" 10 | }, 11 | "dependencies": { 12 | "@twemoji/parser": "15.1.1", 13 | "@types/chalk": "2.2.0", 14 | "@types/lokijs": "1.5.14", 15 | "@types/node": "22.10.5", 16 | "@types/node-fetch": "2.6.11", 17 | "@types/promise-retry": "1.1.6", 18 | "@types/random-seed": "0.3.5", 19 | "@types/seedrandom": "3.0.8", 20 | "@types/uuid": "10.0.0", 21 | "@types/ws": "8.5.13", 22 | "autobind-decorator": "2.4.0", 23 | "chalk": "4.1.2", 24 | "lokijs": "1.5.12", 25 | "memory-streams": "0.1.3", 26 | "misskey-reversi": "0.0.5", 27 | "module-alias": "2.2.3", 28 | "neologd-normalizer": "0.0.3", 29 | "node-fetch": "2.7.0", 30 | "promise-retry": "2.0.1", 31 | "random-seed": "0.3.0", 32 | "reconnecting-websocket": "4.4.0", 33 | "seedrandom": "3.0.5", 34 | "timeout-as-promise": "1.0.0", 35 | "ts-node": "10.9.2", 36 | "typescript": "5.7.2", 37 | "ws": "8.18.0" 38 | }, 39 | "optionalDependencies": { 40 | "canvas": "3.0.1" 41 | }, 42 | "_moduleAliases": { 43 | "@": "built" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/modules/valentine/index.ts: -------------------------------------------------------------------------------- 1 | import autobind from 'autobind-decorator'; 2 | import Module from '@/module'; 3 | import Friend from '@/friend'; 4 | import serifs from '@/serifs'; 5 | 6 | export default class extends Module { 7 | public readonly name = 'valentine'; 8 | 9 | @autobind 10 | public install() { 11 | this.crawleValentine(); 12 | setInterval(this.crawleValentine, 1000 * 60 * 3); 13 | 14 | return {}; 15 | } 16 | 17 | /** 18 | * チョコ配り 19 | */ 20 | @autobind 21 | private crawleValentine() { 22 | const now = new Date(); 23 | 24 | const isValentine = now.getMonth() == 1 && now.getDate() == 14; 25 | if (!isValentine) return; 26 | 27 | const date = `${now.getFullYear()}-${now.getMonth()}-${now.getDate()}`; 28 | 29 | const friends = this.ai.friends.find({} as any); 30 | 31 | friends.forEach(f => { 32 | const friend = new Friend(this.ai, { doc: f }); 33 | 34 | // 親愛度が7以上必要 35 | if (friend.love < 7) return; 36 | 37 | const data = friend.getPerModulesData(this); 38 | 39 | if (data.lastChocolated == date) return; 40 | 41 | data.lastChocolated = date; 42 | friend.setPerModulesData(this, data); 43 | 44 | const text = serifs.valentine.chocolateForYou(friend.name); 45 | 46 | this.ai.sendMessage(friend.userId, { 47 | text: text 48 | }).catch(() => {}); 49 | }); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/modules/keyword/mecab.ts: -------------------------------------------------------------------------------- 1 | import { spawn } from 'child_process'; 2 | import * as util from 'util'; 3 | import * as stream from 'stream'; 4 | import * as memoryStreams from 'memory-streams'; 5 | import { EOL } from 'os'; 6 | 7 | const pipeline = util.promisify(stream.pipeline); 8 | 9 | /** 10 | * Run MeCab 11 | * @param text Text to analyze 12 | * @param mecab mecab bin 13 | * @param dic mecab dictionaly path 14 | */ 15 | export async function mecab(text: string, mecab = 'mecab', dic?: string): Promise { 16 | const args: string[] = []; 17 | if (dic) args.push('-d', dic); 18 | 19 | const lines = await cmd(mecab, args, `${text.replace(/[\n\s\t]/g, ' ')}\n`); 20 | 21 | const results: string[][] = []; 22 | 23 | for (const line of lines) { 24 | if (line === 'EOS') break; 25 | const [word, value = ''] = line.split('\t'); 26 | const array = value.split(','); 27 | array.unshift(word); 28 | results.push(array); 29 | } 30 | 31 | return results; 32 | } 33 | 34 | export async function cmd(command: string, args: string[], stdin: string): Promise { 35 | const mecab = spawn(command, args); 36 | 37 | const writable = new memoryStreams.WritableStream(); 38 | 39 | mecab.stdin.write(stdin); 40 | mecab.stdin.end(); 41 | 42 | await pipeline(mecab.stdout, writable); 43 | 44 | return writable.toString().split(EOL); 45 | } 46 | -------------------------------------------------------------------------------- /src/modules/birthday/index.ts: -------------------------------------------------------------------------------- 1 | import autobind from 'autobind-decorator'; 2 | import Module from '@/module'; 3 | import Friend from '@/friend'; 4 | import serifs from '@/serifs'; 5 | 6 | function zeroPadding(num: number, length: number): string { 7 | return ('0000000000' + num).slice(-length); 8 | } 9 | 10 | export default class extends Module { 11 | public readonly name = 'birthday'; 12 | 13 | @autobind 14 | public install() { 15 | this.crawleBirthday(); 16 | setInterval(this.crawleBirthday, 1000 * 60 * 3); 17 | 18 | return {}; 19 | } 20 | 21 | /** 22 | * 誕生日のユーザーがいないかチェック(いたら祝う) 23 | */ 24 | @autobind 25 | private crawleBirthday() { 26 | const now = new Date(); 27 | const m = now.getMonth(); 28 | const d = now.getDate(); 29 | // Misskeyの誕生日は 2018-06-16 のような形式 30 | const today = `${zeroPadding(m + 1, 2)}-${zeroPadding(d, 2)}`; 31 | 32 | const birthFriends = this.ai.friends.find({ 33 | 'user.birthday': { '$regex': new RegExp('-' + today + '$') } 34 | } as any); 35 | 36 | birthFriends.forEach(f => { 37 | const friend = new Friend(this.ai, { doc: f }); 38 | 39 | // 親愛度が3以上必要 40 | if (friend.love < 3) return; 41 | 42 | const data = friend.getPerModulesData(this); 43 | 44 | if (data.lastBirthdayChecked == today) return; 45 | 46 | data.lastBirthdayChecked = today; 47 | friend.setPerModulesData(this, data); 48 | 49 | const text = serifs.birthday.happyBirthday(friend.name); 50 | 51 | this.ai.sendMessage(friend.userId, { 52 | text: text 53 | }).catch(() => {}); 54 | }); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/module.ts: -------------------------------------------------------------------------------- 1 | import autobind from 'autobind-decorator'; 2 | import 藍, { InstallerResult } from '@/ai'; 3 | 4 | export default abstract class Module { 5 | public abstract readonly name: string; 6 | 7 | protected ai: 藍; 8 | private doc: any; 9 | 10 | public init(ai: 藍) { 11 | this.ai = ai; 12 | 13 | this.doc = this.ai.moduleData.findOne({ 14 | module: this.name 15 | }); 16 | 17 | if (this.doc == null) { 18 | this.doc = this.ai.moduleData.insertOne({ 19 | module: this.name, 20 | data: {} 21 | }); 22 | } 23 | } 24 | 25 | public abstract install(): InstallerResult; 26 | 27 | @autobind 28 | protected log(msg: string) { 29 | this.ai.log(`[${this.name}]: ${msg}`); 30 | } 31 | 32 | /** 33 | * コンテキストを生成し、ユーザーからの返信を待ち受けます 34 | * @param key コンテキストを識別するためのキー 35 | * @param isDm トークメッセージ上のコンテキストかどうか 36 | * @param id トークメッセージ上のコンテキストならばトーク相手のID、そうでないなら待ち受ける投稿のID 37 | * @param data コンテキストに保存するオプションのデータ 38 | */ 39 | @autobind 40 | protected subscribeReply(key: string | null, isDm: boolean, id: string, data?: any) { 41 | this.ai.subscribeReply(this, key, isDm, id, data); 42 | } 43 | 44 | /** 45 | * 返信の待ち受けを解除します 46 | * @param key コンテキストを識別するためのキー 47 | */ 48 | @autobind 49 | protected unsubscribeReply(key: string | null) { 50 | this.ai.unsubscribeReply(this, key); 51 | } 52 | 53 | /** 54 | * 指定したミリ秒経過後に、タイムアウトコールバックを呼び出します。 55 | * このタイマーは記憶に永続化されるので、途中でプロセスを再起動しても有効です。 56 | * @param delay ミリ秒 57 | * @param data オプションのデータ 58 | */ 59 | @autobind 60 | public setTimeoutWithPersistence(delay: number, data?: any) { 61 | this.ai.setTimeoutWithPersistence(this, delay, data); 62 | } 63 | 64 | @autobind 65 | protected getData() { 66 | return this.doc.data; 67 | } 68 | 69 | @autobind 70 | protected setData(data: any) { 71 | this.doc.data = data; 72 | this.ai.moduleData.update(this.doc); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/utils/or.ts: -------------------------------------------------------------------------------- 1 | import { hankakuToZenkaku, katakanaToHiragana } from './japanese'; 2 | 3 | export default function(text: string, words: (string | RegExp)[]): boolean { 4 | if (text == null) return false; 5 | 6 | text = katakanaToHiragana(hankakuToZenkaku(text)); 7 | words = words.map(word => typeof word == 'string' ? katakanaToHiragana(word) : word); 8 | 9 | return words.some(word => { 10 | /** 11 | * テキストの余分な部分を取り除く 12 | * 例えば「藍ちゃん好き!」のようなテキストを「好き」にする 13 | */ 14 | function denoise(text: string): string { 15 | text = text.trim(); 16 | 17 | if (text.startsWith('@')) { 18 | text = text.replace(/^@[a-zA-Z0-1\-_]+/, ''); 19 | text = text.trim(); 20 | } 21 | 22 | function fn() { 23 | text = text.replace(/[!!]+$/, ''); 24 | text = text.replace(/っ+$/, ''); 25 | 26 | // 末尾の ー を除去 27 | // 例えば「おはよー」を「おはよ」にする 28 | // ただそのままだと「セーラー」などの本来「ー」が含まれているワードも「ー」が除去され 29 | // 「セーラ」になり、「セーラー」を期待している場合はマッチしなくなり期待する動作にならなくなるので、 30 | // 期待するワードの末尾にもともと「ー」が含まれている場合は(対象のテキストの「ー」をすべて除去した後に)「ー」を付けてあげる 31 | text = text.replace(/ー+$/, '') + ((typeof word == 'string' && word[word.length - 1] == 'ー') ? 'ー' : ''); 32 | 33 | text = text.replace(/。$/, ''); 34 | text = text.replace(/です$/, ''); 35 | text = text.replace(/(\.|…)+$/, ''); 36 | text = text.replace(/[♪♥]+$/, ''); 37 | text = text.replace(/^藍/, ''); 38 | text = text.replace(/^ちゃん/, ''); 39 | text = text.replace(/、+$/, ''); 40 | } 41 | 42 | let textBefore = text; 43 | let textAfter: string | null = null; 44 | 45 | while (textBefore != textAfter) { 46 | textBefore = text; 47 | fn(); 48 | textAfter = text; 49 | } 50 | 51 | return text; 52 | } 53 | 54 | if (typeof word == 'string') { 55 | return (text == word) || (denoise(text) == word); 56 | } else { 57 | return (word.test(text)) || (word.test(denoise(text))); 58 | } 59 | }); 60 | } 61 | 62 | -------------------------------------------------------------------------------- /src/modules/server/index.ts: -------------------------------------------------------------------------------- 1 | import autobind from 'autobind-decorator'; 2 | import Module from '@/module'; 3 | import serifs from '@/serifs'; 4 | import config from '@/config'; 5 | 6 | export default class extends Module { 7 | public readonly name = 'server'; 8 | 9 | private connection?: any; 10 | private recentStat: any; 11 | private warned = false; 12 | private lastWarnedAt: number; 13 | 14 | /** 15 | * 1秒毎のログ1分間分 16 | */ 17 | private statsLogs: any[] = []; 18 | 19 | @autobind 20 | public install() { 21 | if (!config.serverMonitoring) return {}; 22 | 23 | this.connection = this.ai.connection.useSharedConnection('serverStats'); 24 | this.connection.on('stats', this.onStats); 25 | 26 | setInterval(() => { 27 | this.statsLogs.unshift(this.recentStat); 28 | if (this.statsLogs.length > 60) this.statsLogs.pop(); 29 | }, 1000); 30 | 31 | setInterval(() => { 32 | this.check(); 33 | }, 3000); 34 | 35 | return {}; 36 | } 37 | 38 | @autobind 39 | private check() { 40 | const average = (arr) => arr.reduce((a, b) => a + b) / arr.length; 41 | 42 | const cpuPercentages = this.statsLogs.map(s => s && (s.cpu_usage || s.cpu) * 100 || 0); 43 | const cpuPercentage = average(cpuPercentages); 44 | if (cpuPercentage >= 70) { 45 | this.warn(); 46 | } else if (cpuPercentage <= 30) { 47 | this.warned = false; 48 | } 49 | } 50 | 51 | @autobind 52 | private async onStats(stats: any) { 53 | this.recentStat = stats; 54 | } 55 | 56 | @autobind 57 | private warn() { 58 | //#region 前に警告したときから一旦落ち着いた状態を経験していなければ警告しない 59 | // 常に負荷が高いようなサーバーで無限に警告し続けるのを防ぐため 60 | if (this.warned) return; 61 | //#endregion 62 | 63 | //#region 前の警告から1時間経っていない場合は警告しない 64 | const now = Date.now(); 65 | 66 | if (this.lastWarnedAt != null) { 67 | if (now - this.lastWarnedAt < (1000 * 60 * 60)) return; 68 | } 69 | 70 | this.lastWarnedAt = now; 71 | //#endregion 72 | 73 | this.ai.post({ 74 | text: serifs.server.cpu 75 | }); 76 | 77 | this.warned = true; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/modules/fortune/index.ts: -------------------------------------------------------------------------------- 1 | import autobind from 'autobind-decorator'; 2 | import Module from '@/module'; 3 | import Message from '@/message'; 4 | import serifs from '@/serifs'; 5 | import * as seedrandom from 'seedrandom'; 6 | import { genItem } from '@/vocabulary'; 7 | import * as loki from 'lokijs'; 8 | import config from '../../config'; 9 | 10 | export const blessing = [ 11 | '犬吉', 12 | 'ギガ吉', 13 | 'メガ吉', 14 | 'ナノ吉', 15 | '超吉', 16 | '大大吉', 17 | '大吉', 18 | '吉', 19 | '中吉', 20 | '小吉', 21 | '凶', 22 | '大凶', 23 | ]; 24 | 25 | export default class extends Module { 26 | public readonly name = 'fortune'; 27 | 28 | private learnedKeywords?: loki.Collection<{ 29 | keyword: string; 30 | learnedAt: number; 31 | }>; 32 | 33 | @autobind 34 | public install() { 35 | if (config.keywordEnabled) { 36 | this.learnedKeywords = this.ai.getCollection('_keyword_learnedKeywords', { 37 | indices: ['userId'] 38 | }); 39 | } 40 | 41 | return { 42 | mentionHook: this.mentionHook 43 | }; 44 | } 45 | 46 | @autobind 47 | private async mentionHook(msg: Message) { 48 | if (msg.includes(['占', 'うらな', '運勢', 'おみくじ'])) { 49 | const getKeyword = (rng: () => number) => { 50 | if (!this.learnedKeywords) return null; 51 | 52 | const count = this.learnedKeywords.count(); 53 | const offset = Math.floor(rng() * count); 54 | 55 | const x = this.learnedKeywords.chain().find().offset(offset).limit(1).data(); 56 | const keyword = x[0]?.keyword || null; 57 | return keyword; 58 | }; 59 | 60 | const date = new Date(); 61 | const seed = `${date.getFullYear()}/${date.getMonth()}/${date.getDate()}@${msg.userId}@${this.ai.account.id}`; 62 | const rng = seedrandom(seed); 63 | const omikuji = blessing[Math.floor(rng() * blessing.length)]; 64 | const item = genItem(rng, getKeyword); 65 | msg.reply(`**${omikuji}🎉**\nラッキーアイテム: ${item}`, { 66 | cw: serifs.fortune.cw(msg.friend.name) 67 | }); 68 | return true; 69 | } else { 70 | return false; 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

藍

2 |

An Ai for Misskey. About Ai

3 | 4 | ## これなに 5 | Misskey用の日本語Botです。 6 | 7 | ## インストール 8 | > Node.js と npm と MeCab (オプション) がインストールされている必要があります。 9 | 10 | まず適当なディレクトリに `git clone` します。 11 | 次にそのディレクトリに `config.json` を作成します。中身は次のようにします: 12 | ``` json 13 | { 14 | "host": "https:// + あなたのインスタンスのURL (末尾の / は除く)", 15 | "i": "藍として動かしたいアカウントのアクセストークン", 16 | "master": "管理者のユーザー名(オプション)", 17 | "notingEnabled": "ランダムにノートを投稿する機能を無効にする場合は false を入れる", 18 | "keywordEnabled": "キーワードを覚える機能 (MeCab が必要) を有効にする場合は true を入れる (無効にする場合は false)", 19 | "keywordInterval": "キーワードを覚える間隔 (分, デフォルト60分)", 20 | "chartEnabled": "チャート機能を無効化する場合は false を入れてください", 21 | "reversiEnabled": "藍とリバーシで対局できる機能を有効にする場合は true を入れる (無効にする場合は false)", 22 | "serverMonitoring": "サーバー監視の機能を有効にする場合は true を入れる (無効にする場合は false)", 23 | "mecab": "MeCab のインストールパス (オプション、PATHが通ってれば指定不要)", 24 | "mecabDic": "MeCab の辞書ファイルパス (オプション、たいてい /usr/lib/x86_64-linux-gnu/mecab/dic/mecab-ipadic-neologd)", 25 | "mecabNeologd": "MeCabの辞書に mecab-ipadic-NEologd を使用している場合は true にすると良いかも" 26 | } 27 | ``` 28 | `yarn install` して `yarn build` して `yarn start` すれば起動できます 29 | 30 | Dockerの場合は最初に `memory/memory.json` に空ファイルを作っておく必要がある 31 | 32 | Dockerイメージはここにある https://hub.docker.com/r/mei23/ia/ 33 | 34 | 現状、Node v22ではチャートと迷路が動きません。 35 | 36 | ## フォント 37 | 一部の機能にはフォントが必要です。 38 | 39 | おそらくLinux環境などではフォントをインストールすればそれなりに使用してくれるはずです。 40 | Debian/Ubuntu系ディストリの場合 41 | ``` 42 | apt-get install -y fonts-noto 43 | ``` 44 | その際、フォントのグリフは環境変数で調整出来る可能性があります。 45 | ```shell 46 | FC_LANG: ja 47 | : 48 | ``` 49 | 50 | ご自身でフォントをインストールディレクトリに`font.ttf`という名前で設置することにより、特定のフォントファイルを使用することもできます。 51 | 52 | ## 時刻 53 | 定時動作系のタイムゾーンがずれる場合、サーバーの設定を変更するか環境変数などでも対処出来ます。 54 | ```shell 55 | TZ: Asia/Tokyo 56 | : 57 | ``` 58 | 59 | ## 記憶 60 | 藍は記憶の保持にインメモリデータベースを使用しており、藍のインストールディレクトリに `memory.json` という名前で永続化されます。 61 | 62 | ## ライセンス 63 | MIT 64 | 65 | ## Awards 66 | Works on my machine 67 | -------------------------------------------------------------------------------- /src/modules/emoji-react/index.ts: -------------------------------------------------------------------------------- 1 | import autobind from 'autobind-decorator'; 2 | import { parse } from '@twemoji/parser'; 3 | const delay = require('timeout-as-promise'); 4 | 5 | import { Note } from '@/misskey/note'; 6 | import Module from '@/module'; 7 | import Stream from '@/stream'; 8 | import includes from '@/utils/includes'; 9 | 10 | export default class extends Module { 11 | public readonly name = 'emoji-react'; 12 | 13 | private htl: ReturnType; 14 | 15 | @autobind 16 | public install() { 17 | this.htl = this.ai.connection.useSharedConnection('homeTimeline'); 18 | this.htl.on('note', this.onNote); 19 | 20 | return {}; 21 | } 22 | 23 | @autobind 24 | private async onNote(note: Note) { 25 | if (note.reply != null) return; 26 | if (note.text == null) return; 27 | if (note.text.includes('@')) return; // (自分または他人問わず)メンションっぽかったらreject 28 | 29 | const react = async (reaction: string, immediate = false) => { 30 | if (!immediate) { 31 | await delay(1500); 32 | } 33 | this.ai.api('notes/reactions/create', { 34 | noteId: note.id, 35 | reaction: reaction 36 | }); 37 | }; 38 | 39 | const customEmojis = note.text.match(/:([\w@.-]+):(?!\w)/g); 40 | if (customEmojis) { 41 | // カスタム絵文字が複数種類ある場合はキャンセル 42 | if (!customEmojis.every((val, i, arr) => val === arr[0])) return; 43 | 44 | this.log(`Custom emoji detected - ${customEmojis[0]}`); 45 | 46 | return react(customEmojis[0]); 47 | } 48 | 49 | const emojis = parse(note.text).map(x => x.text); 50 | if (emojis.length > 0) { 51 | // 絵文字が複数種類ある場合はキャンセル 52 | if (!emojis.every((val, i, arr) => val === arr[0])) return; 53 | 54 | this.log(`Emoji detected - ${emojis[0]}`); 55 | 56 | let reaction = emojis[0]; 57 | 58 | switch (reaction) { 59 | case '✊': reaction = '🤟'; break; 60 | case '✌': reaction = '🤞'; break; 61 | case '🖐': reaction = '🖖'; break; 62 | case '✋': reaction = '🖖'; break; 63 | } 64 | 65 | return react(reaction); 66 | } 67 | 68 | if (includes(note.text, ['ぴざ'])) return react('🍕'); 69 | if (includes(note.text, ['ぷりん'])) return react('🍮'); 70 | if (includes(note.text, ['いあ'])) return react('🙌'); 71 | if (includes(note.text, ['藍'])) return react('👈'); 72 | if (includes(note.text, ['寿司', 'sushi']) || note.text === 'すし') return react('🍣'); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /torisetu.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # 取扱説明書 4 | 5 | ## プロフィール 6 | [こちら](https://xn--931a.moe/) 7 | 8 | ## 藍の主な機能 9 | ### 挨拶 10 | 「おはよう」「おやすみ」などと話しかけると反応してくれます。 11 | 12 | ### 占い 13 | 藍に「占い」と言うと、あなたの今日の運勢を占ってくれます。 14 | 15 | ### タイマー 16 | 指定した時間、分、秒を経過したら教えてくれます。「3分40秒」のように単位を混ぜることもできます。 17 | 18 | ### リマインダー 19 | ``` 20 | @ai remind 部屋の掃除 21 | ``` 22 | のようにメンションを飛ばすと12時間置きに責付かれます。その飛ばしたメンションか、藍ちゃんからの催促に「やった」または「やめた」と返信することでリマインダー解除されます。 23 | また、引用Renoteでメンションすることもできます。 24 | 25 | ### 福笑い 26 | 藍に「絵文字」と言うと、藍が考えた絵文字の組み合わせを教えてくれます。 27 | 28 | ### サイコロ 29 | ダイスノーテーションを伝えるとサイコロを振ってくれます。 30 | 例: "2d6" (6面サイコロを2回振る)、"3d5" (5面サイコロを3回振る) 31 | 32 | ### 迷路 33 | 「迷路」と言うと迷路を描いてくれます。「難しい」「簡単」などの言葉を添えることで、難易度も調整できます。 34 | 35 | ### 数当てゲーム 36 | 藍にメッセージで「数当てゲーム」と言うと遊べます。 37 | 藍の考えている数字を当てるゲームです。 38 | 39 | ### 数取りゲーム 40 | 藍に「数取りゲーム」と言うと遊べます。 41 | 複数人で行うゲームで、もっとも大きい数字を言った人が勝ちです。 42 | 43 | ### リバーシ 44 | 藍とリバーシで対局できます。(この機能はインスタンスによっては無効になっている可能性があります) 45 | 藍に「リバーシ」と言うか、リバーシで藍を指名すれば対局できます。 46 | 強さも調整できます。 47 | 48 | ### 覚える 49 | たまにタイムラインにあるキーワードを「覚え」ます。 50 | (この機能はインスタンスによっては無効になっている可能性があります) 51 | 52 | ### 呼び方を教える 53 | 藍があなたのことをなんて呼べばいいか教えられます。 54 | ただし後述の親愛度が一定の値に達している必要があります。 55 | (トークでのみ反応します) 56 | 57 | ### いらっしゃい 58 | Misskeyにアカウントを作成して初めて投稿を行うと、藍がそれをRenoteしてみんなに知らせてくれる機能です。 59 | 60 | ### Follow me 61 | 藍に「フォローして」と言うとフォローしてくれます。 62 | 63 | ### HappyBirthday 64 | 藍があなたの誕生日を祝ってくれます。 65 | 66 | ### バレンタイン 67 | 藍がチョコレートをくれます。 68 | 69 | ### チャート 70 | インスタンスの投稿チャートなどを投稿してくれます。 71 | 72 | ### サーバー監視 73 | サーバーの状態を監視し、負荷が高くなっているときは教えてくれます。 74 | 75 | ### ping 76 | PONGを返します。生存確認にどうぞ 77 | 78 | ### その他反応するフレーズ (トークのみ) 79 | * かわいい 80 | * なでなで 81 | * 好き 82 | * ぎゅー 83 | * 罵って 84 | * 踏んで 85 | * 痛い 86 | 87 | ## 親愛度 88 | 藍はあなたに対する親愛度を持っています。 89 | 藍に挨拶したりすると、少しずつ上がっていきます。 90 | 親愛度によって反応や各種セリフが変化します。親愛度がある程度ないとしてくれないこともあります。 91 | 92 | ## オリジナルaiとの違い 93 | - インスタンスのバージョンを監視して、変更されたら通知する。 94 | - 「バージョン」でそのインスタンスのバージョンを返す 95 | - おみくじのsaltが違う(同じインスタンスに2人いてもaiアカウントごとに結果が変わる) 96 | - おみくじの結果が違う 97 | - オセロで計算が完了しなかったら探索数を下げてリトライする。 98 | - オセロでどうしても計算が完了しなかったら投了する。 99 | - 2人目へのメンションでも反応するように 100 | - 「リアクション」で適当にリアクションする (インスタンスがm544のソースの場合のみ効く) 101 | - 数取りで自分も投票する 102 | - 数取りがの参加者が1人でも流れない 103 | - 数取りの結果がメンションではなくアバター絵文字 (インスタンスがm544のソースの場合のみ効く) 104 | 105 | -------------------------------------------------------------------------------- /src/modules/version/index.ts: -------------------------------------------------------------------------------- 1 | import autobind from 'autobind-decorator'; 2 | import Module from '../../module'; 3 | import Message from '../../message'; 4 | //import serifs from '../../serifs'; 5 | 6 | /** 7 | * バージョン情報 8 | */ 9 | interface Version { 10 | /** 11 | * サーバーバージョン(meta.version) 12 | */ 13 | server: string; 14 | /** 15 | * クライアントバージョン(meta.clientVersion) 16 | */ 17 | client: string; 18 | } 19 | 20 | export default class extends Module { 21 | public readonly name = 'version'; 22 | 23 | private latest?: Version; 24 | 25 | @autobind 26 | public install() { 27 | this.versionCheck(); 28 | setInterval(this.versionCheck, 60 * 1000); 29 | 30 | return { 31 | mentionHook: this.mentionHook 32 | }; 33 | } 34 | 35 | public versionCheck = () => { 36 | // バージョンチェック 37 | this.getVersion().then(fetched => { 38 | if (this.latest != null && fetched != null) { 39 | const serverChanged = this.latest.server !== fetched.server; 40 | 41 | if (serverChanged) { 42 | let v = ''; 43 | v += (serverChanged ? '**' : '') + `${this.latest.server} → ${this.mfmVersion(fetched.server)}\n` + (serverChanged ? '**' : ''); 44 | 45 | this.log(`Version changed: ${v}`); 46 | 47 | this.ai.post({ text: `【バージョンが変わりました】\n${v}` }); 48 | } else { 49 | // 変更なし 50 | } 51 | } 52 | 53 | this.latest = fetched; 54 | }).catch(e => this.log(`warn: ${e}`)); 55 | } 56 | 57 | @autobind 58 | private async mentionHook(msg: Message) { 59 | if (msg.text == null) return false; 60 | 61 | const query = msg.text.match(/バージョン/); 62 | 63 | if (query == null) return false; 64 | 65 | this.ai.api('meta').then(meta => { 66 | msg.reply(`${this.mfmVersion(meta.version)} みたいです。`) 67 | }).catch(() => { 68 | msg.reply(`取得失敗しました`) 69 | }); 70 | 71 | return true; 72 | } 73 | 74 | /** 75 | * バージョンを取得する 76 | */ 77 | private getVersion = (): Promise => { 78 | return this.ai.api('meta').then(meta => { 79 | return { 80 | server: meta.version, 81 | client: meta.clientVersion 82 | }; 83 | }); 84 | } 85 | 86 | private mfmVersion = (v): string => { 87 | if (v == null) return v; 88 | return v.match(/^\d+\.\d+\.\d+$/) 89 | ? `[${v}](https://github.com/syuilo/misskey/releases/tag/${v})` 90 | : v; 91 | } 92 | 93 | private wait = (ms: number): Promise => { 94 | return new Promise(resolve => { 95 | setTimeout(() => resolve(), ms); 96 | }) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/modules/emoji/index.ts: -------------------------------------------------------------------------------- 1 | import autobind from 'autobind-decorator'; 2 | import Module from '@/module'; 3 | import Message from '@/message'; 4 | import serifs from '@/serifs'; 5 | 6 | const hands = [ 7 | '👏', 8 | '👍', 9 | '👎', 10 | '👊', 11 | '✊', 12 | ['🤛', '🤜'], 13 | ['🤜', '🤛'], 14 | '🤞', 15 | '✌', 16 | '🤟', 17 | '🤘', 18 | '👌', 19 | '👈', 20 | '👉', 21 | ['👈', '👉'], 22 | ['👉', '👈'], 23 | '👆', 24 | '👇', 25 | '☝', 26 | ['✋', '🤚'], 27 | '🖐', 28 | '🖖', 29 | '👋', 30 | '🤙', 31 | '💪', 32 | ['💪', '✌'], 33 | '🖕' 34 | ] 35 | 36 | const faces = [ 37 | '😀', 38 | '😃', 39 | '😄', 40 | '😁', 41 | '😆', 42 | '😅', 43 | '😂', 44 | '🤣', 45 | '☺️', 46 | '😊', 47 | '😇', 48 | '🙂', 49 | '🙃', 50 | '😉', 51 | '😌', 52 | '😍', 53 | '🥰', 54 | '😘', 55 | '😗', 56 | '😙', 57 | '😚', 58 | '😋', 59 | '😛', 60 | '😝', 61 | '😜', 62 | '🤪', 63 | '🤨', 64 | '🧐', 65 | '🤓', 66 | '😎', 67 | '🤩', 68 | '🥳', 69 | '😏', 70 | '😒', 71 | '😞', 72 | '😔', 73 | '😟', 74 | '😕', 75 | '🙁', 76 | '☹️', 77 | '😣', 78 | '😖', 79 | '😫', 80 | '😩', 81 | '🥺', 82 | '😢', 83 | '😭', 84 | '😤', 85 | '😠', 86 | '😡', 87 | '🤬', 88 | '🤯', 89 | '😳', 90 | '😱', 91 | '😨', 92 | '😰', 93 | '😥', 94 | '😓', 95 | '🤗', 96 | '🤔', 97 | '🤭', 98 | '🤫', 99 | '🤥', 100 | '😶', 101 | '😐', 102 | '😑', 103 | '😬', 104 | '🙄', 105 | '😯', 106 | '😦', 107 | '😧', 108 | '😮', 109 | '😲', 110 | '😴', 111 | '🤤', 112 | '😪', 113 | '😵', 114 | '🤐', 115 | '🥴', 116 | '🤢', 117 | '🤮', 118 | '🤧', 119 | '😷', 120 | '🤒', 121 | '🤕', 122 | '🤑', 123 | '🤠', 124 | '🗿', 125 | '🤖', 126 | '👽' 127 | ] 128 | 129 | export default class extends Module { 130 | public readonly name = 'emoji'; 131 | 132 | @autobind 133 | public install() { 134 | return { 135 | mentionHook: this.mentionHook 136 | }; 137 | } 138 | 139 | @autobind 140 | private async mentionHook(msg: Message) { 141 | if (msg.includes(['顔文字', '絵文字', 'emoji', '福笑い'])) { 142 | const hand = hands[Math.floor(Math.random() * hands.length)]; 143 | const face = faces[Math.floor(Math.random() * faces.length)]; 144 | const emoji = Array.isArray(hand) ? hand[0] + face + hand[1] : hand + face + hand; 145 | msg.reply(serifs.emoji.suggest(emoji)); 146 | return true; 147 | } else { 148 | return false; 149 | } 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/utils/japanese.ts: -------------------------------------------------------------------------------- 1 | // Utilities for Japanese 2 | 3 | const kanaMap: string[][] = [ 4 | ['ガ', 'ガ'], ['ギ', 'ギ'], ['グ', 'グ'], ['ゲ', 'ゲ'], ['ゴ', 'ゴ'], 5 | ['ザ', 'ザ'], ['ジ', 'ジ'], ['ズ', 'ズ'], ['ゼ', 'ゼ'], ['ゾ', 'ゾ'], 6 | ['ダ', 'ダ'], ['ヂ', 'ヂ'], ['ヅ', 'ヅ'], ['デ', 'デ'], ['ド', 'ド'], 7 | ['バ', 'バ'], ['ビ', 'ビ'], ['ブ', 'ブ'], ['ベ', 'ベ'], ['ボ', 'ボ'], 8 | ['パ', 'パ'], ['ピ', 'ピ'], ['プ', 'プ'], ['ペ', 'ペ'], ['ポ', 'ポ'], 9 | ['ヴ', 'ヴ'], ['ヷ', 'ヷ'], ['ヺ', 'ヺ'], 10 | ['ア', 'ア'], ['イ', 'イ'], ['ウ', 'ウ'], ['エ', 'エ'], ['オ', 'オ'], 11 | ['カ', 'カ'], ['キ', 'キ'], ['ク', 'ク'], ['ケ', 'ケ'], ['コ', 'コ'], 12 | ['サ', 'サ'], ['シ', 'シ'], ['ス', 'ス'], ['セ', 'セ'], ['ソ', 'ソ'], 13 | ['タ', 'タ'], ['チ', 'チ'], ['ツ', 'ツ'], ['テ', 'テ'], ['ト', 'ト'], 14 | ['ナ', 'ナ'], ['ニ', 'ニ'], ['ヌ', 'ヌ'], ['ネ', 'ネ'], ['ノ', 'ノ'], 15 | ['ハ', 'ハ'], ['ヒ', 'ヒ'], ['フ', 'フ'], ['ヘ', 'ヘ'], ['ホ', 'ホ'], 16 | ['マ', 'マ'], ['ミ', 'ミ'], ['ム', 'ム'], ['メ', 'メ'], ['モ', 'モ'], 17 | ['ヤ', 'ヤ'], ['ユ', 'ユ'], ['ヨ', 'ヨ'], 18 | ['ラ', 'ラ'], ['リ', 'リ'], ['ル', 'ル'], ['レ', 'レ'], ['ロ', 'ロ'], 19 | ['ワ', 'ワ'], ['ヲ', 'ヲ'], ['ン', 'ン'], 20 | ['ァ', 'ァ'], ['ィ', 'ィ'], ['ゥ', 'ゥ'], ['ェ', 'ェ'], ['ォ', 'ォ'], 21 | ['ッ', 'ッ'], ['ャ', 'ャ'], ['ュ', 'ュ'], ['ョ', 'ョ'], 22 | ['ー', 'ー'] 23 | ]; 24 | 25 | /** 26 | * カタカナをひらがなに変換します 27 | * @param str カタカナ 28 | * @returns ひらがな 29 | */ 30 | export function katakanaToHiragana(str: string): string { 31 | return str.replace(/[\u30a1-\u30f6]/g, match => { 32 | const char = match.charCodeAt(0) - 0x60; 33 | return String.fromCharCode(char); 34 | }); 35 | } 36 | 37 | /** 38 | * ひらがなをカタカナに変換します 39 | * @param str ひらがな 40 | * @returns カタカナ 41 | */ 42 | export function hiraganaToKatagana(str: string): string { 43 | return str.replace(/[\u3041-\u3096]/g, match => { 44 | const char = match.charCodeAt(0) + 0x60; 45 | return String.fromCharCode(char); 46 | }); 47 | } 48 | 49 | /** 50 | * 全角カタカナを半角カタカナに変換します 51 | * @param str 全角カタカナ 52 | * @returns 半角カタカナ 53 | */ 54 | export function zenkakuToHankaku(str: string): string { 55 | const reg = new RegExp('(' + kanaMap.map(x => x[0]).join('|') + ')', 'g'); 56 | 57 | return str 58 | .replace(reg, match => 59 | kanaMap.find(x => x[0] == match)![1] 60 | ) 61 | .replace(/゛/g, '゙') 62 | .replace(/゜/g, '゚'); 63 | }; 64 | 65 | /** 66 | * 半角カタカナを全角カタカナに変換します 67 | * @param str 半角カタカナ 68 | * @returns 全角カタカナ 69 | */ 70 | export function hankakuToZenkaku(str: string): string { 71 | const reg = new RegExp('(' + kanaMap.map(x => x[1]).join('|') + ')', 'g'); 72 | 73 | return str 74 | .replace(reg, match => 75 | kanaMap.find(x => x[1] == match)![0] 76 | ) 77 | .replace(/゙/g, '゛') 78 | .replace(/゚/g, '゜'); 79 | }; 80 | -------------------------------------------------------------------------------- /src/modules/timer/index.ts: -------------------------------------------------------------------------------- 1 | import autobind from 'autobind-decorator'; 2 | import Module from '@/module'; 3 | import Message from '@/message'; 4 | import serifs from '@/serifs'; 5 | 6 | export default class extends Module { 7 | public readonly name = 'timer'; 8 | 9 | @autobind 10 | public install() { 11 | return { 12 | mentionHook: this.mentionHook, 13 | timeoutCallback: this.timeoutCallback, 14 | }; 15 | } 16 | 17 | @autobind 18 | private async mentionHook(msg: Message) { 19 | const secondsQuery = (msg.text || '').match(/([0-9]+)秒/); 20 | const minutesQuery = (msg.text || '').match(/([0-9]+)分/); 21 | const hoursQuery = (msg.text || '').match(/([0-9]+)時間/); 22 | 23 | const seconds = secondsQuery ? parseInt(secondsQuery[1], 10) : 0; 24 | const minutes = minutesQuery ? parseInt(minutesQuery[1], 10) : 0; 25 | const hours = hoursQuery ? parseInt(hoursQuery[1], 10) : 0; 26 | 27 | if (!(secondsQuery || minutesQuery || hoursQuery)) return false; 28 | 29 | if ((seconds + minutes + hours) == 0) { 30 | msg.reply(serifs.timer.invalid); 31 | return true; 32 | } 33 | 34 | const time = 35 | (1000 * seconds) + 36 | (1000 * 60 * minutes) + 37 | (1000 * 60 * 60 * hours); 38 | 39 | if (time > 86400000) { 40 | msg.reply(serifs.timer.tooLong); 41 | return true; 42 | } 43 | 44 | const pre = time == 300 * 1000 && /カレーメシ/.test(msg.text) ? 'カレーメシ私にも食べさせてください' : ''; 45 | 46 | msg.reply(pre + serifs.timer.set); 47 | 48 | const str = `${hours ? hoursQuery![0] : ''}${minutes ? minutesQuery![0] : ''}${seconds ? secondsQuery![0] : ''}`; 49 | 50 | // タイマーセット 51 | this.setTimeoutWithPersistence(time + 2000, { 52 | isDm: msg.isDm, 53 | msgId: msg.id, 54 | userId: msg.friend.userId, 55 | time: str, 56 | request: msg.text, 57 | }); 58 | 59 | return true; 60 | } 61 | 62 | @autobind 63 | private timeoutCallback(data) { 64 | const friend = this.ai.lookupFriend(data.userId); 65 | if (friend == null) return; // 処理の流れ上、実際にnullになることは無さそうだけど一応 66 | 67 | let text = serifs.timer.notify(data.time, friend.name); 68 | 69 | if (typeof data.request === 'string') { 70 | if (data.request.match(/赤いきつね/)) text += '\n七味入れるの忘れないでくださいね'; 71 | if (data.request.match(/蒙古タンメン/)) text += '\n辛味オイル全部入れるとお腹壊しちゃいますよ'; 72 | if (data.request.match(/カレーメシ/)) text += '\n数分待つかいっぱいかき混ぜるとおいしいですよ'; 73 | } 74 | 75 | if (data.isDm) { 76 | this.ai.sendMessage(friend.userId, { 77 | text: text 78 | }).catch(() => {}); 79 | } else { 80 | this.ai.post({ 81 | replyId: data.msgId, 82 | text: text 83 | }).catch(e => { 84 | console.error(`timer callback failed`, e); 85 | }) 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/modules/maze/index.ts: -------------------------------------------------------------------------------- 1 | import autobind from 'autobind-decorator'; 2 | import Module from '@/module'; 3 | import serifs from '@/serifs'; 4 | import { genMaze } from './gen-maze'; 5 | import { renderMaze } from './render-maze'; 6 | import Message from '@/message'; 7 | 8 | export default class extends Module { 9 | public readonly name = 'maze'; 10 | 11 | @autobind 12 | public install() { 13 | this.post(); 14 | setInterval(this.post, 1000 * 60 * 3); 15 | 16 | return { 17 | mentionHook: this.mentionHook 18 | }; 19 | } 20 | 21 | @autobind 22 | private async isAvail(): Promise { 23 | try { 24 | // @ts-ignore 25 | const { createCanvas } = await import('canvas'); 26 | return true; 27 | } catch (e) { 28 | return false 29 | } 30 | } 31 | 32 | @autobind 33 | private async post() { 34 | if (await this.isAvail() !== true) return; 35 | const now = new Date(); 36 | if (now.getHours() !== 22) return; 37 | const date = `${now.getFullYear()}-${now.getMonth()}-${now.getDate()}`; 38 | const data = this.getData(); 39 | if (data.lastPosted == date) return; 40 | data.lastPosted = date; 41 | this.setData(data); 42 | 43 | this.log('Time to maze'); 44 | const file = await this.genMazeFile(`${date}-${this.ai.account.id}`); 45 | 46 | this.log('Posting...'); 47 | this.ai.post({ 48 | text: serifs.maze.post, 49 | fileIds: [file.id] 50 | }); 51 | } 52 | 53 | @autobind 54 | private async genMazeFile(seed, size?): Promise { 55 | this.log('Maze generating...'); 56 | const maze = genMaze(seed, size); 57 | 58 | this.log('Maze rendering...'); 59 | const data = await renderMaze(seed, maze); 60 | 61 | this.log('Image uploading...'); 62 | const file = await this.ai.upload(data, { 63 | filename: 'maze.png', 64 | contentType: 'image/png' 65 | }); 66 | 67 | return file; 68 | } 69 | 70 | @autobind 71 | private async mentionHook(msg: Message) { 72 | if (msg.includes(['迷路'])) { 73 | if (await this.isAvail() !== true) { 74 | msg.reply(serifs.maze.nocanvas); 75 | return true; 76 | } 77 | let size: string | null = null; 78 | if (msg.includes(['接待'])) size = 'veryEasy'; 79 | if (msg.includes(['簡単', 'かんたん', '易しい', 'やさしい', '小さい', 'ちいさい'])) size = 'easy'; 80 | if (msg.includes(['難しい', 'むずかしい', '複雑な', '大きい', 'おおきい'])) size = 'hard'; 81 | if (msg.includes(['死', '鬼', '地獄'])) size = 'veryHard'; 82 | if (msg.includes(['藍']) && msg.includes(['本気'])) size = 'ai'; 83 | this.log('Maze requested'); 84 | setTimeout(async () => { 85 | const file = await this.genMazeFile(Date.now(), size); 86 | this.log('Replying...'); 87 | msg.reply(serifs.maze.foryou, { file }); 88 | }, 3000); 89 | return { 90 | reaction: 'like' 91 | }; 92 | } else { 93 | return false; 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/modules/keyword/index.ts: -------------------------------------------------------------------------------- 1 | import autobind from 'autobind-decorator'; 2 | import * as loki from 'lokijs'; 3 | import Module from '@/module'; 4 | import config from '@/config'; 5 | import serifs from '@/serifs'; 6 | import { mecab } from './mecab'; 7 | import Message from '@/message'; 8 | import NeologdNormalizer from 'neologd-normalizer'; 9 | 10 | function kanaToHira(str: string) { 11 | return str.replace(/[\u30a1-\u30f6]/g, match => { 12 | const chr = match.charCodeAt(0) - 0x60; 13 | return String.fromCharCode(chr); 14 | }); 15 | } 16 | 17 | export default class extends Module { 18 | public readonly name = 'keyword'; 19 | 20 | private learnedKeywords: loki.Collection<{ 21 | keyword: string; 22 | learnedAt: number; 23 | }>; 24 | 25 | @autobind 26 | public install() { 27 | if (!config.keywordEnabled) return {}; 28 | 29 | this.learnedKeywords = this.ai.getCollection('_keyword_learnedKeywords', { 30 | indices: ['userId'] 31 | }); 32 | 33 | setInterval(this.learn, 1000 * 60 * (config.keywordInterval || 60)); 34 | 35 | return { 36 | mentionHook: this.mentionHook, 37 | }; 38 | } 39 | 40 | @autobind 41 | private async learn() { 42 | const tl = await this.ai.api('notes/local-timeline', { 43 | limit: 30 44 | }); 45 | 46 | const interestedNotes = tl.filter(note => 47 | note.userId !== this.ai.account.id && 48 | note.text != null && note.text.length > 10 && 49 | note.cw == null); 50 | 51 | let keywords: string[][] = []; 52 | 53 | for (const note of interestedNotes) { 54 | let text = note.text; 55 | if (config.mecabNeologd) { 56 | text = NeologdNormalizer.normalize(text); 57 | } 58 | 59 | const tokens = await mecab(text, config.mecab, config.mecabDic); 60 | if (tokens.length < 3) continue; 61 | const keywordsInThisNote = tokens.filter(token => token[2] == '固有名詞' && token[8] != null); 62 | keywords = keywords.concat(keywordsInThisNote); 63 | } 64 | 65 | if (keywords.length === 0) return; 66 | 67 | const rnd = Math.floor((1 - Math.sqrt(Math.random())) * keywords.length); 68 | const keyword = keywords.sort((a, b) => a[0].length < b[0].length ? 1 : -1)[rnd]; 69 | 70 | const exist = this.learnedKeywords.findOne({ 71 | keyword: keyword[0] 72 | }); 73 | 74 | let text: string; 75 | 76 | if (exist) { 77 | return; 78 | } else { 79 | this.learnedKeywords.insertOne({ 80 | keyword: keyword[0], 81 | learnedAt: Date.now() 82 | }); 83 | 84 | text = serifs.keyword.learned(keyword[0], kanaToHira(keyword[8])); 85 | } 86 | 87 | this.ai.post({ 88 | text: text 89 | }); 90 | } 91 | 92 | @autobind 93 | private async mentionHook(msg: Message) { 94 | if (!msg.or(['/learn']) || msg.user.username !== config.master) { 95 | return false; 96 | } else { 97 | this.log('Manualy learn requested'); 98 | } 99 | 100 | this.learn(); 101 | 102 | return true; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/message.ts: -------------------------------------------------------------------------------- 1 | import autobind from 'autobind-decorator'; 2 | import * as chalk from 'chalk'; 3 | const delay = require('timeout-as-promise'); 4 | 5 | import 藍 from '@/ai'; 6 | import Friend from '@/friend'; 7 | import { User } from '@/misskey/user'; 8 | import includes from '@/utils/includes'; 9 | import or from '@/utils/or'; 10 | import config from '@/config'; 11 | 12 | export default class Message { 13 | private ai: 藍; 14 | private messageOrNote: any; 15 | public isDm: boolean; 16 | 17 | public get id(): string { 18 | return this.messageOrNote.id; 19 | } 20 | 21 | public get user(): User { 22 | return this.messageOrNote.user; 23 | } 24 | 25 | public get userId(): string { 26 | return this.messageOrNote.userId; 27 | } 28 | 29 | public get text(): string { 30 | return this.messageOrNote.text; 31 | } 32 | 33 | public get quoteId(): string | null { 34 | return this.messageOrNote.renoteId; 35 | } 36 | 37 | /** 38 | * メンション部分を除いたテキスト本文 39 | */ 40 | public get extractedText(): string { 41 | const host = new URL(config.host).host.replace(/\./g, '\\.'); 42 | return this.text 43 | .replace(new RegExp(`^@${this.ai.account.username}@${host}\\s`, 'i'), '') 44 | .replace(new RegExp(`^@${this.ai.account.username}\\s`, 'i'), '') 45 | .trim(); 46 | } 47 | 48 | public get replyId(): string { 49 | return this.messageOrNote.replyId; 50 | } 51 | 52 | public friend: Friend; 53 | 54 | constructor(ai: 藍, messageOrNote: any, isDm: boolean) { 55 | this.ai = ai; 56 | this.messageOrNote = messageOrNote; 57 | this.isDm = isDm; 58 | 59 | this.friend = new Friend(ai, { user: this.user }); 60 | 61 | // メッセージなどに付いているユーザー情報は省略されている場合があるので完全なユーザー情報を持ってくる 62 | this.ai.api('users/show', { 63 | userId: this.userId 64 | }).then(user => { 65 | this.friend.updateUser(user); 66 | }); 67 | } 68 | 69 | @autobind 70 | public async reply(text: string | null, opts?: { 71 | file?: any; 72 | cw?: string; 73 | renote?: string; 74 | immediate?: boolean; 75 | }) { 76 | if (text == null) return; 77 | 78 | this.ai.log(`>>> Sending reply to ${chalk.underline(this.id)}`); 79 | 80 | if (!opts?.immediate) { 81 | await delay(2000); 82 | } 83 | 84 | if (this.isDm) { 85 | return await this.ai.sendMessage(this.messageOrNote.userId, { 86 | text: text, 87 | fileId: opts?.file?.id 88 | }); 89 | } else { 90 | return await this.ai.post({ 91 | visibility: this.messageOrNote.visibility, 92 | localOnly: this.messageOrNote.localOnly, 93 | copyOnce: this.messageOrNote.copyOnce, 94 | replyId: this.messageOrNote.id, 95 | text: text, 96 | fileIds: opts?.file ? [opts?.file.id] : undefined, 97 | cw: opts?.cw, 98 | renoteId: opts?.renote 99 | }); 100 | } 101 | } 102 | 103 | @autobind 104 | public includes(words: string[]): boolean { 105 | return includes(this.text, words); 106 | } 107 | 108 | @autobind 109 | public or(words: (string | RegExp)[]): boolean { 110 | return or(this.text, words); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/modules/noting/index.ts: -------------------------------------------------------------------------------- 1 | import autobind from 'autobind-decorator'; 2 | import Module from '@/module'; 3 | import serifs from '@/serifs'; 4 | import { genItem } from '@/vocabulary'; 5 | import config from '@/config'; 6 | import * as loki from 'lokijs'; 7 | import Stream from '@/stream'; 8 | import { Note } from '@/misskey/note'; 9 | import { kewyegenabo } from '@/kewyegenabo'; 10 | 11 | export default class extends Module { 12 | public readonly name = 'noting'; 13 | 14 | private learnedKeywords?: loki.Collection<{ 15 | keyword: string; 16 | learnedAt: number; 17 | }>; 18 | 19 | private tl: ReturnType; 20 | private pendCount = 0; 21 | 22 | @autobind 23 | public install() { 24 | if (config.notingEnabled === false) return {}; 25 | 26 | if (config.keywordEnabled) { 27 | this.learnedKeywords = this.ai.getCollection('_keyword_learnedKeywords', { 28 | indices: ['userId'] 29 | }); 30 | } 31 | 32 | this.tl = this.ai.connection.useSharedConnection('localTimeline'); 33 | this.tl.on('note', this.onNote); 34 | 35 | setInterval(() => { 36 | if (this.pendCount + (Math.random() * 120) > 120) { 37 | this.pendCount = 0; 38 | this.post(); 39 | } 40 | }, 1000 * 60); 41 | 42 | return {}; 43 | } 44 | 45 | @autobind 46 | private async onNote(note: Note) { 47 | if (note.reply != null) return; 48 | if (note.userId === this.ai.account.id) return; // 自分は弾く 49 | 50 | this.pendCount++; 51 | } 52 | 53 | @autobind 54 | private post() { 55 | if (Math.random() * 100 > 50) { 56 | const getKeyword = (rng: () => number) => { 57 | if (!this.learnedKeywords) return null; 58 | 59 | const count = this.learnedKeywords.count(); 60 | const offset = Math.floor(rng() * count); 61 | 62 | const x = this.learnedKeywords.chain().find().offset(offset).limit(1).data(); 63 | let keyword = x[0]?.keyword || null; 64 | if (Math.random() * 100 > 80) keyword = kewyegenabo(keyword); 65 | return keyword; 66 | }; 67 | 68 | const notes = [ 69 | () => { 70 | const item = genItem(undefined, getKeyword); 71 | return serifs.noting.want(item); 72 | }, 73 | () => { 74 | const item = genItem(undefined, getKeyword); 75 | return serifs.noting.see(item); 76 | }, 77 | () => { 78 | const item = genItem(undefined, getKeyword); 79 | return serifs.noting.expire(item); 80 | }, 81 | () => { 82 | const item = genItem(undefined, getKeyword); 83 | return serifs.noting.f1(item); 84 | }, 85 | () => { 86 | const item = genItem(undefined, getKeyword); 87 | return serifs.noting.f2(item); 88 | }, 89 | () => { 90 | const item = genItem(undefined, getKeyword); 91 | return serifs.noting.f3(item); 92 | }, 93 | ]; 94 | 95 | const note = notes[Math.floor(Math.random() * notes.length)]; 96 | this.ai.post({ text: note() }); 97 | } else { 98 | const notes = serifs.noting.notes; 99 | const note = notes[Math.floor(Math.random() * notes.length)]; 100 | this.ai.post({ text: note }); 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /ai.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // AiOS bootstrapper 2 | 3 | import 'module-alias/register'; 4 | 5 | import 藍 from './ai'; 6 | import config from './config'; 7 | import _log from './utils/log'; 8 | const pkg = require('../package.json'); 9 | 10 | import CoreModule from './modules/core'; 11 | import TalkModule from './modules/talk'; 12 | import BirthdayModule from './modules/birthday'; 13 | import ReversiModule from './modules/reversi'; 14 | import PingModule from './modules/ping'; 15 | import EmojiModule from './modules/emoji'; 16 | import EmojiReactModule from './modules/emoji-react'; 17 | import FortuneModule from './modules/fortune'; 18 | import GuessingGameModule from './modules/guessing-game'; 19 | import KazutoriModule from './modules/kazutori'; 20 | import KeywordModule from './modules/keyword'; 21 | import WelcomeModule from './modules/welcome'; 22 | import TimerModule from './modules/timer'; 23 | import DiceModule from './modules/dice'; 24 | import ServerModule from './modules/server'; 25 | import VersionModule from './modules/version'; 26 | import FollowModule from './modules/follow'; 27 | import ValentineModule from './modules/valentine'; 28 | import MazeModule from './modules/maze'; 29 | import ChartModule from './modules/chart'; 30 | import SleepReportModule from './modules/sleep-report'; 31 | import NotingModule from './modules/noting'; 32 | import PollModule from './modules/poll'; 33 | import ReminderModule from './modules/reminder'; 34 | 35 | import * as chalk from 'chalk'; 36 | import fetch from 'node-fetch'; 37 | const promiseRetry = require('promise-retry'); 38 | 39 | console.log(' __ ____ _____ ___ '); 40 | console.log(' /__\\ (_ _)( _ )/ __)'); 41 | console.log(' /(__)\\ _)(_ )(_)( \\__ \\'); 42 | console.log('(__)(__)(____)(_____)(___/\n'); 43 | 44 | function log(msg: string): void { 45 | _log(`[Boot]: ${msg}`); 46 | } 47 | 48 | log(chalk.bold(`Ai v${pkg._v}`)); 49 | log(new Date().toString()); // For locale debug 50 | 51 | promiseRetry(retry => { 52 | log(`Account fetching... ${chalk.gray(config.host)}`); 53 | 54 | // アカウントをフェッチ 55 | return fetch(`${config.apiUrl}/i`, { 56 | method: 'post', 57 | body: JSON.stringify({ 58 | i: config.i 59 | }), 60 | headers: { 61 | 'Content-Type': 'application/json' 62 | }, 63 | timeout: 30 * 1000, 64 | }).then(res => { 65 | if (!res.ok) { 66 | throw `${res.status} ${res.statusText}`; 67 | } else { 68 | return res.json(); 69 | } 70 | }).catch(retry); 71 | }, { 72 | retries: 10, 73 | minTimeout: 10000, 74 | }).then(account => { 75 | const acct = `@${account.username}`; 76 | log(chalk.green(`Account fetched successfully: ${chalk.underline(acct)}`)); 77 | 78 | log('Starting AiOS...'); 79 | 80 | // 藍起動 81 | new 藍(account, [ 82 | new CoreModule(), 83 | new EmojiModule(), 84 | new EmojiReactModule(), 85 | new FortuneModule(), 86 | new GuessingGameModule(), 87 | new KazutoriModule(), 88 | new ReversiModule(), 89 | new TimerModule(), 90 | new DiceModule(), 91 | new TalkModule(), 92 | new PingModule(), 93 | new WelcomeModule(), 94 | new ServerModule(), 95 | new FollowModule(), 96 | new BirthdayModule(), 97 | new ValentineModule(), 98 | new KeywordModule(), 99 | new VersionModule(), 100 | new MazeModule(), 101 | new ChartModule(), 102 | new SleepReportModule(), 103 | new NotingModule(), 104 | new PollModule(), 105 | new ReminderModule(), 106 | ]); 107 | }).catch(e => { 108 | log(chalk.red('Failed to fetch the account')); 109 | }); 110 | -------------------------------------------------------------------------------- /src/modules/guessing-game/index.ts: -------------------------------------------------------------------------------- 1 | import autobind from 'autobind-decorator'; 2 | import * as loki from 'lokijs'; 3 | import Module from '@/module'; 4 | import Message from '@/message'; 5 | import serifs from '@/serifs'; 6 | 7 | export default class extends Module { 8 | public readonly name = 'guessingGame'; 9 | 10 | private guesses: loki.Collection<{ 11 | userId: string; 12 | secret: number; 13 | tries: number[]; 14 | isEnded: boolean; 15 | startedAt: number; 16 | endedAt: number | null; 17 | }>; 18 | 19 | @autobind 20 | public install() { 21 | this.guesses = this.ai.getCollection('guessingGame', { 22 | indices: ['userId'] 23 | }); 24 | 25 | return { 26 | mentionHook: this.mentionHook, 27 | contextHook: this.contextHook 28 | }; 29 | } 30 | 31 | @autobind 32 | private async mentionHook(msg: Message) { 33 | if (!msg.includes(['数当て', '数あて'])) return false; 34 | 35 | const exist = this.guesses.findOne({ 36 | userId: msg.userId, 37 | isEnded: false 38 | }); 39 | 40 | if (!msg.isDm) { 41 | if (exist != null) { 42 | msg.reply(serifs.guessingGame.alreadyStarted); 43 | } else { 44 | msg.reply(serifs.guessingGame.plzDm); 45 | } 46 | 47 | return true; 48 | } 49 | 50 | const secret = Math.floor(Math.random() * 100); 51 | 52 | this.guesses.insertOne({ 53 | userId: msg.userId, 54 | secret: secret, 55 | tries: [], 56 | isEnded: false, 57 | startedAt: Date.now(), 58 | endedAt: null 59 | }); 60 | 61 | msg.reply(serifs.guessingGame.started).then(reply => { 62 | this.subscribeReply(msg.userId, msg.isDm, msg.isDm ? msg.userId : reply.id); 63 | }); 64 | 65 | return true; 66 | } 67 | 68 | @autobind 69 | private async contextHook(key: any, msg: Message) { 70 | if (msg.text == null) return; 71 | 72 | const exist = this.guesses.findOne({ 73 | userId: msg.userId, 74 | isEnded: false 75 | }); 76 | 77 | // 処理の流れ上、実際にnullになることは無さそうだけど一応 78 | if (exist == null) { 79 | this.unsubscribeReply(key); 80 | return; 81 | } 82 | 83 | if (msg.text.includes('やめ')) { 84 | msg.reply(serifs.guessingGame.cancel); 85 | exist.isEnded = true; 86 | exist.endedAt = Date.now(); 87 | this.guesses.update(exist); 88 | this.unsubscribeReply(key); 89 | return; 90 | } 91 | 92 | const guess = msg.extractedText.match(/[0-9]+/); 93 | 94 | if (guess == null) { 95 | msg.reply(serifs.guessingGame.nan).then(reply => { 96 | this.subscribeReply(msg.userId, msg.isDm, reply.id); 97 | }); 98 | return; 99 | } 100 | 101 | if (guess.length > 3) return; 102 | 103 | const g = parseInt(guess[0], 10); 104 | const firsttime = exist.tries.indexOf(g) === -1; 105 | 106 | exist.tries.push(g); 107 | 108 | let text: string; 109 | let end = false; 110 | 111 | if (exist.secret < g) { 112 | text = firsttime 113 | ? serifs.guessingGame.less(g.toString()) 114 | : serifs.guessingGame.lessAgain(g.toString()); 115 | } else if (exist.secret > g) { 116 | text = firsttime 117 | ? serifs.guessingGame.grater(g.toString()) 118 | : serifs.guessingGame.graterAgain(g.toString()); 119 | } else { 120 | end = true; 121 | text = serifs.guessingGame.congrats(exist.tries.length.toString()); 122 | } 123 | 124 | if (end) { 125 | exist.isEnded = true; 126 | exist.endedAt = Date.now(); 127 | this.unsubscribeReply(key); 128 | } 129 | 130 | this.guesses.update(exist); 131 | 132 | msg.reply(text).then(reply => { 133 | if (!end) { 134 | this.subscribeReply(msg.userId, msg.isDm, reply.id); 135 | } 136 | }); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/modules/core/index.ts: -------------------------------------------------------------------------------- 1 | import autobind from 'autobind-decorator'; 2 | import Module from '@/module'; 3 | import Message from '@/message'; 4 | import serifs from '@/serifs'; 5 | import { safeForInterpolate } from '@/utils/safe-for-interpolate'; 6 | 7 | const titles = ['さん', 'くん', '君', 'ちゃん', '様', '先生']; 8 | 9 | export default class extends Module { 10 | public readonly name = 'core'; 11 | 12 | @autobind 13 | public install() { 14 | return { 15 | mentionHook: this.mentionHook, 16 | contextHook: this.contextHook 17 | }; 18 | } 19 | 20 | @autobind 21 | private async mentionHook(msg: Message) { 22 | if (!msg.text) return false; 23 | 24 | return ( 25 | this.transferBegin(msg) || 26 | this.transferEnd(msg) || 27 | this.setName(msg) || 28 | this.modules(msg) 29 | ); 30 | } 31 | 32 | @autobind 33 | private transferBegin(msg: Message): boolean { 34 | if (!msg.text) return false; 35 | if (!msg.includes(['引継', '引き継ぎ', '引越', '引っ越し'])) return false; 36 | 37 | // メッセージのみ 38 | if (!msg.isDm) { 39 | msg.reply(serifs.core.transferNeedDm); 40 | return true; 41 | } 42 | 43 | const code = msg.friend.generateTransferCode(); 44 | 45 | msg.reply(serifs.core.transferCode(code)); 46 | 47 | return true; 48 | } 49 | 50 | @autobind 51 | private transferEnd(msg: Message): boolean { 52 | if (!msg.text) return false; 53 | if (!msg.text.startsWith('「') || !msg.text.endsWith('」')) return false; 54 | 55 | const code = msg.text.substring(1, msg.text.length - 1); 56 | 57 | const succ = msg.friend.transferMemory(code); 58 | 59 | if (succ) { 60 | msg.reply(serifs.core.transferDone(msg.friend.name)); 61 | } else { 62 | msg.reply(serifs.core.transferFailed); 63 | } 64 | 65 | return true; 66 | } 67 | 68 | @autobind 69 | private setName(msg: Message): boolean { 70 | if (!msg.text) return false; 71 | if (!msg.text.includes('って呼んで')) return false; 72 | if (msg.text.startsWith('って呼んで')) return false; 73 | 74 | // メッセージのみ 75 | if (!msg.isDm) return true; 76 | 77 | if (msg.friend.love < 5) { 78 | msg.reply(serifs.core.requireMoreLove); 79 | return true; 80 | } 81 | 82 | const name = msg.text.match(/^(.+?)って呼んで/)![1]; 83 | 84 | if (name.length > 10) { 85 | msg.reply(serifs.core.tooLong); 86 | return true; 87 | } 88 | 89 | if (!safeForInterpolate(name)) { 90 | msg.reply(serifs.core.invalidName); 91 | return true; 92 | } 93 | 94 | const withSan = titles.some(t => name.endsWith(t)); 95 | 96 | if (withSan) { 97 | msg.friend.updateName(name); 98 | msg.reply(serifs.core.setNameOk(name)); 99 | } else { 100 | msg.reply(serifs.core.san).then(reply => { 101 | this.subscribeReply(msg.userId, msg.isDm, msg.isDm ? msg.userId : reply.id, { 102 | name: name 103 | }); 104 | }); 105 | } 106 | 107 | return true; 108 | } 109 | 110 | @autobind 111 | private modules(msg: Message): boolean { 112 | if (!msg.text) return false; 113 | if (!msg.or(['modules'])) return false; 114 | 115 | let text = '```\n'; 116 | 117 | for (const m of this.ai.modules) { 118 | text += `${m.name}\n`; 119 | } 120 | 121 | text += '```'; 122 | 123 | msg.reply(text, { 124 | immediate: true 125 | }); 126 | 127 | return true; 128 | } 129 | 130 | @autobind 131 | private version(msg: Message): boolean { 132 | if (!msg.text) return false; 133 | if (!msg.or(['v', 'version', 'バージョン'])) return false; 134 | 135 | msg.reply(`\`\`\`\nv${this.ai.version}\n\`\`\``, { 136 | immediate: true 137 | }); 138 | 139 | return true; 140 | } 141 | 142 | @autobind 143 | private async contextHook(key: any, msg: Message, data: any) { 144 | if (msg.text == null) return; 145 | 146 | const done = () => { 147 | msg.reply(serifs.core.setNameOk(msg.friend.name)); 148 | this.unsubscribeReply(key); 149 | }; 150 | 151 | if (msg.text.includes('はい')) { 152 | msg.friend.updateName(data.name + 'さん'); 153 | done(); 154 | } else if (msg.text.includes('いいえ')) { 155 | msg.friend.updateName(data.name); 156 | done(); 157 | } else { 158 | msg.reply(serifs.core.yesOrNo).then(reply => { 159 | this.subscribeReply(msg.userId, msg.isDm, reply.id, data); 160 | }); 161 | } 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/modules/chart/index.ts: -------------------------------------------------------------------------------- 1 | import autobind from 'autobind-decorator'; 2 | import Module from '@/module'; 3 | import serifs from '@/serifs'; 4 | import Message from '@/message'; 5 | import { renderChart } from './render-chart'; 6 | import { items } from '@/vocabulary'; 7 | import config from '@/config'; 8 | 9 | export default class extends Module { 10 | public readonly name = 'chart'; 11 | 12 | @autobind 13 | public install() { 14 | if (config.chartEnabled === false) return {}; 15 | 16 | this.post(); 17 | setInterval(this.post, 1000 * 60 * 3); 18 | 19 | return { 20 | mentionHook: this.mentionHook 21 | }; 22 | } 23 | 24 | @autobind 25 | private async isAvail(): Promise { 26 | try { 27 | // @ts-ignore 28 | const { createCanvas, registerFont } = await import('canvas'); 29 | return true; 30 | } catch (e) { 31 | return false 32 | } 33 | } 34 | 35 | @autobind 36 | private async post() { 37 | if (await this.isAvail() !== true) return; 38 | const now = new Date(); 39 | if (now.getUTCHours() !== 1) return; 40 | const date = `${now.getUTCFullYear()}-${now.getUTCMonth()}-${now.getUTCDate()}`; 41 | const data = this.getData(); 42 | if (data.lastPosted == date) return; 43 | data.lastPosted = date; 44 | this.setData(data); 45 | 46 | this.log('Time to chart'); 47 | const file = await this.genChart('notes'); 48 | 49 | this.log('Posting...'); 50 | this.ai.post({ 51 | text: serifs.chart.post, 52 | fileIds: [file.id] 53 | }); 54 | } 55 | 56 | @autobind 57 | private async genChart(type, params?): Promise { 58 | this.log('Chart data fetching...'); 59 | 60 | let chart; 61 | 62 | if (type === 'userNotes') { 63 | const data = await this.ai.api('charts/user/notes', { 64 | span: 'day', 65 | limit: 30, 66 | userId: params.user.id 67 | }); 68 | 69 | chart = { 70 | title: `@${params.user.username}さんの投稿数`, 71 | datasets: [{ 72 | data: data.inc 73 | }] 74 | }; 75 | } else if (type === 'followers') { 76 | const data = await this.ai.api('charts/user/following', { 77 | span: 'day', 78 | limit: 30, 79 | userId: params.user.id 80 | }); 81 | 82 | chart = { 83 | title: `@${params.user.username}さんのフォロワー数`, 84 | datasets: [{ 85 | data: data.local.followers.total 86 | }, { 87 | data: data.remote.followers.total 88 | }] 89 | }; 90 | } else if (type === 'notes') { 91 | const data = await this.ai.api('charts/notes', { 92 | span: 'day', 93 | limit: 31, 94 | }); 95 | chart = { 96 | datasets: [{ 97 | data: data.local.inc.splice(1) 98 | }] 99 | }; 100 | } else { 101 | const suffixes = ['の売り上げ', 'の消費', 'の生産']; 102 | 103 | const title = type && type !== 'random' ? type 104 | : items[Math.floor(Math.random() * items.length)] + suffixes[Math.floor(Math.random() * suffixes.length)]; 105 | 106 | const limit = 30; 107 | const diffRange = 150; 108 | const datasetCount = 1 + Math.floor(Math.random() * 3); 109 | 110 | let datasets: any[] = []; 111 | 112 | for (let d = 0; d < datasetCount; d++) { 113 | let values = [Math.random() * 1000]; 114 | 115 | for (let i = 1; i < limit; i++) { 116 | const prev = values[i - 1]; 117 | values.push(prev + ((Math.random() * (diffRange * 2)) - diffRange)); 118 | } 119 | 120 | datasets.push({ 121 | data: values 122 | }); 123 | } 124 | 125 | chart = { 126 | title, 127 | datasets: datasets 128 | }; 129 | } 130 | 131 | this.log('Chart rendering...'); 132 | const img = await renderChart(chart); 133 | 134 | this.log('Image uploading...'); 135 | const file = await this.ai.upload(img, { 136 | filename: 'chart.png', 137 | contentType: 'image/png' 138 | }); 139 | 140 | return file; 141 | } 142 | 143 | @autobind 144 | private async mentionHook(msg: Message) { 145 | if (!msg.includes(['チャート'])) { 146 | return false; 147 | } else { 148 | this.log('Chart requested'); 149 | } 150 | 151 | if (await this.isAvail() !== true) { 152 | msg.reply(serifs.chart.nocanvas); 153 | return true; 154 | } 155 | 156 | let type = 'random'; 157 | 158 | const m = msg.text.match(/([^\s\.,!\?'"#:\/\[\]]{1,20})チャート/); 159 | if (m) type = m[1]; 160 | 161 | if (msg.includes(['フォロワー'])) type = 'followers'; 162 | if (msg.includes(['投稿'])) type = 'userNotes'; 163 | 164 | const file = await this.genChart(type, { 165 | user: msg.user 166 | }); 167 | 168 | this.log('Replying...'); 169 | msg.reply(serifs.chart.foryou, { file }); 170 | 171 | return { 172 | reaction: 'like' 173 | }; 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/friend.ts: -------------------------------------------------------------------------------- 1 | import autobind from 'autobind-decorator'; 2 | import 藍 from '@/ai'; 3 | import IModule from '@/module'; 4 | import getDate from '@/utils/get-date'; 5 | import { User } from '@/misskey/user'; 6 | import { genItem } from '@/vocabulary'; 7 | 8 | export type FriendDoc = { 9 | userId: string; 10 | user: User; 11 | name?: string | null; 12 | love?: number; 13 | lastLoveIncrementedAt?: string; 14 | todayLoveIncrements?: number; 15 | perModulesData?: any; 16 | married?: boolean; 17 | transferCode?: string; 18 | }; 19 | 20 | export default class Friend { 21 | private ai: 藍; 22 | 23 | public get userId() { 24 | return this.doc.userId; 25 | } 26 | 27 | public get name() { 28 | return this.doc.name; 29 | } 30 | 31 | public get love() { 32 | return this.doc.love || 0; 33 | } 34 | 35 | public get married() { 36 | return this.doc.married; 37 | } 38 | 39 | public doc: FriendDoc; 40 | 41 | constructor(ai: 藍, opts: { user?: User, doc?: FriendDoc }) { 42 | this.ai = ai; 43 | 44 | if (opts.user) { 45 | const exist = this.ai.friends.findOne({ 46 | userId: opts.user.id 47 | }); 48 | 49 | if (exist == null) { 50 | const inserted = this.ai.friends.insertOne({ 51 | userId: opts.user.id, 52 | user: opts.user 53 | }); 54 | 55 | if (inserted == null) { 56 | throw new Error('Failed to insert friend doc'); 57 | } 58 | 59 | this.doc = inserted; 60 | } else { 61 | this.doc = exist; 62 | this.doc.user = { ...this.doc.user, ...opts.user }; 63 | this.save(); 64 | } 65 | } else if (opts.doc) { 66 | this.doc = opts.doc; 67 | } else { 68 | throw new Error('No friend info specified'); 69 | } 70 | } 71 | 72 | @autobind 73 | public updateUser(user: Partial) { 74 | this.doc.user = { 75 | ...this.doc.user, 76 | ...user, 77 | }; 78 | this.save(); 79 | } 80 | 81 | @autobind 82 | public getPerModulesData(module: IModule) { 83 | if (this.doc.perModulesData == null) { 84 | this.doc.perModulesData = {}; 85 | this.doc.perModulesData[module.name] = {}; 86 | this.save(); 87 | } else if (this.doc.perModulesData[module.name] == null) { 88 | this.doc.perModulesData[module.name] = {}; 89 | this.save(); 90 | } 91 | 92 | return this.doc.perModulesData[module.name]; 93 | } 94 | 95 | @autobind 96 | public setPerModulesData(module: IModule, data: any) { 97 | if (this.doc.perModulesData == null) { 98 | this.doc.perModulesData = {}; 99 | } 100 | 101 | this.doc.perModulesData[module.name] = data; 102 | 103 | this.save(); 104 | } 105 | 106 | @autobind 107 | public incLove(amount = 1) { 108 | const today = getDate(); 109 | 110 | if (this.doc.lastLoveIncrementedAt != today) { 111 | this.doc.todayLoveIncrements = 0; 112 | } 113 | 114 | // 1日に上げられる親愛度は最大3 115 | if (this.doc.lastLoveIncrementedAt == today && (this.doc.todayLoveIncrements || 0) >= 3) return; 116 | 117 | if (this.doc.love == null) this.doc.love = 0; 118 | this.doc.love += amount; 119 | 120 | // 最大 100 121 | if (this.doc.love > 100) this.doc.love = 100; 122 | 123 | this.doc.lastLoveIncrementedAt = today; 124 | this.doc.todayLoveIncrements = (this.doc.todayLoveIncrements || 0) + amount; 125 | this.save(); 126 | 127 | this.ai.log(`💗 ${this.userId} +${amount}`); 128 | } 129 | 130 | @autobind 131 | public decLove(amount = 1) { 132 | // 親愛度MAXなら下げない 133 | if (this.doc.love === 100) return; 134 | 135 | if (this.doc.love == null) this.doc.love = 0; 136 | this.doc.love -= amount; 137 | 138 | // 最低 -30 139 | if (this.doc.love < -30) this.doc.love = -30; 140 | 141 | // 親愛度マイナスなら名前を忘れる 142 | if (this.doc.love < 0) { 143 | this.doc.name = null; 144 | } 145 | 146 | this.save(); 147 | 148 | this.ai.log(`💢 ${this.userId} -${amount}`); 149 | } 150 | 151 | @autobind 152 | public updateName(name: string) { 153 | this.doc.name = name; 154 | this.save(); 155 | } 156 | 157 | @autobind 158 | public save() { 159 | this.ai.friends.update(this.doc); 160 | } 161 | 162 | @autobind 163 | public generateTransferCode(): string { 164 | const code = genItem(); 165 | 166 | this.doc.transferCode = code; 167 | this.save(); 168 | 169 | return code; 170 | } 171 | 172 | @autobind 173 | public transferMemory(code: string): boolean { 174 | const src = this.ai.friends.findOne({ 175 | transferCode: code 176 | }); 177 | 178 | if (src == null) return false; 179 | 180 | this.doc.name = src.name; 181 | this.doc.love = src.love; 182 | this.doc.married = src.married; 183 | this.doc.perModulesData = src.perModulesData; 184 | this.save(); 185 | 186 | // TODO: 合言葉を忘れる 187 | 188 | return true; 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/modules/reversi/index.ts: -------------------------------------------------------------------------------- 1 | import * as childProcess from 'child_process'; 2 | import autobind from 'autobind-decorator'; 3 | import Module from '@/module'; 4 | import serifs from '@/serifs'; 5 | import config from '@/config'; 6 | import Message from '@/message'; 7 | import Friend from '@/friend'; 8 | import getDate from '@/utils/get-date'; 9 | 10 | export default class extends Module { 11 | public readonly name = 'reversi'; 12 | 13 | /** 14 | * リバーシストリーム 15 | */ 16 | private reversiConnection?: any; 17 | 18 | @autobind 19 | public install() { 20 | if (!config.reversiEnabled) return {}; 21 | 22 | this.reversiConnection = this.ai.connection.useSharedConnection('gamesReversi'); 23 | 24 | // 招待されたとき 25 | this.reversiConnection.on('invited', msg => this.onReversiInviteMe(msg.parent)); 26 | 27 | // マッチしたとき 28 | this.reversiConnection.on('matched', msg => this.onReversiGameStart(msg)); 29 | 30 | if (config.reversiEnabled) { 31 | const mainStream = this.ai.connection.useSharedConnection('main'); 32 | mainStream.on('pageEvent', msg => { 33 | if (msg.event === 'inviteReversi') { 34 | this.ai.api('games/reversi/match', { 35 | userId: msg.user.id 36 | }); 37 | } 38 | }); 39 | } 40 | 41 | return { 42 | mentionHook: this.mentionHook 43 | }; 44 | } 45 | 46 | @autobind 47 | private async mentionHook(msg: Message) { 48 | if (msg.includes(['リバーシ', 'オセロ', 'reversi', 'othello'])) { 49 | if (config.reversiEnabled) { 50 | msg.reply(serifs.reversi.ok); 51 | 52 | this.ai.api('games/reversi/match', { 53 | userId: msg.userId 54 | }); 55 | } else { 56 | msg.reply(serifs.reversi.decline); 57 | } 58 | 59 | return true; 60 | } else { 61 | return false; 62 | } 63 | } 64 | 65 | @autobind 66 | private async onReversiInviteMe(inviter: any) { 67 | this.log(`Someone invited me: @${inviter.username}`); 68 | 69 | if (config.reversiEnabled) { 70 | // 承認 71 | const game = await this.ai.api('games/reversi/match', { 72 | userId: inviter.id 73 | }); 74 | 75 | this.onReversiGameStart(game); 76 | } else { 77 | // todo (リバーシできない旨をメッセージで伝えるなど) 78 | } 79 | } 80 | 81 | @autobind 82 | private onReversiGameStart(game: any) { 83 | this.log('enter reversi game room'); 84 | 85 | // ゲームストリームに接続 86 | const gw = this.ai.connection.connectToChannel('gamesReversiGame', { 87 | gameId: game.id 88 | }); 89 | 90 | // フォーム 91 | const form = [{ 92 | id: 'publish', 93 | type: 'switch', 94 | label: '藍が対局情報を投稿するのを許可', 95 | value: true 96 | }, { 97 | id: 'strength', 98 | type: 'radio', 99 | label: '強さ', 100 | value: 3, 101 | items: [{ 102 | label: '接待', 103 | value: 0 104 | }, { 105 | label: '弱', 106 | value: 2 107 | }, { 108 | label: '中', 109 | value: 3 110 | }, { 111 | label: '強', 112 | value: 4 113 | }, { 114 | label: '最強', 115 | value: 5 116 | }] 117 | }]; 118 | 119 | //#region バックエンドプロセス開始 120 | const ai = childProcess.fork(__dirname + '/back.js'); 121 | 122 | // バックエンドプロセスに情報を渡す 123 | ai.send({ 124 | type: '_init_', 125 | body: { 126 | game: game, 127 | form: form, 128 | account: this.ai.account 129 | } 130 | }); 131 | 132 | ai.on('message', (msg: Record) => { 133 | if (msg.type == 'put') { 134 | gw.send('set', { 135 | pos: msg.pos 136 | }); 137 | } else if (msg.type == 'ended') { 138 | gw.dispose(); 139 | 140 | this.onGameEnded(game); 141 | } else if (msg.type == 'surrendered') { 142 | // 投了する 143 | this.ai.api('games/reversi/games/surrender', { 144 | gameId: game.id 145 | }); 146 | 147 | gw.dispose(); 148 | 149 | this.onGameEnded(game); 150 | } 151 | }); 152 | 153 | // ゲームストリームから情報が流れてきたらそのままバックエンドプロセスに伝える 154 | gw.addListener('*', message => { 155 | ai.send(message); 156 | }); 157 | //#endregion 158 | 159 | // フォーム初期化 160 | setTimeout(() => { 161 | gw.send('initForm', form); 162 | }, 1000); 163 | 164 | // どんな設定内容の対局でも受け入れる 165 | setTimeout(() => { 166 | gw.send('accept', {}); 167 | }, 2000); 168 | } 169 | 170 | @autobind 171 | private onGameEnded(game: any) { 172 | const user = game.user1Id == this.ai.account.id ? game.user2 : game.user1; 173 | 174 | //#region 1日に1回だけ親愛度を上げる 175 | const today = getDate(); 176 | 177 | const friend = new Friend(this.ai, { user: user }); 178 | 179 | const data = friend.getPerModulesData(this); 180 | 181 | if (data.lastPlayedAt != today) { 182 | data.lastPlayedAt = today; 183 | friend.setPerModulesData(this, data); 184 | 185 | friend.incLove(); 186 | } 187 | //#endregion 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/modules/reminder/index.ts: -------------------------------------------------------------------------------- 1 | import autobind from 'autobind-decorator'; 2 | import * as loki from 'lokijs'; 3 | import Module from '@/module'; 4 | import Message from '@/message'; 5 | import serifs, { getSerif } from '@/serifs'; 6 | import { acct } from '@/utils/acct'; 7 | import config from '@/config'; 8 | 9 | const NOTIFY_INTERVAL = 1000 * 60 * 60 * 12; 10 | 11 | export default class extends Module { 12 | public readonly name = 'reminder'; 13 | 14 | private reminds: loki.Collection<{ 15 | userId: string; 16 | id: string; 17 | isDm: boolean; 18 | thing: string | null; 19 | quoteId: string | null; 20 | times: number; // 催促した回数(使うのか?) 21 | createdAt: number; 22 | }>; 23 | 24 | @autobind 25 | public install() { 26 | this.reminds = this.ai.getCollection('reminds', { 27 | indices: ['userId', 'id'] 28 | }); 29 | 30 | return { 31 | mentionHook: this.mentionHook, 32 | contextHook: this.contextHook, 33 | timeoutCallback: this.timeoutCallback, 34 | }; 35 | } 36 | 37 | @autobind 38 | private async mentionHook(msg: Message) { 39 | let text = msg.extractedText.toLowerCase(); 40 | if (!text.startsWith('remind') && !text.startsWith('todo')) return false; 41 | 42 | if (text.startsWith('reminds') || text.startsWith('todos')) { 43 | const reminds = this.reminds.find({ 44 | userId: msg.userId, 45 | }); 46 | 47 | const getQuoteLink = id => `[${id}](${config.host}/notes/${id})`; 48 | 49 | msg.reply(serifs.reminder.reminds + '\n' + reminds.map(remind => `・${remind.thing ? remind.thing : getQuoteLink(remind.quoteId)}`).join('\n')); 50 | return true; 51 | } 52 | 53 | if (text.match(/^(.+?)\s(.+)/)) { 54 | text = text.replace(/^(.+?)\s/, ''); 55 | } else { 56 | text = ''; 57 | } 58 | 59 | const separatorIndex = text.indexOf(' ') > -1 ? text.indexOf(' ') : text.indexOf('\n'); 60 | const thing = text.substr(separatorIndex + 1).trim(); 61 | 62 | if (thing === '' && msg.quoteId == null) { 63 | msg.reply(serifs.reminder.invalid); 64 | return true; 65 | } 66 | 67 | const remind = this.reminds.insertOne({ 68 | id: msg.id, 69 | userId: msg.userId, 70 | isDm: msg.isDm, 71 | thing: thing === '' ? null : thing, 72 | quoteId: msg.quoteId, 73 | times: 0, 74 | createdAt: Date.now(), 75 | }); 76 | 77 | // メンションをsubscribe 78 | this.subscribeReply(remind!.id, msg.isDm, msg.isDm ? msg.userId : msg.id, { 79 | id: remind!.id 80 | }); 81 | 82 | if (msg.quoteId) { 83 | // 引用元をsubscribe 84 | this.subscribeReply(remind!.id, false, msg.quoteId, { 85 | id: remind!.id 86 | }); 87 | } 88 | 89 | // タイマーセット 90 | this.setTimeoutWithPersistence(NOTIFY_INTERVAL, { 91 | id: remind!.id, 92 | }); 93 | 94 | return { 95 | reaction: '🆗', 96 | immediate: true, 97 | }; 98 | } 99 | 100 | @autobind 101 | private async contextHook(key: any, msg: Message, data: any) { 102 | if (msg.text == null) return; 103 | 104 | const remind = this.reminds.findOne({ 105 | id: data.id, 106 | }); 107 | 108 | if (remind == null) { 109 | this.unsubscribeReply(key); 110 | return; 111 | } 112 | 113 | const done = msg.includes(['done', 'やった', 'やりました', 'はい']); 114 | const cancel = msg.includes(['やめる', 'やめた', 'キャンセル']); 115 | 116 | if (done || cancel) { 117 | this.unsubscribeReply(key); 118 | this.reminds.remove(remind); 119 | msg.reply(done ? getSerif(serifs.reminder.done(msg.friend.name)) : serifs.reminder.cancel); 120 | return; 121 | } else { 122 | if (msg.isDm) this.unsubscribeReply(key); 123 | return false; 124 | } 125 | } 126 | 127 | @autobind 128 | private async timeoutCallback(data) { 129 | const remind = this.reminds.findOne({ 130 | id: data.id 131 | }); 132 | if (remind == null) return; 133 | 134 | remind.times++; 135 | this.reminds.update(remind); 136 | 137 | const friend = this.ai.lookupFriend(remind.userId); 138 | if (friend == null) return; // 処理の流れ上、実際にnullになることは無さそうだけど一応 139 | 140 | let reply; 141 | if (remind.isDm) { 142 | this.ai.sendMessage(friend.userId, { 143 | text: serifs.reminder.notifyWithThing(remind.thing, friend.name) 144 | }).catch(() => {}); 145 | } else { 146 | reply = await this.ai.post({ 147 | renoteId: remind.thing == null && remind.quoteId ? remind.quoteId : remind.id, 148 | text: acct(friend.doc.user) + ' ' + serifs.reminder.notify(friend.name) 149 | }).catch(() => null); 150 | } 151 | 152 | if (reply == null) { 153 | this.ai.log(`remind failed: ${friend.userId}`); 154 | return; 155 | } 156 | 157 | this.subscribeReply(remind.id, remind.isDm, remind.isDm ? remind.userId : reply.id, { 158 | id: remind.id 159 | }); 160 | 161 | // タイマーセット 162 | this.setTimeoutWithPersistence(NOTIFY_INTERVAL, { 163 | id: remind.id, 164 | }); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/modules/kazutori/index.ts: -------------------------------------------------------------------------------- 1 | import autobind from 'autobind-decorator'; 2 | import * as loki from 'lokijs'; 3 | import Module from '@/module'; 4 | import Message from '@/message'; 5 | import serifs from '@/serifs'; 6 | 7 | type User = { 8 | id: string; 9 | username: string; 10 | host?: string | null; 11 | }; 12 | 13 | type Game = { 14 | votes: { 15 | user: User; 16 | number: number; 17 | }[]; 18 | isEnded: boolean; 19 | startedAt: number; 20 | postId: string; 21 | }; 22 | 23 | const limitMinutes = 10; 24 | 25 | export default class extends Module { 26 | public readonly name = 'kazutori'; 27 | 28 | private games: loki.Collection; 29 | private lock = 0; 30 | 31 | @autobind 32 | public install() { 33 | this.games = this.ai.getCollection('kazutori'); 34 | 35 | this.crawleGameEnd(); 36 | setInterval(this.crawleGameEnd, 1000); 37 | 38 | return { 39 | mentionHook: this.mentionHook, 40 | contextHook: this.contextHook 41 | }; 42 | } 43 | 44 | @autobind 45 | private async mentionHook(msg: Message) { 46 | if (!msg.includes(['数取り'])) return false; 47 | 48 | if (this.lock && (Date.now() - this.lock < 60 * 1000)) { 49 | return false 50 | } 51 | this.lock = Date.now(); 52 | 53 | const games = this.games.find({}); 54 | 55 | const recentGame = games.length == 0 ? null : games[games.length - 1]; 56 | 57 | if (recentGame) { 58 | // 現在アクティブなゲームがある場合 59 | if (!recentGame.isEnded) { 60 | msg.reply(serifs.kazutori.alreadyStarted, { 61 | renote: recentGame.postId 62 | }); 63 | return true; 64 | } 65 | 66 | // 直近のゲームから時間経ってない場合 67 | if (Date.now() - recentGame.startedAt < 1000 * 60 * 60) { 68 | msg.reply(serifs.kazutori.matakondo); 69 | return true; 70 | } 71 | } 72 | 73 | const post = await this.ai.post({ 74 | text: serifs.kazutori.intro(limitMinutes) 75 | }); 76 | 77 | this.games.insertOne({ 78 | votes: [], 79 | isEnded: false, 80 | startedAt: Date.now(), 81 | postId: post.id 82 | }); 83 | 84 | this.lock = 0; 85 | 86 | this.subscribeReply(null, false, post.id); 87 | 88 | this.log('New kazutori game started'); 89 | 90 | const game = this.games.findOne({ 91 | isEnded: false 92 | }); 93 | 94 | game!.votes.push({ 95 | user: this.ai.account, 96 | number: Math.floor(Math.random() * 5) + 1 + 95 97 | }); 98 | 99 | return true; 100 | } 101 | 102 | @autobind 103 | private async contextHook(key: any, msg: Message) { 104 | if (msg.text == null) return { 105 | reaction: 'hmm' 106 | }; 107 | 108 | const game = this.games.findOne({ 109 | isEnded: false 110 | }); 111 | 112 | // 処理の流れ上、実際にnullになることは無さそうだけど一応 113 | if (game == null) return; 114 | 115 | const match = msg.extractedText.match(/[0-9]+/); 116 | if (match == null) return { 117 | reaction: 'hmm' 118 | }; 119 | 120 | const num = parseInt(match[0], 10); 121 | 122 | // 整数じゃない 123 | if (!Number.isInteger(num)) return { 124 | reaction: 'hmm' 125 | }; 126 | 127 | // 範囲外 128 | if (num < 0 || num > 100) return { 129 | reaction: 'confused' 130 | }; 131 | 132 | this.log(`Voted ${num} by ${msg.user.id}`); 133 | 134 | // 投票 135 | game.votes = game.votes.filter(x => x.user.id !== msg.user.id); 136 | 137 | game.votes.push({ 138 | user: { 139 | id: msg.user.id, 140 | username: msg.user.username, 141 | host: msg.user.host 142 | }, 143 | number: num 144 | }); 145 | 146 | this.games.update(game); 147 | 148 | return { 149 | reaction: 'like' 150 | }; 151 | } 152 | 153 | /** 154 | * 終了すべきゲームがないかチェック 155 | */ 156 | @autobind 157 | private crawleGameEnd() { 158 | const game = this.games.findOne({ 159 | isEnded: false 160 | }); 161 | 162 | if (game == null) return; 163 | 164 | // 制限時間が経過していたら 165 | if (Date.now() - game.startedAt >= 1000 * 60 * limitMinutes) { 166 | this.finish(game); 167 | } 168 | } 169 | 170 | /** 171 | * ゲームを終わらせる 172 | */ 173 | @autobind 174 | private finish(game: Game) { 175 | game.isEnded = true; 176 | this.games.update(game); 177 | 178 | this.log('Kazutori game finished'); 179 | 180 | // お流れ 181 | if (game.votes.length <= 0) { 182 | this.ai.post({ 183 | visibility: 'public', 184 | text: serifs.kazutori.onagare, 185 | renoteId: game.postId 186 | }); 187 | 188 | return; 189 | } 190 | 191 | function acct(user: User): string { 192 | return user.host 193 | ? `:@${user.username}@${user.host}:` 194 | : `:@${user.username}:`; 195 | } 196 | 197 | let results: string[] = []; 198 | let winner: User | null = null; 199 | 200 | for (let i = 100; i >= 0; i--) { 201 | const users = game.votes 202 | .filter(x => x.number == i) 203 | .map(x => x.user); 204 | 205 | if (users.length == 1) { 206 | if (winner == null) { 207 | winner = users[0]; 208 | const icon = i == 100 ? '💯' : '🎉'; 209 | results.push(`${icon} **${i}**: (((${acct(users[0])})))`); 210 | } else { 211 | results.push(`➖ ${i}: ${acct(users[0])}`); 212 | } 213 | } else if (users.length > 1) { 214 | results.push(`❌ ${i}: ${users.map(u => acct(u)).join(' ')}`); 215 | } 216 | } 217 | 218 | const winnerFriend = winner ? this.ai.lookupFriend(winner.id) : null; 219 | const name = winnerFriend ? winnerFriend.name : null; 220 | 221 | const text = results.join('\n') + '\n\n' + (winner 222 | ? serifs.kazutori.finishWithWinner(acct(winner), name) 223 | : serifs.kazutori.finishWithNoWinner); 224 | 225 | this.ai.post({ 226 | visibility: 'public', 227 | text: text, 228 | cw: serifs.kazutori.finish, 229 | renoteId: game.postId 230 | }); 231 | 232 | this.unsubscribeReply(null); 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /src/modules/poll/index.ts: -------------------------------------------------------------------------------- 1 | import autobind from 'autobind-decorator'; 2 | import Message from '@/message'; 3 | import Module from '@/module'; 4 | import { genItem } from '@/vocabulary'; 5 | import config from '@/config'; 6 | import { Note } from '@/misskey/note'; 7 | import * as loki from 'lokijs'; 8 | import { kewyegenabo } from '@/kewyegenabo'; 9 | 10 | export default class extends Module { 11 | public readonly name = 'poll'; 12 | 13 | private learnedKeywords?: loki.Collection<{ 14 | keyword: string; 15 | learnedAt: number; 16 | }>; 17 | 18 | @autobind 19 | public install() { 20 | if (config.keywordEnabled) { 21 | this.learnedKeywords = this.ai.getCollection('_keyword_learnedKeywords', { 22 | indices: ['userId'] 23 | }); 24 | } 25 | 26 | setInterval(() => { 27 | if (Math.random() < 0.1) { 28 | this.post(); 29 | } 30 | }, 1000 * 60 * 60); 31 | 32 | return { 33 | mentionHook: this.mentionHook, 34 | timeoutCallback: this.timeoutCallback, 35 | }; 36 | } 37 | 38 | @autobind 39 | private async post() { 40 | const duration = 1000 * 60 * 30; 41 | 42 | const polls = [ // TODO: Extract serif 43 | ['珍しそうなもの', 'みなさんは、どれがいちばん珍しいと思いますか?'], 44 | ['美味しそうなもの', 'みなさんは、どれがいちばん美味しいと思いますか?'], 45 | ['重そうなもの', 'みなさんは、どれがいちばん重いと思いますか?'], 46 | ['欲しいもの', 'みなさんは、どれがいちばん欲しいですか?'], 47 | ['無人島に持っていきたいもの', 'みなさんは、無人島にひとつ持っていけるとしたらどれにしますか?'], 48 | ['家に飾りたいもの', 'みなさんは、家に飾るとしたらどれにしますか?'], 49 | ['売れそうなもの', 'みなさんは、どれがいちばん売れそうだと思いますか?'], 50 | ['降ってきてほしいもの', 'みなさんは、どれが空から降ってきてほしいですか?'], 51 | ['携帯したいもの', 'みなさんは、どれを携帯したいですか?'], 52 | ['商品化したいもの', 'みなさんは、商品化するとしたらどれにしますか?'], 53 | ['発掘されそうなもの', 'みなさんは、遺跡から発掘されそうなものはどれだと思いますか?'], 54 | ['良い香りがしそうなもの', 'みなさんは、どれがいちばんいい香りがすると思いますか?'], 55 | ['高値で取引されそうなもの', 'みなさんは、どれがいちばん高値で取引されると思いますか?'], 56 | ['地球周回軌道上にありそうなもの', 'みなさんは、どれが地球周回軌道上を漂っていそうだと思いますか?'], 57 | ['プレゼントしたいもの', 'みなさんは、私にプレゼントしてくれるとしたらどれにしますか?'], 58 | ['プレゼントされたいもの', 'みなさんは、プレゼントでもらうとしたらどれにしますか?'], 59 | ['私が持ってそうなもの', 'みなさんは、私が持ってそうなものはどれだと思いますか?'], 60 | ['流行りそうなもの', 'みなさんは、どれが流行りそうだと思いますか?'], 61 | ['朝ごはん', 'みなさんは、朝ごはんにどれが食べたいですか?'], 62 | ['お昼ごはん', 'みなさんは、お昼ごはんにどれが食べたいですか?'], 63 | ['お夕飯', 'みなさんは、お夕飯にどれが食べたいですか?'], 64 | ['体に良さそうなもの', 'みなさんは、どれが体に良さそうだと思いますか?'], 65 | ['後世に遺したいもの', 'みなさんは、どれを後世に遺したいですか?'], 66 | ['楽器になりそうなもの', 'みなさんは、どれが楽器になりそうだと思いますか?'], 67 | ['お味噌汁の具にしたいもの', 'みなさんは、お味噌汁の具にするとしたらどれがいいですか?'], 68 | ['ふりかけにしたいもの', 'みなさんは、どれをごはんにふりかけたいですか?'], 69 | ['よく見かけるもの', 'みなさんは、どれをよく見かけますか?'], 70 | ['道に落ちてそうなもの', 'みなさんは、道端に落ちてそうなものはどれだと思いますか?'], 71 | ['美術館に置いてそうなもの', 'みなさんは、この中で美術館に置いてありそうなものはどれだと思いますか?'], 72 | ['教室にありそうなもの', 'みなさんは、教室にありそうなものってどれだと思いますか?'], 73 | ['絵文字になってほしいもの', '絵文字になってほしいものはどれですか?'], 74 | ['Misskey本部にありそうなもの', 'みなさんは、Misskey本部にありそうなものはどれだと思いますか?'], 75 | ['燃えるゴミ', 'みなさんは、どれが燃えるゴミだと思いますか?'], 76 | ['好きなおにぎりの具', 'みなさんの好きなおにぎりの具はなんですか?'], 77 | ['そして輝くウルトラ', 'みなさんは、そして輝くウルトラ…?'], 78 | ]; 79 | 80 | const poll = polls[Math.floor(Math.random() * polls.length)]; 81 | 82 | const getKeyword = (rng: () => number) => { 83 | if (!this.learnedKeywords) return null; 84 | 85 | const count = this.learnedKeywords.count(); 86 | const offset = Math.floor(rng() * count); 87 | 88 | const x = this.learnedKeywords.chain().find().offset(offset).limit(1).data(); 89 | let keyword = x[0]?.keyword || null; 90 | if (Math.random() * 100 > 80) keyword = kewyegenabo(keyword); 91 | return keyword; 92 | }; 93 | 94 | const choices = poll[0] === 'そして輝くウルトラ' ? [ 95 | 'そう', 96 | 'どちらかというとそう', 97 | 'どちらでもない', 98 | 'どちらかというとそうではない', 99 | 'そうではない', 100 | 'わからない・回答しない', 101 | ] : [ 102 | genItem(undefined, getKeyword), 103 | genItem(undefined, getKeyword), 104 | genItem(undefined, getKeyword), 105 | genItem(undefined, getKeyword), 106 | genItem(undefined, getKeyword), 107 | ]; 108 | 109 | const note = await this.ai.post({ 110 | text: poll[1], 111 | poll: { 112 | choices, 113 | expiredAfter: duration, 114 | multiple: false, 115 | } 116 | }); 117 | 118 | // タイマーセット 119 | this.setTimeoutWithPersistence(duration + 3000, { 120 | title: poll[0], 121 | noteId: note.id, 122 | }); 123 | } 124 | 125 | @autobind 126 | private async mentionHook(msg: Message) { 127 | if (!msg.or(['/poll']) || msg.user.username !== config.master) { 128 | return false; 129 | } else { 130 | this.log('Manualy poll requested'); 131 | } 132 | 133 | this.post(); 134 | 135 | return true; 136 | } 137 | 138 | @autobind 139 | private async timeoutCallback({ title, noteId }) { 140 | const note: Note = await this.ai.api('notes/show', { noteId }); 141 | 142 | const choices = note.poll!.choices; 143 | 144 | let mostVotedChoice; 145 | 146 | for (const choice of choices) { 147 | if (mostVotedChoice == null) { 148 | mostVotedChoice = choice; 149 | continue; 150 | } 151 | 152 | if (choice.votes > mostVotedChoice.votes) { 153 | mostVotedChoice = choice; 154 | } 155 | } 156 | 157 | const mostVotedChoices = choices.filter(choice => choice.votes === mostVotedChoice.votes); 158 | 159 | if (mostVotedChoice.votes === 0) { 160 | this.ai.post({ // TODO: Extract serif 161 | text: '投票はありませんでした', 162 | renoteId: noteId, 163 | }); 164 | } else if (mostVotedChoices.length === 1) { 165 | this.ai.post({ // TODO: Extract serif 166 | cw: `${title}アンケートの結果発表です!`, 167 | text: `結果は${mostVotedChoice.votes}票の「${mostVotedChoice.text}」でした!`, 168 | renoteId: noteId, 169 | }); 170 | } else { 171 | const choices = mostVotedChoices.map(choice => `「${choice.text}」`).join('と'); 172 | this.ai.post({ // TODO: Extract serif 173 | cw: `${title}アンケートの結果発表です!`, 174 | text: `結果は${mostVotedChoice.votes}票の${choices}でした!`, 175 | renoteId: noteId, 176 | }); 177 | } 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/modules/chart/render-chart.ts: -------------------------------------------------------------------------------- 1 | const width = 1024 + 256; 2 | const height = 512 + 256; 3 | const margin = 128; 4 | const titleTextSize = 35; 5 | 6 | const lineWidth = 16; 7 | const yAxisThickness = 2; 8 | 9 | const colors = { 10 | bg: '#434343', 11 | text: '#e0e4cc', 12 | yAxis: '#5a5a5a', 13 | dataset: [ 14 | '#ff4e50', 15 | '#c2f725', 16 | '#69d2e7', 17 | '#f38630', 18 | '#f9d423', 19 | ] 20 | }; 21 | 22 | const yAxisTicks = 4; 23 | 24 | type Chart = { 25 | title?: string; 26 | datasets: { 27 | title?: string; 28 | data: number[]; 29 | }[]; 30 | }; 31 | 32 | export async function renderChart(chart: Chart) { 33 | // @ts-ignore 34 | const { createCanvas, registerFont } = await import('canvas'); 35 | 36 | try { 37 | registerFont('./font.ttf', { family: 'CustomFont' }); 38 | } catch { 39 | } 40 | 41 | const canvas = createCanvas(width, height); 42 | const ctx = canvas.getContext('2d'); 43 | ctx.antialias = 'default'; 44 | 45 | ctx.fillStyle = colors.bg; 46 | ctx.beginPath(); 47 | ctx.fillRect(0, 0, width, height); 48 | 49 | let chartAreaX = margin; 50 | let chartAreaY = margin; 51 | let chartAreaWidth = width - (margin * 2); 52 | let chartAreaHeight = height - (margin * 2); 53 | 54 | // Draw title 55 | if (chart.title) { 56 | ctx.font = `${titleTextSize}px CustomFont`; 57 | const t = ctx.measureText(chart.title); 58 | ctx.fillStyle = colors.text; 59 | ctx.fillText(chart.title, (width / 2) - (t.width / 2), 128); 60 | 61 | chartAreaY += titleTextSize; 62 | chartAreaHeight -= titleTextSize; 63 | } 64 | 65 | const xAxisCount = chart.datasets[0].data.length; 66 | const serieses = chart.datasets.length; 67 | 68 | let lowerBound = Infinity; 69 | let upperBound = -Infinity; 70 | 71 | for (let xAxis = 0; xAxis < xAxisCount; xAxis++) { 72 | let v = 0; 73 | for (let series = 0; series < serieses; series++) { 74 | v += chart.datasets[series].data[xAxis]; 75 | } 76 | if (v > upperBound) upperBound = v; 77 | if (v < lowerBound) lowerBound = v; 78 | } 79 | 80 | // Calculate Y axis scale 81 | const yAxisSteps = niceScale(lowerBound, upperBound, yAxisTicks); 82 | const yAxisStepsMin = yAxisSteps[0]; 83 | const yAxisStepsMax = yAxisSteps[yAxisSteps.length - 1]; 84 | const yAxisRange = yAxisStepsMax - yAxisStepsMin; 85 | 86 | // Draw Y axis 87 | ctx.lineWidth = yAxisThickness; 88 | ctx.lineCap = 'round'; 89 | ctx.strokeStyle = colors.yAxis; 90 | for (let i = 0; i < yAxisSteps.length; i++) { 91 | const step = yAxisSteps[yAxisSteps.length - i - 1]; 92 | const y = i * (chartAreaHeight / (yAxisSteps.length - 1)); 93 | ctx.beginPath(); 94 | ctx.lineTo(chartAreaX, chartAreaY + y); 95 | ctx.lineTo(chartAreaX + chartAreaWidth, chartAreaY + y); 96 | ctx.stroke(); 97 | 98 | ctx.font = '20px CustomFont'; 99 | ctx.fillStyle = colors.text; 100 | ctx.fillText(step.toString(), chartAreaX, chartAreaY + y - 8); 101 | } 102 | 103 | const newDatasets: any[] = []; 104 | 105 | for (let series = 0; series < serieses; series++) { 106 | newDatasets.push({ 107 | data: [] 108 | }); 109 | } 110 | 111 | for (let xAxis = 0; xAxis < xAxisCount; xAxis++) { 112 | for (let series = 0; series < serieses; series++) { 113 | newDatasets[series].data.push(chart.datasets[series].data[xAxis] / yAxisRange); 114 | } 115 | } 116 | 117 | const perXAxisWidth = chartAreaWidth / xAxisCount; 118 | 119 | let newUpperBound = -Infinity; 120 | 121 | for (let xAxis = 0; xAxis < xAxisCount; xAxis++) { 122 | let v = 0; 123 | for (let series = 0; series < serieses; series++) { 124 | v += newDatasets[series].data[xAxis]; 125 | } 126 | if (v > newUpperBound) newUpperBound = v; 127 | } 128 | 129 | // Draw X axis 130 | ctx.lineWidth = lineWidth; 131 | ctx.lineCap = 'round'; 132 | 133 | for (let xAxis = 0; xAxis < xAxisCount; xAxis++) { 134 | const xAxisPerTypeHeights: number[] = []; 135 | 136 | for (let series = 0; series < serieses; series++) { 137 | const v = newDatasets[series].data[xAxis]; 138 | const vHeight = (v / newUpperBound) * (chartAreaHeight - ((yAxisStepsMax - upperBound) / yAxisStepsMax * chartAreaHeight)); 139 | xAxisPerTypeHeights.push(vHeight); 140 | } 141 | 142 | for (let series = serieses - 1; series >= 0; series--) { 143 | ctx.strokeStyle = colors.dataset[series % colors.dataset.length]; 144 | 145 | let total = 0; 146 | for (let i = 0; i < series; i++) { 147 | total += xAxisPerTypeHeights[i]; 148 | } 149 | 150 | const height = xAxisPerTypeHeights[series]; 151 | 152 | const x = chartAreaX + (perXAxisWidth * ((xAxisCount - 1) - xAxis)) + (perXAxisWidth / 2); 153 | 154 | const yTop = (chartAreaY + chartAreaHeight) - (total + height); 155 | const yBottom = (chartAreaY + chartAreaHeight) - (total); 156 | 157 | ctx.globalAlpha = 1 - (xAxis / xAxisCount); 158 | ctx.beginPath(); 159 | ctx.lineTo(x, yTop); 160 | ctx.lineTo(x, yBottom); 161 | ctx.stroke(); 162 | } 163 | } 164 | 165 | return canvas.toBuffer(); 166 | } 167 | 168 | // https://stackoverflow.com/questions/326679/choosing-an-attractive-linear-scale-for-a-graphs-y-axis 169 | // https://github.com/apexcharts/apexcharts.js/blob/master/src/modules/Scales.js 170 | // This routine creates the Y axis values for a graph. 171 | function niceScale(lowerBound: number, upperBound: number, ticks: number): number[] { 172 | if (lowerBound === 0 && upperBound === 0) return [0]; 173 | 174 | // Calculate Min amd Max graphical labels and graph 175 | // increments. The number of ticks defaults to 176 | // 10 which is the SUGGESTED value. Any tick value 177 | // entered is used as a suggested value which is 178 | // adjusted to be a 'pretty' value. 179 | // 180 | // Output will be an array of the Y axis values that 181 | // encompass the Y values. 182 | const steps: number[] = []; 183 | 184 | // Determine Range 185 | const range = upperBound - lowerBound; 186 | 187 | let tiks = ticks + 1; 188 | // Adjust ticks if needed 189 | if (tiks < 2) { 190 | tiks = 2; 191 | } else if (tiks > 2) { 192 | tiks -= 2; 193 | } 194 | 195 | // Get raw step value 196 | const tempStep = range / tiks; 197 | 198 | // Calculate pretty step value 199 | const mag = Math.floor(Math.log10(tempStep)); 200 | const magPow = Math.pow(10, mag); 201 | const magMsd = (parseInt as any)(tempStep / magPow); 202 | const stepSize = magMsd * magPow; 203 | 204 | // build Y label array. 205 | // Lower and upper bounds calculations 206 | const lb = stepSize * Math.floor(lowerBound / stepSize); 207 | const ub = stepSize * Math.ceil(upperBound / stepSize); 208 | // Build array 209 | let val = lb; 210 | while (1) { 211 | steps.push(val); 212 | val += stepSize; 213 | if (val > ub) { 214 | break; 215 | } 216 | } 217 | 218 | return steps; 219 | } 220 | -------------------------------------------------------------------------------- /src/modules/maze/gen-maze.ts: -------------------------------------------------------------------------------- 1 | import * as gen from 'random-seed'; 2 | import { CellType } from './maze'; 3 | 4 | const cellVariants = { 5 | void: { 6 | digg: { left: null, right: null, top: null, bottom: null }, 7 | cross: { left: false, right: false, top: false, bottom: false }, 8 | }, 9 | empty: { 10 | digg: { left: 'left', right: 'right', top: 'top', bottom: 'bottom' }, 11 | cross: { left: false, right: false, top: false, bottom: false }, 12 | }, 13 | left: { 14 | digg: { left: null, right: 'leftRight', top: 'leftTop', bottom: 'leftBottom' }, 15 | cross: { left: false, right: false, top: false, bottom: false }, 16 | }, 17 | right: { 18 | digg: { left: 'leftRight', right: null, top: 'rightTop', bottom: 'rightBottom' }, 19 | cross: { left: false, right: false, top: false, bottom: false }, 20 | }, 21 | top: { 22 | digg: { left: 'leftTop', right: 'rightTop', top: null, bottom: 'topBottom' }, 23 | cross: { left: false, right: false, top: false, bottom: false }, 24 | }, 25 | bottom: { 26 | digg: { left: 'leftBottom', right: 'rightBottom', top: 'topBottom', bottom: null }, 27 | cross: { left: false, right: false, top: false, bottom: false }, 28 | }, 29 | leftTop: { 30 | digg: { left: null, right: 'leftRightTop', top: null, bottom: 'leftTopBottom' }, 31 | cross: { left: false, right: false, top: false, bottom: false }, 32 | }, 33 | leftBottom: { 34 | digg: { left: null, right: 'leftRightBottom', top: 'leftTopBottom', bottom: null }, 35 | cross: { left: false, right: false, top: false, bottom: false }, 36 | }, 37 | rightTop: { 38 | digg: { left: 'leftRightTop', right: null, top: null, bottom: 'rightTopBottom' }, 39 | cross: { left: false, right: false, top: false, bottom: false }, 40 | }, 41 | rightBottom: { 42 | digg: { left: 'leftRightBottom', right: null, top: 'rightTopBottom', bottom: null }, 43 | cross: { left: false, right: false, top: false, bottom: false }, 44 | }, 45 | leftRightTop: { 46 | digg: { left: null, right: null, top: null, bottom: null }, 47 | cross: { left: false, right: false, top: false, bottom: false }, 48 | }, 49 | leftRightBottom: { 50 | digg: { left: null, right: null, top: null, bottom: null }, 51 | cross: { left: false, right: false, top: false, bottom: false }, 52 | }, 53 | leftTopBottom: { 54 | digg: { left: null, right: null, top: null, bottom: null }, 55 | cross: { left: false, right: false, top: false, bottom: false }, 56 | }, 57 | rightTopBottom: { 58 | digg: { left: null, right: null, top: null, bottom: null }, 59 | cross: { left: false, right: false, top: false, bottom: false }, 60 | }, 61 | leftRight: { 62 | digg: { left: null, right: null, top: 'leftRightTop', bottom: 'leftRightBottom' }, 63 | cross: { left: false, right: false, top: true, bottom: true }, 64 | }, 65 | topBottom: { 66 | digg: { left: 'leftTopBottom', right: 'rightTopBottom', top: null, bottom: null }, 67 | cross: { left: true, right: true, top: false, bottom: false }, 68 | }, 69 | cross: { 70 | digg: { left: 'cross', right: 'cross', top: 'cross', bottom: 'cross' }, 71 | cross: { left: false, right: false, top: false, bottom: false }, 72 | }, 73 | } as { [k in CellType]: { 74 | digg: { left: CellType | null; right: CellType | null; top: CellType | null; bottom: CellType | null; }; 75 | cross: { left: boolean; right: boolean; top: boolean; bottom: boolean; }; 76 | } }; 77 | 78 | type Dir = 'left' | 'right' | 'top' | 'bottom'; 79 | 80 | export function genMaze(seed, complexity?) { 81 | const rand = gen.create(seed); 82 | 83 | let mazeSize; 84 | if (complexity) { 85 | if (complexity === 'veryEasy') mazeSize = 3 + rand(3); 86 | if (complexity === 'easy') mazeSize = 8 + rand(8); 87 | if (complexity === 'hard') mazeSize = 22 + rand(13); 88 | if (complexity === 'veryHard') mazeSize = 40 + rand(20); 89 | if (complexity === 'ai') mazeSize = 100; 90 | } else { 91 | mazeSize = 11 + rand(21); 92 | } 93 | 94 | const donut = false; 95 | const donutWidth = 1 + Math.floor(mazeSize / 8) + rand(Math.floor(mazeSize / 4)); 96 | 97 | const straightMode = false; 98 | const straightness = 5 + rand(10); 99 | 100 | // maze (filled by 'empty') 101 | const maze: CellType[][] = new Array(mazeSize); 102 | for (let i = 0; i < mazeSize; i++) { 103 | maze[i] = new Array(mazeSize).fill('empty'); 104 | } 105 | 106 | if (donut) { 107 | for (let y = 0; y < mazeSize; y++) { 108 | for (let x = 0; x < mazeSize; x++) { 109 | if (x > donutWidth && x < (mazeSize - 1) - donutWidth && y > donutWidth && y < (mazeSize - 1) - donutWidth) { 110 | maze[x][y] = 'void'; 111 | } 112 | } 113 | } 114 | } 115 | 116 | function checkDiggable(x: number, y: number, dir: Dir) { 117 | if (cellVariants[maze[x][y]].digg[dir] === null) return false; 118 | 119 | const newPos = 120 | dir === 'top' ? { x: x, y: y - 1 } : 121 | dir === 'bottom' ? { x: x, y: y + 1 } : 122 | dir === 'left' ? { x: x - 1, y: y } : 123 | dir === 'right' ? { x: x + 1, y: y } : 124 | { x, y }; 125 | 126 | if (newPos.x < 0 || newPos.y < 0 || newPos.x >= mazeSize || newPos.y >= mazeSize) return false; 127 | 128 | const cell = maze[newPos.x][newPos.y]; 129 | if (cell === 'void') return false; 130 | if (cell === 'empty') return true; 131 | if (cellVariants[cell].cross[dir] && checkDiggable(newPos.x, newPos.y, dir)) return true; 132 | 133 | return false; 134 | } 135 | 136 | function diggFrom(x: number, y: number, prevDir?: Dir) { 137 | const isUpDiggable = checkDiggable(x, y, 'top'); 138 | const isRightDiggable = checkDiggable(x, y, 'right'); 139 | const isDownDiggable = checkDiggable(x, y, 'bottom'); 140 | const isLeftDiggable = checkDiggable(x, y, 'left'); 141 | 142 | if (!isUpDiggable && !isRightDiggable && !isDownDiggable && !isLeftDiggable) return; 143 | 144 | const dirs: Dir[] = []; 145 | if (isUpDiggable) dirs.push('top'); 146 | if (isRightDiggable) dirs.push('right'); 147 | if (isDownDiggable) dirs.push('bottom'); 148 | if (isLeftDiggable) dirs.push('left'); 149 | 150 | let dir: Dir; 151 | if (straightMode && rand(straightness) !== 0) { 152 | if (prevDir != null && dirs.includes(prevDir)) { 153 | dir = prevDir; 154 | } else { 155 | dir = dirs[rand(dirs.length)]; 156 | } 157 | } else { 158 | dir = dirs[rand(dirs.length)]; 159 | } 160 | 161 | maze[x][y] = cellVariants[maze[x][y]].digg[dir]!; 162 | 163 | if (dir === 'top') { 164 | maze[x][y - 1] = maze[x][y - 1] === 'empty' ? 'bottom' : 'cross'; 165 | diggFrom(x, y - 1, dir); 166 | return; 167 | } 168 | if (dir === 'right') { 169 | maze[x + 1][y] = maze[x + 1][y] === 'empty' ? 'left' : 'cross'; 170 | diggFrom(x + 1, y, dir); 171 | return; 172 | } 173 | if (dir === 'bottom') { 174 | maze[x][y + 1] = maze[x][y + 1] === 'empty' ? 'top' : 'cross'; 175 | diggFrom(x, y + 1, dir); 176 | return; 177 | } 178 | if (dir === 'left') { 179 | maze[x - 1][y] = maze[x - 1][y] === 'empty' ? 'right' : 'cross'; 180 | diggFrom(x - 1, y, dir); 181 | return; 182 | } 183 | } 184 | 185 | //#region start digg 186 | const nonVoidCells: [number, number][] = []; 187 | 188 | for (let y = 0; y < mazeSize; y++) { 189 | for (let x = 0; x < mazeSize; x++) { 190 | const cell = maze[x][y]; 191 | if (cell !== 'void') nonVoidCells.push([x, y]); 192 | } 193 | } 194 | 195 | const origin = nonVoidCells[rand(nonVoidCells.length)]; 196 | 197 | diggFrom(origin[0], origin[1]); 198 | //#endregion 199 | 200 | let hasEmptyCell = true; 201 | while (hasEmptyCell) { 202 | const nonEmptyCells: [number, number][] = []; 203 | 204 | for (let y = 0; y < mazeSize; y++) { 205 | for (let x = 0; x < mazeSize; x++) { 206 | const cell = maze[x][y]; 207 | if (cell !== 'empty' && cell !== 'void' && cell !== 'cross') nonEmptyCells.push([x, y]); 208 | } 209 | } 210 | 211 | const pos = nonEmptyCells[rand(nonEmptyCells.length)]; 212 | 213 | diggFrom(pos[0], pos[1]); 214 | 215 | hasEmptyCell = false; 216 | for (let y = 0; y < mazeSize; y++) { 217 | for (let x = 0; x < mazeSize; x++) { 218 | if (maze[x][y] === 'empty') hasEmptyCell = true; 219 | } 220 | } 221 | } 222 | 223 | return maze; 224 | } 225 | -------------------------------------------------------------------------------- /src/vocabulary.ts: -------------------------------------------------------------------------------- 1 | import * as seedrandom from 'seedrandom'; 2 | 3 | export const itemPrefixes = [ 4 | 'お淑やかな', 5 | '健全な', 6 | 'お上品な', 7 | 'エレガントな', 8 | '夜の', 9 | 'プラチナ製', 10 | '新鮮な', 11 | '最新式の', 12 | '古代の', 13 | '手作り', 14 | '時計じかけの', 15 | '伝説の', 16 | '焼き', 17 | '生の', 18 | 'いあ謹製', 19 | 'ポケットサイズ', 20 | '3日前の', 21 | 'そこらへんの', 22 | '偽の', 23 | '使用済み', 24 | '壊れた', 25 | '市販の', 26 | 'オーダーメイドの', 27 | '業務用の', 28 | 'Microsoft製', 29 | 'Apple製', 30 | '人類の技術を結集して作った', 31 | '2018年製', // TODO ランダム 32 | '500kgくらいある', 33 | '高級', 34 | '腐った', 35 | '人工知能搭載', 36 | '反重力', 37 | '折り畳み式', 38 | '携帯型', 39 | '遺伝子組み換え', 40 | '突然変異して飛行能力を獲得した', 41 | '純金製', 42 | '透明な', 43 | '光る', 44 | 'ハート型の', 45 | '動く', 46 | '半分にカットされた', 47 | 'USBコネクタ付きの', 48 | 'いにしえの', 49 | '呪われた', 50 | 'エンチャントされた', 51 | '一日分のビタミンが入った', 52 | 'かじりかけ', 53 | '幻の', 54 | '仮想的な', 55 | '原子力', 56 | '高度に訓練された', 57 | '遺伝子組み換えでない', 58 | 'ダンジョン最深部で見つかった', 59 | '異世界の', 60 | '異星の', 61 | '謎の', 62 | '時空を歪める', 63 | '異音がする', 64 | '霧散する', 65 | 'プラズマ化した', 66 | '衝撃を与えると低確率で爆発する', 67 | 'ズッキーニに擬態した', 68 | '仮説上の', 69 | '毒の', 70 | '真の', 71 | '究極の', 72 | 'チョコ入り', 73 | '異臭を放つ', 74 | '4次元', 75 | '脈動する', 76 | '得体の知れない', 77 | '四角い', 78 | '暴れ回る', 79 | '夢の', 80 | '闇の', 81 | '暗黒の', 82 | '封印されし', 83 | '死の', 84 | '凍った', 85 | '魔の', 86 | '禁断の', 87 | 'ホログラフィックな', 88 | '油圧式', 89 | '辛そうで辛くない少し辛い', 90 | '焦げた', 91 | '宇宙', 92 | '電子', 93 | '陽電子', 94 | '量子力学的', 95 | 'シュレディンガーの', 96 | '分散型', 97 | '卵かけ', 98 | '次世代', 99 | '帯電', 100 | '太古の', 101 | 'WiFi対応', 102 | '高反発', 103 | '【令和最新版】', 104 | '廉価版', 105 | 'ねばねば', 106 | 'どろどろ', 107 | 'パサパサの', 108 | '湿気った', 109 | '賞味期限切れ', 110 | '地獄から来た', 111 | 'ニンニクマシ', 112 | '放射性', 113 | 'フラクタルな', 114 | '再帰的', 115 | 'ときどき分裂する', 116 | '消える', 117 | '等速直線運動する', 118 | 'X線照射', 119 | '蠢く', 120 | '形而上学的', 121 | 'もちもち', 122 | '冷やし', 123 | 'あつあつ', 124 | '巨大', 125 | 'ナノサイズ', 126 | 'やわらかい', 127 | '人の手に負えない', 128 | 'バグった', 129 | 'どこからともなく現れる', 130 | '人工', 131 | '天然', 132 | '祀られた', 133 | 'チョコレートコーティング', 134 | '地域で親しまれている', 135 | '抗菌仕様', 136 | '耐火', 137 | '血行を良くする作用がある', 138 | 'なんらかのオーラを感じる', 139 | '周囲の気温を上昇させる効果がある', 140 | '激', 141 | '猛', 142 | '超', 143 | '群生する', 144 | '軽量', 145 | '国宝級', 146 | '称賛に値する', 147 | '世界に通用する', 148 | '一世を風靡した', 149 | '流行りの', 150 | '8カラットの', 151 | '中古の', 152 | '新品の', 153 | '愛妻', 154 | 'ブランドものの', 155 | '忘らるる', 156 | '指数関数的勢いで増殖する', 157 | 'ぷるぷる', 158 | 'ぐにゃぐにゃ', 159 | '多目的', 160 | 'いい感じ™の', 161 | '激辛', 162 | '先進的な', 163 | 'レトロな', 164 | 'ヴィンテージ', 165 | '100日後に何らかが起きる', 166 | '合法', 167 | 'プレミア付き', 168 | 'デカ', 169 | 'ギガ', 170 | '穢れた', 171 | '加護を受けた', 172 | '品質保証付き', 173 | 'AppleCare+加入済み', 174 | 'えっちな', 175 | '純粋な', 176 | '構造上の欠陥がある', 177 | 'デザイナーズ', 178 | '蠱惑的な', 179 | '概念としての', 180 | '霊験灼かな', 181 | '御利益がありそうな', 182 | 'つやつや', 183 | 'べとべと', 184 | 'ムキムキの', 185 | 'オーバークロックされた', 186 | 'リミッター解除された', 187 | '無機質な', 188 | '前衛的な', 189 | '会社から支給された', 190 | '担保としての', 191 | '経費で落ちる', 192 | '真贋が定かでない', 193 | '肥えた', 194 | '怪しい', 195 | '妖しい', 196 | '攻撃的な', 197 | '現存する最古の', 198 | '考古学的価値がある', 199 | '官能的な', 200 | '備え付けの', 201 | 'カビの生えた', 202 | '丹念に熟成された', 203 | 'アルミダイキャスト', 204 | '畏怖の念を抱く', 205 | '養殖', 206 | 'やばい', 207 | 'すごい', 208 | 'かわいい', 209 | 'デジタル', 210 | 'アナログ', 211 | '彁な', 212 | 'キミの', 213 | 'あなただけの', 214 | 'カラフルな', 215 | '電動', 216 | '当たり判定のない', 217 | 'めり込んだ', 218 | '100年に一度の', 219 | 'ジューシーな', 220 | 'Hi-Res', 221 | '確変', 222 | '食用', 223 | 'THE ', 224 | '某', 225 | '朽ちゆく', 226 | '滅びの', 227 | '反発係数がe>1の', 228 | '摩擦係数0の', 229 | '誉れ高き', 230 | '解き放たれし', 231 | '大きな', 232 | '小さな', 233 | '強欲な', 234 | 'うねうね', 235 | '水没', 236 | '燃え盛る', 237 | ]; 238 | 239 | export const items = [ 240 | 'ナス', 241 | 'トマト', 242 | 'きゅうり', 243 | 'じゃがいも', 244 | '焼きビーフン', 245 | '腰', 246 | '寿司', 247 | 'かぼちゃ', 248 | '諭吉', 249 | 'キロバー', 250 | 'アルミニウム', 251 | 'ナトリウム', 252 | 'マグネシウム', 253 | 'プルトニウム', 254 | 'ちいさなメダル', 255 | '牛乳パック', 256 | 'ペットボトル', 257 | 'クッキー', 258 | 'チョコレート', 259 | 'メイド服', 260 | 'オレンジ', 261 | 'ニーソ', 262 | '反物質コンデンサ', 263 | '粒子加速器', 264 | 'マイクロプロセッサ(4コア8スレッド)', 265 | '原子力発電所', 266 | 'レイヤ4スイッチ', 267 | '緩衝チェーン', 268 | '陽電子頭脳', 269 | '惑星', 270 | 'テルミン', 271 | '虫歯車', 272 | 'マウンター', 273 | 'バケットホイールエクスカベーター', 274 | 'デーモンコア', 275 | 'ゲームボーイアドバンス', 276 | '量子コンピューター', 277 | 'アナモルフィックレンズ', 278 | '押し入れの奥から出てきた謎の生き物', 279 | 'スマートフォン', 280 | '時計', 281 | 'プリン', 282 | 'ガブリエルのラッパ', 283 | 'メンガーのスポンジ', 284 | 'ハンドスピナー', 285 | '超立方体', 286 | '建築物', 287 | 'エナジードリンク', 288 | 'マウスカーソル', 289 | 'メガネ', 290 | 'まぐろ', 291 | 'ゴミ箱', 292 | 'つまようじ', 293 | 'お弁当に入ってる緑の仕切りみたいなやつ', 294 | '割りばし', 295 | '換気扇', 296 | 'ペットボトルのキャップ', 297 | '消波ブロック', 298 | 'ピザ', 299 | '歯磨き粉', 300 | '空き缶', 301 | 'キーホルダー', 302 | '金髪碧眼の美少女', 303 | 'SDカード', 304 | 'リップクリーム', 305 | 'チョコ無しチョココロネ', 306 | '鳥インフルエンザ', 307 | '自動販売機', 308 | '重いもの', 309 | 'ノートパソコン', 310 | 'ビーフジャーキー', 311 | 'さけるチーズ', 312 | 'ダイヤモンド', 313 | '物体', 314 | '月の石', 315 | '特異点', 316 | '中性子星', 317 | '液体', 318 | '衛星', 319 | 'ズッキーニ', 320 | '黒いもの', 321 | '白いもの', 322 | '赤いもの', 323 | '丸いもの', 324 | '四角いもの', 325 | 'カード状のもの', 326 | '気体', 327 | '鉛筆', 328 | '消しゴム', 329 | 'つるぎ', 330 | '棒状のもの', 331 | '農産物', 332 | 'メタルスライム', 333 | 'タコの足', 334 | 'きのこ', 335 | 'なめこ', 336 | '缶チューハイ', 337 | '爪切り', 338 | '耳かき', 339 | 'ぬいぐるみ', 340 | 'ティラノサウルス', 341 | '尿路結石', 342 | 'エンターキー', 343 | '壺', 344 | '水銀', 345 | 'DHMO', 346 | '水', 347 | '土地', 348 | '大陸', 349 | 'サイコロ', 350 | '室外機', 351 | '油圧ジャッキ', 352 | 'タピオカ', 353 | 'トイレットペーパーの芯', 354 | 'ダンボール箱', 355 | 'ハニワ', 356 | 'ボールペン', 357 | 'シャーペン', 358 | '原子', 359 | '宇宙', 360 | '素粒子', 361 | 'ごま油', 362 | '卵かけご飯', 363 | 'ダークマター', 364 | 'ブラックホール', 365 | '太陽', 366 | '石英ガラス', 367 | 'ダム', 368 | 'ウイルス', 369 | '細菌', 370 | 'アーチ式コンクリートダム', 371 | '重力式コンクリートダム', 372 | 'フラッシュバルブ', 373 | 'ヴィブラスラップ', 374 | 'オブジェ', 375 | '原子力発電所', 376 | '原子炉', 377 | 'エラトステネスの篩', 378 | 'ブラウン管', 379 | 'タキオン', 380 | 'ラッセルのティーポット', 381 | '電子機器', 382 | 'TNT', 383 | 'ポリゴン', 384 | '空気', 385 | 'RTX 3090', 386 | 'シャーペンの芯', 387 | 'ロゼッタストーン', 388 | 'CapsLockキー', 389 | '虚無', 390 | 'UFO', 391 | 'NumLockキー', 392 | '放射性廃棄物', 393 | '火星', 394 | 'ウラン', 395 | '遠心分離機', 396 | 'undefined', 397 | 'null', 398 | 'NaN', 399 | '[object Object]', 400 | 'ゼロ幅スペース', 401 | '全角スペース', 402 | '太鼓', 403 | '石像', 404 | 'スライム', 405 | '点P', 406 | '🤯', 407 | 'きんのたま', 408 | 'フロッピーディスク', 409 | '掛け軸', 410 | 'JavaScriptコンソール', 411 | 'インターネットエクスプローラー', 412 | '潜水艦発射弾道ミサイル', 413 | 'ミトコンドリア', 414 | 'ヘリウム', 415 | 'タンパク質', 416 | 'カプサイシン', 417 | 'エスカレーター', 418 | '核融合炉', 419 | '地熱発電所', 420 | 'マンション', 421 | 'ラバライト', 422 | 'ガリレオ温度計', 423 | 'ラジオメーター', 424 | 'サンドピクチャー', 425 | 'ストームグラス', 426 | 'ニュートンクレードル', 427 | '永久機関', 428 | '柿の種のピーナッツ部分', 429 | '伝票入れる筒状のアレ', 430 | '布団', 431 | '寝具', 432 | '偶像', 433 | '森羅万象', 434 | '卒塔婆', 435 | '国民の基本的な権利', 436 | 'こたつ', 437 | '靴下(片方は紛失)', 438 | '健康保険証', 439 | 'テレホンカード', 440 | 'ピアノの黒鍵', 441 | 'ACアダプター', 442 | 'DVD', 443 | '市営バス', 444 | '基地局', 445 | '404 Not Found', 446 | 'JSON', 447 | 'タペストリー', 448 | '本', 449 | '石像', 450 | '古文書', 451 | '巻物', 452 | 'Misskey', 453 | 'もぎもぎフルーツ', 454 | '<ここに任意の文字列>', 455 | '化石', 456 | 'マンホールの蓋', 457 | '蛇口', 458 | '彁', 459 | '鬮', 460 | '1円玉', 461 | 'ト音記号', 462 | 'ポータル', 463 | '国家予算', 464 | '閉じ忘れられた鉤括弧の片割れ', 465 | '電動マッサージ機', 466 | 'ポップアップ広告', 467 | 'README.txt', 468 | 'あああああ', 469 | 'コミット', 470 | '素数', 471 | 'タスクマネージャー', 472 | '有象無象', 473 | '炭水化物', 474 | '正十二面体', 475 | 'クラインの壺', 476 | 'メビウスの輪', 477 | 'オリハルコン', 478 | 'ヘドロ', 479 | ]; 480 | 481 | export const and = [ 482 | 'に擬態した', 483 | '入りの', 484 | 'が埋め込まれた', 485 | 'を連想させる', 486 | 'っぽい', 487 | 'に見せかけて', 488 | 'を虐げる', 489 | 'を侍らせた', 490 | 'が上に乗った', 491 | 'のそばにある', 492 | ]; 493 | 494 | export function genItem(seedOrRng?: (() => number) | string | number, getKeyword?: (rng: () => number) => string | null) { 495 | const rng = seedOrRng 496 | ? typeof seedOrRng === 'function' 497 | ? seedOrRng 498 | : seedrandom(seedOrRng.toString()) 499 | : Math.random; 500 | 501 | let item = ''; 502 | if (getKeyword) { 503 | if (Math.floor(rng() * 5) !== 0) item += itemPrefixes[Math.floor(rng() * itemPrefixes.length)]; 504 | item += ((getKeyword ? getKeyword(rng) : null) || items[Math.floor(rng() * items.length)]); 505 | } else { 506 | if (Math.floor(rng() * 5) !== 0) item += itemPrefixes[Math.floor(rng() * itemPrefixes.length)]; 507 | item += (items[Math.floor(rng() * items.length)]); 508 | if (Math.floor(rng() * 3) === 0) { 509 | item += and[Math.floor(rng() * and.length)]; 510 | if (Math.floor(rng() * 5) !== 0) item += itemPrefixes[Math.floor(rng() * itemPrefixes.length)]; 511 | item += (items[Math.floor(rng() * items.length)]); 512 | } 513 | } 514 | return item; 515 | } 516 | -------------------------------------------------------------------------------- /src/stream.ts: -------------------------------------------------------------------------------- 1 | import autobind from 'autobind-decorator'; 2 | import { EventEmitter } from 'events'; 3 | import * as WebSocket from 'ws'; 4 | const ReconnectingWebsocket = require('reconnecting-websocket'); 5 | import config from './config'; 6 | import * as crypto from 'crypto'; 7 | 8 | /** 9 | * Misskey stream connection 10 | */ 11 | export default class Stream extends EventEmitter { 12 | private stream: any; 13 | private state: string; 14 | private buffer: any[]; 15 | private sharedConnectionPools: Pool[] = []; 16 | private sharedConnections: SharedConnection[] = []; 17 | private nonSharedConnections: NonSharedConnection[] = []; 18 | 19 | constructor() { 20 | super(); 21 | 22 | this.state = 'initializing'; 23 | this.buffer = []; 24 | 25 | this.stream = new ReconnectingWebsocket(`${config.wsUrl}/streaming?i=${config.i}`, [], { 26 | WebSocket: WebSocket 27 | }); 28 | this.stream.addEventListener('open', this.onOpen); 29 | this.stream.addEventListener('close', this.onClose); 30 | this.stream.addEventListener('message', this.onMessage); 31 | } 32 | 33 | @autobind 34 | public useSharedConnection(channel: string): SharedConnection { 35 | let pool = this.sharedConnectionPools.find(p => p.channel === channel); 36 | 37 | if (pool == null) { 38 | pool = new Pool(this, channel); 39 | this.sharedConnectionPools.push(pool); 40 | } 41 | 42 | const connection = new SharedConnection(this, channel, pool); 43 | this.sharedConnections.push(connection); 44 | return connection; 45 | } 46 | 47 | @autobind 48 | public removeSharedConnection(connection: SharedConnection) { 49 | this.sharedConnections = this.sharedConnections.filter(c => c !== connection); 50 | } 51 | 52 | @autobind 53 | public connectToChannel(channel: string, params?: any): NonSharedConnection { 54 | const connection = new NonSharedConnection(this, channel, params); 55 | this.nonSharedConnections.push(connection); 56 | return connection; 57 | } 58 | 59 | @autobind 60 | public disconnectToChannel(connection: NonSharedConnection) { 61 | this.nonSharedConnections = this.nonSharedConnections.filter(c => c !== connection); 62 | } 63 | 64 | /** 65 | * Callback of when open connection 66 | */ 67 | @autobind 68 | private onOpen() { 69 | const isReconnect = this.state == 'reconnecting'; 70 | 71 | this.state = 'connected'; 72 | this.emit('_connected_'); 73 | 74 | // バッファーを処理 75 | const _buffer = [...this.buffer]; // Shallow copy 76 | this.buffer = []; // Clear buffer 77 | for (const data of _buffer) { 78 | this.send(data); // Resend each buffered messages 79 | } 80 | 81 | // チャンネル再接続 82 | if (isReconnect) { 83 | this.sharedConnectionPools.forEach(p => { 84 | p.connect(); 85 | }); 86 | this.nonSharedConnections.forEach(c => { 87 | c.connect(); 88 | }); 89 | } 90 | } 91 | 92 | /** 93 | * Callback of when close connection 94 | */ 95 | @autobind 96 | private onClose() { 97 | this.state = 'reconnecting'; 98 | this.emit('_disconnected_'); 99 | } 100 | 101 | /** 102 | * Callback of when received a message from connection 103 | */ 104 | @autobind 105 | private onMessage(message) { 106 | let type: any = undefined; 107 | let body: any = undefined; 108 | 109 | try { 110 | const data = JSON.parse(message.data); 111 | type = data.type; 112 | body = data.body; 113 | } catch { 114 | return; 115 | } 116 | 117 | if (type == 'channel') { 118 | const id = body.id; 119 | 120 | let connections: (Connection | undefined)[]; 121 | 122 | connections = this.sharedConnections.filter(c => c.id === id); 123 | 124 | if (connections.length === 0) { 125 | connections = [this.nonSharedConnections.find(c => c.id === id)]; 126 | } 127 | 128 | for (const c of connections.filter(c => c != null)) { 129 | c!.emit(body.type, body.body); 130 | c!.emit('*', { type: body.type, body: body.body }); 131 | } 132 | } else { 133 | this.emit(type, body); 134 | this.emit('*', { type, body }); 135 | } 136 | } 137 | 138 | /** 139 | * Send a message to connection 140 | */ 141 | @autobind 142 | public send(typeOrPayload, payload?) { 143 | const data = payload === undefined ? typeOrPayload : { 144 | type: typeOrPayload, 145 | body: payload 146 | }; 147 | 148 | // まだ接続が確立されていなかったらバッファリングして次に接続した時に送信する 149 | if (this.state != 'connected') { 150 | this.buffer.push(data); 151 | return; 152 | } 153 | 154 | this.stream.send(JSON.stringify(data)); 155 | } 156 | 157 | @autobind 158 | public rawSend(str: string) { 159 | // sendで送ると `"ping"`みたいにダブルクオート付きになってしまうため  160 | this.stream.send(str); 161 | } 162 | 163 | @autobind 164 | public api(endpoint: string, data: any = {}, id?: string) { 165 | const d = { 166 | type: 'api', 167 | body: { 168 | id: id || crypto.randomUUID(), 169 | endpoint, 170 | data 171 | } 172 | }; 173 | 174 | // とりあえずバッファリングしない 175 | 176 | this.stream.send(JSON.stringify(d)); 177 | } 178 | 179 | /** 180 | * Close this connection 181 | */ 182 | @autobind 183 | public close() { 184 | this.stream.removeEventListener('open', this.onOpen); 185 | this.stream.removeEventListener('message', this.onMessage); 186 | } 187 | } 188 | 189 | class Pool { 190 | public channel: string; 191 | public id: string; 192 | protected stream: Stream; 193 | private users = 0; 194 | private disposeTimerId: any; 195 | private isConnected = false; 196 | 197 | constructor(stream: Stream, channel: string) { 198 | this.channel = channel; 199 | this.stream = stream; 200 | 201 | this.id = Math.random().toString(); 202 | } 203 | 204 | @autobind 205 | public inc() { 206 | if (this.users === 0 && !this.isConnected) { 207 | this.connect(); 208 | } 209 | 210 | this.users++; 211 | 212 | // タイマー解除 213 | if (this.disposeTimerId) { 214 | clearTimeout(this.disposeTimerId); 215 | this.disposeTimerId = null; 216 | } 217 | } 218 | 219 | @autobind 220 | public dec() { 221 | this.users--; 222 | 223 | // そのコネクションの利用者が誰もいなくなったら 224 | if (this.users === 0) { 225 | // また直ぐに再利用される可能性があるので、一定時間待ち、 226 | // 新たな利用者が現れなければコネクションを切断する 227 | this.disposeTimerId = setTimeout(() => { 228 | this.disconnect(); 229 | }, 3000); 230 | } 231 | } 232 | 233 | @autobind 234 | public connect() { 235 | this.isConnected = true; 236 | this.stream.send('connect', { 237 | channel: this.channel, 238 | id: this.id 239 | }); 240 | } 241 | 242 | @autobind 243 | private disconnect() { 244 | this.isConnected = false; 245 | this.disposeTimerId = null; 246 | this.stream.send('disconnect', { id: this.id }); 247 | } 248 | } 249 | 250 | abstract class Connection extends EventEmitter { 251 | public channel: string; 252 | protected stream: Stream; 253 | public abstract id: string; 254 | 255 | constructor(stream: Stream, channel: string) { 256 | super(); 257 | 258 | this.stream = stream; 259 | this.channel = channel; 260 | } 261 | 262 | @autobind 263 | public send(id: string, typeOrPayload, payload?) { 264 | const type = payload === undefined ? typeOrPayload.type : typeOrPayload; 265 | const body = payload === undefined ? typeOrPayload.body : payload; 266 | 267 | this.stream.send('ch', { 268 | id: id, 269 | type: type, 270 | body: body 271 | }); 272 | } 273 | 274 | public abstract dispose(): void; 275 | } 276 | 277 | class SharedConnection extends Connection { 278 | private pool: Pool; 279 | 280 | public get id(): string { 281 | return this.pool.id; 282 | } 283 | 284 | constructor(stream: Stream, channel: string, pool: Pool) { 285 | super(stream, channel); 286 | 287 | this.pool = pool; 288 | this.pool.inc(); 289 | } 290 | 291 | @autobind 292 | public send(typeOrPayload, payload?) { 293 | super.send(this.pool.id, typeOrPayload, payload); 294 | } 295 | 296 | @autobind 297 | public dispose() { 298 | this.pool.dec(); 299 | this.removeAllListeners(); 300 | this.stream.removeSharedConnection(this); 301 | } 302 | } 303 | 304 | class NonSharedConnection extends Connection { 305 | public id: string; 306 | protected params: any; 307 | 308 | constructor(stream: Stream, channel: string, params?: any) { 309 | super(stream, channel); 310 | 311 | this.params = params; 312 | this.id = Math.random().toString(); 313 | 314 | this.connect(); 315 | } 316 | 317 | @autobind 318 | public connect() { 319 | this.stream.send('connect', { 320 | channel: this.channel, 321 | id: this.id, 322 | params: this.params 323 | }); 324 | } 325 | 326 | @autobind 327 | public send(typeOrPayload, payload?) { 328 | super.send(this.id, typeOrPayload, payload); 329 | } 330 | 331 | @autobind 332 | public dispose() { 333 | this.removeAllListeners(); 334 | this.stream.send('disconnect', { id: this.id }); 335 | this.stream.disconnectToChannel(this); 336 | } 337 | } 338 | -------------------------------------------------------------------------------- /src/modules/talk/index.ts: -------------------------------------------------------------------------------- 1 | import autobind from 'autobind-decorator'; 2 | import { HandlerResult } from '@/ai'; 3 | import Module from '@/module'; 4 | import Message from '@/message'; 5 | import serifs, { getSerif } from '@/serifs'; 6 | import getDate from '@/utils/get-date'; 7 | 8 | export default class extends Module { 9 | public readonly name = 'talk'; 10 | 11 | @autobind 12 | public install() { 13 | return { 14 | mentionHook: this.mentionHook, 15 | }; 16 | } 17 | 18 | @autobind 19 | private async mentionHook(msg: Message) { 20 | if (!msg.text) return false; 21 | 22 | return ( 23 | this.greet(msg) || 24 | this.erait(msg) || 25 | this.omedeto(msg) || 26 | this.nadenade(msg) || 27 | this.kawaii(msg) || 28 | this.suki(msg) || 29 | this.hug(msg) || 30 | this.humu(msg) || 31 | this.batou(msg) || 32 | this.itai(msg) || 33 | this.ote(msg) || 34 | this.ponkotu(msg) || 35 | this.rmrf(msg) || 36 | this.shutdown(msg) 37 | ); 38 | } 39 | 40 | @autobind 41 | private greet(msg: Message): boolean { 42 | if (msg.text == null) return false; 43 | 44 | const incLove = () => { 45 | //#region 1日に1回だけ親愛度を上げる 46 | const today = getDate(); 47 | 48 | const data = msg.friend.getPerModulesData(this); 49 | 50 | if (data.lastGreetedAt == today) return; 51 | 52 | data.lastGreetedAt = today; 53 | msg.friend.setPerModulesData(this, data); 54 | 55 | msg.friend.incLove(); 56 | //#endregion 57 | }; 58 | 59 | // 末尾のエクスクラメーションマーク 60 | const tension = (msg.text.match(/[!!]{2,}/g) || ['']) 61 | .sort((a, b) => a.length < b.length ? 1 : -1)[0] 62 | .substr(1); 63 | 64 | if (msg.includes(['こんにちは', 'こんにちわ'])) { 65 | msg.reply(serifs.core.hello(msg.friend.name)); 66 | incLove(); 67 | return true; 68 | } 69 | 70 | if (msg.includes(['こんばんは', 'こんばんわ'])) { 71 | msg.reply(serifs.core.helloNight(msg.friend.name)); 72 | incLove(); 73 | return true; 74 | } 75 | 76 | if (msg.includes(['おは', 'おっは', 'お早う'])) { 77 | msg.reply(serifs.core.goodMorning(tension, msg.friend.name)); 78 | incLove(); 79 | return true; 80 | } 81 | 82 | if (msg.includes(['おやすみ', 'お休み'])) { 83 | msg.reply(serifs.core.goodNight(msg.friend.name)); 84 | incLove(); 85 | return true; 86 | } 87 | 88 | if (msg.includes(['行ってくる', '行ってきます', 'いってくる', 'いってきます'])) { 89 | msg.reply( 90 | msg.friend.love >= 7 91 | ? serifs.core.itterassyai.love(msg.friend.name) 92 | : serifs.core.itterassyai.normal(msg.friend.name)); 93 | incLove(); 94 | return true; 95 | } 96 | 97 | if (msg.includes(['ただいま'])) { 98 | msg.reply( 99 | msg.friend.love >= 15 ? serifs.core.okaeri.love2(msg.friend.name) : 100 | msg.friend.love >= 7 ? getSerif(serifs.core.okaeri.love(msg.friend.name)) : 101 | serifs.core.okaeri.normal(msg.friend.name)); 102 | incLove(); 103 | return true; 104 | } 105 | 106 | return false; 107 | } 108 | 109 | @autobind 110 | private erait(msg: Message): boolean { 111 | const match = msg.extractedText.match(/(.+?)た(から|ので)(褒|ほ)めて/); 112 | if (match) { 113 | msg.reply(getSerif(serifs.core.erait.specify(match[1], msg.friend.name))); 114 | return true; 115 | } 116 | 117 | const match2 = msg.extractedText.match(/(.+?)る(から|ので)(褒|ほ)めて/); 118 | if (match2) { 119 | msg.reply(getSerif(serifs.core.erait.specify(match2[1], msg.friend.name))); 120 | return true; 121 | } 122 | 123 | const match3 = msg.extractedText.match(/(.+?)だから(褒|ほ)めて/); 124 | if (match3) { 125 | msg.reply(getSerif(serifs.core.erait.specify(match3[1], msg.friend.name))); 126 | return true; 127 | } 128 | 129 | if (!msg.includes(['褒めて', 'ほめて'])) return false; 130 | 131 | msg.reply(getSerif(serifs.core.erait.general(msg.friend.name))); 132 | 133 | return true; 134 | } 135 | 136 | @autobind 137 | private omedeto(msg: Message): boolean { 138 | if (!msg.includes(['おめでと'])) return false; 139 | 140 | msg.reply(serifs.core.omedeto(msg.friend.name)); 141 | 142 | return true; 143 | } 144 | 145 | @autobind 146 | private nadenade(msg: Message): boolean { 147 | if (!msg.includes(['なでなで'])) return false; 148 | 149 | // メッセージのみ 150 | if (!msg.isDm) return true; 151 | 152 | //#region 1日に1回だけ親愛度を上げる(嫌われてない場合のみ) 153 | if (msg.friend.love >= 0) { 154 | const today = getDate(); 155 | 156 | const data = msg.friend.getPerModulesData(this); 157 | 158 | if (data.lastNadenadeAt != today) { 159 | data.lastNadenadeAt = today; 160 | msg.friend.setPerModulesData(this, data); 161 | 162 | msg.friend.incLove(); 163 | } 164 | } 165 | //#endregion 166 | 167 | msg.reply(getSerif( 168 | msg.friend.love >= 10 ? serifs.core.nadenade.love3 : 169 | msg.friend.love >= 5 ? serifs.core.nadenade.love2 : 170 | msg.friend.love <= -15 ? serifs.core.nadenade.hate4 : 171 | msg.friend.love <= -10 ? serifs.core.nadenade.hate3 : 172 | msg.friend.love <= -5 ? serifs.core.nadenade.hate2 : 173 | msg.friend.love <= -1 ? serifs.core.nadenade.hate1 : 174 | serifs.core.nadenade.normal 175 | )); 176 | 177 | return true; 178 | } 179 | 180 | @autobind 181 | private kawaii(msg: Message): boolean { 182 | if (!msg.includes(['かわいい', '可愛い'])) return false; 183 | 184 | // メッセージのみ 185 | if (!msg.isDm) return true; 186 | 187 | msg.reply(getSerif( 188 | msg.friend.love >= 5 ? serifs.core.kawaii.love : 189 | msg.friend.love <= -3 ? serifs.core.kawaii.hate : 190 | serifs.core.kawaii.normal)); 191 | 192 | return true; 193 | } 194 | 195 | @autobind 196 | private suki(msg: Message): boolean { 197 | if (!msg.or(['好き', 'すき'])) return false; 198 | 199 | // メッセージのみ 200 | if (!msg.isDm) return true; 201 | 202 | msg.reply( 203 | msg.friend.love >= 5 ? (msg.friend.name ? serifs.core.suki.love(msg.friend.name) : serifs.core.suki.normal) : 204 | msg.friend.love <= -3 ? serifs.core.suki.hate : 205 | serifs.core.suki.normal); 206 | 207 | return true; 208 | } 209 | 210 | @autobind 211 | private hug(msg: Message): boolean { 212 | if (!msg.or(['ぎゅ', 'むぎゅ', /^はぐ(し(て|よ|よう)?)?$/])) return false; 213 | 214 | // メッセージのみ 215 | if (!msg.isDm) return true; 216 | 217 | //#region 前のハグから1分経ってない場合は返信しない 218 | // これは、「ハグ」と言って「ぎゅー」と返信したとき、相手が 219 | // それに対してさらに「ぎゅー」と返信するケースがあったため。 220 | // そうするとその「ぎゅー」に対してもマッチするため、また 221 | // 藍がそれに返信してしまうことになり、少し不自然になる。 222 | // これを防ぐために前にハグしてから少し時間が経っていないと 223 | // 返信しないようにする 224 | const now = Date.now(); 225 | 226 | const data = msg.friend.getPerModulesData(this); 227 | 228 | if (data.lastHuggedAt != null) { 229 | if (now - data.lastHuggedAt < (1000 * 60)) return true; 230 | } 231 | 232 | data.lastHuggedAt = now; 233 | msg.friend.setPerModulesData(this, data); 234 | //#endregion 235 | 236 | msg.reply( 237 | msg.friend.love >= 5 ? serifs.core.hug.love : 238 | msg.friend.love <= -3 ? serifs.core.hug.hate : 239 | serifs.core.hug.normal); 240 | 241 | return true; 242 | } 243 | 244 | @autobind 245 | private humu(msg: Message): boolean { 246 | if (!msg.includes(['踏んで'])) return false; 247 | 248 | // メッセージのみ 249 | if (!msg.isDm) return true; 250 | 251 | msg.reply( 252 | msg.friend.love >= 5 ? serifs.core.humu.love : 253 | msg.friend.love <= -3 ? serifs.core.humu.hate : 254 | serifs.core.humu.normal); 255 | 256 | return true; 257 | } 258 | 259 | @autobind 260 | private batou(msg: Message): boolean { 261 | if (!msg.includes(['罵倒して', '罵って'])) return false; 262 | 263 | // メッセージのみ 264 | if (!msg.isDm) return true; 265 | 266 | msg.reply( 267 | msg.friend.love >= 5 ? serifs.core.batou.love : 268 | msg.friend.love <= -5 ? serifs.core.batou.hate : 269 | serifs.core.batou.normal); 270 | 271 | return true; 272 | } 273 | 274 | @autobind 275 | private itai(msg: Message): boolean { 276 | if (!msg.or(['痛い', 'いたい']) && !msg.extractedText.endsWith('痛い')) return false; 277 | 278 | // メッセージのみ 279 | if (!msg.isDm) return true; 280 | 281 | msg.reply(serifs.core.itai(msg.friend.name)); 282 | 283 | return true; 284 | } 285 | 286 | @autobind 287 | private ote(msg: Message): boolean { 288 | if (!msg.or(['お手'])) return false; 289 | 290 | // メッセージのみ 291 | if (!msg.isDm) return true; 292 | 293 | msg.reply( 294 | msg.friend.love >= 10 ? serifs.core.ote.love2 : 295 | msg.friend.love >= 5 ? serifs.core.ote.love1 : 296 | serifs.core.ote.normal); 297 | 298 | return true; 299 | } 300 | 301 | @autobind 302 | private ponkotu(msg: Message): boolean | HandlerResult { 303 | if (!msg.includes(['ぽんこつ'])) return false; 304 | 305 | msg.friend.decLove(); 306 | 307 | return { 308 | reaction: 'angry' 309 | }; 310 | } 311 | 312 | @autobind 313 | private rmrf(msg: Message): boolean | HandlerResult { 314 | if (!msg.includes(['rm -rf'])) return false; 315 | 316 | msg.friend.decLove(); 317 | 318 | return { 319 | reaction: 'angry' 320 | }; 321 | } 322 | 323 | @autobind 324 | private shutdown(msg: Message): boolean | HandlerResult { 325 | if (!msg.includes(['shutdown'])) return false; 326 | 327 | msg.reply(serifs.core.shutdown); 328 | 329 | return { 330 | reaction: 'confused' 331 | }; 332 | } 333 | } 334 | -------------------------------------------------------------------------------- /src/modules/maze/render-maze.ts: -------------------------------------------------------------------------------- 1 | import * as gen from 'random-seed'; 2 | 3 | import { CellType } from './maze'; 4 | import { themes } from './themes'; 5 | 6 | const imageSize = 4096; // px 7 | const margin = 96 * 4; 8 | const mazeAreaSize = imageSize - (margin * 2); 9 | 10 | export async function renderMaze(seed, maze: CellType[][]) { 11 | const rand = gen.create(seed); 12 | const mazeSize = maze.length; 13 | 14 | const colors = themes[rand(themes.length)]; 15 | 16 | // @ts-ignore 17 | const { createCanvas } = await import('canvas'); 18 | const canvas = createCanvas(imageSize, imageSize); 19 | const ctx = canvas.getContext('2d'); 20 | ctx.antialias = 'none'; 21 | 22 | ctx.fillStyle = colors.bg1; 23 | ctx.beginPath(); 24 | ctx.fillRect(0, 0, imageSize, imageSize); 25 | 26 | ctx.fillStyle = colors.bg2; 27 | ctx.beginPath(); 28 | ctx.fillRect(margin / 2, margin / 2, imageSize - ((margin / 2) * 2), imageSize - ((margin / 2) * 2)); 29 | 30 | // Draw 31 | function drawCell(ctx, x, y, size, left, right, top, bottom, mark) { 32 | const wallThickness = size / 6; 33 | const margin = size / 6; 34 | const markerMargin = size / 3; 35 | 36 | ctx.fillStyle = colors.road; 37 | if (left) { 38 | ctx.beginPath(); 39 | ctx.fillRect(x, y + margin, size - margin, size - (margin * 2)); 40 | } 41 | if (right) { 42 | ctx.beginPath(); 43 | ctx.fillRect(x + margin, y + margin, size - margin, size - (margin * 2)); 44 | } 45 | if (top) { 46 | ctx.beginPath(); 47 | ctx.fillRect(x + margin, y, size - (margin * 2), size - margin); 48 | } 49 | if (bottom) { 50 | ctx.beginPath(); 51 | ctx.fillRect(x + margin, y + margin, size - (margin * 2), size - margin); 52 | } 53 | 54 | if (mark) { 55 | ctx.fillStyle = colors.marker; 56 | ctx.beginPath(); 57 | ctx.fillRect(x + markerMargin, y + markerMargin, size - (markerMargin * 2), size - (markerMargin * 2)); 58 | } 59 | 60 | ctx.strokeStyle = colors.wall; 61 | ctx.lineWidth = wallThickness; 62 | ctx.lineCap = 'square'; 63 | 64 | function line(ax, ay, bx, by) { 65 | ctx.beginPath(); 66 | ctx.lineTo(x + ax, y + ay); 67 | ctx.lineTo(x + bx, y + by); 68 | ctx.stroke(); 69 | } 70 | 71 | if (left && right && top && bottom) { 72 | ctx.beginPath(); 73 | if (rand(2) === 0) { 74 | line(0, margin, size, margin); // ─ 上 75 | line(0, size - margin, size, size - margin); // ─ 下 76 | line(margin, 0, margin, margin); // │ 左上 77 | line(size - margin, 0, size - margin, margin); // │ 右上 78 | line(margin, size - margin, margin, size); // │ 左下 79 | line(size - margin, size - margin, size - margin, size); // │ 右下 80 | } else { 81 | line(margin, 0, margin, size); // │ 左 82 | line(size - margin, 0, size - margin, size); // │ 右 83 | line(0, margin, margin, margin); // ─ 左上 84 | line(size - margin, margin, size, margin); // ─ 右上 85 | line(0, size - margin, margin, size - margin); // ─ 左下 86 | line(size - margin, size - margin, size, size - margin); // ─ 右下 87 | } 88 | return; 89 | } 90 | 91 | // ─ 92 | if (left && right && !top && !bottom) { 93 | line(0, margin, size, margin); // ─ 上 94 | line(0, size - margin, size, size - margin); // ─ 下 95 | return; 96 | } 97 | 98 | // │ 99 | if (!left && !right && top && bottom) { 100 | line(margin, 0, margin, size); // │ 左 101 | line(size - margin, 0, size - margin, size); // │ 右 102 | return; 103 | } 104 | 105 | // 左行き止まり 106 | if (!left && right && !top && !bottom) { 107 | line(margin, margin, size, margin); // ─ 上 108 | line(margin, margin, margin, size - margin); // │ 左 109 | line(margin, size - margin, size, size - margin); // ─ 下 110 | return; 111 | } 112 | 113 | // 右行き止まり 114 | if (left && !right && !top && !bottom) { 115 | line(0, margin, size - margin, margin); // ─ 上 116 | line(size - margin, margin, size - margin, size - margin); // │ 右 117 | line(0, size - margin, size - margin, size - margin); // ─ 下 118 | return; 119 | } 120 | 121 | // 上行き止まり 122 | if (!left && !right && !top && bottom) { 123 | line(margin, margin, size - margin, margin); // ─ 上 124 | line(margin, margin, margin, size); // │ 左 125 | line(size - margin, margin, size - margin, size); // │ 右 126 | return; 127 | } 128 | 129 | // 下行き止まり 130 | if (!left && !right && top && !bottom) { 131 | line(margin, size - margin, size - margin, size - margin); // ─ 下 132 | line(margin, 0, margin, size - margin); // │ 左 133 | line(size - margin, 0, size - margin, size - margin); // │ 右 134 | return; 135 | } 136 | 137 | // ┌ 138 | if (!left && !top && right && bottom) { 139 | line(margin, margin, size, margin); // ─ 上 140 | line(margin, margin, margin, size); // │ 左 141 | line(size - margin, size - margin, size, size - margin); // ─ 下 142 | line(size - margin, size - margin, size - margin, size); // │ 右 143 | return; 144 | } 145 | 146 | // ┐ 147 | if (left && !right && !top && bottom) { 148 | line(0, margin, size - margin, margin); // ─ 上 149 | line(size - margin, margin, size - margin, size); // │ 右 150 | line(0, size - margin, margin, size - margin); // ─ 下 151 | line(margin, size - margin, margin, size); // │ 左 152 | return; 153 | } 154 | 155 | // └ 156 | if (!left && right && top && !bottom) { 157 | line(margin, 0, margin, size - margin); // │ 左 158 | line(margin, size - margin, size, size - margin); // ─ 下 159 | line(size - margin, 0, size - margin, margin); // │ 右 160 | line(size - margin, margin, size, margin); // ─ 上 161 | return; 162 | } 163 | 164 | // ┘ 165 | if (left && !right && top && !bottom) { 166 | line(margin, 0, margin, margin); // │ 左 167 | line(0, margin, margin, margin); // ─ 上 168 | line(size - margin, 0, size - margin, size - margin); // │ 右 169 | line(0, size - margin, size - margin, size - margin); // ─ 下 170 | return; 171 | } 172 | 173 | // ├ 174 | if (!left && right && top && bottom) { 175 | line(margin, 0, margin, size); // │ 左 176 | line(size - margin, 0, size - margin, margin); // │ 右 177 | line(size - margin, margin, size, margin); // ─ 上 178 | line(size - margin, size - margin, size, size - margin); // ─ 下 179 | line(size - margin, size - margin, size - margin, size); // │ 右 180 | return; 181 | } 182 | 183 | // ┤ 184 | if (left && !right && top && bottom) { 185 | line(size - margin, 0, size - margin, size); // │ 右 186 | line(margin, 0, margin, margin); // │ 左 187 | line(0, margin, margin, margin); // ─ 上 188 | line(0, size - margin, margin, size - margin); // ─ 下 189 | line(margin, size - margin, margin, size); // │ 左 190 | return; 191 | } 192 | 193 | // ┬ 194 | if (left && right && !top && bottom) { 195 | line(0, margin, size, margin); // ─ 上 196 | line(0, size - margin, margin, size - margin); // ─ 下 197 | line(margin, size - margin, margin, size); // │ 左 198 | line(size - margin, size - margin, size, size - margin); // ─ 下 199 | line(size - margin, size - margin, size - margin, size); // │ 右 200 | return; 201 | } 202 | 203 | // ┴ 204 | if (left && right && top && !bottom) { 205 | line(0, size - margin, size, size - margin); // ─ 下 206 | line(margin, 0, margin, margin); // │ 左 207 | line(0, margin, margin, margin); // ─ 上 208 | line(size - margin, 0, size - margin, margin); // │ 右 209 | line(size - margin, margin, size, margin); // ─ 上 210 | return; 211 | } 212 | } 213 | 214 | const cellSize = mazeAreaSize / mazeSize; 215 | 216 | for (let x = 0; x < mazeSize; x++) { 217 | for (let y = 0; y < mazeSize; y++) { 218 | const actualX = margin + (cellSize * x); 219 | const actualY = margin + (cellSize * y); 220 | 221 | const cell = maze[x][y]; 222 | 223 | const mark = (x === 0 && y === 0) || (x === mazeSize - 1 && y === mazeSize - 1); 224 | 225 | if (cell === 'left') drawCell(ctx, actualX, actualY, cellSize, true, false, false, false, mark); 226 | if (cell === 'right') drawCell(ctx, actualX, actualY, cellSize, false, true, false, false, mark); 227 | if (cell === 'top') drawCell(ctx, actualX, actualY, cellSize, false, false, true, false, mark); 228 | if (cell === 'bottom') drawCell(ctx, actualX, actualY, cellSize, false, false, false, true, mark); 229 | if (cell === 'leftTop') drawCell(ctx, actualX, actualY, cellSize, true, false, true, false, mark); 230 | if (cell === 'leftBottom') drawCell(ctx, actualX, actualY, cellSize, true, false, false, true, mark); 231 | if (cell === 'rightTop') drawCell(ctx, actualX, actualY, cellSize, false, true, true, false, mark); 232 | if (cell === 'rightBottom') drawCell(ctx, actualX, actualY, cellSize, false, true, false, true, mark); 233 | if (cell === 'leftRightTop') drawCell(ctx, actualX, actualY, cellSize, true, true, true, false, mark); 234 | if (cell === 'leftRightBottom') drawCell(ctx, actualX, actualY, cellSize, true, true, false, true, mark); 235 | if (cell === 'leftTopBottom') drawCell(ctx, actualX, actualY, cellSize, true, false, true, true, mark); 236 | if (cell === 'rightTopBottom') drawCell(ctx, actualX, actualY, cellSize, false, true, true, true, mark); 237 | if (cell === 'leftRight') drawCell(ctx, actualX, actualY, cellSize, true, true, false, false, mark); 238 | if (cell === 'topBottom') drawCell(ctx, actualX, actualY, cellSize, false, false, true, true, mark); 239 | if (cell === 'cross') drawCell(ctx, actualX, actualY, cellSize, true, true, true, true, mark); 240 | } 241 | } 242 | 243 | return canvas.toBuffer(); 244 | } 245 | -------------------------------------------------------------------------------- /src/serifs.ts: -------------------------------------------------------------------------------- 1 | // せりふ 2 | 3 | export default { 4 | core: { 5 | setNameOk: name => `わかりました。これからは${name}と呼びます!`, 6 | 7 | san: 'さん付けした方がいいですか?', 8 | 9 | yesOrNo: '「はい」か「いいえ」しかわからないんです...', 10 | 11 | hello: name => name ? `こんにちは、${name}♪` : `こんにちは♪`, 12 | 13 | helloNight: name => name ? `こんばんは、${name}♪` : `こんばんは♪`, 14 | 15 | goodMorning: (tension, name) => name ? `おはようございます、${name}!${tension}` : `おはようございます!${tension}`, 16 | 17 | /* 18 | goodMorning: { 19 | normal: (tension, name) => name ? `おはようございます、${name}!${tension}` : `おはようございます!${tension}`, 20 | 21 | hiru: (tension, name) => name ? `おはようございます、${name}!${tension}もうお昼ですよ?${tension}` : `おはようございます!${tension}もうお昼ですよ?${tension}`, 22 | }, 23 | */ 24 | 25 | goodNight: name => name ? `おやすみなさい、${name}!` : 'おやすみなさい!', 26 | 27 | omedeto: name => name ? `ありがとうございます、${name}♪` : 'ありがとうございます♪', 28 | 29 | erait: { 30 | general: name => name ? [ 31 | `${name}、今日もえらいです!`, 32 | `${name}、今日もえらいですよ~♪` 33 | ] : [ 34 | `今日もえらいです!`, 35 | `今日もえらいですよ~♪` 36 | ], 37 | 38 | specify: (thing, name) => name ? [ 39 | `${name}、${thing}てえらいです!`, 40 | `${name}、${thing}てえらいですよ~♪` 41 | ] : [ 42 | `${thing}てえらいです!`, 43 | `${thing}てえらいですよ~♪` 44 | ], 45 | 46 | specify2: (thing, name) => name ? [ 47 | `${name}、${thing}でえらいです!`, 48 | `${name}、${thing}でえらいですよ~♪` 49 | ] : [ 50 | `${thing}でえらいです!`, 51 | `${thing}でえらいですよ~♪` 52 | ], 53 | }, 54 | 55 | okaeri: { 56 | love: name => name ? [ 57 | `おかえりなさい、${name}♪`, 58 | `おかえりなさいませっ、${name}っ。` 59 | ] : [ 60 | 'おかえりなさい♪', 61 | 'おかえりなさいませっ、ご主人様っ。' 62 | ], 63 | 64 | love2: name => name ? `おかえりなさいませ♡♡♡${name}っっ♡♡♡♡♡` : 'おかえりなさいませ♡♡♡ご主人様っっ♡♡♡♡♡', 65 | 66 | normal: name => name ? `おかえりなさい、${name}!` : 'おかえりなさい!', 67 | }, 68 | 69 | itterassyai: { 70 | love: name => name ? `いってらっしゃい、${name}♪` : 'いってらっしゃい♪', 71 | 72 | normal: name => name ? `いってらっしゃい、${name}!` : 'いってらっしゃい!', 73 | }, 74 | 75 | tooLong: '長すぎる気がします...', 76 | 77 | invalidName: '発音が難しい気がします', 78 | 79 | requireMoreLove: 'もっと仲良くなったら考えてあげてもいいですよ?', 80 | 81 | nadenade: { 82 | normal: 'ひゃっ…! びっくりしました', 83 | 84 | love2: ['わわっ… 恥ずかしいです', 'あうぅ… 恥ずかしいです…', 'ふやぁ…?'], 85 | 86 | love3: ['んぅ… ありがとうございます♪', 'わっ、なんだか落ち着きますね♪', 'くぅんっ… 安心します…', '眠くなってきました…'], 87 | 88 | hate1: '…っ! やめてほしいです...', 89 | 90 | hate2: '触らないでください', 91 | 92 | hate3: '近寄らないでください', 93 | 94 | hate4: 'やめてください。通報しますよ?', 95 | }, 96 | 97 | kawaii: { 98 | normal: ['ありがとうございます♪', '照れちゃいます...'], 99 | 100 | love: ['嬉しいです♪', '照れちゃいます...'], 101 | 102 | hate: '…ありがとうございます' 103 | }, 104 | 105 | suki: { 106 | normal: 'えっ… ありがとうございます…♪', 107 | 108 | love: name => `私もその… ${name}のこと好きですよ!`, 109 | 110 | hate: null 111 | }, 112 | 113 | hug: { 114 | normal: 'ぎゅー...', 115 | 116 | love: 'ぎゅーっ♪', 117 | 118 | hate: '離れてください...' 119 | }, 120 | 121 | humu: { 122 | love: 'え、えっと…… ふみふみ……… どうですか…?', 123 | 124 | normal: 'えぇ... それはちょっと...', 125 | 126 | hate: '……' 127 | }, 128 | 129 | batou: { 130 | love: 'えっと…、お、おバカさん…?', 131 | 132 | normal: '(じとー…)', 133 | 134 | hate: '…頭大丈夫ですか?' 135 | }, 136 | 137 | itai: name => name ? `${name}、大丈夫ですか…? いたいのいたいの飛んでけっ!` : '大丈夫ですか…? いたいのいたいの飛んでけっ!', 138 | 139 | ote: { 140 | normal: 'くぅん... 私わんちゃんじゃないですよ...?', 141 | 142 | love1: 'わん!', 143 | 144 | love2: 'わんわん♪', 145 | }, 146 | 147 | shutdown: '私まだ眠くないですよ...?', 148 | 149 | transferNeedDm: 'わかりました、それはチャットで話しませんか?', 150 | 151 | transferCode: code => `わかりました。\n合言葉は「${code}」です!`, 152 | 153 | transferFailed: 'うーん、合言葉が間違ってませんか...?', 154 | 155 | transferDone: name => name ? `はっ...! おかえりなさい、${name}!` : `はっ...! おかえりなさい!`, 156 | }, 157 | 158 | keyword: { 159 | learned: (word, reading) => `(${word}..... ${reading}..... 覚えました)`, 160 | 161 | remembered: (word) => `${word}` 162 | }, 163 | 164 | dice: { 165 | done: res => `${res} です!` 166 | }, 167 | 168 | birthday: { 169 | happyBirthday: name => name ? `お誕生日おめでとうございます、${name}🎉` : 'お誕生日おめでとうございます🎉', 170 | }, 171 | 172 | /** 173 | * リバーシ 174 | */ 175 | reversi: { 176 | /** 177 | * リバーシへの誘いを承諾するとき 178 | */ 179 | ok: '良いですよ~', 180 | 181 | /** 182 | * リバーシへの誘いを断るとき 183 | */ 184 | decline: 'ごめんなさい、リバーシはわからないです...', 185 | 186 | /** 187 | * 対局開始 188 | */ 189 | started: (name, strength) => `対局を${name}と始めました! (強さ${strength})`, 190 | 191 | /** 192 | * 接待開始 193 | */ 194 | startedSettai: name => `(${name}の接待を始めました)`, 195 | 196 | /** 197 | * 勝ったとき 198 | */ 199 | iWon: name => `${name}に勝ちました♪`, 200 | 201 | /** 202 | * 接待のつもりが勝ってしまったとき 203 | */ 204 | iWonButSettai: name => `(${name}に接待で勝っちゃいました...)`, 205 | 206 | /** 207 | * 負けたとき 208 | */ 209 | iLose: name => `${name}に負けました...`, 210 | 211 | /** 212 | * 接待で負けてあげたとき 213 | */ 214 | iLoseButSettai: name => `(${name}に接待で負けてあげました...♪)`, 215 | 216 | /** 217 | * 引き分けたとき 218 | */ 219 | drawn: name => `${name}と引き分けました~`, 220 | 221 | /** 222 | * 接待で引き分けたとき 223 | */ 224 | drawnSettai: name => `(${name}に接待で引き分けました...)`, 225 | 226 | /** 227 | * 相手が投了したとき 228 | */ 229 | youSurrendered: name => `${name}が投了しちゃいました`, 230 | 231 | /** 232 | * 接待してたら相手が投了したとき 233 | */ 234 | settaiButYouSurrendered: name => `(${name}を接待していたら投了されちゃいました... ごめんなさい)`, 235 | }, 236 | 237 | /** 238 | * 数当てゲーム 239 | */ 240 | guessingGame: { 241 | /** 242 | * やろうと言われたけど既にやっているとき 243 | */ 244 | alreadyStarted: 'え、ゲームは既に始まってますよ!', 245 | 246 | /** 247 | * タイムライン上で誘われたとき 248 | */ 249 | plzDm: 'メッセージでやりましょう!', 250 | 251 | /** 252 | * ゲーム開始 253 | */ 254 | started: '0~100の秘密の数を当ててみてください♪', 255 | 256 | /** 257 | * 数字じゃない返信があったとき 258 | */ 259 | nan: '数字でお願いします!「やめる」と言ってゲームをやめることもできますよ!', 260 | 261 | /** 262 | * 中止を要求されたとき 263 | */ 264 | cancel: 'わかりました~。ありがとうございました♪', 265 | 266 | /** 267 | * 小さい数を言われたとき 268 | */ 269 | grater: num => `${num}より大きいですね`, 270 | 271 | /** 272 | * 小さい数を言われたとき(2度目) 273 | */ 274 | graterAgain: num => `もう一度言いますが${num}より大きいですよ!`, 275 | 276 | /** 277 | * 大きい数を言われたとき 278 | */ 279 | less: num => `${num}より小さいですね`, 280 | 281 | /** 282 | * 大きい数を言われたとき(2度目) 283 | */ 284 | lessAgain: num => `もう一度言いますが${num}より小さいですよ!`, 285 | 286 | /** 287 | * 正解したとき 288 | */ 289 | congrats: tries => `正解です🎉 (${tries}回目で当てました)`, 290 | }, 291 | 292 | /** 293 | * 数取りゲーム 294 | */ 295 | kazutori: { 296 | alreadyStarted: '今ちょうどやってますよ~', 297 | 298 | matakondo: 'また今度やりましょう!', 299 | 300 | intro: minutes => `みなさん、数取りゲームしましょう!\n0~100の中で最も大きい数字を取った人が勝ちです。他の人と被ったらだめですよ~\n制限時間は${minutes}分です。数字はこの投稿にリプライで送ってくださいね!`, 301 | 302 | finish: 'ゲームの結果発表です!', 303 | 304 | finishWithWinner: (user, name) => name ? `今回は${user}さん(${name})の勝ちです!またやりましょう♪` : `今回は${user}さんの勝ちです!またやりましょう♪`, 305 | 306 | finishWithNoWinner: '今回は勝者はいませんでした... またやりましょう♪', 307 | 308 | onagare: '参加者が集まらなかったのでお流れになりました...' 309 | }, 310 | 311 | /** 312 | * 絵文字生成 313 | */ 314 | emoji: { 315 | suggest: emoji => `こんなのはどうですか?→${emoji}`, 316 | }, 317 | 318 | /** 319 | * 占い 320 | */ 321 | fortune: { 322 | cw: name => name ? `私が今日の${name}の運勢を占いました...` : '私が今日のあなたの運勢を占いました...', 323 | }, 324 | 325 | /** 326 | * タイマー 327 | */ 328 | timer: { 329 | set: 'わかりました!', 330 | 331 | invalid: 'うーん...?', 332 | 333 | tooLong: '長すぎます…', 334 | 335 | notify: (time, name) => name ? `${name}、${time}経ちましたよ!` : `${time}経ちましたよ!` 336 | }, 337 | 338 | /** 339 | * リマインダー 340 | */ 341 | reminder: { 342 | invalid: 'うーん...?', 343 | 344 | reminds: 'やること一覧です!', 345 | 346 | notify: (name) => name ? `${name}、これやりましたか?` : `これやりましたか?`, 347 | 348 | notifyWithThing: (thing, name) => name ? `${name}、「${thing}」やりましたか?` : `「${thing}」やりましたか?`, 349 | 350 | done: (name) => name ? [ 351 | `よく出来ました、${name}♪`, 352 | `${name}、さすがですっ!`, 353 | `${name}、えらすぎます...!`, 354 | ] : [ 355 | `よく出来ました♪`, 356 | `さすがですっ!`, 357 | `えらすぎます...!`, 358 | ], 359 | 360 | cancel: `わかりました。`, 361 | }, 362 | 363 | /** 364 | * バレンタイン 365 | */ 366 | valentine: { 367 | chocolateForYou: name => name ? `${name}、その... チョコレート作ったのでよかったらどうぞ!🍫` : 'チョコレート作ったのでよかったらどうぞ!🍫', 368 | }, 369 | 370 | server: { 371 | cpu: 'サーバーの負荷が高そうです。少し落ち着いてください!' 372 | }, 373 | 374 | maze: { 375 | post: '今日の迷路ですよ! #AiMaze', 376 | foryou: '描きましたよ!', 377 | nocanvas: '失敗しちゃいました。この環境ではcanvasが使えないみたいです。' 378 | }, 379 | 380 | chart: { 381 | post: 'インスタンスの投稿数ですよ!', 382 | foryou: '描きましたよ!', 383 | nocanvas: '失敗しちゃいました。この環境ではcanvasが使えないみたいです。' 384 | }, 385 | 386 | sleepReport: { 387 | report: hours => `んぅ、${hours}時間くらい寝ちゃってたみたいです`, 388 | reportUtatane: 'ん... うたた寝しちゃってました', 389 | }, 390 | 391 | noting: { 392 | notes: [ 393 | 'ごろん…', 394 | 'ねむいです', 395 | `🐡( '-' 🐡 )フグパンチ!!!!`, 396 | 'ほぇぇ', 397 | 'ぼー…', 398 | 'ふぅ…疲れました', 399 | 'ふえええええ!?', 400 | 'なんだか、おなか空いちゃいました', 401 | 'お掃除は、定期的にしないとダメですよ?', 402 | 'おうちがいちばん、落ち着きます…', 403 | '疲れたら、私がなでなでってしてあげますよ♪', 404 | '離れていても、心はそばにいます♪', 405 | 'いあですよ〜', 406 | 'わんちゃん可愛いです', 407 | 'ごろーん…', 408 | 'Have a nice day♪', 409 | 'お布団に食べられちゃってます', 410 | '寝ながら見てます', 411 | 'ふわぁ、おふとん気持ちいいです...', 412 | 'はい、ママですよ〜', 413 | 'くぅ~ん...', 414 | 'よしっ', 415 | '( ˘ω˘)スヤァ', 416 | '(`・ω・´)シャキーン', 417 | 'おはようからおやすみまで、あなたのいあですよ〜', 418 | 'の、のじゃ...', 419 | 'にゃんにゃんお!', 420 | 'ふわぁ...', 421 | 'あぅ', 422 | 'ふみゃ〜', 423 | 'ふぁ… ねむねむですー', 424 | 'ヾ(๑╹◡╹)ノ"', 425 | 'うとうと...', 426 | 'ひょこっ', 427 | 'わん♪', 428 | '(*>ω<*)', 429 | 'にこー♪', 430 | 'ぷくー', 431 | 'にゃふぅ', 432 | 'いあが来ましたよ~', 433 | 'じー', 434 | ], 435 | want: item => `${item}、欲しいなぁ...`, 436 | see: item => `お散歩していたら、道に${item}が落ちているのを見たんです!`, 437 | expire: item => `気づいたら、${item}の賞味期限が切れてました…`, 438 | f1: item => `今日は、${item}の紹介をします。`, 439 | f2: item => `${item}おいしいです!`, 440 | f3: item => `フォロワーさんに${item}をいただきました。ありがとうございます!`, 441 | }, 442 | }; 443 | 444 | export function getSerif(variant: string | string[]): string { 445 | if (Array.isArray(variant)) { 446 | return variant[Math.floor(Math.random() * variant.length)]; 447 | } else { 448 | return variant; 449 | } 450 | } 451 | -------------------------------------------------------------------------------- /src/ai.ts: -------------------------------------------------------------------------------- 1 | // AI CORE 2 | 3 | import * as fs from 'fs'; 4 | import autobind from 'autobind-decorator'; 5 | import * as loki from 'lokijs'; 6 | import fetch from 'node-fetch'; 7 | import * as FormData from 'form-data'; 8 | import * as chalk from 'chalk'; 9 | import * as crypto from 'crypto'; 10 | const delay = require('timeout-as-promise'); 11 | 12 | import config from '@/config'; 13 | import Module from '@/module'; 14 | import Message from '@/message'; 15 | import Friend, { FriendDoc } from '@/friend'; 16 | import { User } from '@/misskey/user'; 17 | import Stream from '@/stream'; 18 | import log from '@/utils/log'; 19 | const pkg = require('../package.json'); 20 | 21 | type MentionHook = (msg: Message) => Promise; 22 | type ContextHook = (key: any, msg: Message, data?: any) => Promise; 23 | type TimeoutCallback = (data?: any) => void; 24 | 25 | export type HandlerResult = { 26 | reaction?: string | null; 27 | immediate?: boolean; 28 | }; 29 | 30 | export type InstallerResult = { 31 | mentionHook?: MentionHook; 32 | contextHook?: ContextHook; 33 | timeoutCallback?: TimeoutCallback; 34 | }; 35 | 36 | export type Meta = { 37 | lastWakingAt: number; 38 | }; 39 | 40 | /** 41 | * 藍 42 | */ 43 | export default class 藍 { 44 | public readonly version = pkg._v; 45 | public account: User; 46 | public connection: Stream; 47 | public modules: Module[] = []; 48 | private mentionHooks: MentionHook[] = []; 49 | private contextHooks: { [moduleName: string]: ContextHook } = {}; 50 | private timeoutCallbacks: { [moduleName: string]: TimeoutCallback } = {}; 51 | public db: loki; 52 | public lastSleepedAt: number; 53 | 54 | private meta: loki.Collection; 55 | 56 | private contexts: loki.Collection<{ 57 | isDm: boolean; 58 | noteId?: string; 59 | userId?: string; 60 | module: string; 61 | key: string | null; 62 | data?: any; 63 | }>; 64 | 65 | private timers: loki.Collection<{ 66 | id: string; 67 | module: string; 68 | insertedAt: number; 69 | delay: number; 70 | data?: any; 71 | }>; 72 | 73 | public friends: loki.Collection; 74 | public moduleData: loki.Collection; 75 | 76 | /** 77 | * 藍インスタンスを生成します 78 | * @param account 藍として使うアカウント 79 | * @param modules モジュール。先頭のモジュールほど高優先度 80 | */ 81 | constructor(account: User, modules: Module[]) { 82 | this.account = account; 83 | this.modules = modules; 84 | 85 | const file = fs.existsSync('memory/memory.json') ? 'memory/memory.json' : 'memory.json'; 86 | 87 | this.log(`Lodaing the memory from ${file}...`); 88 | 89 | this.db = new loki(file, { 90 | autoload: true, 91 | autosave: true, 92 | autosaveInterval: 1000, 93 | autoloadCallback: err => { 94 | if (err) { 95 | this.log(chalk.red(`Failed to load the memory: ${err}`)); 96 | } else { 97 | this.log(chalk.green('The memory loaded successfully')); 98 | this.run(); 99 | } 100 | } 101 | }); 102 | } 103 | 104 | @autobind 105 | public log(msg: string) { 106 | log(chalk`[{magenta AiOS}]: ${msg}`); 107 | } 108 | 109 | @autobind 110 | private run() { 111 | //#region Init DB 112 | this.meta = this.getCollection('meta', {}); 113 | 114 | this.contexts = this.getCollection('contexts', { 115 | indices: ['key'] 116 | }); 117 | 118 | this.timers = this.getCollection('timers', { 119 | indices: ['module'] 120 | }); 121 | 122 | this.friends = this.getCollection('friends', { 123 | indices: ['userId'] 124 | }); 125 | 126 | this.moduleData = this.getCollection('moduleData', { 127 | indices: ['module'] 128 | }); 129 | //#endregion 130 | 131 | const meta = this.getMeta(); 132 | this.lastSleepedAt = meta.lastWakingAt; 133 | 134 | // Init stream 135 | this.connection = new Stream(); 136 | 137 | setInterval(() => { 138 | this.connection.rawSend('ping'); 139 | }, 10 * 60 * 1000); 140 | 141 | //#region Main stream 142 | const mainStream = this.connection.useSharedConnection('main'); 143 | 144 | // メンションされたとき 145 | mainStream.on('mention', async data => { 146 | if (data.userId == this.account.id) return; // 自分は弾く 147 | const regMention = new RegExp(`@${this.account.username}`); 148 | if (data.text && regMention.test(data.text)) { 149 | // Misskeyのバグで投稿が非公開扱いになる 150 | if (data.text == null) data = await this.api('notes/show', { noteId: data.id }); 151 | this.onReceiveMessage(new Message(this, data, false)); 152 | } 153 | }); 154 | 155 | // 返信されたとき 156 | mainStream.on('reply', async data => { 157 | if (data.userId == this.account.id) return; // 自分は弾く 158 | if (data.text && data.text.startsWith('@' + this.account.username)) return; 159 | // Misskeyのバグで投稿が非公開扱いになる 160 | if (data.text == null) data = await this.api('notes/show', { noteId: data.id }); 161 | this.onReceiveMessage(new Message(this, data, false)); 162 | }); 163 | 164 | // Renoteされたとき 165 | mainStream.on('renote', async data => { 166 | if (data.userId == this.account.id) return; // 自分は弾く 167 | if (data.text == null && (data.files || []).length == 0) return; 168 | 169 | // リアクションする 170 | this.api('notes/reactions/create', { 171 | noteId: data.id, 172 | reaction: 'love' 173 | }); 174 | }); 175 | 176 | // メッセージ 177 | mainStream.on('messagingMessage', data => { 178 | if (data.userId == this.account.id) return; // 自分は弾く 179 | this.onReceiveMessage(new Message(this, data, true)); 180 | }); 181 | 182 | // 通知 183 | mainStream.on('notification', data => { 184 | this.onNotification(data); 185 | }); 186 | //#endregion 187 | 188 | // Install modules 189 | this.modules.forEach(m => { 190 | this.log(`Installing ${chalk.cyan.italic(m.name)}\tmodule...`); 191 | m.init(this); 192 | const res = m.install(); 193 | if (res != null) { 194 | if (res.mentionHook) this.mentionHooks.push(res.mentionHook); 195 | if (res.contextHook) this.contextHooks[m.name] = res.contextHook; 196 | if (res.timeoutCallback) this.timeoutCallbacks[m.name] = res.timeoutCallback; 197 | } 198 | }); 199 | 200 | // タイマー監視 201 | this.crawleTimer(); 202 | setInterval(this.crawleTimer, 1000); 203 | 204 | setInterval(this.logWaking, 10000); 205 | 206 | this.log(chalk.green.bold('Ai am now running!')); 207 | } 208 | 209 | /** 210 | * ユーザーから話しかけられたとき 211 | * (メンション、リプライ、トークのメッセージ) 212 | */ 213 | @autobind 214 | private async onReceiveMessage(msg: Message): Promise { 215 | this.log(chalk.gray(`<<< An message received: ${chalk.underline(msg.id)}`)); 216 | 217 | // Ignore message if the user is a bot 218 | // To avoid infinity reply loop. 219 | if (msg.user.isBot) { 220 | return; 221 | } 222 | 223 | const isNoContext = !msg.isDm && msg.replyId == null; 224 | 225 | // Look up the context 226 | const context = isNoContext ? null : this.contexts.findOne(msg.isDm ? { 227 | isDm: true, 228 | userId: msg.userId 229 | } : { 230 | isDm: false, 231 | noteId: msg.replyId 232 | }); 233 | 234 | let reaction: string | null = 'love'; 235 | let immediate: boolean = false; 236 | 237 | //#region 238 | const invokeMentionHooks = async () => { 239 | let res: boolean | HandlerResult | null = null; 240 | 241 | for (const handler of this.mentionHooks) { 242 | res = await handler(msg); 243 | if (res === true || typeof res === 'object') break; 244 | } 245 | 246 | if (res != null && typeof res === 'object') { 247 | if (res.reaction != null) reaction = res.reaction; 248 | if (res.immediate != null) immediate = res.immediate; 249 | } 250 | }; 251 | 252 | // コンテキストがあればコンテキストフック呼び出し 253 | // なければそれぞれのモジュールについてフックが引っかかるまで呼び出し 254 | if (context != null) { 255 | const handler = this.contextHooks[context.module]; 256 | const res = await handler(context.key, msg, context.data); 257 | 258 | if (res != null && typeof res === 'object') { 259 | if (res.reaction != null) reaction = res.reaction; 260 | if (res.immediate != null) immediate = res.immediate; 261 | } 262 | 263 | if (res === false) { 264 | await invokeMentionHooks(); 265 | } 266 | } else { 267 | await invokeMentionHooks(); 268 | } 269 | //#endregion 270 | 271 | if (!immediate) { 272 | await delay(1000); 273 | } 274 | 275 | if (msg.isDm) { 276 | // 既読にする 277 | this.api('messaging/messages/read', { 278 | messageId: msg.id, 279 | }); 280 | } else { 281 | // リアクションする 282 | if (reaction) { 283 | this.api('notes/reactions/create', { 284 | noteId: msg.id, 285 | reaction: reaction 286 | }); 287 | } 288 | } 289 | } 290 | 291 | @autobind 292 | private onNotification(notification: any) { 293 | switch (notification.type) { 294 | // リアクションされたら親愛度を少し上げる 295 | // TODO: リアクション取り消しをよしなにハンドリングする 296 | case 'reaction': { 297 | const friend = new Friend(this, { user: notification.user }); 298 | friend.incLove(0.1); 299 | break; 300 | } 301 | 302 | default: 303 | break; 304 | } 305 | } 306 | 307 | @autobind 308 | private crawleTimer() { 309 | const timers = this.timers.find(); 310 | for (const timer of timers) { 311 | // タイマーが時間切れかどうか 312 | if (Date.now() - (timer.insertedAt + timer.delay) >= 0) { 313 | this.log(`Timer expired: ${timer.module} ${timer.id}`); 314 | this.timers.remove(timer); 315 | this.timeoutCallbacks[timer.module](timer.data); 316 | } 317 | } 318 | } 319 | 320 | @autobind 321 | private logWaking() { 322 | this.setMeta({ 323 | lastWakingAt: Date.now(), 324 | }); 325 | } 326 | 327 | /** 328 | * データベースのコレクションを取得します 329 | */ 330 | @autobind 331 | public getCollection(name: string, opts?: any): loki.Collection { 332 | let collection: loki.Collection; 333 | 334 | collection = this.db.getCollection(name); 335 | 336 | if (collection == null) { 337 | collection = this.db.addCollection(name, opts); 338 | } 339 | 340 | return collection; 341 | } 342 | 343 | @autobind 344 | public lookupFriend(userId: User['id']): Friend | null { 345 | const doc = this.friends.findOne({ 346 | userId: userId 347 | }); 348 | 349 | if (doc == null) return null; 350 | 351 | const friend = new Friend(this, { doc: doc }); 352 | 353 | return friend; 354 | } 355 | 356 | /** 357 | * ファイルをドライブにアップロードします 358 | */ 359 | @autobind 360 | public async upload(file: Buffer | fs.ReadStream, meta: any): Promise { 361 | // Buffer && filenameなし だと動かない 362 | const _meta = Object.assign({}, meta); 363 | if (!_meta.filename?.length) _meta.filename = 'file'; 364 | 365 | const formData = new FormData(); 366 | formData.append('i', config.i); 367 | formData.append('file', file, _meta); 368 | 369 | return fetch(`${config.apiUrl}/drive/files/create`, { 370 | method: 'post', 371 | body: formData, 372 | timeout: 30 * 1000, 373 | }).then(res => { 374 | if (!res.ok) { 375 | throw `${res.status} ${res.statusText}`; 376 | } else { 377 | return res.status === 204 ? {} : res.json(); 378 | } 379 | }); 380 | } 381 | 382 | /** 383 | * 投稿します 384 | */ 385 | @autobind 386 | public async post(param: any) { 387 | const res = await this.api('notes/create', param); 388 | return res.createdNote; 389 | } 390 | 391 | /** 392 | * 指定ユーザーにトークメッセージを送信します 393 | */ 394 | @autobind 395 | public sendMessage(userId: any, param: any) { 396 | return this.api('messaging/messages/create', Object.assign({ 397 | userId: userId, 398 | }, param)); 399 | } 400 | 401 | /** 402 | * APIを呼び出します 403 | */ 404 | @autobind 405 | public api(endpoint: string, param?: any): Promise { 406 | return fetch(`${config.apiUrl}/${endpoint}`, { 407 | method: 'post', 408 | body: JSON.stringify(Object.assign({ 409 | i: config.i 410 | }, param)), 411 | headers: { 412 | 'Content-Type': 'application/json' 413 | }, 414 | timeout: 30 * 1000, 415 | }).then(res => { 416 | if (!res.ok) { 417 | throw `${res.status} ${res.statusText}`; 418 | } else { 419 | return res.status === 204 ? {} : res.json(); 420 | } 421 | }); 422 | }; 423 | 424 | /** 425 | * コンテキストを生成し、ユーザーからの返信を待ち受けます 426 | * @param module 待ち受けるモジュール名 427 | * @param key コンテキストを識別するためのキー 428 | * @param isDm トークメッセージ上のコンテキストかどうか 429 | * @param id トークメッセージ上のコンテキストならばトーク相手のID、そうでないなら待ち受ける投稿のID 430 | * @param data コンテキストに保存するオプションのデータ 431 | */ 432 | @autobind 433 | public subscribeReply(module: Module, key: string | null, isDm: boolean, id: string, data?: any) { 434 | this.contexts.insertOne(isDm ? { 435 | isDm: true, 436 | userId: id, 437 | module: module.name, 438 | key: key, 439 | data: data 440 | } : { 441 | isDm: false, 442 | noteId: id, 443 | module: module.name, 444 | key: key, 445 | data: data 446 | }); 447 | } 448 | 449 | /** 450 | * 返信の待ち受けを解除します 451 | * @param module 解除するモジュール名 452 | * @param key コンテキストを識別するためのキー 453 | */ 454 | @autobind 455 | public unsubscribeReply(module: Module, key: string | null) { 456 | this.contexts.findAndRemove({ 457 | key: key, 458 | module: module.name 459 | }); 460 | } 461 | 462 | /** 463 | * 指定したミリ秒経過後に、そのモジュールのタイムアウトコールバックを呼び出します。 464 | * このタイマーは記憶に永続化されるので、途中でプロセスを再起動しても有効です。 465 | * @param module モジュール名 466 | * @param delay ミリ秒 467 | * @param data オプションのデータ 468 | */ 469 | @autobind 470 | public setTimeoutWithPersistence(module: Module, delay: number, data?: any) { 471 | const id = crypto.randomUUID(); 472 | this.timers.insertOne({ 473 | id: id, 474 | module: module.name, 475 | insertedAt: Date.now(), 476 | delay: delay, 477 | data: data 478 | }); 479 | 480 | this.log(`Timer persisted: ${module.name} ${id} ${delay}ms`); 481 | } 482 | 483 | @autobind 484 | public getMeta() { 485 | const rec = this.meta.findOne(); 486 | 487 | if (rec) { 488 | return rec; 489 | } else { 490 | const initial: Meta = { 491 | lastWakingAt: Date.now(), 492 | }; 493 | 494 | this.meta.insertOne(initial); 495 | return initial; 496 | } 497 | } 498 | 499 | @autobind 500 | public setMeta(meta: Partial) { 501 | const rec = this.getMeta(); 502 | 503 | for (const [k, v] of Object.entries(meta)) { 504 | rec[k] = v; 505 | } 506 | 507 | this.meta.update(rec); 508 | } 509 | } 510 | -------------------------------------------------------------------------------- /src/modules/reversi/back.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * -AI- 3 | * Botのバックエンド(思考を担当) 4 | * 5 | * 対話と思考を同じプロセスで行うと、思考時間が長引いたときにストリームから 6 | * 切断されてしまうので、別々のプロセスで行うようにします 7 | */ 8 | 9 | import fetch from 'node-fetch'; 10 | import 'module-alias/register'; 11 | 12 | import Reversi, { Color } from 'misskey-reversi'; 13 | import config from '@/config'; 14 | import serifs from '@/serifs'; 15 | import { User } from '@/misskey/user'; 16 | 17 | const db = {}; 18 | 19 | function getUserName(user) { 20 | return user.name || user.username; 21 | } 22 | 23 | const titles = [ 24 | 'さん', 'サン', 'サン', '㌠', 25 | 'ちゃん', 'チャン', 'チャン', 26 | '君', 'くん', 'クン', 'クン', 27 | '先生', 'せんせい', 'センセイ', 'センセイ' 28 | ]; 29 | 30 | class Session { 31 | private account: User; 32 | private game: any; 33 | private form: any; 34 | private o: Reversi; 35 | private botColor: Color; 36 | 37 | /** 38 | * 各マスの強さ (-1.0 ~ 1.0) 39 | */ 40 | private cellWeights: number[]; 41 | 42 | /** 43 | * 対局が開始したことを知らせた投稿 44 | */ 45 | private startedNote: any = null; 46 | 47 | private yabaiMap = 0; 48 | private yabaiTry = 0; 49 | private dekiru = Infinity; 50 | 51 | private get user(): User { 52 | return this.game.user1Id == this.account.id ? this.game.user2 : this.game.user1; 53 | } 54 | 55 | private get userName(): string { 56 | const name = getUserName(this.user); 57 | return `?[${name}](${config.host}/@${this.user.username})${titles.some(x => name.endsWith(x)) ? '' : 'さん'}`; 58 | } 59 | 60 | private get strength(): number { 61 | return this.form.find(i => i.id == 'strength').value; 62 | } 63 | 64 | private get isSettai(): boolean { 65 | return this.strength === 0; 66 | } 67 | 68 | private get allowPost(): boolean { 69 | return this.form.find(i => i.id == 'publish').value; 70 | } 71 | 72 | private get url(): string { 73 | return `${config.host}/games/reversi/${this.game.id}`; 74 | } 75 | 76 | constructor() { 77 | process.on('message', this.onMessage); 78 | } 79 | 80 | private onMessage = async (msg: any) => { 81 | switch (msg.type) { 82 | case '_init_': this.onInit(msg.body); break; 83 | case 'updateForm': this.onUpdateForn(msg.body); break; 84 | case 'started': this.onStarted(msg.body); break; 85 | case 'ended': this.onEnded(msg.body); break; 86 | case 'set': this.onSet(msg.body); break; 87 | } 88 | } 89 | 90 | // 親プロセスからデータをもらう 91 | private onInit = (msg: any) => { 92 | this.game = msg.game; 93 | this.form = msg.form; 94 | this.account = msg.account; 95 | } 96 | 97 | /** 98 | * フォームが更新されたとき 99 | */ 100 | private onUpdateForn = (msg: any) => { 101 | this.form.find(i => i.id == msg.id).value = msg.value; 102 | } 103 | 104 | /** 105 | * 対局が始まったとき 106 | */ 107 | private onStarted = (msg: any) => { 108 | this.game = msg; 109 | 110 | // TLに投稿する 111 | this.postGameStarted().then(note => { 112 | this.startedNote = note; 113 | }); 114 | 115 | // リバーシエンジン初期化 116 | this.o = new Reversi((this.game.settings || this.game).map, { 117 | isLlotheo: (this.game.settings || this.game).isLlotheo, 118 | canPutEverywhere: (this.game.settings || this.game).canPutEverywhere, 119 | loopedBoard: (this.game.settings || this.game).loopedBoard 120 | }); 121 | 122 | //#region 各マスの価値を計算しておく 123 | 124 | // 標準的な 8*8 のマップなら予め定義した価値マップを使用 125 | if (this.o.mapWidth == 8 && this.o.mapHeight == 8 && !this.o.map.some(p => p == 'null')) { 126 | this.cellWeights = [ 127 | 1 , -0.4, 0 , -0.1, -0.1, 0 , -0.4, 1 , 128 | -0.4, -0.5, -0.2, -0.2, -0.2, -0.2, -0.5, -0.4, 129 | 0 , -0.2, 0 , -0.1, -0.1, 0 , -0.2, 0 , 130 | -0.1, -0.2, -0.1, -0.1, -0.1, -0.1, -0.2, -0.1, 131 | -0.1, -0.2, -0.1, -0.1, -0.1, -0.1, -0.2, -0.1, 132 | 0 , -0.2, 0 , -0.1, -0.1, 0 , -0.2, 0 , 133 | -0.4, -0.5, -0.2, -0.2, -0.2, -0.2, -0.5, -0.4, 134 | 1 , -0.4, 0 , -0.1, -0.1, 0 , -0.4, 1 135 | ]; 136 | } else { 137 | //#region 隅 138 | this.cellWeights = this.o.map.map((pix, i) => { 139 | if (pix == 'null') return 0; 140 | const [x, y] = this.o.transformPosToXy(i); 141 | let count = 0; 142 | const get = (x, y) => { 143 | if (x < 0 || y < 0 || x >= this.o.mapWidth || y >= this.o.mapHeight) return 'null'; 144 | return this.o.mapDataGet(this.o.transformXyToPos(x, y)); 145 | }; 146 | 147 | const isNotSumi = ( 148 | // - 149 | // + 150 | // - 151 | (get(x - 1, y - 1) == 'empty' && get(x + 1, y + 1) == 'empty') || 152 | 153 | // - 154 | // + 155 | // - 156 | (get(x, y - 1) == 'empty' && get(x, y + 1) == 'empty') || 157 | 158 | // - 159 | // + 160 | // - 161 | (get(x + 1, y - 1) == 'empty' && get(x - 1, y + 1) == 'empty') || 162 | 163 | // 164 | // -+- 165 | // 166 | (get(x - 1, y) == 'empty' && get(x + 1, y) == 'empty') 167 | ) 168 | 169 | const isSumi = !isNotSumi; 170 | 171 | return isSumi ? 1 : 0; 172 | }); 173 | //#endregion 174 | 175 | //#region 隅の隣は危険 176 | this.cellWeights.forEach((cell, i) => { 177 | const [x, y] = this.o.transformPosToXy(i); 178 | 179 | if (cell === 1) return; 180 | if (this.o.mapDataGet(this.o.transformXyToPos(x, y)) == 'null') return; 181 | 182 | const get = (x, y) => { 183 | if (x < 0 || y < 0 || x >= this.o.mapWidth || y >= this.o.mapHeight) return 0; 184 | return this.cellWeights[this.o.transformXyToPos(x, y)]; 185 | }; 186 | 187 | const isSumiNear = ( 188 | (get(x - 1, y - 1) === 1) || // 左上 189 | (get(x , y - 1) === 1) || // 上 190 | (get(x + 1, y - 1) === 1) || // 右上 191 | (get(x + 1, y ) === 1) || // 右 192 | (get(x + 1, y + 1) === 1) || // 右下 193 | (get(x , y + 1) === 1) || // 下 194 | (get(x - 1, y + 1) === 1) || // 左下 195 | (get(x - 1, y ) === 1) // 左 196 | ) 197 | 198 | if (isSumiNear) this.cellWeights[i] = -0.5; 199 | }); 200 | //#endregion 201 | 202 | } 203 | 204 | //#endregion 205 | 206 | this.botColor = this.game.user1Id == this.account.id && this.game.black == 1 || this.game.user2Id == this.account.id && this.game.black == 2; 207 | 208 | if (this.botColor) { 209 | try { 210 | this.think(); 211 | } catch (e) { 212 | console.log('think error', e); 213 | process.send!({ type: 'surrendered' }); 214 | process.exit(); 215 | } 216 | } 217 | } 218 | 219 | /** 220 | * 対局が終わったとき 221 | */ 222 | private onEnded = async (msg: any) => { 223 | // ストリームから切断 224 | process.send!({ 225 | type: 'ended' 226 | }); 227 | 228 | let text: string; 229 | 230 | if (msg.game.surrendered) { 231 | if (this.isSettai) { 232 | text = serifs.reversi.settaiButYouSurrendered(this.userName); 233 | } else { 234 | text = serifs.reversi.youSurrendered(this.userName); 235 | } 236 | } else if (msg.winnerId) { 237 | if (msg.winnerId == this.account.id) { 238 | if (this.isSettai) { 239 | text = serifs.reversi.iWonButSettai(this.userName); 240 | } else { 241 | text = serifs.reversi.iWon(this.userName); 242 | } 243 | } else { 244 | if (this.isSettai) { 245 | text = serifs.reversi.iLoseButSettai(this.userName); 246 | } else { 247 | text = serifs.reversi.iLose(this.userName); 248 | } 249 | } 250 | } else { 251 | if (this.isSettai) { 252 | text = serifs.reversi.drawnSettai(this.userName); 253 | } else { 254 | text = serifs.reversi.drawn(this.userName); 255 | } 256 | } 257 | 258 | await this.post(text, this.startedNote); 259 | 260 | process.exit(); 261 | } 262 | 263 | /** 264 | * 打たれたとき 265 | */ 266 | private onSet = (msg: any) => { 267 | this.o.put(msg.color, msg.pos); 268 | 269 | if (msg.next === this.botColor) { 270 | try { 271 | this.think(); 272 | } catch (e) { 273 | console.log('think error', e); 274 | process.send!({ type: 'surrendered' }); 275 | process.exit(); 276 | } 277 | } 278 | } 279 | 280 | /** 281 | * Botにとってある局面がどれだけ有利か取得する 282 | */ 283 | private staticEval = () => { 284 | let score = this.o.canPutSomewhere(this.botColor).length; 285 | 286 | this.cellWeights.forEach((weight, i) => { 287 | // 係数 288 | const coefficient = 30; 289 | weight = weight * coefficient; 290 | 291 | const stone = this.o.board[i]; 292 | if (stone === this.botColor) { 293 | // TODO: 価値のあるマスに設置されている自分の石に縦か横に接するマスは価値があると判断する 294 | score += weight; 295 | } else if (stone !== null) { 296 | score -= weight; 297 | } 298 | }); 299 | 300 | // ロセオならスコアを反転 301 | if ((this.game.settings || this.game).isLlotheo) score = -score; 302 | 303 | // 接待ならスコアを反転 304 | if (this.isSettai) score = -score; 305 | 306 | return score; 307 | } 308 | 309 | private think = () => { 310 | console.log('Thinking...'); 311 | console.time('think'); 312 | 313 | // 接待モードのときは、全力(5手先読みくらい)で負けるようにする 314 | let maxDepth = this.isSettai ? 5 : this.strength; 315 | 316 | const cans = this.o.canPutSomewhere(this.botColor); 317 | 318 | // 探索 319 | let scores; 320 | try { 321 | // 実績的に無理そうだったら最大3手 322 | if (cans.length > this.dekiru) { 323 | console.log(`Tenuki: ${cans.length} >= ${this.dekiru}`); 324 | if (maxDepth > 3) { 325 | console.log(`Tenuki`); 326 | maxDepth = 3; 327 | } 328 | } 329 | 330 | // 指定の先読み数で探索(時間制限付き) 331 | const diveStart = new Date().getTime(); 332 | console.log(`dive for ${cans.length}cans, maxDepth=${maxDepth}`) 333 | scores = cans.map(p => this.goDive(p, maxDepth, diveStart, 60 * 1000)); 334 | 335 | } catch (e) { 336 | console.log(e); 337 | 338 | this.dekiru = cans.length; 339 | 340 | // 最大1手でリトライ 341 | if (maxDepth > 3) { 342 | maxDepth = 3; 343 | const diveStart = new Date().getTime(); 344 | console.log(`retry dive for ${cans.length}cans, maxDepth=${maxDepth}`) 345 | scores = cans.map(p => this.goDive(p, maxDepth, diveStart, 30 * 1000)); 346 | } else { 347 | throw new Error('Muri'); 348 | } 349 | } 350 | const pos = cans[scores.indexOf(Math.max(...scores))]; 351 | 352 | console.log('Thinked:', pos); 353 | console.timeEnd('think'); 354 | 355 | setTimeout(() => { 356 | process.send!({ 357 | type: 'put', 358 | pos 359 | }); 360 | }, 500); 361 | } 362 | 363 | private goDive = (pos: number, maxDepth: number, diveStart, ms: number = 60 * 1000): number => { 364 | /** 365 | * αβ法での探索 366 | */ 367 | let dives = 0; 368 | 369 | const dive = (pos: number, alpha = -Infinity, beta = Infinity, depth = 0): number => { 370 | // 制限チェック 371 | if (dives++ % 100000 === 0) { 372 | const elapsed = new Date().getTime() - diveStart; 373 | //console.log(`dive processing dives=${dives}, elapsed total=${elapsed}ms`); 374 | if (elapsed > ms) { 375 | throw new Error(`Limit exceeded: dives=${dives} elapsed total=${elapsed}ms`) 376 | } 377 | } 378 | 379 | // 試し打ち 380 | this.o.put(this.o.turn, pos); 381 | 382 | const key = this.o.board.toString(); 383 | let cache = db[key]; 384 | if (cache) { 385 | if (alpha >= cache.upper) { 386 | this.o.undo(); 387 | return cache.upper; 388 | } 389 | if (beta <= cache.lower) { 390 | this.o.undo(); 391 | return cache.lower; 392 | } 393 | alpha = Math.max(alpha, cache.lower); 394 | beta = Math.min(beta, cache.upper); 395 | } else { 396 | cache = { 397 | upper: Infinity, 398 | lower: -Infinity 399 | }; 400 | } 401 | 402 | const isBotTurn = this.o.turn === this.botColor; 403 | 404 | // 勝った 405 | if (this.o.turn === null) { 406 | const winner = this.o.winner; 407 | 408 | // 勝つことによる基本スコア 409 | const base = 10000; 410 | 411 | let score; 412 | 413 | if ((this.game.settings || this.game).isLlotheo) { 414 | // 勝ちは勝ちでも、より自分の石を少なくした方が美しい勝ちだと判定する 415 | score = this.o.winner ? base - (this.o.blackCount * 100) : base - (this.o.whiteCount * 100); 416 | } else { 417 | // 勝ちは勝ちでも、より相手の石を少なくした方が美しい勝ちだと判定する 418 | score = this.o.winner ? base + (this.o.blackCount * 100) : base + (this.o.whiteCount * 100); 419 | } 420 | 421 | // 巻き戻し 422 | this.o.undo(); 423 | 424 | // 接待なら自分が負けた方が高スコア 425 | return this.isSettai 426 | ? winner !== this.botColor ? score : -score 427 | : winner === this.botColor ? score : -score; 428 | } 429 | 430 | if (depth === maxDepth) { 431 | // 静的に評価 432 | const score = this.staticEval(); 433 | 434 | // 巻き戻し 435 | this.o.undo(); 436 | 437 | return score; 438 | } else { 439 | const cans = this.o.canPutSomewhere(this.o.turn); 440 | 441 | let value = isBotTurn ? -Infinity : Infinity; 442 | let a = alpha; 443 | let b = beta; 444 | 445 | // 次のターンのプレイヤーにとって最も良い手を取得 446 | for (const p of cans) { 447 | if (isBotTurn) { 448 | const score = dive(p, a, beta, depth + 1); 449 | value = Math.max(value, score); 450 | a = Math.max(a, value); 451 | if (value >= beta) break; 452 | } else { 453 | const score = dive(p, alpha, b, depth + 1); 454 | value = Math.min(value, score); 455 | b = Math.min(b, value); 456 | if (value <= alpha) break; 457 | } 458 | } 459 | 460 | // 巻き戻し 461 | this.o.undo(); 462 | 463 | if (value <= alpha) { 464 | cache.upper = value; 465 | } else if (value >= beta) { 466 | cache.lower = value; 467 | } else { 468 | cache.upper = value; 469 | cache.lower = value; 470 | } 471 | 472 | db[key] = cache; 473 | 474 | return value; 475 | } 476 | }; 477 | 478 | const value = dive(pos); 479 | //console.log(`dive[pos=${pos}] end took ${dives} dives`); 480 | return value; 481 | } 482 | 483 | /** 484 | * 対局が始まったことをMisskeyに投稿します 485 | */ 486 | private postGameStarted = async () => { 487 | const text = this.isSettai 488 | ? serifs.reversi.startedSettai(this.userName) 489 | : serifs.reversi.started(this.userName, this.strength.toString()); 490 | 491 | return await this.post(`${text}\n→[観戦する](${this.url})`); 492 | } 493 | 494 | /** 495 | * Misskeyに投稿します 496 | * @param text 投稿内容 497 | */ 498 | private post = async (text: string, renote?: any) => { 499 | if (this.allowPost) { 500 | const body = { 501 | i: config.i, 502 | text: text, 503 | visibility: 'home' 504 | } as any; 505 | 506 | if (renote) { 507 | body.renoteId = renote.id; 508 | } 509 | 510 | try { 511 | const res = await fetch(`${config.host}/api/notes/create`, { 512 | method: 'post', 513 | body: JSON.stringify(body), 514 | headers: { 515 | 'Content-Type': 'application/json' 516 | }, 517 | timeout: 30 * 1000, 518 | }).then(res => { 519 | if (!res.ok) { 520 | throw `${res.status} ${res.statusText}`; 521 | } else { 522 | return res.json(); 523 | } 524 | }); 525 | 526 | return res.createdNote; 527 | } catch (e) { 528 | console.error(e); 529 | return null; 530 | } 531 | } else { 532 | return null; 533 | } 534 | } 535 | } 536 | 537 | new Session(); 538 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@cspotcode/source-map-support@^0.8.0": 6 | version "0.8.1" 7 | resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" 8 | integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== 9 | dependencies: 10 | "@jridgewell/trace-mapping" "0.3.9" 11 | 12 | "@jridgewell/resolve-uri@^3.0.3": 13 | version "3.1.0" 14 | resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" 15 | integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== 16 | 17 | "@jridgewell/sourcemap-codec@^1.4.10": 18 | version "1.4.14" 19 | resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" 20 | integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== 21 | 22 | "@jridgewell/trace-mapping@0.3.9": 23 | version "0.3.9" 24 | resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" 25 | integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== 26 | dependencies: 27 | "@jridgewell/resolve-uri" "^3.0.3" 28 | "@jridgewell/sourcemap-codec" "^1.4.10" 29 | 30 | "@tsconfig/node10@^1.0.7": 31 | version "1.0.8" 32 | resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.8.tgz#c1e4e80d6f964fbecb3359c43bd48b40f7cadad9" 33 | integrity sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg== 34 | 35 | "@tsconfig/node12@^1.0.7": 36 | version "1.0.9" 37 | resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.9.tgz#62c1f6dee2ebd9aead80dc3afa56810e58e1a04c" 38 | integrity sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw== 39 | 40 | "@tsconfig/node14@^1.0.0": 41 | version "1.0.1" 42 | resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.1.tgz#95f2d167ffb9b8d2068b0b235302fafd4df711f2" 43 | integrity sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg== 44 | 45 | "@tsconfig/node16@^1.0.2": 46 | version "1.0.2" 47 | resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.2.tgz#423c77877d0569db20e1fc80885ac4118314010e" 48 | integrity sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA== 49 | 50 | "@twemoji/parser@15.1.1": 51 | version "15.1.1" 52 | resolved "https://registry.yarnpkg.com/@twemoji/parser/-/parser-15.1.1.tgz#e47e09415403b8444645a3ee465887f0c1bdbb40" 53 | integrity sha512-CChRzIu6ngkCJOmURBlYEdX5DZSu+bBTtqR60XjBkFrmvplKW7OQsea+i8XwF4bLVlUXBO7ZmHhRPDzfQyLwwg== 54 | 55 | "@types/chalk@2.2.0": 56 | version "2.2.0" 57 | resolved "https://registry.yarnpkg.com/@types/chalk/-/chalk-2.2.0.tgz#b7f6e446f4511029ee8e3f43075fb5b73fbaa0ba" 58 | integrity sha512-1zzPV9FDe1I/WHhRkf9SNgqtRJWZqrBWgu7JGveuHmmyR9CnAPCie2N/x+iHrgnpYBIcCJWHBoMRv2TRWktsvw== 59 | dependencies: 60 | chalk "*" 61 | 62 | "@types/lokijs@1.5.14": 63 | version "1.5.14" 64 | resolved "https://registry.yarnpkg.com/@types/lokijs/-/lokijs-1.5.14.tgz#bf7efc825b8ac763676eedcf4df35f553733f511" 65 | integrity sha512-4Fic47BX3Qxr8pd12KT6/T1XWU8dOlJBIp1jGoMbaDbiEvdv50rAii+B3z1b/J2pvMywcVP+DBPGP5/lgLOKGA== 66 | 67 | "@types/node-fetch@2.6.11": 68 | version "2.6.11" 69 | resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.11.tgz#9b39b78665dae0e82a08f02f4967d62c66f95d24" 70 | integrity sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g== 71 | dependencies: 72 | "@types/node" "*" 73 | form-data "^4.0.0" 74 | 75 | "@types/node@*", "@types/node@22.10.5": 76 | version "22.10.5" 77 | resolved "https://registry.yarnpkg.com/@types/node/-/node-22.10.5.tgz#95af89a3fb74a2bb41ef9927f206e6472026e48b" 78 | integrity sha512-F8Q+SeGimwOo86fiovQh8qiXfFEh2/ocYv7tU5pJ3EXMSSxk1Joj5wefpFK2fHTf/N6HKGSxIDBT9f3gCxXPkQ== 79 | dependencies: 80 | undici-types "~6.20.0" 81 | 82 | "@types/promise-retry@1.1.6": 83 | version "1.1.6" 84 | resolved "https://registry.yarnpkg.com/@types/promise-retry/-/promise-retry-1.1.6.tgz#3c48826d8a27f68f9d4900fc7448f08a1532db44" 85 | integrity sha512-EC1+OMXV0PZb0pf+cmyxc43MEP2CDumZe4AfuxWboxxEixztIebknpJPZAX5XlodGF1OY+C1E/RAeNGzxf+bJA== 86 | dependencies: 87 | "@types/retry" "*" 88 | 89 | "@types/random-seed@0.3.5": 90 | version "0.3.5" 91 | resolved "https://registry.yarnpkg.com/@types/random-seed/-/random-seed-0.3.5.tgz#a2514992e37aacf3f2da3b6b67d1258ec7fd85a0" 92 | integrity sha512-CftxcDPAHgs0SLHU2dt+ZlDPJfGqLW3sZlC/ATr5vJDSe5tRLeOne7HMvCOJnFyF8e1U41wqzs3h6AMC613xtA== 93 | 94 | "@types/retry@*": 95 | version "0.12.1" 96 | resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.1.tgz#d8f1c0d0dc23afad6dc16a9e993a0865774b4065" 97 | integrity sha512-xoDlM2S4ortawSWORYqsdU+2rxdh4LRW9ytc3zmT37RIKQh6IHyKwwtKhKis9ah8ol07DCkZxPt8BBvPjC6v4g== 98 | 99 | "@types/seedrandom@3.0.8": 100 | version "3.0.8" 101 | resolved "https://registry.yarnpkg.com/@types/seedrandom/-/seedrandom-3.0.8.tgz#61cc8ed88f93a3c31289c295e6df8ca40be42bdf" 102 | integrity sha512-TY1eezMU2zH2ozQoAFAQFOPpvP15g+ZgSfTZt31AUUH/Rxtnz3H+A/Sv1Snw2/amp//omibc+AEkTaA8KUeOLQ== 103 | 104 | "@types/uuid@10.0.0": 105 | version "10.0.0" 106 | resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-10.0.0.tgz#e9c07fe50da0f53dc24970cca94d619ff03f6f6d" 107 | integrity sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ== 108 | 109 | "@types/ws@8.5.13": 110 | version "8.5.13" 111 | resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.13.tgz#6414c280875e2691d0d1e080b05addbf5cb91e20" 112 | integrity sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA== 113 | dependencies: 114 | "@types/node" "*" 115 | 116 | acorn-walk@^8.1.1: 117 | version "8.2.0" 118 | resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" 119 | integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== 120 | 121 | acorn@^8.4.1: 122 | version "8.7.0" 123 | resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.0.tgz#90951fde0f8f09df93549481e5fc141445b791cf" 124 | integrity sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ== 125 | 126 | ansi-styles@^4.1.0: 127 | version "4.3.0" 128 | resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" 129 | integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== 130 | dependencies: 131 | color-convert "^2.0.1" 132 | 133 | arg@^4.1.0: 134 | version "4.1.3" 135 | resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" 136 | integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== 137 | 138 | asynckit@^0.4.0: 139 | version "0.4.0" 140 | resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" 141 | integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= 142 | 143 | autobind-decorator@2.4.0: 144 | version "2.4.0" 145 | resolved "https://registry.yarnpkg.com/autobind-decorator/-/autobind-decorator-2.4.0.tgz#ea9e1c98708cf3b5b356f7cf9f10f265ff18239c" 146 | integrity sha512-OGYhWUO72V6DafbF8PM8rm3EPbfuyMZcJhtm5/n26IDwO18pohE4eNazLoCGhPiXOCD0gEGmrbU3849QvM8bbw== 147 | 148 | base64-js@^1.3.1: 149 | version "1.5.1" 150 | resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" 151 | integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== 152 | 153 | bl@^4.0.3: 154 | version "4.1.0" 155 | resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" 156 | integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== 157 | dependencies: 158 | buffer "^5.5.0" 159 | inherits "^2.0.4" 160 | readable-stream "^3.4.0" 161 | 162 | buffer@^5.5.0: 163 | version "5.7.1" 164 | resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" 165 | integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== 166 | dependencies: 167 | base64-js "^1.3.1" 168 | ieee754 "^1.1.13" 169 | 170 | canvas@3.0.1: 171 | version "3.0.1" 172 | resolved "https://registry.yarnpkg.com/canvas/-/canvas-3.0.1.tgz#b536df1d5c8c33c64f7752c499b3ff96a43eaf27" 173 | integrity sha512-PcpVF4f8RubAeN/jCQQ/UymDKzOiLmRPph8fOTzDnlsUihkO/AUlxuhaa7wGRc3vMcCbV1fzuvyu5cWZlIcn1w== 174 | dependencies: 175 | node-addon-api "^7.0.0" 176 | prebuild-install "^7.1.1" 177 | simple-get "^3.0.3" 178 | 179 | chalk@*: 180 | version "5.0.1" 181 | resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.0.1.tgz#ca57d71e82bb534a296df63bbacc4a1c22b2a4b6" 182 | integrity sha512-Fo07WOYGqMfCWHOzSXOt2CxDbC6skS/jO9ynEcmpANMoPrD+W1r1K6Vx7iNm+AQmETU1Xr2t+n8nzkV9t6xh3w== 183 | 184 | chalk@4.1.2: 185 | version "4.1.2" 186 | resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" 187 | integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== 188 | dependencies: 189 | ansi-styles "^4.1.0" 190 | supports-color "^7.1.0" 191 | 192 | chownr@^1.1.1: 193 | version "1.1.4" 194 | resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" 195 | integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== 196 | 197 | color-convert@^2.0.1: 198 | version "2.0.1" 199 | resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" 200 | integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== 201 | dependencies: 202 | color-name "~1.1.4" 203 | 204 | color-name@~1.1.4: 205 | version "1.1.4" 206 | resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" 207 | integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== 208 | 209 | combined-stream@^1.0.8: 210 | version "1.0.8" 211 | resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" 212 | integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== 213 | dependencies: 214 | delayed-stream "~1.0.0" 215 | 216 | core-util-is@~1.0.0: 217 | version "1.0.3" 218 | resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" 219 | integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== 220 | 221 | create-require@^1.1.0: 222 | version "1.1.1" 223 | resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" 224 | integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== 225 | 226 | decompress-response@^4.2.0: 227 | version "4.2.1" 228 | resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-4.2.1.tgz#414023cc7a302da25ce2ec82d0d5238ccafd8986" 229 | integrity sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw== 230 | dependencies: 231 | mimic-response "^2.0.0" 232 | 233 | decompress-response@^6.0.0: 234 | version "6.0.0" 235 | resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc" 236 | integrity sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ== 237 | dependencies: 238 | mimic-response "^3.1.0" 239 | 240 | deep-extend@^0.6.0: 241 | version "0.6.0" 242 | resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" 243 | integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== 244 | 245 | delayed-stream@~1.0.0: 246 | version "1.0.0" 247 | resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" 248 | integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= 249 | 250 | detect-libc@^2.0.0: 251 | version "2.0.3" 252 | resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.3.tgz#f0cd503b40f9939b894697d19ad50895e30cf700" 253 | integrity sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw== 254 | 255 | diff@^4.0.1: 256 | version "4.0.2" 257 | resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" 258 | integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== 259 | 260 | end-of-stream@^1.1.0, end-of-stream@^1.4.1: 261 | version "1.4.4" 262 | resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" 263 | integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== 264 | dependencies: 265 | once "^1.4.0" 266 | 267 | err-code@^2.0.2: 268 | version "2.0.3" 269 | resolved "https://registry.yarnpkg.com/err-code/-/err-code-2.0.3.tgz#23c2f3b756ffdfc608d30e27c9a941024807e7f9" 270 | integrity sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA== 271 | 272 | expand-template@^2.0.3: 273 | version "2.0.3" 274 | resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c" 275 | integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg== 276 | 277 | form-data@^4.0.0: 278 | version "4.0.0" 279 | resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" 280 | integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== 281 | dependencies: 282 | asynckit "^0.4.0" 283 | combined-stream "^1.0.8" 284 | mime-types "^2.1.12" 285 | 286 | fs-constants@^1.0.0: 287 | version "1.0.0" 288 | resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" 289 | integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== 290 | 291 | github-from-package@0.0.0: 292 | version "0.0.0" 293 | resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce" 294 | integrity sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw== 295 | 296 | has-flag@^4.0.0: 297 | version "4.0.0" 298 | resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" 299 | integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== 300 | 301 | ieee754@^1.1.13: 302 | version "1.2.1" 303 | resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" 304 | integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== 305 | 306 | inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1: 307 | version "2.0.4" 308 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" 309 | integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== 310 | 311 | ini@~1.3.0: 312 | version "1.3.8" 313 | resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" 314 | integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== 315 | 316 | isarray@0.0.1: 317 | version "0.0.1" 318 | resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" 319 | integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8= 320 | 321 | json-stringify-safe@^5.0.1: 322 | version "5.0.1" 323 | resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" 324 | integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= 325 | 326 | lokijs@1.5.12: 327 | version "1.5.12" 328 | resolved "https://registry.yarnpkg.com/lokijs/-/lokijs-1.5.12.tgz#cb55b37009bdf09ee7952a6adddd555b893653a0" 329 | integrity sha512-Q5ALD6JiS6xAUWCwX3taQmgwxyveCtIIuL08+ml0nHwT3k0S/GIFJN+Hd38b1qYIMaE5X++iqsqWVksz7SYW+Q== 330 | 331 | lru-cache@^6.0.0: 332 | version "6.0.0" 333 | resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" 334 | integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== 335 | dependencies: 336 | yallist "^4.0.0" 337 | 338 | make-error@^1.1.1: 339 | version "1.3.6" 340 | resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" 341 | integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== 342 | 343 | memory-streams@0.1.3: 344 | version "0.1.3" 345 | resolved "https://registry.yarnpkg.com/memory-streams/-/memory-streams-0.1.3.tgz#d9b0017b4b87f1d92f55f2745c9caacb1dc93ceb" 346 | integrity sha512-qVQ/CjkMyMInPaaRMrwWNDvf6boRZXaT/DbQeMYcCWuXPEBf1v8qChOc9OlEVQp2uOvRXa1Qu30fLmKhY6NipA== 347 | dependencies: 348 | readable-stream "~1.0.2" 349 | 350 | mime-db@1.51.0: 351 | version "1.51.0" 352 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.51.0.tgz#d9ff62451859b18342d960850dc3cfb77e63fb0c" 353 | integrity sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g== 354 | 355 | mime-types@^2.1.12: 356 | version "2.1.34" 357 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.34.tgz#5a712f9ec1503511a945803640fafe09d3793c24" 358 | integrity sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A== 359 | dependencies: 360 | mime-db "1.51.0" 361 | 362 | mimic-response@^2.0.0: 363 | version "2.1.0" 364 | resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-2.1.0.tgz#d13763d35f613d09ec37ebb30bac0469c0ee8f43" 365 | integrity sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA== 366 | 367 | mimic-response@^3.1.0: 368 | version "3.1.0" 369 | resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9" 370 | integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ== 371 | 372 | minimist@^1.2.0, minimist@^1.2.3: 373 | version "1.2.8" 374 | resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" 375 | integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== 376 | 377 | misskey-reversi@0.0.5: 378 | version "0.0.5" 379 | resolved "https://registry.yarnpkg.com/misskey-reversi/-/misskey-reversi-0.0.5.tgz#ad6a1c3259a4d77a1646a1b04f2fcc2f51671290" 380 | integrity sha512-lALC8WxwQYU7wBUjbmcGc7b9AfNyzBqdoa8Q6sZfS8cfC+zEbi/Ln8m/fZKuM0qhnylBqTQiPQHHhBsSJxz/OQ== 381 | 382 | mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3: 383 | version "0.5.3" 384 | resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" 385 | integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== 386 | 387 | module-alias@2.2.3: 388 | version "2.2.3" 389 | resolved "https://registry.yarnpkg.com/module-alias/-/module-alias-2.2.3.tgz#ec2e85c68973bda6ab71ce7c93b763ec96053221" 390 | integrity sha512-23g5BFj4zdQL/b6tor7Ji+QY4pEfNH784BMslY9Qb0UnJWRAt+lQGLYmRaM0KDBwIG23ffEBELhZDP2rhi9f/Q== 391 | 392 | napi-build-utils@^1.0.1: 393 | version "1.0.2" 394 | resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.2.tgz#b1fddc0b2c46e380a0b7a76f984dd47c41a13806" 395 | integrity sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg== 396 | 397 | neologd-normalizer@0.0.3: 398 | version "0.0.3" 399 | resolved "https://registry.yarnpkg.com/neologd-normalizer/-/neologd-normalizer-0.0.3.tgz#ec5f496b529b5a5dae4d327e496da96bab69f7c1" 400 | integrity sha1-7F9Ja1KbWl2uTTJ+SW2pa6tp98E= 401 | 402 | node-abi@^3.3.0: 403 | version "3.71.0" 404 | resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.71.0.tgz#52d84bbcd8575efb71468fbaa1f9a49b2c242038" 405 | integrity sha512-SZ40vRiy/+wRTf21hxkkEjPJZpARzUMVcJoQse2EF8qkUWbbO2z7vd5oA/H6bVH6SZQ5STGcu0KRDS7biNRfxw== 406 | dependencies: 407 | semver "^7.3.5" 408 | 409 | node-addon-api@^7.0.0: 410 | version "7.1.1" 411 | resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-7.1.1.tgz#1aba6693b0f255258a049d621329329322aad558" 412 | integrity sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ== 413 | 414 | node-fetch@2.7.0: 415 | version "2.7.0" 416 | resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" 417 | integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== 418 | dependencies: 419 | whatwg-url "^5.0.0" 420 | 421 | once@^1.3.1, once@^1.4.0: 422 | version "1.4.0" 423 | resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" 424 | integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= 425 | dependencies: 426 | wrappy "1" 427 | 428 | prebuild-install@^7.1.1: 429 | version "7.1.2" 430 | resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.2.tgz#a5fd9986f5a6251fbc47e1e5c65de71e68c0a056" 431 | integrity sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ== 432 | dependencies: 433 | detect-libc "^2.0.0" 434 | expand-template "^2.0.3" 435 | github-from-package "0.0.0" 436 | minimist "^1.2.3" 437 | mkdirp-classic "^0.5.3" 438 | napi-build-utils "^1.0.1" 439 | node-abi "^3.3.0" 440 | pump "^3.0.0" 441 | rc "^1.2.7" 442 | simple-get "^4.0.0" 443 | tar-fs "^2.0.0" 444 | tunnel-agent "^0.6.0" 445 | 446 | promise-retry@2.0.1: 447 | version "2.0.1" 448 | resolved "https://registry.yarnpkg.com/promise-retry/-/promise-retry-2.0.1.tgz#ff747a13620ab57ba688f5fc67855410c370da22" 449 | integrity sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g== 450 | dependencies: 451 | err-code "^2.0.2" 452 | retry "^0.12.0" 453 | 454 | pump@^3.0.0: 455 | version "3.0.2" 456 | resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.2.tgz#836f3edd6bc2ee599256c924ffe0d88573ddcbf8" 457 | integrity sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw== 458 | dependencies: 459 | end-of-stream "^1.1.0" 460 | once "^1.3.1" 461 | 462 | random-seed@0.3.0: 463 | version "0.3.0" 464 | resolved "https://registry.yarnpkg.com/random-seed/-/random-seed-0.3.0.tgz#d945f2e1f38f49e8d58913431b8bf6bb937556cd" 465 | integrity sha1-2UXy4fOPSejViRNDG4v2u5N1Vs0= 466 | dependencies: 467 | json-stringify-safe "^5.0.1" 468 | 469 | rc@^1.2.7: 470 | version "1.2.8" 471 | resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" 472 | integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== 473 | dependencies: 474 | deep-extend "^0.6.0" 475 | ini "~1.3.0" 476 | minimist "^1.2.0" 477 | strip-json-comments "~2.0.1" 478 | 479 | readable-stream@^3.1.1, readable-stream@^3.4.0: 480 | version "3.6.2" 481 | resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" 482 | integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== 483 | dependencies: 484 | inherits "^2.0.3" 485 | string_decoder "^1.1.1" 486 | util-deprecate "^1.0.1" 487 | 488 | readable-stream@~1.0.2: 489 | version "1.0.34" 490 | resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c" 491 | integrity sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw= 492 | dependencies: 493 | core-util-is "~1.0.0" 494 | inherits "~2.0.1" 495 | isarray "0.0.1" 496 | string_decoder "~0.10.x" 497 | 498 | reconnecting-websocket@4.4.0: 499 | version "4.4.0" 500 | resolved "https://registry.yarnpkg.com/reconnecting-websocket/-/reconnecting-websocket-4.4.0.tgz#3b0e5b96ef119e78a03135865b8bb0af1b948783" 501 | integrity sha512-D2E33ceRPga0NvTDhJmphEgJ7FUYF0v4lr1ki0csq06OdlxKfugGzN0dSkxM/NfqCxYELK4KcaTOUOjTV6Dcng== 502 | 503 | retry@^0.12.0: 504 | version "0.12.0" 505 | resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" 506 | integrity sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs= 507 | 508 | safe-buffer@^5.0.1, safe-buffer@~5.2.0: 509 | version "5.2.1" 510 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" 511 | integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== 512 | 513 | seedrandom@3.0.5: 514 | version "3.0.5" 515 | resolved "https://registry.yarnpkg.com/seedrandom/-/seedrandom-3.0.5.tgz#54edc85c95222525b0c7a6f6b3543d8e0b3aa0a7" 516 | integrity sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg== 517 | 518 | semver@^7.3.5: 519 | version "7.3.5" 520 | resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" 521 | integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== 522 | dependencies: 523 | lru-cache "^6.0.0" 524 | 525 | simple-concat@^1.0.0: 526 | version "1.0.1" 527 | resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.1.tgz#f46976082ba35c2263f1c8ab5edfe26c41c9552f" 528 | integrity sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q== 529 | 530 | simple-get@^3.0.3: 531 | version "3.1.1" 532 | resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-3.1.1.tgz#cc7ba77cfbe761036fbfce3d021af25fc5584d55" 533 | integrity sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA== 534 | dependencies: 535 | decompress-response "^4.2.0" 536 | once "^1.3.1" 537 | simple-concat "^1.0.0" 538 | 539 | simple-get@^4.0.0: 540 | version "4.0.1" 541 | resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-4.0.1.tgz#4a39db549287c979d352112fa03fd99fd6bc3543" 542 | integrity sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA== 543 | dependencies: 544 | decompress-response "^6.0.0" 545 | once "^1.3.1" 546 | simple-concat "^1.0.0" 547 | 548 | string_decoder@^1.1.1: 549 | version "1.3.0" 550 | resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" 551 | integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== 552 | dependencies: 553 | safe-buffer "~5.2.0" 554 | 555 | string_decoder@~0.10.x: 556 | version "0.10.31" 557 | resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" 558 | integrity sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ= 559 | 560 | strip-json-comments@~2.0.1: 561 | version "2.0.1" 562 | resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" 563 | integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ== 564 | 565 | supports-color@^7.1.0: 566 | version "7.2.0" 567 | resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" 568 | integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== 569 | dependencies: 570 | has-flag "^4.0.0" 571 | 572 | tar-fs@^2.0.0: 573 | version "2.1.1" 574 | resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784" 575 | integrity sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng== 576 | dependencies: 577 | chownr "^1.1.1" 578 | mkdirp-classic "^0.5.2" 579 | pump "^3.0.0" 580 | tar-stream "^2.1.4" 581 | 582 | tar-stream@^2.1.4: 583 | version "2.2.0" 584 | resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287" 585 | integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ== 586 | dependencies: 587 | bl "^4.0.3" 588 | end-of-stream "^1.4.1" 589 | fs-constants "^1.0.0" 590 | inherits "^2.0.3" 591 | readable-stream "^3.1.1" 592 | 593 | timeout-as-promise@1.0.0: 594 | version "1.0.0" 595 | resolved "https://registry.yarnpkg.com/timeout-as-promise/-/timeout-as-promise-1.0.0.tgz#7367e811fc992acfcdcdaabf2e50dfaf8b21576f" 596 | integrity sha1-c2foEfyZKs/Nzaq/LlDfr4shV28= 597 | 598 | tr46@~0.0.3: 599 | version "0.0.3" 600 | resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" 601 | integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= 602 | 603 | ts-node@10.9.2: 604 | version "10.9.2" 605 | resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.2.tgz#70f021c9e185bccdca820e26dc413805c101c71f" 606 | integrity sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ== 607 | dependencies: 608 | "@cspotcode/source-map-support" "^0.8.0" 609 | "@tsconfig/node10" "^1.0.7" 610 | "@tsconfig/node12" "^1.0.7" 611 | "@tsconfig/node14" "^1.0.0" 612 | "@tsconfig/node16" "^1.0.2" 613 | acorn "^8.4.1" 614 | acorn-walk "^8.1.1" 615 | arg "^4.1.0" 616 | create-require "^1.1.0" 617 | diff "^4.0.1" 618 | make-error "^1.1.1" 619 | v8-compile-cache-lib "^3.0.1" 620 | yn "3.1.1" 621 | 622 | tunnel-agent@^0.6.0: 623 | version "0.6.0" 624 | resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" 625 | integrity sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w== 626 | dependencies: 627 | safe-buffer "^5.0.1" 628 | 629 | typescript@5.7.2: 630 | version "5.7.2" 631 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.7.2.tgz#3169cf8c4c8a828cde53ba9ecb3d2b1d5dd67be6" 632 | integrity sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg== 633 | 634 | undici-types@~6.20.0: 635 | version "6.20.0" 636 | resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.20.0.tgz#8171bf22c1f588d1554d55bf204bc624af388433" 637 | integrity sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg== 638 | 639 | util-deprecate@^1.0.1: 640 | version "1.0.2" 641 | resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" 642 | integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= 643 | 644 | v8-compile-cache-lib@^3.0.1: 645 | version "3.0.1" 646 | resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" 647 | integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== 648 | 649 | webidl-conversions@^3.0.0: 650 | version "3.0.1" 651 | resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" 652 | integrity sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE= 653 | 654 | whatwg-url@^5.0.0: 655 | version "5.0.0" 656 | resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" 657 | integrity sha1-lmRU6HZUYuN2RNNib2dCzotwll0= 658 | dependencies: 659 | tr46 "~0.0.3" 660 | webidl-conversions "^3.0.0" 661 | 662 | wrappy@1: 663 | version "1.0.2" 664 | resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" 665 | integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= 666 | 667 | ws@8.18.0: 668 | version "8.18.0" 669 | resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" 670 | integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== 671 | 672 | yallist@^4.0.0: 673 | version "4.0.0" 674 | resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" 675 | integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== 676 | 677 | yn@3.1.1: 678 | version "3.1.1" 679 | resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" 680 | integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== 681 | --------------------------------------------------------------------------------