Commit b9c69af3 authored by nanahira's avatar nanahira

first

parent 6c650032
# compiled output
/dist
/node_modules
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# OS
.DS_Store
# Tests
/coverage
/.nyc_output
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
/data
/output
/config.yaml
.git*
Dockerfile
.dockerignore
webpack.config.js
dist/*
build/*
\ No newline at end of file
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.json',
sourceType: 'module',
},
plugins: ['@typescript-eslint/eslint-plugin'],
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
],
root: true,
env: {
node: true,
jest: true,
},
ignorePatterns: ['.eslintrc.js'],
rules: {
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
},
};
# compiled output
/dist
/node_modules
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# OS
.DS_Store
# Tests
/coverage
/.nyc_output
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
/data
/output
/config.yaml
\ No newline at end of file
stages:
- build
- deploy
variables:
GIT_DEPTH: "1"
build:
stage: build
tags:
- linux
script:
- npm ci
- npm run build
artifacts:
paths:
- dist/
upload_to_minio:
stage: deploy
dependencies:
- build
tags:
- linux
script:
- aws s3 --endpoint=https://minio.mycard.moe:9000 sync --delete dist/full/ s3://nanahira/koishi-plugin/greeting
only:
- master
deploy_npm:
stage: deploy
dependencies:
- build
tags:
- linux
script:
- apt update;apt -y install coreutils
- echo $NPMRC | base64 --decode > ~/.npmrc
- npm publish . --access public
only:
- master
/install-npm.sh
.git*
/data
/output
/config.yaml
.idea
.dockerignore
Dockerfile
/src
/dist/full
{
"singleQuote": true,
"trailingComma": "all"
}
\ No newline at end of file
The MIT License (MIT)
Copyright (c) 2021 Nanahira
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
# koishi-plugin-greeting
打招呼(早晚安)的 Koishi 插件。
\ No newline at end of file
打招呼(早晚安)的 Koishi 插件。
## 安装
### npm
```bash
npm install koishi-plugin-greeting
```
### 直接安装
在 https://cdn02.moecube.com:444/nanahira/koishi-plugin/greeting/index.js 下载即可。
## 配置
详见 `config.ts` 部分,或详见 Schema 描述配置。
#!/bin/bash
npm install --save \
source-map-support \
koishi-thirdeye
npm install --save-dev \
@types/node \
typescript \
'@typescript-eslint/eslint-plugin@^4.28.2' \
'@typescript-eslint/parser@^4.28.2 '\
'eslint@^7.30.0' \
'eslint-config-prettier@^8.3.0' \
'eslint-plugin-prettier@^3.4.0' \
prettier \
raw-loader \
ts-loader \
webpack \
webpack-cli \
koishi@latest \
ws
This diff is collapsed.
{
"name": "koishi-plugin-greeting",
"description": "打招呼(早晚安)的 Koishi 插件。",
"version": "1.0.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"lint": "eslint --fix .",
"build": "webpack && env PACK_ALL=1 webpack"
},
"repository": {
"type": "git",
"url": "https://code.mycard.moe/3rdeye/koishi-plugin-greeting.git"
},
"author": "Nanahira <nanahira@momobako.com>",
"license": "MIT",
"keywords": [
"Koishi.js",
"qqbot",
"cqhttp",
"onebot"
],
"bugs": {
"url": "https://code.mycard.moe/3rdeye/koishi-plugin-greeting/issues"
},
"homepage": "https://code.mycard.moe/3rdeye/koishi-plugin-greeting",
"peerDependencies": {
"koishi": "^4.0.0"
},
"dependencies": {
"koishi-thirdeye": "^7.4.0",
"lodash": "^4.17.21",
"moment": "^2.29.1",
"node-schedule": "^2.1.0",
"source-map-support": "^0.5.21"
},
"devDependencies": {
"@types/lodash": "^4.14.178",
"@types/node": "^17.0.8",
"@types/node-schedule": "^1.3.2",
"@typescript-eslint/eslint-plugin": "^4.33.0",
"@typescript-eslint/parser": "^4.33.0",
"eslint": "^7.32.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^3.4.1",
"koishi": "^4.0.0",
"prettier": "^2.5.1",
"raw-loader": "^4.0.2",
"ts-loader": "^9.2.6",
"typescript": "^4.5.4",
"webpack": "^5.66.0",
"webpack-cli": "^4.9.1",
"ws": "^8.4.2"
}
}
import 'source-map-support/register';
import { DefineSchema, RegisterSchema } from 'koishi-thirdeye';
import _ from 'lodash';
import { Adapter, Bot, Logger, Random } from 'koishi';
import moment from 'moment';
import { createHash, Hash } from 'crypto';
const logger = new Logger('greeting');
interface Salt {
salt?: string;
}
function updateSalt(hash: Hash, saltObjects: Salt[]) {
for (const saltObject of saltObjects) {
if (saltObject?.salt) {
hash.update(saltObject.salt);
}
}
return hash;
}
function hashRandom(saltObjects: Salt[]) {
const hash = updateSalt(createHash('md5'), saltObjects);
return parseInt(hash.digest('hex'), 16) % 4294967295;
}
@RegisterSchema()
export class Word {
constructor(config: Partial<Word>) {}
@DefineSchema({ description: '文本。', required: true })
word: string;
@DefineSchema({ description: '权重。', default: 1 })
weight?: number;
toEntry() {
return [this.word, this.weight || 1];
}
}
@RegisterSchema()
export class Pattern {
constructor(config: Partial<Pattern>) {}
@DefineSchema({
description: '词语。该语段中会随机悬着其中1种词语出现。',
type: Word,
})
words: Word[];
pickWord() {
return Random.weightedPick(
Object.fromEntries(this.words.map((word) => word.toEntry())),
);
}
}
@RegisterSchema()
export class Rule implements Salt {
constructor(config: Partial<Rule>) {}
@DefineSchema({ description: '最早出现的时间。', default: '00:00' })
fromTime?: string;
@DefineSchema({ description: '最晚出现的时间。默认为与 fromTime 相同。' })
toTime?: string;
getTimeRange() {
const nowDate = moment().format('YYYY-MM-DD');
const fromTime = moment(`${nowDate} ${this.fromTime}`);
const toTime = moment(`${nowDate} ${this.toTime || this.fromTime}`);
return [fromTime, toTime] as const;
}
shouldSend(target: Target) {
const now = moment();
const [fromTime, toTime] = this.getTimeRange();
const fromMinutes = Math.floor(fromTime.unix() / 60);
const toMinutes = Math.floor(toTime.unix() / 60);
const nowMinutes = Math.floor(now.unix() / 60);
let sendingMinutes: number;
if (fromMinutes === toMinutes) {
sendingMinutes = fromMinutes;
} else {
const scale = toMinutes - fromMinutes + 1;
const random =
hashRandom([this, target, { salt: now.format('YYYY-MM-DD') }]) % scale;
sendingMinutes = fromMinutes + random;
}
const sendingTime = moment.unix(sendingMinutes * 60);
logger.warn(
`${nowMinutes}: Should send ${this.generateText()} to ${target.getDescription()} at ${sendingTime.format(
'YYYY-MM-DD HH:mm',
)}`,
);
return nowMinutes === sendingMinutes;
}
@DefineSchema({ description: '随机时间盐值。' })
salt?: string;
@DefineSchema({ description: '语段。', type: Pattern })
patterns: Pattern[];
generateText() {
return this.patterns.map((pattern) => pattern.pickWord()).join('');
}
}
@RegisterSchema()
export class Target implements Salt {
constructor(config: Partial<Target>) {}
@DefineSchema({ description: '私聊用户 ID。若存在此项则为私聊。' })
userId?: string;
@DefineSchema({ description: '频道 ID。' })
channelId?: string;
@DefineSchema({ description: '群组 ID。' })
guildId?: string;
getDescription() {
if (this.userId) {
return `User ${this.userId}`;
} else if (this.channelId) {
return `Channel ${this.channelId}`;
} else if (this.guildId) {
return `Group ${this.guildId}`;
} else {
return 'Unknown target';
}
}
get salt() {
return this.userId || this.channelId || this.guildId;
}
set salt(s: string) {}
send(bot: Bot, text: string) {
if (this.userId) {
return bot.sendPrivateMessage(this.userId, text);
} else {
return bot.sendMessage(this.channelId, text, this.guildId);
}
}
}
@RegisterSchema()
export class Instance {
constructor(config: Partial<Instance>) {}
@DefineSchema({
description:
'机器人 ID,应写成 platform:id 的形式。例如 onebot:111111111。',
required: true,
})
from: string;
getBot(list: Adapter.BotList) {
return list.get(this.from);
}
@DefineSchema({
description: '目标列表。',
type: Target,
})
to: Target[];
@DefineSchema({
description: '规则列表。',
type: Rule,
})
rules: Rule[];
async run(list: Adapter.BotList) {
const bot = this.getBot(list);
if (bot?.status !== 'online') {
logger.info(`Skipping job for ${this.from} because it's not online.`);
return this;
}
logger.warn(
`${moment().format('YYYY-MM-DD HH:mm')}: Running job for ${this.from}...`,
);
const prom: Promise<string[]>[] = [];
for (const target of this.to) {
for (const rule of this.rules) {
if (rule.shouldSend(target)) {
const text = rule.generateText();
logger.info(
`Sending from ${this.from} to ${target.getDescription()}: ${text}`,
);
prom.push(
target.send(bot, text).catch((e) => {
logger.error(
`Failed to send from ${
this.from
} to ${target.getDescription()}: ${e.message}`,
);
return undefined;
}),
);
}
}
}
return;
}
}
@RegisterSchema()
export class GreetingPluginConfig {
constructor(config: GreetingPluginConfigLike) {}
@DefineSchema({ description: '实例列表。', type: Instance })
instances: Instance[];
async runInstances(list: Adapter.BotList, instances = this.instances) {
const failedInstances = _.compact(
await Promise.all(instances.map((instance) => instance.run(list))),
);
return failedInstances;
}
}
export type GreetingPluginConfigLike = Partial<GreetingPluginConfig>;
import 'source-map-support/register';
import { GreetingPluginConfig, Instance } from './config';
import {
DefinePlugin,
BasePlugin,
Inject,
LifecycleEvents,
UseEvent,
} from 'koishi-thirdeye';
import { Adapter, Bot } from 'koishi';
import { Job, scheduleJob } from 'node-schedule';
import _ from 'lodash';
export * from './config';
@DefinePlugin({ name: 'greeting', schema: GreetingPluginConfig })
export default class GreetingPlugin
extends BasePlugin<GreetingPluginConfig>
implements LifecycleEvents {
@Inject()
private bots: Adapter.BotList;
private job: Job;
private pendingInstances = new Map<string, Instance[]>();
private addPending(instance: Instance) {
const instances = this.pendingInstances.get(instance.from) || [];
instances.push(instance);
this.pendingInstances.set(instance.from, instances);
}
@UseEvent('bot-status-updated')
async onBotStatusUpdated(bot: Bot) {
if (bot.status === 'online') {
await this.resolvePending(bot.sid);
}
}
private async resolvePending(from: string) {
const instances = this.pendingInstances.get(from);
if (!instances) return;
this.pendingInstances.delete(from);
return this.run(_.uniq(instances));
}
async run(instances?: Instance[]) {
if (!instances) {
this.pendingInstances.clear();
}
const failedInstances = await this.config.runInstances(
this.bots,
instances,
);
for (const instance of failedInstances) {
this.addPending(instance);
}
}
onConnect() {
this.job = scheduleJob('0 * * * * *', () => this.run());
this.job.invoke();
}
onDisconnect() {
if (this.job) this.job.cancel();
}
}
{
"compilerOptions": {
"outDir": "dist",
"module": "commonjs",
"target": "es2021",
"esModuleInterop": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"declaration": true,
"sourceMap": true,
"skipLibCheck": true
},
"compileOnSave": true,
"allowJs": true,
"include": [
"*.ts",
"src/**/*.ts"
]
}
const path = require('path');
const packgeInfo = require('./package.json');
function externalsFromDep() {
return Object.fromEntries(
[
...Object.keys(packgeInfo.dependencies || {}),
...Object.keys(packgeInfo.peerDependencies || {}),
]
.filter((dep) => dep !== 'source-map-support')
.map((dep) => [dep, dep]),
);
}
const packAll = !!process.env.PACK_ALL;
module.exports = {
entry: './src/index.ts',
mode: 'production',
target: 'node',
devtool: 'source-map',
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
{ test: /\.mustache$/, use: 'raw-loader' },
],
},
resolve: {
extensions: ['.tsx', '.ts', '.js'],
},
output: {
filename: 'index.js',
library: {
type: 'commonjs',
},
path: path.resolve(__dirname, packAll ? 'dist/full' : 'dist'),
},
externals: {
koishi: 'koishi',
...(packAll ? {} : externalsFromDep()),
},
};
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment