koishi-plugin-githubsth 1.0.2-test1 → 1.0.2
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/lib/commands/subscribe.d.ts +1 -1
- package/lib/commands/subscribe.js +62 -5
- package/lib/config.d.ts +2 -0
- package/lib/config.js +4 -0
- package/lib/services/formatter.js +47 -44
- package/lib/services/notifier.d.ts +1 -0
- package/lib/services/notifier.js +214 -38
- package/package.json +1 -1
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import { Context } from 'koishi';
|
|
2
|
-
export declare function apply(ctx: Context): void;
|
|
2
|
+
export declare function apply(ctx: Context, config: any): void;
|
|
@@ -1,11 +1,37 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.apply = apply;
|
|
4
|
-
function apply(ctx) {
|
|
4
|
+
function apply(ctx, config) {
|
|
5
5
|
const logger = ctx.logger('githubsth');
|
|
6
6
|
const repoRegex = /^[\w-]+\/[\w-\.]+$/;
|
|
7
|
+
const validEvents = [
|
|
8
|
+
'push', 'issues', 'issue_comment', 'pull_request',
|
|
9
|
+
'pull_request_review', 'star', 'fork', 'release',
|
|
10
|
+
'discussion', 'workflow_run'
|
|
11
|
+
];
|
|
12
|
+
const defaultConfigEvents = config.defaultEvents || ['push', 'issues', 'issue_comment', 'pull_request', 'pull_request_review', 'release', 'star', 'fork'];
|
|
7
13
|
ctx.command('githubsth.subscribe <repo> [events:text]', '订阅 GitHub 仓库')
|
|
8
14
|
.alias('gh.sub')
|
|
15
|
+
.usage(`
|
|
16
|
+
订阅 GitHub 仓库通知。
|
|
17
|
+
如果不指定事件,默认订阅: ${defaultConfigEvents.join(', ')}
|
|
18
|
+
|
|
19
|
+
可选事件:
|
|
20
|
+
- push: 代码推送
|
|
21
|
+
- issues: Issue 创建/关闭/重开
|
|
22
|
+
- issue_comment: Issue 评论
|
|
23
|
+
- pull_request: PR 创建/关闭/重开
|
|
24
|
+
- pull_request_review: PR 审查
|
|
25
|
+
- star: 标星
|
|
26
|
+
- fork: 仓库 Fork
|
|
27
|
+
- release: 发布新版本
|
|
28
|
+
- discussion: 讨论区更新
|
|
29
|
+
- workflow_run: Workflow 运行
|
|
30
|
+
|
|
31
|
+
示例:
|
|
32
|
+
gh.sub koishijs/koishi
|
|
33
|
+
gh.sub koishijs/koishi push,issues,star
|
|
34
|
+
`)
|
|
9
35
|
.action(async ({ session }, repo, eventsStr) => {
|
|
10
36
|
if (!repo)
|
|
11
37
|
return '请指定仓库名称 (owner/repo)。';
|
|
@@ -19,15 +45,46 @@ function apply(ctx) {
|
|
|
19
45
|
return '该仓库不在信任列表中,无法订阅。请联系管理员添加。';
|
|
20
46
|
}
|
|
21
47
|
// Parse events
|
|
22
|
-
|
|
48
|
+
let events;
|
|
49
|
+
if (eventsStr) {
|
|
50
|
+
// Split by comma, Chinese comma, or whitespace
|
|
51
|
+
events = eventsStr.split(/[,,\s]+/).map(e => e.trim()).filter(Boolean);
|
|
52
|
+
// Normalize events (kebab-case to snake_case)
|
|
53
|
+
events = events.map(e => e.replace(/-/g, '_'));
|
|
54
|
+
// Validate events
|
|
55
|
+
const invalidEvents = events.filter(e => !validEvents.includes(e) && e !== '*');
|
|
56
|
+
if (invalidEvents.length > 0) {
|
|
57
|
+
return `无效的事件类型: ${invalidEvents.join(', ')}。\n可选事件: ${validEvents.join(', ')}`;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
// Default events
|
|
62
|
+
events = [...defaultConfigEvents];
|
|
63
|
+
}
|
|
23
64
|
try {
|
|
24
|
-
|
|
65
|
+
// Check if subscription exists
|
|
66
|
+
const existing = await ctx.database.get('github_subscription', {
|
|
25
67
|
repo,
|
|
26
68
|
channelId: session.channelId,
|
|
27
69
|
platform: session.platform || 'unknown',
|
|
28
|
-
events,
|
|
29
70
|
});
|
|
30
|
-
|
|
71
|
+
if (existing.length > 0) {
|
|
72
|
+
// Update existing subscription
|
|
73
|
+
await ctx.database.set('github_subscription', { id: existing[0].id }, {
|
|
74
|
+
events,
|
|
75
|
+
});
|
|
76
|
+
return `已更新 ${repo} 的订阅,当前监听事件: ${events.join(', ')}。`;
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
// Create new subscription
|
|
80
|
+
await ctx.database.create('github_subscription', {
|
|
81
|
+
repo,
|
|
82
|
+
channelId: session.channelId,
|
|
83
|
+
platform: session.platform || 'unknown',
|
|
84
|
+
events,
|
|
85
|
+
});
|
|
86
|
+
return `已订阅 ${repo} 的 ${events.join(', ')} 事件。`;
|
|
87
|
+
}
|
|
31
88
|
}
|
|
32
89
|
catch (e) {
|
|
33
90
|
logger.warn(e);
|
package/lib/config.d.ts
CHANGED
package/lib/config.js
CHANGED
|
@@ -6,6 +6,10 @@ exports.Config = koishi_1.Schema.object({
|
|
|
6
6
|
defaultOwner: koishi_1.Schema.string().description('默认仓库拥有者'),
|
|
7
7
|
defaultRepo: koishi_1.Schema.string().description('默认仓库名称'),
|
|
8
8
|
debug: koishi_1.Schema.boolean().default(false).description('启用调试模式,输出详细日志'),
|
|
9
|
+
logUnhandledEvents: koishi_1.Schema.boolean().default(false).description('是否记录未处理的 Webhook 事件 (Unknown events)'),
|
|
10
|
+
defaultEvents: koishi_1.Schema.array(koishi_1.Schema.string())
|
|
11
|
+
.default(['push', 'issues', 'issue_comment', 'pull_request', 'pull_request_review', 'release', 'star', 'fork'])
|
|
12
|
+
.description('默认订阅事件列表 (当不指定事件时使用)'),
|
|
9
13
|
rules: koishi_1.Schema.array(koishi_1.Schema.object({
|
|
10
14
|
repo: koishi_1.Schema.string().required(),
|
|
11
15
|
channelId: koishi_1.Schema.string().required(),
|
|
@@ -8,39 +8,40 @@ class Formatter extends koishi_1.Service {
|
|
|
8
8
|
ctx.logger('githubsth').info('Formatter service initialized');
|
|
9
9
|
}
|
|
10
10
|
formatPush(payload) {
|
|
11
|
-
const { repository, pusher, commits, compare } = payload;
|
|
11
|
+
const { repository, pusher, commits, compare, ref } = payload;
|
|
12
12
|
if (!commits || commits.length === 0)
|
|
13
13
|
return null;
|
|
14
14
|
const commitLines = commits.map((c) => {
|
|
15
|
-
const shortHash = c.id
|
|
16
|
-
const message = c.message
|
|
17
|
-
|
|
15
|
+
const shortHash = c.id?.substring(0, 7) || '???????';
|
|
16
|
+
const message = c.message?.split('\n')[0] || 'No message';
|
|
17
|
+
const author = c.author?.name || 'Unknown';
|
|
18
|
+
return `[${shortHash}] ${message} - ${author}`;
|
|
18
19
|
}).join('\n');
|
|
19
20
|
return (0, koishi_1.h)('message', [
|
|
20
|
-
(0, koishi_1.h)('text', { content: `[GitHub] ${repository
|
|
21
|
-
(0, koishi_1.h)('text', { content: `提交者: ${pusher
|
|
22
|
-
(0, koishi_1.h)('text', { content: `分支: ${
|
|
23
|
-
(0, koishi_1.h)('text', { content: `详情: ${compare}\n` }),
|
|
21
|
+
(0, koishi_1.h)('text', { content: `[GitHub] ${repository?.full_name || 'Unknown Repo'} 收到新的推送\n` }),
|
|
22
|
+
(0, koishi_1.h)('text', { content: `提交者: ${pusher?.name || 'Unknown'}\n` }),
|
|
23
|
+
(0, koishi_1.h)('text', { content: `分支: ${ref ? ref.replace('refs/heads/', '') : 'unknown'}\n` }),
|
|
24
|
+
(0, koishi_1.h)('text', { content: `详情: ${compare || ''}\n` }),
|
|
24
25
|
(0, koishi_1.h)('text', { content: commitLines })
|
|
25
26
|
]);
|
|
26
27
|
}
|
|
27
28
|
formatIssue(payload) {
|
|
28
29
|
const { action, issue, repository, sender } = payload;
|
|
29
30
|
return (0, koishi_1.h)('message', [
|
|
30
|
-
(0, koishi_1.h)('text', { content: `[GitHub] ${repository
|
|
31
|
-
(0, koishi_1.h)('text', { content: `标题: #${issue
|
|
32
|
-
(0, koishi_1.h)('text', { content: `发起人: ${sender
|
|
33
|
-
(0, koishi_1.h)('text', { content: `链接: ${issue
|
|
31
|
+
(0, koishi_1.h)('text', { content: `[GitHub] ${repository?.full_name || 'Unknown Repo'} Issue ${action || 'updated'}\n` }),
|
|
32
|
+
(0, koishi_1.h)('text', { content: `标题: #${issue?.number || '?'} ${issue?.title || 'No Title'}\n` }),
|
|
33
|
+
(0, koishi_1.h)('text', { content: `发起人: ${sender?.login || 'Unknown'}\n` }),
|
|
34
|
+
(0, koishi_1.h)('text', { content: `链接: ${issue?.html_url || ''}` })
|
|
34
35
|
]);
|
|
35
36
|
}
|
|
36
37
|
formatPullRequest(payload) {
|
|
37
38
|
const { action, pull_request, repository, sender } = payload;
|
|
38
39
|
return (0, koishi_1.h)('message', [
|
|
39
|
-
(0, koishi_1.h)('text', { content: `[GitHub] ${repository
|
|
40
|
-
(0, koishi_1.h)('text', { content: `标题: #${pull_request
|
|
41
|
-
(0, koishi_1.h)('text', { content: `发起人: ${sender
|
|
42
|
-
(0, koishi_1.h)('text', { content: `状态: ${pull_request
|
|
43
|
-
(0, koishi_1.h)('text', { content: `链接: ${pull_request
|
|
40
|
+
(0, koishi_1.h)('text', { content: `[GitHub] ${repository?.full_name || 'Unknown Repo'} PR ${action || 'updated'}\n` }),
|
|
41
|
+
(0, koishi_1.h)('text', { content: `标题: #${pull_request?.number || '?'} ${pull_request?.title || 'No Title'}\n` }),
|
|
42
|
+
(0, koishi_1.h)('text', { content: `发起人: ${sender?.login || 'Unknown'}\n` }),
|
|
43
|
+
(0, koishi_1.h)('text', { content: `状态: ${pull_request?.state || 'unknown'}\n` }),
|
|
44
|
+
(0, koishi_1.h)('text', { content: `链接: ${pull_request?.html_url || ''}` })
|
|
44
45
|
]);
|
|
45
46
|
}
|
|
46
47
|
formatStar(payload) {
|
|
@@ -48,15 +49,15 @@ class Formatter extends koishi_1.Service {
|
|
|
48
49
|
if (action !== 'created')
|
|
49
50
|
return null;
|
|
50
51
|
return (0, koishi_1.h)('message', [
|
|
51
|
-
(0, koishi_1.h)('text', { content: `[GitHub] ${sender
|
|
52
|
-
(0, koishi_1.h)('text', { content: `当前 Star 数: ${repository.stargazers_count}` })
|
|
52
|
+
(0, koishi_1.h)('text', { content: `[GitHub] ${sender?.login || 'Unknown'} Star 了仓库 ${repository?.full_name || 'Unknown Repo'} 🌟\n` }),
|
|
53
|
+
(0, koishi_1.h)('text', { content: `当前 Star 数: ${repository?.stargazers_count !== undefined ? repository.stargazers_count : '?'}` })
|
|
53
54
|
]);
|
|
54
55
|
}
|
|
55
56
|
formatFork(payload) {
|
|
56
57
|
const { forkee, repository, sender } = payload;
|
|
57
58
|
return (0, koishi_1.h)('message', [
|
|
58
|
-
(0, koishi_1.h)('text', { content: `[GitHub] ${sender
|
|
59
|
-
(0, koishi_1.h)('text', { content: `新仓库: ${forkee
|
|
59
|
+
(0, koishi_1.h)('text', { content: `[GitHub] ${sender?.login || 'Unknown'} Fork 了仓库 ${repository?.full_name || 'Unknown Repo'}\n` }),
|
|
60
|
+
(0, koishi_1.h)('text', { content: `新仓库: ${forkee?.full_name || 'Unknown'}` })
|
|
60
61
|
]);
|
|
61
62
|
}
|
|
62
63
|
formatRelease(payload) {
|
|
@@ -64,43 +65,45 @@ class Formatter extends koishi_1.Service {
|
|
|
64
65
|
if (action !== 'published')
|
|
65
66
|
return null;
|
|
66
67
|
return (0, koishi_1.h)('message', [
|
|
67
|
-
(0, koishi_1.h)('text', { content: `[GitHub] ${repository
|
|
68
|
-
(0, koishi_1.h)('text', { content: `标题: ${release
|
|
69
|
-
(0, koishi_1.h)('text', { content: `链接: ${release
|
|
68
|
+
(0, koishi_1.h)('text', { content: `[GitHub] ${repository?.full_name || 'Unknown Repo'} 发布了新版本 ${release?.tag_name || 'unknown'} 🎉\n` }),
|
|
69
|
+
(0, koishi_1.h)('text', { content: `标题: ${release?.name || 'No Title'}\n` }),
|
|
70
|
+
(0, koishi_1.h)('text', { content: `链接: ${release?.html_url || ''}` })
|
|
70
71
|
]);
|
|
71
72
|
}
|
|
72
73
|
formatDiscussion(payload) {
|
|
73
74
|
const { action, discussion, repository, sender } = payload;
|
|
74
75
|
return (0, koishi_1.h)('message', [
|
|
75
|
-
(0, koishi_1.h)('text', { content: `[GitHub] ${repository
|
|
76
|
-
(0, koishi_1.h)('text', { content: `标题: #${discussion
|
|
77
|
-
(0, koishi_1.h)('text', { content: `发起人: ${sender
|
|
78
|
-
(0, koishi_1.h)('text', { content: `链接: ${discussion
|
|
76
|
+
(0, koishi_1.h)('text', { content: `[GitHub] ${repository?.full_name || 'Unknown Repo'} Discussion ${action || 'updated'}\n` }),
|
|
77
|
+
(0, koishi_1.h)('text', { content: `标题: #${discussion?.number || '?'} ${discussion?.title || 'No Title'}\n` }),
|
|
78
|
+
(0, koishi_1.h)('text', { content: `发起人: ${sender?.login || 'Unknown'}\n` }),
|
|
79
|
+
(0, koishi_1.h)('text', { content: `链接: ${discussion?.html_url || ''}` })
|
|
79
80
|
]);
|
|
80
81
|
}
|
|
81
82
|
formatWorkflowRun(payload) {
|
|
82
83
|
const { action, workflow_run, repository } = payload;
|
|
83
84
|
if (action !== 'completed')
|
|
84
85
|
return null;
|
|
85
|
-
const statusIcon = workflow_run
|
|
86
|
+
const statusIcon = workflow_run?.conclusion === 'success' ? '✅' : '❌';
|
|
86
87
|
return (0, koishi_1.h)('message', [
|
|
87
|
-
(0, koishi_1.h)('text', { content: `[GitHub] ${repository
|
|
88
|
-
(0, koishi_1.h)('text', { content: `工作流: ${workflow_run
|
|
89
|
-
(0, koishi_1.h)('text', { content: `结果: ${workflow_run
|
|
90
|
-
(0, koishi_1.h)('text', { content: `分支: ${workflow_run
|
|
91
|
-
(0, koishi_1.h)('text', { content: `链接: ${workflow_run
|
|
88
|
+
(0, koishi_1.h)('text', { content: `[GitHub] ${repository?.full_name || 'Unknown Repo'} 工作流运行完成 ${statusIcon}\n` }),
|
|
89
|
+
(0, koishi_1.h)('text', { content: `工作流: ${workflow_run?.name || 'Unknown'}\n` }),
|
|
90
|
+
(0, koishi_1.h)('text', { content: `结果: ${workflow_run?.conclusion || 'unknown'}\n` }),
|
|
91
|
+
(0, koishi_1.h)('text', { content: `分支: ${workflow_run?.head_branch || 'unknown'}\n` }),
|
|
92
|
+
(0, koishi_1.h)('text', { content: `链接: ${workflow_run?.html_url || ''}` })
|
|
92
93
|
]);
|
|
93
94
|
}
|
|
94
95
|
formatIssueComment(payload) {
|
|
95
96
|
const { action, issue, comment, repository, sender } = payload;
|
|
96
97
|
if (action !== 'created')
|
|
97
98
|
return null;
|
|
99
|
+
const body = comment?.body || '';
|
|
100
|
+
const shortBody = body.length > 100 ? body.substring(0, 100) + '...' : body;
|
|
98
101
|
return (0, koishi_1.h)('message', [
|
|
99
|
-
(0, koishi_1.h)('text', { content: `[GitHub] ${repository
|
|
100
|
-
(0, koishi_1.h)('text', { content: `标题: #${issue
|
|
101
|
-
(0, koishi_1.h)('text', { content: `评论人: ${sender
|
|
102
|
-
(0, koishi_1.h)('text', { content: `内容: ${
|
|
103
|
-
(0, koishi_1.h)('text', { content: `链接: ${comment
|
|
102
|
+
(0, koishi_1.h)('text', { content: `[GitHub] ${repository?.full_name || 'Unknown Repo'} Issue 收到新评论\n` }),
|
|
103
|
+
(0, koishi_1.h)('text', { content: `标题: #${issue?.number || '?'} ${issue?.title || 'No Title'}\n` }),
|
|
104
|
+
(0, koishi_1.h)('text', { content: `评论人: ${sender?.login || 'Unknown'}\n` }),
|
|
105
|
+
(0, koishi_1.h)('text', { content: `内容: ${shortBody}\n` }),
|
|
106
|
+
(0, koishi_1.h)('text', { content: `链接: ${comment?.html_url || ''}` })
|
|
104
107
|
]);
|
|
105
108
|
}
|
|
106
109
|
formatPullRequestReview(payload) {
|
|
@@ -108,11 +111,11 @@ class Formatter extends koishi_1.Service {
|
|
|
108
111
|
if (action !== 'submitted')
|
|
109
112
|
return null;
|
|
110
113
|
return (0, koishi_1.h)('message', [
|
|
111
|
-
(0, koishi_1.h)('text', { content: `[GitHub] ${repository
|
|
112
|
-
(0, koishi_1.h)('text', { content: `标题: #${pull_request
|
|
113
|
-
(0, koishi_1.h)('text', { content: `Reviewer: ${sender
|
|
114
|
-
(0, koishi_1.h)('text', { content: `状态: ${review
|
|
115
|
-
(0, koishi_1.h)('text', { content: `链接: ${review
|
|
114
|
+
(0, koishi_1.h)('text', { content: `[GitHub] ${repository?.full_name || 'Unknown Repo'} PR 收到 Review\n` }),
|
|
115
|
+
(0, koishi_1.h)('text', { content: `标题: #${pull_request?.number || '?'} ${pull_request?.title || 'No Title'}\n` }),
|
|
116
|
+
(0, koishi_1.h)('text', { content: `Reviewer: ${sender?.login || 'Unknown'}\n` }),
|
|
117
|
+
(0, koishi_1.h)('text', { content: `状态: ${review?.state || 'unknown'}\n` }),
|
|
118
|
+
(0, koishi_1.h)('text', { content: `链接: ${review?.html_url || ''}` })
|
|
116
119
|
]);
|
|
117
120
|
}
|
|
118
121
|
}
|
package/lib/services/notifier.js
CHANGED
|
@@ -34,31 +34,40 @@ class Notifier extends koishi_1.Service {
|
|
|
34
34
|
if (this.config.debug) {
|
|
35
35
|
this.ctx.logger('githubsth').info('Found payload in session, attempting to handle');
|
|
36
36
|
}
|
|
37
|
+
// Check if payload is wrapped (adapter-github structure)
|
|
38
|
+
const realPayload = payload.payload || payload;
|
|
37
39
|
// Infer event type
|
|
38
40
|
let eventType = 'unknown';
|
|
39
|
-
|
|
41
|
+
// Check inner payload first if it exists
|
|
42
|
+
if (realPayload.issue && realPayload.comment)
|
|
40
43
|
eventType = 'issue_comment';
|
|
41
|
-
else if (
|
|
44
|
+
else if (realPayload.issue)
|
|
42
45
|
eventType = 'issues';
|
|
43
|
-
else if (
|
|
46
|
+
else if (realPayload.pull_request && realPayload.review)
|
|
44
47
|
eventType = 'pull_request_review';
|
|
45
|
-
else if (
|
|
48
|
+
else if (realPayload.pull_request)
|
|
46
49
|
eventType = 'pull_request';
|
|
47
|
-
else if (
|
|
50
|
+
else if (realPayload.commits)
|
|
48
51
|
eventType = 'push';
|
|
49
|
-
else if (
|
|
52
|
+
else if (realPayload.starred_at !== undefined || (realPayload.action === 'started'))
|
|
50
53
|
eventType = 'star'; // star event usually has action 'created' but check payload structure
|
|
51
|
-
else if (
|
|
54
|
+
else if (realPayload.forkee)
|
|
52
55
|
eventType = 'fork';
|
|
53
|
-
else if (
|
|
56
|
+
else if (realPayload.release)
|
|
54
57
|
eventType = 'release';
|
|
55
|
-
else if (
|
|
58
|
+
else if (realPayload.discussion)
|
|
56
59
|
eventType = 'discussion';
|
|
57
|
-
else if (
|
|
60
|
+
else if (realPayload.workflow_run)
|
|
58
61
|
eventType = 'workflow_run';
|
|
62
|
+
// Handle raw star event if it has repository info directly
|
|
63
|
+
else if (realPayload.repository && (realPayload.action === 'created' || realPayload.action === 'started'))
|
|
64
|
+
eventType = 'star';
|
|
59
65
|
if (eventType !== 'unknown') {
|
|
60
66
|
this.handleEvent(eventType, payload);
|
|
61
67
|
}
|
|
68
|
+
else if (this.config.logUnhandledEvents) {
|
|
69
|
+
this.ctx.logger('githubsth').info(`Unhandled payload structure. Keys: ${Object.keys(realPayload).join(', ')}`);
|
|
70
|
+
}
|
|
62
71
|
}
|
|
63
72
|
});
|
|
64
73
|
}
|
|
@@ -69,6 +78,14 @@ class Notifier extends koishi_1.Service {
|
|
|
69
78
|
// Check if payload is nested in an 'event' object (common in some adapter versions)
|
|
70
79
|
// or if the event data is directly in payload
|
|
71
80
|
const realPayload = payload.payload || payload;
|
|
81
|
+
// Extract sender from wrapper if available (adapter-github often puts it in 'actor')
|
|
82
|
+
if (payload.actor && !realPayload.sender) {
|
|
83
|
+
realPayload.sender = payload.actor;
|
|
84
|
+
}
|
|
85
|
+
// Extract repository from wrapper if available
|
|
86
|
+
if (payload.repository && !realPayload.repository) {
|
|
87
|
+
realPayload.repository = payload.repository;
|
|
88
|
+
}
|
|
72
89
|
let repoName = realPayload.repository?.full_name;
|
|
73
90
|
// Try to fallback if repoName is missing
|
|
74
91
|
if (!repoName && realPayload.issue?.repository_url) {
|
|
@@ -80,17 +97,57 @@ class Notifier extends koishi_1.Service {
|
|
|
80
97
|
if (!repoName && realPayload.pull_request?.base?.repo?.full_name) {
|
|
81
98
|
repoName = realPayload.pull_request.base.repo.full_name;
|
|
82
99
|
}
|
|
100
|
+
// Special handling for 'star' event (which might be 'watch' event with action 'started')
|
|
101
|
+
// The payload might be missing repository info in the main object but have it in the original session payload
|
|
102
|
+
if (!repoName && event === 'star') {
|
|
103
|
+
// Sometimes the repository info is at the root of the payload, not inside 'payload' property
|
|
104
|
+
if (payload.repository?.full_name) {
|
|
105
|
+
repoName = payload.repository.full_name;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
83
108
|
if (!repoName) {
|
|
84
109
|
if (this.config.debug) {
|
|
85
|
-
this.ctx.logger('githubsth').warn(`Missing repo info for event: ${event}`);
|
|
86
|
-
this.ctx.logger('notifier').warn(`Event ${event} missing repository info. Keys: ${Object.keys(realPayload).join(', ')}`);
|
|
110
|
+
this.ctx.logger('githubsth').warn(`Missing repo info for event: ${event}. Keys: ${Object.keys(realPayload).join(', ')}`);
|
|
87
111
|
}
|
|
88
|
-
else {
|
|
89
|
-
// Log at warning level
|
|
90
|
-
// But only log keys to avoid leaking sensitive data
|
|
112
|
+
else if (this.config.logUnhandledEvents) {
|
|
113
|
+
// Log at warning level if repo info is missing and logUnhandledEvents is on
|
|
91
114
|
this.ctx.logger('githubsth').warn(`Missing repo info for event: ${event}. Keys: ${Object.keys(realPayload).join(', ')}`);
|
|
92
115
|
}
|
|
93
|
-
return
|
|
116
|
+
// Do not return here, let patching logic handle it with defaultRepo
|
|
117
|
+
}
|
|
118
|
+
// Patch realPayload with extracted repo info if missing
|
|
119
|
+
// This is crucial for formatter to work correctly as it expects repository object
|
|
120
|
+
if (!realPayload.repository) {
|
|
121
|
+
realPayload.repository = { full_name: repoName || 'Unknown/Repo' };
|
|
122
|
+
}
|
|
123
|
+
else if (!realPayload.repository.full_name) {
|
|
124
|
+
realPayload.repository.full_name = repoName || 'Unknown/Repo';
|
|
125
|
+
}
|
|
126
|
+
// Patch realPayload with sender info if missing (e.g. issues event)
|
|
127
|
+
if (!realPayload.sender) {
|
|
128
|
+
if (realPayload.issue?.user) {
|
|
129
|
+
realPayload.sender = realPayload.issue.user;
|
|
130
|
+
}
|
|
131
|
+
else if (realPayload.pull_request?.user) {
|
|
132
|
+
realPayload.sender = realPayload.pull_request.user;
|
|
133
|
+
}
|
|
134
|
+
else if (realPayload.discussion?.user) {
|
|
135
|
+
realPayload.sender = realPayload.discussion.user;
|
|
136
|
+
}
|
|
137
|
+
else if (realPayload.pusher) {
|
|
138
|
+
realPayload.sender = { login: realPayload.pusher.name || 'Pusher' };
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
// Fallback sender
|
|
142
|
+
realPayload.sender = { login: 'GitHub' };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// Comprehensive patching for specific events to prevent formatter crashes
|
|
146
|
+
try {
|
|
147
|
+
this.patchPayloadForEvent(event, realPayload, repoName || 'Unknown/Repo');
|
|
148
|
+
}
|
|
149
|
+
catch (e) {
|
|
150
|
+
this.ctx.logger('githubsth').warn(`Failed to patch payload for ${event}:`, e);
|
|
94
151
|
}
|
|
95
152
|
if (this.config.debug) {
|
|
96
153
|
this.ctx.logger('githubsth').info(`Processing event ${event} for ${repoName}`);
|
|
@@ -99,6 +156,12 @@ class Notifier extends koishi_1.Service {
|
|
|
99
156
|
}
|
|
100
157
|
// Get rules from database
|
|
101
158
|
// Try to match both exact name and lowercase name to handle case sensitivity
|
|
159
|
+
// If repoName is missing (undefined), we can't query rules effectively by repo name
|
|
160
|
+
// But we might want to support global subscriptions or handle it gracefully
|
|
161
|
+
if (!repoName) {
|
|
162
|
+
this.ctx.logger('githubsth').warn('Cannot query rules: repoName is missing');
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
102
165
|
const repoNames = [repoName];
|
|
103
166
|
if (repoName !== repoName.toLowerCase()) {
|
|
104
167
|
repoNames.push(repoName.toLowerCase());
|
|
@@ -126,6 +189,9 @@ class Notifier extends koishi_1.Service {
|
|
|
126
189
|
this.ctx.logger('githubsth').info(`No matching rules for ${repoName} (event: ${event})`);
|
|
127
190
|
this.ctx.logger('notifier').debug(`No matching rules for ${repoName} (event: ${event})`);
|
|
128
191
|
}
|
|
192
|
+
else if (this.config.logUnhandledEvents) {
|
|
193
|
+
this.ctx.logger('githubsth').warn(`No matching rules for ${repoName} (event: ${event})`);
|
|
194
|
+
}
|
|
129
195
|
return;
|
|
130
196
|
}
|
|
131
197
|
if (this.config.debug) {
|
|
@@ -138,50 +204,160 @@ class Notifier extends koishi_1.Service {
|
|
|
138
204
|
this.ctx.logger('notifier').warn('Formatter service not available');
|
|
139
205
|
return;
|
|
140
206
|
}
|
|
207
|
+
try {
|
|
208
|
+
switch (event) {
|
|
209
|
+
case 'push':
|
|
210
|
+
message = this.ctx.githubsthFormatter.formatPush(realPayload);
|
|
211
|
+
break;
|
|
212
|
+
case 'issues':
|
|
213
|
+
message = this.ctx.githubsthFormatter.formatIssue(realPayload);
|
|
214
|
+
break;
|
|
215
|
+
case 'pull_request':
|
|
216
|
+
message = this.ctx.githubsthFormatter.formatPullRequest(realPayload);
|
|
217
|
+
break;
|
|
218
|
+
case 'star':
|
|
219
|
+
message = this.ctx.githubsthFormatter.formatStar(realPayload);
|
|
220
|
+
break;
|
|
221
|
+
case 'fork':
|
|
222
|
+
message = this.ctx.githubsthFormatter.formatFork(realPayload);
|
|
223
|
+
break;
|
|
224
|
+
case 'release':
|
|
225
|
+
message = this.ctx.githubsthFormatter.formatRelease(realPayload);
|
|
226
|
+
break;
|
|
227
|
+
case 'discussion':
|
|
228
|
+
message = this.ctx.githubsthFormatter.formatDiscussion(realPayload);
|
|
229
|
+
break;
|
|
230
|
+
case 'workflow_run':
|
|
231
|
+
message = this.ctx.githubsthFormatter.formatWorkflowRun(realPayload);
|
|
232
|
+
break;
|
|
233
|
+
case 'issue_comment':
|
|
234
|
+
message = this.ctx.githubsthFormatter.formatIssueComment(realPayload);
|
|
235
|
+
break;
|
|
236
|
+
case 'pull_request_review':
|
|
237
|
+
message = this.ctx.githubsthFormatter.formatPullRequestReview(realPayload);
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
catch (e) {
|
|
242
|
+
this.ctx.logger('githubsth').error(`Error formatting event ${event}:`, e);
|
|
243
|
+
if (this.config.debug) {
|
|
244
|
+
this.ctx.logger('notifier').error(`Error formatting event ${event}:`, e);
|
|
245
|
+
}
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
if (!message) {
|
|
249
|
+
if (this.config.debug) {
|
|
250
|
+
this.ctx.logger('notifier').debug(`Formatter returned null for event ${event}`);
|
|
251
|
+
}
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
for (const rule of matchedRules) {
|
|
255
|
+
if (this.config.debug) {
|
|
256
|
+
this.ctx.logger('notifier').debug(`Sending message to channel ${rule.channelId} (platform: ${rule.platform || 'any'})`);
|
|
257
|
+
}
|
|
258
|
+
await this.sendMessage(rule, message);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
patchPayloadForEvent(event, payload, repoName) {
|
|
262
|
+
// Ensure sender exists (handled before, but good for type safety)
|
|
263
|
+
const defaultUser = { login: 'GitHub', id: 0, avatar_url: '' };
|
|
264
|
+
if (!payload.sender)
|
|
265
|
+
payload.sender = defaultUser;
|
|
266
|
+
// Ensure repository exists (handled before, but good for type safety)
|
|
267
|
+
const defaultRepo = { full_name: repoName, stargazers_count: 0, html_url: `https://github.com/${repoName}` };
|
|
268
|
+
if (!payload.repository)
|
|
269
|
+
payload.repository = defaultRepo;
|
|
141
270
|
switch (event) {
|
|
142
271
|
case 'push':
|
|
143
|
-
|
|
272
|
+
if (!payload.pusher)
|
|
273
|
+
payload.pusher = { name: payload.sender.login };
|
|
274
|
+
if (!payload.commits)
|
|
275
|
+
payload.commits = [];
|
|
276
|
+
if (!payload.ref)
|
|
277
|
+
payload.ref = 'refs/heads/unknown';
|
|
278
|
+
if (!payload.compare)
|
|
279
|
+
payload.compare = '';
|
|
280
|
+
// Ensure author exists in commits
|
|
281
|
+
if (payload.commits.length > 0) {
|
|
282
|
+
payload.commits.forEach((c) => {
|
|
283
|
+
if (!c.author)
|
|
284
|
+
c.author = { name: 'Unknown' };
|
|
285
|
+
if (!c.id)
|
|
286
|
+
c.id = '0000000';
|
|
287
|
+
if (!c.message)
|
|
288
|
+
c.message = 'No message';
|
|
289
|
+
});
|
|
290
|
+
}
|
|
144
291
|
break;
|
|
145
292
|
case 'issues':
|
|
146
|
-
|
|
293
|
+
if (!payload.action)
|
|
294
|
+
payload.action = 'updated';
|
|
295
|
+
if (!payload.issue)
|
|
296
|
+
payload.issue = { number: 0, title: 'Unknown Issue', html_url: '', user: payload.sender };
|
|
297
|
+
// Ensure user exists in issue
|
|
298
|
+
if (!payload.issue.user)
|
|
299
|
+
payload.issue.user = payload.sender;
|
|
147
300
|
break;
|
|
148
301
|
case 'pull_request':
|
|
149
|
-
|
|
302
|
+
if (!payload.action)
|
|
303
|
+
payload.action = 'updated';
|
|
304
|
+
if (!payload.pull_request)
|
|
305
|
+
payload.pull_request = { number: 0, title: 'Unknown PR', state: 'unknown', html_url: '', user: payload.sender };
|
|
306
|
+
if (!payload.pull_request.user)
|
|
307
|
+
payload.pull_request.user = payload.sender;
|
|
150
308
|
break;
|
|
151
309
|
case 'star':
|
|
152
|
-
|
|
310
|
+
if (!payload.action)
|
|
311
|
+
payload.action = 'created';
|
|
312
|
+
if (payload.repository && payload.repository.stargazers_count === undefined) {
|
|
313
|
+
payload.repository.stargazers_count = '?';
|
|
314
|
+
}
|
|
153
315
|
break;
|
|
154
316
|
case 'fork':
|
|
155
|
-
|
|
317
|
+
if (!payload.forkee)
|
|
318
|
+
payload.forkee = { full_name: 'unknown/fork' };
|
|
156
319
|
break;
|
|
157
320
|
case 'release':
|
|
158
|
-
|
|
321
|
+
if (!payload.action)
|
|
322
|
+
payload.action = 'published';
|
|
323
|
+
if (!payload.release)
|
|
324
|
+
payload.release = { tag_name: 'unknown', name: 'Unknown Release', html_url: '' };
|
|
159
325
|
break;
|
|
160
326
|
case 'discussion':
|
|
161
|
-
|
|
327
|
+
if (!payload.action)
|
|
328
|
+
payload.action = 'updated';
|
|
329
|
+
if (!payload.discussion)
|
|
330
|
+
payload.discussion = { number: 0, title: 'Unknown Discussion', html_url: '', user: payload.sender };
|
|
331
|
+
if (!payload.discussion.user)
|
|
332
|
+
payload.discussion.user = payload.sender;
|
|
162
333
|
break;
|
|
163
334
|
case 'workflow_run':
|
|
164
|
-
|
|
335
|
+
if (!payload.action)
|
|
336
|
+
payload.action = 'completed';
|
|
337
|
+
if (!payload.workflow_run)
|
|
338
|
+
payload.workflow_run = { conclusion: 'unknown', name: 'Unknown Workflow', head_branch: 'unknown', html_url: '' };
|
|
165
339
|
break;
|
|
166
340
|
case 'issue_comment':
|
|
167
|
-
|
|
341
|
+
if (!payload.action)
|
|
342
|
+
payload.action = 'created';
|
|
343
|
+
if (!payload.issue)
|
|
344
|
+
payload.issue = { number: 0, title: 'Unknown Issue', html_url: '', user: payload.sender };
|
|
345
|
+
if (!payload.comment)
|
|
346
|
+
payload.comment = { body: '', html_url: '' };
|
|
347
|
+
if (!payload.issue.user)
|
|
348
|
+
payload.issue.user = payload.sender;
|
|
168
349
|
break;
|
|
169
350
|
case 'pull_request_review':
|
|
170
|
-
|
|
351
|
+
if (!payload.action)
|
|
352
|
+
payload.action = 'submitted';
|
|
353
|
+
if (!payload.pull_request)
|
|
354
|
+
payload.pull_request = { number: 0, title: 'Unknown PR', html_url: '', user: payload.sender };
|
|
355
|
+
if (!payload.review)
|
|
356
|
+
payload.review = { state: 'unknown', html_url: '' };
|
|
357
|
+
if (!payload.pull_request.user)
|
|
358
|
+
payload.pull_request.user = payload.sender;
|
|
171
359
|
break;
|
|
172
360
|
}
|
|
173
|
-
if (!message) {
|
|
174
|
-
if (this.config.debug) {
|
|
175
|
-
this.ctx.logger('notifier').debug(`Formatter returned null for event ${event}`);
|
|
176
|
-
}
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
for (const rule of matchedRules) {
|
|
180
|
-
if (this.config.debug) {
|
|
181
|
-
this.ctx.logger('notifier').debug(`Sending message to channel ${rule.channelId} (platform: ${rule.platform || 'any'})`);
|
|
182
|
-
}
|
|
183
|
-
await this.sendMessage(rule, message);
|
|
184
|
-
}
|
|
185
361
|
}
|
|
186
362
|
async sendMessage(rule, message) {
|
|
187
363
|
// Find suitable bots
|
package/package.json
CHANGED