koishi-plugin-githubsth 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +69 -0
- package/lib/commands/admin.d.ts +2 -0
- package/lib/commands/admin.js +59 -0
- package/lib/commands/index.d.ts +3 -0
- package/lib/commands/index.js +40 -0
- package/lib/commands/repo.d.ts +3 -0
- package/lib/commands/repo.js +19 -0
- package/lib/commands/subscribe.d.ts +2 -0
- package/lib/commands/subscribe.js +63 -0
- package/lib/config.d.ts +14 -0
- package/lib/config.js +15 -0
- package/lib/database.d.ts +22 -0
- package/lib/database.js +24 -0
- package/lib/index.d.ts +9 -0
- package/lib/index.js +69 -0
- package/lib/locales/zh-CN.d.ts +17 -0
- package/lib/locales/zh-CN.js +18 -0
- package/lib/services/formatter.d.ts +17 -0
- package/lib/services/formatter.js +94 -0
- package/lib/services/notifier.d.ts +14 -0
- package/lib/services/notifier.js +120 -0
- package/package.json +49 -0
package/README.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# koishi-plugin-githubsth
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/koishi-plugin-githubsth)
|
|
4
|
+
|
|
5
|
+
Koishi 的 GitHub 集成插件,提供强大的事件通知服务和仓库管理命令。支持数据库存储订阅关系,并提供信任仓库管理功能。
|
|
6
|
+
|
|
7
|
+
## 功能特性
|
|
8
|
+
|
|
9
|
+
- **事件通知**: 实时接收 GitHub 事件通知 (Push, Issue, PR, Release, Star, Fork, Discussion, Workflow)。
|
|
10
|
+
- **数据库订阅**: 订阅关系持久化存储在 Koishi 数据库中,支持动态管理。
|
|
11
|
+
- **信任仓库管理**: 管理员可配置允许订阅的仓库白名单,确保安全。
|
|
12
|
+
- **灵活的规则**: 支持按仓库、目标频道和事件类型进行订阅。
|
|
13
|
+
- **丰富格式**: 消息格式美观,包含关键信息。
|
|
14
|
+
- **调试模式**: 支持开启详细日志输出,方便排查问题。
|
|
15
|
+
|
|
16
|
+
## 安装
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install koishi-plugin-githubsth
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## 配置
|
|
23
|
+
|
|
24
|
+
本插件需要 `database` 服务和 `koishi-plugin-adapter-github` 才能正常工作。
|
|
25
|
+
|
|
26
|
+
### 插件配置
|
|
27
|
+
|
|
28
|
+
在 Koishi 控制台中配置插件:
|
|
29
|
+
|
|
30
|
+
- **defaultOwner**: 默认仓库拥有者 (可选)。
|
|
31
|
+
- **defaultRepo**: 默认仓库名称 (可选)。
|
|
32
|
+
- **debug**: 启用调试模式,输出详细日志 (默认 false)。
|
|
33
|
+
|
|
34
|
+
### 订阅管理
|
|
35
|
+
|
|
36
|
+
插件使用数据库管理订阅。
|
|
37
|
+
|
|
38
|
+
#### 管理员命令 (权限等级 3)
|
|
39
|
+
|
|
40
|
+
- `githubsth.trust.add <repo>`: 添加信任仓库 (owner/repo)。
|
|
41
|
+
- `githubsth.trust.remove <repo>`: 移除信任仓库。
|
|
42
|
+
- `githubsth.trust.list`: 列出所有信任仓库。
|
|
43
|
+
- `githubsth.trust.enable <repo>`: 启用信任仓库。
|
|
44
|
+
- `githubsth.trust.disable <repo>`: 禁用信任仓库。
|
|
45
|
+
|
|
46
|
+
#### 用户命令
|
|
47
|
+
|
|
48
|
+
- `githubsth.subscribe <repo> [events]`: 订阅仓库事件。
|
|
49
|
+
- `repo`: 仓库全名 (owner/repo)。
|
|
50
|
+
- `events`: (可选) 订阅的事件列表,逗号分隔。默认为 `push, issues, pull_request`。
|
|
51
|
+
- 注意:仅能订阅已添加到信任列表的仓库。
|
|
52
|
+
- `githubsth.unsubscribe <repo>`: 取消订阅仓库。
|
|
53
|
+
- `githubsth.list`: 查看当前频道的订阅列表。
|
|
54
|
+
- `githubsth.repo [name]`: 获取仓库信息。
|
|
55
|
+
|
|
56
|
+
### 支持的事件
|
|
57
|
+
|
|
58
|
+
- `push`: 代码推送
|
|
59
|
+
- `issues`: Issue 创建、关闭等
|
|
60
|
+
- `pull_request`: PR 创建、合并等
|
|
61
|
+
- `release`: 新版本发布
|
|
62
|
+
- `star`: 仓库标星
|
|
63
|
+
- `fork`: 仓库复刻
|
|
64
|
+
- `discussion`: 讨论区动态
|
|
65
|
+
- `workflow_run`: CI/CD 工作流状态
|
|
66
|
+
|
|
67
|
+
## 许可证
|
|
68
|
+
|
|
69
|
+
MIT
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.apply = apply;
|
|
4
|
+
function apply(ctx) {
|
|
5
|
+
const logger = ctx.logger('githubsth');
|
|
6
|
+
ctx.command('githubsth.trust', '管理信任仓库', { authority: 3 })
|
|
7
|
+
.alias('gh.trust');
|
|
8
|
+
ctx.command('githubsth.trust.add <repo>', '添加信任仓库')
|
|
9
|
+
.action(async ({ session }, repo) => {
|
|
10
|
+
if (!repo)
|
|
11
|
+
return '请指定仓库名称 (owner/repo)。';
|
|
12
|
+
try {
|
|
13
|
+
await ctx.database.create('github_trusted_repo', {
|
|
14
|
+
repo,
|
|
15
|
+
enabled: true,
|
|
16
|
+
addedBy: session?.userId || 'unknown',
|
|
17
|
+
addedAt: new Date(),
|
|
18
|
+
});
|
|
19
|
+
return `已添加信任仓库: ${repo}`;
|
|
20
|
+
}
|
|
21
|
+
catch (e) {
|
|
22
|
+
if (e.code === 'SQLITE_CONSTRAINT') { // Or handle duplicate error generically
|
|
23
|
+
return '该仓库已在信任列表中。';
|
|
24
|
+
}
|
|
25
|
+
logger.warn(e);
|
|
26
|
+
return '添加失败,请查看日志。';
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
ctx.command('githubsth.trust.remove <repo>', '移除信任仓库')
|
|
30
|
+
.action(async ({ session }, repo) => {
|
|
31
|
+
if (!repo)
|
|
32
|
+
return '请指定仓库名称 (owner/repo)。';
|
|
33
|
+
const result = await ctx.database.remove('github_trusted_repo', { repo });
|
|
34
|
+
if (result.matched === 0)
|
|
35
|
+
return '未找到该信任仓库。';
|
|
36
|
+
return `已移除信任仓库: ${repo}`;
|
|
37
|
+
});
|
|
38
|
+
ctx.command('githubsth.trust.list', '列出所有信任仓库')
|
|
39
|
+
.action(async () => {
|
|
40
|
+
const repos = await ctx.database.get('github_trusted_repo', {});
|
|
41
|
+
if (repos.length === 0)
|
|
42
|
+
return '暂无信任仓库。';
|
|
43
|
+
return repos.map(r => `${r.repo} [${r.enabled ? '启用' : '禁用'}]`).join('\n');
|
|
44
|
+
});
|
|
45
|
+
ctx.command('githubsth.trust.enable <repo>', '启用信任仓库')
|
|
46
|
+
.action(async ({ session }, repo) => {
|
|
47
|
+
const result = await ctx.database.set('github_trusted_repo', { repo }, { enabled: true });
|
|
48
|
+
if (result.matched === 0)
|
|
49
|
+
return '未找到该信任仓库。';
|
|
50
|
+
return `已启用信任仓库: ${repo}`;
|
|
51
|
+
});
|
|
52
|
+
ctx.command('githubsth.trust.disable <repo>', '禁用信任仓库')
|
|
53
|
+
.action(async ({ session }, repo) => {
|
|
54
|
+
const result = await ctx.database.set('github_trusted_repo', { repo }, { enabled: false });
|
|
55
|
+
if (result.matched === 0)
|
|
56
|
+
return '未找到该信任仓库。';
|
|
57
|
+
return `已禁用信任仓库: ${repo}`;
|
|
58
|
+
});
|
|
59
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.apply = apply;
|
|
37
|
+
const repo = __importStar(require("./repo"));
|
|
38
|
+
function apply(ctx, config) {
|
|
39
|
+
ctx.plugin(repo, config);
|
|
40
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.apply = apply;
|
|
4
|
+
function apply(ctx, config) {
|
|
5
|
+
ctx.command('githubsth.repo [name:string]')
|
|
6
|
+
.action(async ({ session }, name) => {
|
|
7
|
+
if (!name && !config.defaultRepo) {
|
|
8
|
+
return session?.text('.specify_repo');
|
|
9
|
+
}
|
|
10
|
+
const fullRepo = name || (config.defaultOwner ? `${config.defaultOwner}/${config.defaultRepo}` : name);
|
|
11
|
+
if (!fullRepo)
|
|
12
|
+
return session?.text('.specify_repo');
|
|
13
|
+
// 在实际实现中,我们将在此处使用 GitHub API。
|
|
14
|
+
// 由于我们没有 API 客户端的具体设置细节(例如 Octokit 或适配器内部实现),
|
|
15
|
+
// 我们将返回一个占位符,以确认命令结构正常工作。
|
|
16
|
+
// TODO: 使用 session.bot 或专用服务实现实际的 GitHub API 调用。
|
|
17
|
+
return session?.text('.repo_info', [fullRepo.split('/')[0], fullRepo.split('/')[1] || '?', 'Demo Description', '100']);
|
|
18
|
+
});
|
|
19
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.apply = apply;
|
|
4
|
+
function apply(ctx) {
|
|
5
|
+
const logger = ctx.logger('githubsth');
|
|
6
|
+
ctx.command('githubsth.subscribe <repo> [events:text]', '订阅 GitHub 仓库')
|
|
7
|
+
.alias('gh.sub')
|
|
8
|
+
.action(async ({ session }, repo, eventsStr) => {
|
|
9
|
+
if (!repo)
|
|
10
|
+
return '请指定仓库名称 (owner/repo)。';
|
|
11
|
+
if (!session?.channelId)
|
|
12
|
+
return '请在群组中使用此命令。';
|
|
13
|
+
// Check trusted repo
|
|
14
|
+
const trusted = await ctx.database.get('github_trusted_repo', { repo, enabled: true });
|
|
15
|
+
if (trusted.length === 0) {
|
|
16
|
+
return '该仓库不在信任列表中,无法订阅。请联系管理员添加。';
|
|
17
|
+
}
|
|
18
|
+
// Parse events
|
|
19
|
+
const events = eventsStr ? eventsStr.split(',').map(e => e.trim()) : ['push', 'issues', 'pull_request']; // Default events
|
|
20
|
+
try {
|
|
21
|
+
await ctx.database.create('github_subscription', {
|
|
22
|
+
repo,
|
|
23
|
+
channelId: session.channelId,
|
|
24
|
+
platform: session.platform || 'unknown',
|
|
25
|
+
events,
|
|
26
|
+
});
|
|
27
|
+
return `已订阅 ${repo} 的 ${events.join(', ')} 事件。`;
|
|
28
|
+
}
|
|
29
|
+
catch (e) {
|
|
30
|
+
logger.warn(e);
|
|
31
|
+
return '订阅失败。';
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
ctx.command('githubsth.unsubscribe <repo>', '取消订阅 GitHub 仓库')
|
|
35
|
+
.alias('gh.unsub')
|
|
36
|
+
.action(async ({ session }, repo) => {
|
|
37
|
+
if (!repo)
|
|
38
|
+
return '请指定仓库名称 (owner/repo)。';
|
|
39
|
+
if (!session?.channelId)
|
|
40
|
+
return '请在群组中使用此命令。';
|
|
41
|
+
const result = await ctx.database.remove('github_subscription', {
|
|
42
|
+
repo,
|
|
43
|
+
channelId: session.channelId,
|
|
44
|
+
platform: session.platform || 'unknown',
|
|
45
|
+
});
|
|
46
|
+
if (result.matched === 0)
|
|
47
|
+
return '未找到该订阅。';
|
|
48
|
+
return `已取消订阅 ${repo}。`;
|
|
49
|
+
});
|
|
50
|
+
ctx.command('githubsth.list', '查看当前频道的订阅')
|
|
51
|
+
.alias('gh.list')
|
|
52
|
+
.action(async ({ session }) => {
|
|
53
|
+
if (!session?.channelId)
|
|
54
|
+
return '请在群组中使用此命令。';
|
|
55
|
+
const subs = await ctx.database.get('github_subscription', {
|
|
56
|
+
channelId: session.channelId,
|
|
57
|
+
platform: session.platform || 'unknown',
|
|
58
|
+
});
|
|
59
|
+
if (subs.length === 0)
|
|
60
|
+
return '当前频道没有订阅。';
|
|
61
|
+
return subs.map(s => `${s.repo} [${s.events.join(', ')}]`).join('\n');
|
|
62
|
+
});
|
|
63
|
+
}
|
package/lib/config.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Schema } from 'koishi';
|
|
2
|
+
export interface Rule {
|
|
3
|
+
repo: string;
|
|
4
|
+
channelId: string;
|
|
5
|
+
platform?: string;
|
|
6
|
+
events: string[];
|
|
7
|
+
}
|
|
8
|
+
export interface Config {
|
|
9
|
+
defaultOwner?: string;
|
|
10
|
+
defaultRepo?: string;
|
|
11
|
+
debug: boolean;
|
|
12
|
+
rules?: Rule[];
|
|
13
|
+
}
|
|
14
|
+
export declare const Config: Schema<Config>;
|
package/lib/config.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Config = void 0;
|
|
4
|
+
const koishi_1 = require("koishi");
|
|
5
|
+
exports.Config = koishi_1.Schema.object({
|
|
6
|
+
defaultOwner: koishi_1.Schema.string().description('默认仓库拥有者'),
|
|
7
|
+
defaultRepo: koishi_1.Schema.string().description('默认仓库名称'),
|
|
8
|
+
debug: koishi_1.Schema.boolean().default(false).description('启用调试模式,输出详细日志'),
|
|
9
|
+
rules: koishi_1.Schema.array(koishi_1.Schema.object({
|
|
10
|
+
repo: koishi_1.Schema.string().required(),
|
|
11
|
+
channelId: koishi_1.Schema.string().required(),
|
|
12
|
+
platform: koishi_1.Schema.string(),
|
|
13
|
+
events: koishi_1.Schema.array(koishi_1.Schema.string()).default(['push', 'issues', 'pull_request']),
|
|
14
|
+
})).hidden().description('已废弃,请使用数据库管理订阅'),
|
|
15
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Context } from 'koishi';
|
|
2
|
+
declare module 'koishi' {
|
|
3
|
+
interface Tables {
|
|
4
|
+
github_subscription: GithubSubscription;
|
|
5
|
+
github_trusted_repo: GithubTrustedRepo;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export interface GithubSubscription {
|
|
9
|
+
id: number;
|
|
10
|
+
repo: string;
|
|
11
|
+
channelId: string;
|
|
12
|
+
platform: string;
|
|
13
|
+
events: string[];
|
|
14
|
+
}
|
|
15
|
+
export interface GithubTrustedRepo {
|
|
16
|
+
id: number;
|
|
17
|
+
repo: string;
|
|
18
|
+
enabled: boolean;
|
|
19
|
+
addedBy: string;
|
|
20
|
+
addedAt: Date;
|
|
21
|
+
}
|
|
22
|
+
export declare function apply(ctx: Context): void;
|
package/lib/database.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.apply = apply;
|
|
4
|
+
function apply(ctx) {
|
|
5
|
+
ctx.model.extend('github_subscription', {
|
|
6
|
+
id: 'unsigned',
|
|
7
|
+
repo: 'string',
|
|
8
|
+
channelId: 'string',
|
|
9
|
+
platform: 'string',
|
|
10
|
+
events: 'list',
|
|
11
|
+
}, {
|
|
12
|
+
autoInc: true,
|
|
13
|
+
});
|
|
14
|
+
ctx.model.extend('github_trusted_repo', {
|
|
15
|
+
id: 'unsigned',
|
|
16
|
+
repo: 'string',
|
|
17
|
+
enabled: { type: 'boolean', initial: true },
|
|
18
|
+
addedBy: 'string',
|
|
19
|
+
addedAt: 'timestamp',
|
|
20
|
+
}, {
|
|
21
|
+
autoInc: true,
|
|
22
|
+
unique: ['repo'],
|
|
23
|
+
});
|
|
24
|
+
}
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Context } from 'koishi';
|
|
2
|
+
import { Config } from './config';
|
|
3
|
+
export declare const name = "githubsth";
|
|
4
|
+
export declare const inject: {
|
|
5
|
+
required: string[];
|
|
6
|
+
optional: string[];
|
|
7
|
+
};
|
|
8
|
+
export * from './config';
|
|
9
|
+
export declare function apply(ctx: Context, config: Config): void;
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
36
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
37
|
+
};
|
|
38
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
39
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
40
|
+
};
|
|
41
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
|
+
exports.inject = exports.name = void 0;
|
|
43
|
+
exports.apply = apply;
|
|
44
|
+
const commands = __importStar(require("./commands"));
|
|
45
|
+
const admin = __importStar(require("./commands/admin"));
|
|
46
|
+
const subscribe = __importStar(require("./commands/subscribe"));
|
|
47
|
+
const database = __importStar(require("./database"));
|
|
48
|
+
const zh_CN_1 = __importDefault(require("./locales/zh-CN"));
|
|
49
|
+
const notifier_1 = require("./services/notifier");
|
|
50
|
+
const formatter_1 = require("./services/formatter");
|
|
51
|
+
exports.name = 'githubsth';
|
|
52
|
+
exports.inject = {
|
|
53
|
+
required: ['database'],
|
|
54
|
+
optional: ['github']
|
|
55
|
+
};
|
|
56
|
+
__exportStar(require("./config"), exports);
|
|
57
|
+
function apply(ctx, config) {
|
|
58
|
+
// 本地化
|
|
59
|
+
ctx.i18n.define('zh-CN', zh_CN_1.default);
|
|
60
|
+
// 数据库
|
|
61
|
+
ctx.plugin(database);
|
|
62
|
+
// 注册服务
|
|
63
|
+
ctx.plugin(formatter_1.Formatter);
|
|
64
|
+
ctx.plugin(notifier_1.Notifier, config);
|
|
65
|
+
// 注册命令
|
|
66
|
+
ctx.plugin(commands, config);
|
|
67
|
+
ctx.plugin(admin);
|
|
68
|
+
ctx.plugin(subscribe);
|
|
69
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
declare const _default: {
|
|
2
|
+
commands: {
|
|
3
|
+
githubsth: {
|
|
4
|
+
description: string;
|
|
5
|
+
messages: {
|
|
6
|
+
repo_info: string;
|
|
7
|
+
error: string;
|
|
8
|
+
specify_repo: string;
|
|
9
|
+
not_found: string;
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
'githubsth.repo': {
|
|
13
|
+
description: string;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
export default _default;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = {
|
|
4
|
+
commands: {
|
|
5
|
+
githubsth: {
|
|
6
|
+
description: 'GitHub 交互插件',
|
|
7
|
+
messages: {
|
|
8
|
+
repo_info: '仓库: {0}/{1}\n描述: {2}\nStars: {3}',
|
|
9
|
+
error: '获取信息失败: {0}',
|
|
10
|
+
specify_repo: '请指定仓库名称。',
|
|
11
|
+
not_found: '未找到仓库或无权限访问。'
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
'githubsth.repo': {
|
|
15
|
+
description: '获取仓库信息'
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Context, Service, h } from 'koishi';
|
|
2
|
+
declare module 'koishi' {
|
|
3
|
+
interface Context {
|
|
4
|
+
formatter: Formatter;
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
export declare class Formatter extends Service {
|
|
8
|
+
constructor(ctx: Context);
|
|
9
|
+
formatPush(payload: any): h | null;
|
|
10
|
+
formatIssue(payload: any): h;
|
|
11
|
+
formatPullRequest(payload: any): h;
|
|
12
|
+
formatStar(payload: any): h | null;
|
|
13
|
+
formatFork(payload: any): h;
|
|
14
|
+
formatRelease(payload: any): h | null;
|
|
15
|
+
formatDiscussion(payload: any): h;
|
|
16
|
+
formatWorkflowRun(payload: any): h | null;
|
|
17
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Formatter = void 0;
|
|
4
|
+
const koishi_1 = require("koishi");
|
|
5
|
+
class Formatter extends koishi_1.Service {
|
|
6
|
+
constructor(ctx) {
|
|
7
|
+
super(ctx, 'formatter');
|
|
8
|
+
}
|
|
9
|
+
formatPush(payload) {
|
|
10
|
+
const { repository, pusher, commits, compare } = payload;
|
|
11
|
+
if (!commits || commits.length === 0)
|
|
12
|
+
return null;
|
|
13
|
+
const commitLines = commits.map((c) => {
|
|
14
|
+
const shortHash = c.id.substring(0, 7);
|
|
15
|
+
const message = c.message.split('\n')[0];
|
|
16
|
+
return `[${shortHash}] ${message} - ${c.author.name}`;
|
|
17
|
+
}).join('\n');
|
|
18
|
+
return (0, koishi_1.h)('message', [
|
|
19
|
+
(0, koishi_1.h)('text', { content: `[GitHub] ${repository.full_name} 收到新的推送\n` }),
|
|
20
|
+
(0, koishi_1.h)('text', { content: `提交者: ${pusher.name}\n` }),
|
|
21
|
+
(0, koishi_1.h)('text', { content: `分支: ${payload.ref.replace('refs/heads/', '')}\n` }),
|
|
22
|
+
(0, koishi_1.h)('text', { content: `详情: ${compare}\n` }),
|
|
23
|
+
(0, koishi_1.h)('text', { content: commitLines })
|
|
24
|
+
]);
|
|
25
|
+
}
|
|
26
|
+
formatIssue(payload) {
|
|
27
|
+
const { action, issue, repository, sender } = payload;
|
|
28
|
+
return (0, koishi_1.h)('message', [
|
|
29
|
+
(0, koishi_1.h)('text', { content: `[GitHub] ${repository.full_name} Issue ${action}\n` }),
|
|
30
|
+
(0, koishi_1.h)('text', { content: `标题: #${issue.number} ${issue.title}\n` }),
|
|
31
|
+
(0, koishi_1.h)('text', { content: `发起人: ${sender.login}\n` }),
|
|
32
|
+
(0, koishi_1.h)('text', { content: `链接: ${issue.html_url}` })
|
|
33
|
+
]);
|
|
34
|
+
}
|
|
35
|
+
formatPullRequest(payload) {
|
|
36
|
+
const { action, pull_request, repository, sender } = payload;
|
|
37
|
+
return (0, koishi_1.h)('message', [
|
|
38
|
+
(0, koishi_1.h)('text', { content: `[GitHub] ${repository.full_name} PR ${action}\n` }),
|
|
39
|
+
(0, koishi_1.h)('text', { content: `标题: #${pull_request.number} ${pull_request.title}\n` }),
|
|
40
|
+
(0, koishi_1.h)('text', { content: `发起人: ${sender.login}\n` }),
|
|
41
|
+
(0, koishi_1.h)('text', { content: `状态: ${pull_request.state}\n` }),
|
|
42
|
+
(0, koishi_1.h)('text', { content: `链接: ${pull_request.html_url}` })
|
|
43
|
+
]);
|
|
44
|
+
}
|
|
45
|
+
formatStar(payload) {
|
|
46
|
+
const { action, repository, sender } = payload;
|
|
47
|
+
if (action !== 'created')
|
|
48
|
+
return null;
|
|
49
|
+
return (0, koishi_1.h)('message', [
|
|
50
|
+
(0, koishi_1.h)('text', { content: `[GitHub] ${sender.login} Star 了仓库 ${repository.full_name} 🌟\n` }),
|
|
51
|
+
(0, koishi_1.h)('text', { content: `当前 Star 数: ${repository.stargazers_count}` })
|
|
52
|
+
]);
|
|
53
|
+
}
|
|
54
|
+
formatFork(payload) {
|
|
55
|
+
const { forkee, repository, sender } = payload;
|
|
56
|
+
return (0, koishi_1.h)('message', [
|
|
57
|
+
(0, koishi_1.h)('text', { content: `[GitHub] ${sender.login} Fork 了仓库 ${repository.full_name}\n` }),
|
|
58
|
+
(0, koishi_1.h)('text', { content: `新仓库: ${forkee.full_name}` })
|
|
59
|
+
]);
|
|
60
|
+
}
|
|
61
|
+
formatRelease(payload) {
|
|
62
|
+
const { action, release, repository } = payload;
|
|
63
|
+
if (action !== 'published')
|
|
64
|
+
return null;
|
|
65
|
+
return (0, koishi_1.h)('message', [
|
|
66
|
+
(0, koishi_1.h)('text', { content: `[GitHub] ${repository.full_name} 发布了新版本 ${release.tag_name} 🎉\n` }),
|
|
67
|
+
(0, koishi_1.h)('text', { content: `标题: ${release.name}\n` }),
|
|
68
|
+
(0, koishi_1.h)('text', { content: `链接: ${release.html_url}` })
|
|
69
|
+
]);
|
|
70
|
+
}
|
|
71
|
+
formatDiscussion(payload) {
|
|
72
|
+
const { action, discussion, repository, sender } = payload;
|
|
73
|
+
return (0, koishi_1.h)('message', [
|
|
74
|
+
(0, koishi_1.h)('text', { content: `[GitHub] ${repository.full_name} Discussion ${action}\n` }),
|
|
75
|
+
(0, koishi_1.h)('text', { content: `标题: #${discussion.number} ${discussion.title}\n` }),
|
|
76
|
+
(0, koishi_1.h)('text', { content: `发起人: ${sender.login}\n` }),
|
|
77
|
+
(0, koishi_1.h)('text', { content: `链接: ${discussion.html_url}` })
|
|
78
|
+
]);
|
|
79
|
+
}
|
|
80
|
+
formatWorkflowRun(payload) {
|
|
81
|
+
const { action, workflow_run, repository } = payload;
|
|
82
|
+
if (action !== 'completed')
|
|
83
|
+
return null;
|
|
84
|
+
const statusIcon = workflow_run.conclusion === 'success' ? '✅' : '❌';
|
|
85
|
+
return (0, koishi_1.h)('message', [
|
|
86
|
+
(0, koishi_1.h)('text', { content: `[GitHub] ${repository.full_name} 工作流运行完成 ${statusIcon}\n` }),
|
|
87
|
+
(0, koishi_1.h)('text', { content: `工作流: ${workflow_run.name}\n` }),
|
|
88
|
+
(0, koishi_1.h)('text', { content: `结果: ${workflow_run.conclusion}\n` }),
|
|
89
|
+
(0, koishi_1.h)('text', { content: `分支: ${workflow_run.head_branch}\n` }),
|
|
90
|
+
(0, koishi_1.h)('text', { content: `链接: ${workflow_run.html_url}` })
|
|
91
|
+
]);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
exports.Formatter = Formatter;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Context, Service } from 'koishi';
|
|
2
|
+
import { Config } from '../config';
|
|
3
|
+
declare module 'koishi' {
|
|
4
|
+
interface Context {
|
|
5
|
+
notifier: Notifier;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export declare class Notifier extends Service {
|
|
9
|
+
config: Config;
|
|
10
|
+
constructor(ctx: Context, config: Config);
|
|
11
|
+
private registerListeners;
|
|
12
|
+
private handleEvent;
|
|
13
|
+
private sendMessage;
|
|
14
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Notifier = void 0;
|
|
4
|
+
const koishi_1 = require("koishi");
|
|
5
|
+
class Notifier extends koishi_1.Service {
|
|
6
|
+
// @ts-ignore
|
|
7
|
+
constructor(ctx, config) {
|
|
8
|
+
super(ctx, 'notifier', true);
|
|
9
|
+
this.config = config;
|
|
10
|
+
this.registerListeners();
|
|
11
|
+
}
|
|
12
|
+
registerListeners() {
|
|
13
|
+
this.ctx.on('github/push', (payload) => this.handleEvent('push', payload));
|
|
14
|
+
this.ctx.on('github/issues', (payload) => this.handleEvent('issues', payload));
|
|
15
|
+
this.ctx.on('github/pull_request', (payload) => this.handleEvent('pull_request', payload));
|
|
16
|
+
this.ctx.on('github/star', (payload) => this.handleEvent('star', payload));
|
|
17
|
+
this.ctx.on('github/fork', (payload) => this.handleEvent('fork', payload));
|
|
18
|
+
this.ctx.on('github/release', (payload) => this.handleEvent('release', payload));
|
|
19
|
+
this.ctx.on('github/discussion', (payload) => this.handleEvent('discussion', payload));
|
|
20
|
+
this.ctx.on('github/workflow_run', (payload) => this.handleEvent('workflow_run', payload));
|
|
21
|
+
}
|
|
22
|
+
async handleEvent(event, payload) {
|
|
23
|
+
const repoName = payload.repository?.full_name;
|
|
24
|
+
if (!repoName)
|
|
25
|
+
return;
|
|
26
|
+
if (this.config.debug) {
|
|
27
|
+
this.ctx.logger('notifier').info(`Received event ${event} for ${repoName}`);
|
|
28
|
+
this.ctx.logger('notifier').debug(JSON.stringify(payload, null, 2));
|
|
29
|
+
}
|
|
30
|
+
// Get rules from database
|
|
31
|
+
const dbRules = await this.ctx.database.get('github_subscription', {
|
|
32
|
+
repo: repoName
|
|
33
|
+
});
|
|
34
|
+
// Combine with config rules (if any, for backward compatibility or static rules)
|
|
35
|
+
const configRules = (this.config.rules || []).filter((r) => r.repo === repoName || r.repo === '*');
|
|
36
|
+
const allRules = [
|
|
37
|
+
...dbRules.map(r => ({ ...r, platform: r.platform })),
|
|
38
|
+
...configRules
|
|
39
|
+
];
|
|
40
|
+
const matchedRules = allRules.filter(rule => {
|
|
41
|
+
// Event match
|
|
42
|
+
if (!rule.events.includes('*') && !rule.events.includes(event))
|
|
43
|
+
return false;
|
|
44
|
+
return true;
|
|
45
|
+
});
|
|
46
|
+
if (matchedRules.length === 0)
|
|
47
|
+
return;
|
|
48
|
+
let message = null;
|
|
49
|
+
// Ensure formatter is loaded
|
|
50
|
+
if (!this.ctx.formatter) {
|
|
51
|
+
this.ctx.logger('notifier').warn('Formatter service not available');
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
switch (event) {
|
|
55
|
+
case 'push':
|
|
56
|
+
message = this.ctx.formatter.formatPush(payload);
|
|
57
|
+
break;
|
|
58
|
+
case 'issues':
|
|
59
|
+
message = this.ctx.formatter.formatIssue(payload);
|
|
60
|
+
break;
|
|
61
|
+
case 'pull_request':
|
|
62
|
+
message = this.ctx.formatter.formatPullRequest(payload);
|
|
63
|
+
break;
|
|
64
|
+
case 'star':
|
|
65
|
+
message = this.ctx.formatter.formatStar(payload);
|
|
66
|
+
break;
|
|
67
|
+
case 'fork':
|
|
68
|
+
message = this.ctx.formatter.formatFork(payload);
|
|
69
|
+
break;
|
|
70
|
+
case 'release':
|
|
71
|
+
message = this.ctx.formatter.formatRelease(payload);
|
|
72
|
+
break;
|
|
73
|
+
case 'discussion':
|
|
74
|
+
message = this.ctx.formatter.formatDiscussion(payload);
|
|
75
|
+
break;
|
|
76
|
+
case 'workflow_run':
|
|
77
|
+
message = this.ctx.formatter.formatWorkflowRun(payload);
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
if (!message)
|
|
81
|
+
return;
|
|
82
|
+
for (const rule of matchedRules) {
|
|
83
|
+
await this.sendMessage(rule, message);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
async sendMessage(rule, message) {
|
|
87
|
+
// Find suitable bots
|
|
88
|
+
const bots = this.ctx.bots.filter(bot => {
|
|
89
|
+
if (rule.platform)
|
|
90
|
+
return bot.platform === rule.platform;
|
|
91
|
+
return true; // If platform not specified, try all
|
|
92
|
+
});
|
|
93
|
+
if (bots.length === 0) {
|
|
94
|
+
if (this.config.debug) {
|
|
95
|
+
this.ctx.logger('notifier').debug(`No bot found for channel ${rule.channelId} (platform: ${rule.platform})`);
|
|
96
|
+
}
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
let sent = false;
|
|
100
|
+
for (const bot of bots) {
|
|
101
|
+
try {
|
|
102
|
+
await bot.sendMessage(rule.channelId, message);
|
|
103
|
+
sent = true;
|
|
104
|
+
if (this.config.debug) {
|
|
105
|
+
this.ctx.logger('notifier').info(`Sent message to ${rule.channelId} via ${bot.platform}:${bot.selfId}`);
|
|
106
|
+
}
|
|
107
|
+
break; // Break on first success
|
|
108
|
+
}
|
|
109
|
+
catch (e) {
|
|
110
|
+
if (this.config.debug) {
|
|
111
|
+
this.ctx.logger('notifier').warn(`Bot ${bot.sid} failed to send message: ${e}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (!sent) {
|
|
116
|
+
this.ctx.logger('notifier').warn(`Failed to send message to ${rule.channelId}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
exports.Notifier = Notifier;
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "koishi-plugin-githubsth",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Github Subscriptions Notifications, push notifications for GitHub subscriptions For koishi",
|
|
5
|
+
"main": "lib/index.js",
|
|
6
|
+
"typings": "lib/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"lib",
|
|
9
|
+
"dist"
|
|
10
|
+
],
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsc",
|
|
14
|
+
"dev": "tsc -w",
|
|
15
|
+
"test": "jest"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"koishi",
|
|
19
|
+
"plugin",
|
|
20
|
+
"github",
|
|
21
|
+
"bot"
|
|
22
|
+
],
|
|
23
|
+
"peerDependencies": {
|
|
24
|
+
"koishi": "^4.18.0",
|
|
25
|
+
"koishi-plugin-adapter-github": "^1.0.0"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@types/jest": "^29.5.0",
|
|
29
|
+
"@types/node": "^20.0.0",
|
|
30
|
+
"jest": "^29.5.0",
|
|
31
|
+
"koishi": "^4.18.0",
|
|
32
|
+
"ts-jest": "^29.1.0",
|
|
33
|
+
"typescript": "^5.0.0"
|
|
34
|
+
},
|
|
35
|
+
"koishi": {
|
|
36
|
+
"description": {
|
|
37
|
+
"en": "Github Subscriptions Notifications, push notifications for GitHub subscriptions",
|
|
38
|
+
"zh": "GitHub订阅推送通知,依赖于Github适配器"
|
|
39
|
+
},
|
|
40
|
+
"service": {
|
|
41
|
+
"required": [
|
|
42
|
+
"database"
|
|
43
|
+
],
|
|
44
|
+
"optional": [
|
|
45
|
+
"github"
|
|
46
|
+
]
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|