koishi-plugin-github-webhook-pusher 0.0.1
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/index.d.ts +6 -0
- package/lib/commands/subscription.d.ts +12 -0
- package/lib/commands/trust.d.ts +10 -0
- package/lib/commands/utils.d.ts +12 -0
- package/lib/config.d.ts +20 -0
- package/lib/database.d.ts +35 -0
- package/lib/index.d.ts +18 -0
- package/lib/index.js +1012 -0
- package/lib/message.d.ts +12 -0
- package/lib/parser.d.ts +37 -0
- package/lib/pusher.d.ts +55 -0
- package/lib/repository/delivery.d.ts +38 -0
- package/lib/repository/index.d.ts +6 -0
- package/lib/repository/subscription.d.ts +99 -0
- package/lib/repository/trust.d.ts +73 -0
- package/lib/signature.d.ts +15 -0
- package/lib/types.d.ts +49 -0
- package/lib/webhook.d.ts +21 -0
- package/package.json +55 -0
- package/readme.md +210 -0
package/lib/index.js
ADDED
|
@@ -0,0 +1,1012 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name2 in all)
|
|
9
|
+
__defProp(target, name2, { get: all[name2], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
|
|
21
|
+
// src/index.ts
|
|
22
|
+
var src_exports = {};
|
|
23
|
+
__export(src_exports, {
|
|
24
|
+
Config: () => Config,
|
|
25
|
+
apply: () => apply,
|
|
26
|
+
inject: () => inject,
|
|
27
|
+
name: () => name
|
|
28
|
+
});
|
|
29
|
+
module.exports = __toCommonJS(src_exports);
|
|
30
|
+
var import_koishi5 = require("koishi");
|
|
31
|
+
|
|
32
|
+
// src/config.ts
|
|
33
|
+
var import_koishi = require("koishi");
|
|
34
|
+
|
|
35
|
+
// src/types.ts
|
|
36
|
+
var EVENT_DISPLAY_MAP = {
|
|
37
|
+
issues: { name: "Issue", emoji: "📌" },
|
|
38
|
+
release: { name: "Release", emoji: "🚀" },
|
|
39
|
+
push: { name: "Commit", emoji: "⬆️" },
|
|
40
|
+
pull_request: { name: "PR", emoji: "🔀" },
|
|
41
|
+
star: { name: "Star", emoji: "⭐" }
|
|
42
|
+
};
|
|
43
|
+
function getDisplayType(type) {
|
|
44
|
+
return EVENT_DISPLAY_MAP[type].name;
|
|
45
|
+
}
|
|
46
|
+
__name(getDisplayType, "getDisplayType");
|
|
47
|
+
function getEventEmoji(type) {
|
|
48
|
+
return EVENT_DISPLAY_MAP[type].emoji;
|
|
49
|
+
}
|
|
50
|
+
__name(getEventEmoji, "getEventEmoji");
|
|
51
|
+
|
|
52
|
+
// src/config.ts
|
|
53
|
+
var EVENT_TYPES = ["issues", "release", "push", "pull_request", "star"];
|
|
54
|
+
var Config = import_koishi.Schema.object({
|
|
55
|
+
path: import_koishi.Schema.string().default("/github/webhook").description("Webhook 接收路径"),
|
|
56
|
+
secret: import_koishi.Schema.string().required().description("GitHub Webhook Secret"),
|
|
57
|
+
baseUrl: import_koishi.Schema.string().description("显示用基础 URL"),
|
|
58
|
+
defaultEvents: import_koishi.Schema.array(import_koishi.Schema.union(EVENT_TYPES.map((e) => import_koishi.Schema.const(e)))).default(["issues", "release", "push"]).description("默认订阅事件"),
|
|
59
|
+
debug: import_koishi.Schema.boolean().default(false).description("调试模式"),
|
|
60
|
+
allowUntrusted: import_koishi.Schema.boolean().default(false).description("允许非信任仓库"),
|
|
61
|
+
concurrency: import_koishi.Schema.number().default(5).description("推送并发数")
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// src/database.ts
|
|
65
|
+
function extendDatabase(ctx) {
|
|
66
|
+
ctx.model.extend("github_trusted_repos", {
|
|
67
|
+
id: "unsigned",
|
|
68
|
+
repo: "string",
|
|
69
|
+
enabled: "boolean",
|
|
70
|
+
createdAt: "timestamp",
|
|
71
|
+
updatedAt: "timestamp"
|
|
72
|
+
}, {
|
|
73
|
+
primary: "id",
|
|
74
|
+
unique: ["repo"]
|
|
75
|
+
});
|
|
76
|
+
ctx.model.extend("github_subscriptions", {
|
|
77
|
+
id: "unsigned",
|
|
78
|
+
platform: "string",
|
|
79
|
+
channelId: "string",
|
|
80
|
+
guildId: "string",
|
|
81
|
+
userId: "string",
|
|
82
|
+
repo: "string",
|
|
83
|
+
events: "json",
|
|
84
|
+
enabled: "boolean",
|
|
85
|
+
createdAt: "timestamp",
|
|
86
|
+
updatedAt: "timestamp"
|
|
87
|
+
}, {
|
|
88
|
+
primary: "id"
|
|
89
|
+
});
|
|
90
|
+
ctx.model.extend("github_deliveries", {
|
|
91
|
+
deliveryId: "string",
|
|
92
|
+
repo: "string",
|
|
93
|
+
event: "string",
|
|
94
|
+
receivedAt: "timestamp"
|
|
95
|
+
}, {
|
|
96
|
+
primary: "deliveryId"
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
__name(extendDatabase, "extendDatabase");
|
|
100
|
+
|
|
101
|
+
// src/webhook.ts
|
|
102
|
+
var import_koishi4 = require("koishi");
|
|
103
|
+
|
|
104
|
+
// src/signature.ts
|
|
105
|
+
var import_crypto = require("crypto");
|
|
106
|
+
function createSignature(payload, secret) {
|
|
107
|
+
const hmac = (0, import_crypto.createHmac)("sha256", secret);
|
|
108
|
+
hmac.update(payload, "utf8");
|
|
109
|
+
return `sha256=${hmac.digest("hex")}`;
|
|
110
|
+
}
|
|
111
|
+
__name(createSignature, "createSignature");
|
|
112
|
+
function verifySignature(payload, signature, secret) {
|
|
113
|
+
if (!signature || !signature.startsWith("sha256=")) {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
const expected = createSignature(payload, secret);
|
|
117
|
+
const sigBuffer = Buffer.from(signature);
|
|
118
|
+
const expectedBuffer = Buffer.from(expected);
|
|
119
|
+
if (sigBuffer.length !== expectedBuffer.length) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
return (0, import_crypto.timingSafeEqual)(sigBuffer, expectedBuffer);
|
|
123
|
+
}
|
|
124
|
+
__name(verifySignature, "verifySignature");
|
|
125
|
+
|
|
126
|
+
// src/parser.ts
|
|
127
|
+
var MAX_COMMITS = 3;
|
|
128
|
+
function parseIssuesEvent(payload) {
|
|
129
|
+
const { action, issue, repository, sender } = payload;
|
|
130
|
+
if (!["opened", "closed", "reopened", "edited"].includes(action)) {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
type: "issues",
|
|
135
|
+
displayType: getDisplayType("issues"),
|
|
136
|
+
repo: repository.full_name,
|
|
137
|
+
actor: sender.login,
|
|
138
|
+
action,
|
|
139
|
+
title: issue.title,
|
|
140
|
+
number: issue.number,
|
|
141
|
+
url: issue.html_url,
|
|
142
|
+
body: issue.body
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
__name(parseIssuesEvent, "parseIssuesEvent");
|
|
146
|
+
function parseReleaseEvent(payload) {
|
|
147
|
+
const { action, release, repository, sender } = payload;
|
|
148
|
+
if (!["published", "created"].includes(action)) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
type: "release",
|
|
153
|
+
displayType: getDisplayType("release"),
|
|
154
|
+
repo: repository.full_name,
|
|
155
|
+
actor: sender.login,
|
|
156
|
+
action,
|
|
157
|
+
title: release.name || release.tag_name,
|
|
158
|
+
tagName: release.tag_name,
|
|
159
|
+
url: release.html_url,
|
|
160
|
+
body: release.body
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
__name(parseReleaseEvent, "parseReleaseEvent");
|
|
164
|
+
function parsePushEvent(payload) {
|
|
165
|
+
const { ref, commits, repository, sender, compare } = payload;
|
|
166
|
+
const branch = ref?.replace("refs/heads/", "") || "";
|
|
167
|
+
const allCommits = (commits || []).map((commit) => ({
|
|
168
|
+
sha: commit.id?.substring(0, 7) || "",
|
|
169
|
+
message: commit.message?.split("\n")[0] || "",
|
|
170
|
+
// 只取第一行
|
|
171
|
+
author: commit.author?.name || commit.author?.username || "",
|
|
172
|
+
url: commit.url || ""
|
|
173
|
+
}));
|
|
174
|
+
const displayCommits = allCommits.slice(0, MAX_COMMITS);
|
|
175
|
+
return {
|
|
176
|
+
type: "push",
|
|
177
|
+
displayType: getDisplayType("push"),
|
|
178
|
+
// 显示为 "Commit"
|
|
179
|
+
repo: repository.full_name,
|
|
180
|
+
actor: sender.login,
|
|
181
|
+
ref: branch,
|
|
182
|
+
commits: displayCommits,
|
|
183
|
+
totalCommits: allCommits.length,
|
|
184
|
+
url: compare || `https://github.com/${repository.full_name}`
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
__name(parsePushEvent, "parsePushEvent");
|
|
188
|
+
function parsePullRequestEvent(payload) {
|
|
189
|
+
const { action, pull_request, repository, sender } = payload;
|
|
190
|
+
const validActions = ["opened", "closed", "reopened", "edited"];
|
|
191
|
+
if (!validActions.includes(action)) {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
const actualAction = action === "closed" && pull_request.merged ? "merged" : action;
|
|
195
|
+
return {
|
|
196
|
+
type: "pull_request",
|
|
197
|
+
displayType: getDisplayType("pull_request"),
|
|
198
|
+
repo: repository.full_name,
|
|
199
|
+
actor: sender.login,
|
|
200
|
+
action: actualAction,
|
|
201
|
+
title: pull_request.title,
|
|
202
|
+
number: pull_request.number,
|
|
203
|
+
url: pull_request.html_url,
|
|
204
|
+
body: pull_request.body
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
__name(parsePullRequestEvent, "parsePullRequestEvent");
|
|
208
|
+
function parseStarEvent(payload) {
|
|
209
|
+
const { action, repository, sender } = payload;
|
|
210
|
+
if (!["created", "deleted"].includes(action)) {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
return {
|
|
214
|
+
type: "star",
|
|
215
|
+
displayType: getDisplayType("star"),
|
|
216
|
+
repo: repository.full_name,
|
|
217
|
+
actor: sender.login,
|
|
218
|
+
action,
|
|
219
|
+
starCount: repository.stargazers_count,
|
|
220
|
+
url: repository.html_url
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
__name(parseStarEvent, "parseStarEvent");
|
|
224
|
+
function parseEvent(eventName, payload) {
|
|
225
|
+
switch (eventName) {
|
|
226
|
+
case "issues":
|
|
227
|
+
return parseIssuesEvent(payload);
|
|
228
|
+
case "release":
|
|
229
|
+
return parseReleaseEvent(payload);
|
|
230
|
+
case "push":
|
|
231
|
+
return parsePushEvent(payload);
|
|
232
|
+
case "pull_request":
|
|
233
|
+
return parsePullRequestEvent(payload);
|
|
234
|
+
case "star":
|
|
235
|
+
return parseStarEvent(payload);
|
|
236
|
+
default:
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
__name(parseEvent, "parseEvent");
|
|
241
|
+
|
|
242
|
+
// src/pusher.ts
|
|
243
|
+
var import_koishi3 = require("koishi");
|
|
244
|
+
|
|
245
|
+
// src/repository/subscription.ts
|
|
246
|
+
async function createSubscription(ctx, session, repo, defaultEvents) {
|
|
247
|
+
const now = /* @__PURE__ */ new Date();
|
|
248
|
+
const existing = await ctx.database.get("github_subscriptions", {
|
|
249
|
+
platform: session.platform,
|
|
250
|
+
channelId: session.channelId,
|
|
251
|
+
repo
|
|
252
|
+
});
|
|
253
|
+
if (existing.length > 0) {
|
|
254
|
+
return existing[0];
|
|
255
|
+
}
|
|
256
|
+
await ctx.database.create("github_subscriptions", {
|
|
257
|
+
platform: session.platform,
|
|
258
|
+
channelId: session.channelId,
|
|
259
|
+
guildId: session.guildId || "",
|
|
260
|
+
userId: session.userId || "",
|
|
261
|
+
repo,
|
|
262
|
+
events: defaultEvents,
|
|
263
|
+
enabled: true,
|
|
264
|
+
createdAt: now,
|
|
265
|
+
updatedAt: now
|
|
266
|
+
});
|
|
267
|
+
const [created] = await ctx.database.get("github_subscriptions", {
|
|
268
|
+
platform: session.platform,
|
|
269
|
+
channelId: session.channelId,
|
|
270
|
+
repo
|
|
271
|
+
});
|
|
272
|
+
return created;
|
|
273
|
+
}
|
|
274
|
+
__name(createSubscription, "createSubscription");
|
|
275
|
+
async function removeSubscription(ctx, session, repo) {
|
|
276
|
+
const result = await ctx.database.remove("github_subscriptions", {
|
|
277
|
+
platform: session.platform,
|
|
278
|
+
channelId: session.channelId,
|
|
279
|
+
repo
|
|
280
|
+
});
|
|
281
|
+
return (result.removed ?? 0) > 0;
|
|
282
|
+
}
|
|
283
|
+
__name(removeSubscription, "removeSubscription");
|
|
284
|
+
async function listSubscriptions(ctx, session) {
|
|
285
|
+
return ctx.database.get("github_subscriptions", {
|
|
286
|
+
platform: session.platform,
|
|
287
|
+
channelId: session.channelId
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
__name(listSubscriptions, "listSubscriptions");
|
|
291
|
+
async function getSubscription(ctx, session, repo) {
|
|
292
|
+
const subs = await ctx.database.get("github_subscriptions", {
|
|
293
|
+
platform: session.platform,
|
|
294
|
+
channelId: session.channelId,
|
|
295
|
+
repo
|
|
296
|
+
});
|
|
297
|
+
return subs.length > 0 ? subs[0] : null;
|
|
298
|
+
}
|
|
299
|
+
__name(getSubscription, "getSubscription");
|
|
300
|
+
async function updateEvents(ctx, session, repo, events) {
|
|
301
|
+
const result = await ctx.database.set("github_subscriptions", {
|
|
302
|
+
platform: session.platform,
|
|
303
|
+
channelId: session.channelId,
|
|
304
|
+
repo
|
|
305
|
+
}, {
|
|
306
|
+
events,
|
|
307
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
308
|
+
});
|
|
309
|
+
return (result.modified ?? 0) > 0;
|
|
310
|
+
}
|
|
311
|
+
__name(updateEvents, "updateEvents");
|
|
312
|
+
async function queryTargets(ctx, repo, eventType) {
|
|
313
|
+
const subscriptions = await ctx.database.get("github_subscriptions", {
|
|
314
|
+
repo,
|
|
315
|
+
enabled: true
|
|
316
|
+
});
|
|
317
|
+
return subscriptions.filter((sub) => sub.events.includes(eventType)).map((sub) => ({
|
|
318
|
+
platform: sub.platform,
|
|
319
|
+
channelId: sub.channelId,
|
|
320
|
+
guildId: sub.guildId || void 0,
|
|
321
|
+
userId: sub.userId || void 0
|
|
322
|
+
}));
|
|
323
|
+
}
|
|
324
|
+
__name(queryTargets, "queryTargets");
|
|
325
|
+
|
|
326
|
+
// src/message.ts
|
|
327
|
+
var import_koishi2 = require("koishi");
|
|
328
|
+
function buildMessage(event) {
|
|
329
|
+
switch (event.type) {
|
|
330
|
+
case "issues":
|
|
331
|
+
return buildIssuesMessage(event);
|
|
332
|
+
case "release":
|
|
333
|
+
return buildReleaseMessage(event);
|
|
334
|
+
case "push":
|
|
335
|
+
return buildPushMessage(event);
|
|
336
|
+
case "pull_request":
|
|
337
|
+
return buildPullRequestMessage(event);
|
|
338
|
+
case "star":
|
|
339
|
+
return buildStarMessage(event);
|
|
340
|
+
default:
|
|
341
|
+
return buildGenericMessage(event);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
__name(buildMessage, "buildMessage");
|
|
345
|
+
function buildIssuesMessage(event) {
|
|
346
|
+
const emoji = getEventEmoji(event.type);
|
|
347
|
+
const actionText = getActionText(event.action);
|
|
348
|
+
const lines = [
|
|
349
|
+
`${emoji} [${event.repo}] ${event.displayType}`,
|
|
350
|
+
`${event.actor} ${actionText} #${event.number}: ${event.title}`,
|
|
351
|
+
event.url
|
|
352
|
+
];
|
|
353
|
+
return [(0, import_koishi2.h)("text", { content: lines.join("\n") })];
|
|
354
|
+
}
|
|
355
|
+
__name(buildIssuesMessage, "buildIssuesMessage");
|
|
356
|
+
function buildReleaseMessage(event) {
|
|
357
|
+
const emoji = getEventEmoji(event.type);
|
|
358
|
+
const actionText = getActionText(event.action);
|
|
359
|
+
const lines = [
|
|
360
|
+
`${emoji} [${event.repo}] ${event.displayType}`,
|
|
361
|
+
`${event.actor} ${actionText} ${event.tagName || event.title}`,
|
|
362
|
+
event.url
|
|
363
|
+
];
|
|
364
|
+
return [(0, import_koishi2.h)("text", { content: lines.join("\n") })];
|
|
365
|
+
}
|
|
366
|
+
__name(buildReleaseMessage, "buildReleaseMessage");
|
|
367
|
+
function buildPushMessage(event) {
|
|
368
|
+
const emoji = getEventEmoji(event.type);
|
|
369
|
+
const commits = event.commits || [];
|
|
370
|
+
const totalCommits = event.totalCommits || commits.length;
|
|
371
|
+
const lines = [
|
|
372
|
+
`${emoji} [${event.repo}] ${event.displayType}`,
|
|
373
|
+
`${event.actor} 推送了 ${totalCommits} 个提交到 ${event.ref}`,
|
|
374
|
+
""
|
|
375
|
+
];
|
|
376
|
+
for (const commit of commits) {
|
|
377
|
+
lines.push(`• ${commit.sha} - ${commit.message}`);
|
|
378
|
+
}
|
|
379
|
+
if (totalCommits > commits.length) {
|
|
380
|
+
lines.push("");
|
|
381
|
+
lines.push(`还有 ${totalCommits - commits.length} 条提交...`);
|
|
382
|
+
}
|
|
383
|
+
lines.push(event.url);
|
|
384
|
+
return [(0, import_koishi2.h)("text", { content: lines.join("\n") })];
|
|
385
|
+
}
|
|
386
|
+
__name(buildPushMessage, "buildPushMessage");
|
|
387
|
+
function buildPullRequestMessage(event) {
|
|
388
|
+
const emoji = getEventEmoji(event.type);
|
|
389
|
+
const actionText = getActionText(event.action);
|
|
390
|
+
const lines = [
|
|
391
|
+
`${emoji} [${event.repo}] ${event.displayType}`,
|
|
392
|
+
`${event.actor} ${actionText} #${event.number}: ${event.title}`,
|
|
393
|
+
event.url
|
|
394
|
+
];
|
|
395
|
+
return [(0, import_koishi2.h)("text", { content: lines.join("\n") })];
|
|
396
|
+
}
|
|
397
|
+
__name(buildPullRequestMessage, "buildPullRequestMessage");
|
|
398
|
+
function buildStarMessage(event) {
|
|
399
|
+
const emoji = getEventEmoji(event.type);
|
|
400
|
+
const actionText = event.action === "created" ? "starred" : "unstarred";
|
|
401
|
+
const lines = [
|
|
402
|
+
`${emoji} [${event.repo}] ${event.displayType}`,
|
|
403
|
+
`${event.actor} ${actionText} (⭐ ${event.starCount})`,
|
|
404
|
+
event.url
|
|
405
|
+
];
|
|
406
|
+
return [(0, import_koishi2.h)("text", { content: lines.join("\n") })];
|
|
407
|
+
}
|
|
408
|
+
__name(buildStarMessage, "buildStarMessage");
|
|
409
|
+
function buildGenericMessage(event) {
|
|
410
|
+
const emoji = getEventEmoji(event.type);
|
|
411
|
+
const lines = [
|
|
412
|
+
`${emoji} [${event.repo}] ${event.displayType}`,
|
|
413
|
+
`${event.actor} ${event.action || "triggered"}`,
|
|
414
|
+
event.url
|
|
415
|
+
];
|
|
416
|
+
return [(0, import_koishi2.h)("text", { content: lines.join("\n") })];
|
|
417
|
+
}
|
|
418
|
+
__name(buildGenericMessage, "buildGenericMessage");
|
|
419
|
+
function getActionText(action) {
|
|
420
|
+
const actionMap = {
|
|
421
|
+
opened: "opened",
|
|
422
|
+
closed: "closed",
|
|
423
|
+
reopened: "reopened",
|
|
424
|
+
edited: "edited",
|
|
425
|
+
merged: "merged",
|
|
426
|
+
published: "published",
|
|
427
|
+
created: "created",
|
|
428
|
+
deleted: "deleted"
|
|
429
|
+
};
|
|
430
|
+
return actionMap[action || ""] || action || "";
|
|
431
|
+
}
|
|
432
|
+
__name(getActionText, "getActionText");
|
|
433
|
+
|
|
434
|
+
// src/pusher.ts
|
|
435
|
+
var logger = new import_koishi3.Logger("github-webhook-pusher");
|
|
436
|
+
async function queryTargets2(ctx, repo, eventType) {
|
|
437
|
+
return queryTargets(ctx, repo, eventType);
|
|
438
|
+
}
|
|
439
|
+
__name(queryTargets2, "queryTargets");
|
|
440
|
+
async function sendMessage(ctx, target, message) {
|
|
441
|
+
try {
|
|
442
|
+
const bot = ctx.bots.find((b) => b.platform === target.platform);
|
|
443
|
+
if (!bot) {
|
|
444
|
+
throw new Error(`未找到平台 ${target.platform} 的 bot`);
|
|
445
|
+
}
|
|
446
|
+
await bot.sendMessage(target.channelId, message, target.guildId);
|
|
447
|
+
return { target, success: true };
|
|
448
|
+
} catch (error) {
|
|
449
|
+
return {
|
|
450
|
+
target,
|
|
451
|
+
success: false,
|
|
452
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
__name(sendMessage, "sendMessage");
|
|
457
|
+
async function pushWithConcurrency(ctx, targets, message, concurrency) {
|
|
458
|
+
if (targets.length === 0) {
|
|
459
|
+
return [];
|
|
460
|
+
}
|
|
461
|
+
const results = [];
|
|
462
|
+
const queue = [...targets];
|
|
463
|
+
const workers = Array(Math.min(concurrency, queue.length)).fill(null).map(async () => {
|
|
464
|
+
while (queue.length > 0) {
|
|
465
|
+
const target = queue.shift();
|
|
466
|
+
if (!target) break;
|
|
467
|
+
const result = await sendMessage(ctx, target, message);
|
|
468
|
+
results.push(result);
|
|
469
|
+
if (!result.success && result.error) {
|
|
470
|
+
logger.error(
|
|
471
|
+
`推送失败 [${target.platform}:${target.channelId}]: ${result.error.message}`
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
await Promise.all(workers);
|
|
477
|
+
return results;
|
|
478
|
+
}
|
|
479
|
+
__name(pushWithConcurrency, "pushWithConcurrency");
|
|
480
|
+
async function pushMessage(ctx, event, concurrency = 5) {
|
|
481
|
+
const targets = await queryTargets2(ctx, event.repo, event.type);
|
|
482
|
+
if (targets.length === 0) {
|
|
483
|
+
logger.debug(`仓库 ${event.repo} 的 ${event.type} 事件没有订阅目标`);
|
|
484
|
+
return [];
|
|
485
|
+
}
|
|
486
|
+
logger.info(`准备推送 ${event.type} 事件到 ${targets.length} 个目标`);
|
|
487
|
+
const message = buildMessage(event);
|
|
488
|
+
const results = await pushWithConcurrency(ctx, targets, message, concurrency);
|
|
489
|
+
const successCount = results.filter((r) => r.success).length;
|
|
490
|
+
const failCount = results.filter((r) => !r.success).length;
|
|
491
|
+
logger.info(`推送完成: 成功 ${successCount}, 失败 ${failCount}`);
|
|
492
|
+
return results;
|
|
493
|
+
}
|
|
494
|
+
__name(pushMessage, "pushMessage");
|
|
495
|
+
async function pushEvent(ctx, event, concurrency = 5) {
|
|
496
|
+
const results = await pushMessage(ctx, event, concurrency);
|
|
497
|
+
return {
|
|
498
|
+
pushed: results.filter((r) => r.success).length,
|
|
499
|
+
failed: results.filter((r) => !r.success).length,
|
|
500
|
+
results
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
__name(pushEvent, "pushEvent");
|
|
504
|
+
|
|
505
|
+
// src/repository/trust.ts
|
|
506
|
+
var REPO_NAME_REGEX = /^[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/;
|
|
507
|
+
function isValidRepoFormat(repo) {
|
|
508
|
+
if (!repo || typeof repo !== "string") {
|
|
509
|
+
return false;
|
|
510
|
+
}
|
|
511
|
+
return REPO_NAME_REGEX.test(repo);
|
|
512
|
+
}
|
|
513
|
+
__name(isValidRepoFormat, "isValidRepoFormat");
|
|
514
|
+
async function addTrustedRepo(ctx, repo) {
|
|
515
|
+
if (!isValidRepoFormat(repo)) {
|
|
516
|
+
return null;
|
|
517
|
+
}
|
|
518
|
+
const now = /* @__PURE__ */ new Date();
|
|
519
|
+
const existing = await ctx.database.get("github_trusted_repos", { repo });
|
|
520
|
+
if (existing.length > 0) {
|
|
521
|
+
return existing[0];
|
|
522
|
+
}
|
|
523
|
+
await ctx.database.create("github_trusted_repos", {
|
|
524
|
+
repo,
|
|
525
|
+
enabled: true,
|
|
526
|
+
createdAt: now,
|
|
527
|
+
updatedAt: now
|
|
528
|
+
});
|
|
529
|
+
const [created] = await ctx.database.get("github_trusted_repos", { repo });
|
|
530
|
+
return created;
|
|
531
|
+
}
|
|
532
|
+
__name(addTrustedRepo, "addTrustedRepo");
|
|
533
|
+
async function removeTrustedRepo(ctx, repo) {
|
|
534
|
+
const result = await ctx.database.remove("github_trusted_repos", { repo });
|
|
535
|
+
return (result.removed ?? 0) > 0;
|
|
536
|
+
}
|
|
537
|
+
__name(removeTrustedRepo, "removeTrustedRepo");
|
|
538
|
+
async function listTrustedRepos(ctx) {
|
|
539
|
+
return ctx.database.get("github_trusted_repos", {});
|
|
540
|
+
}
|
|
541
|
+
__name(listTrustedRepos, "listTrustedRepos");
|
|
542
|
+
async function isTrusted(ctx, repo) {
|
|
543
|
+
const repos = await ctx.database.get("github_trusted_repos", { repo, enabled: true });
|
|
544
|
+
return repos.length > 0;
|
|
545
|
+
}
|
|
546
|
+
__name(isTrusted, "isTrusted");
|
|
547
|
+
async function isInTrustList(ctx, repo) {
|
|
548
|
+
const repos = await ctx.database.get("github_trusted_repos", { repo });
|
|
549
|
+
return repos.length > 0;
|
|
550
|
+
}
|
|
551
|
+
__name(isInTrustList, "isInTrustList");
|
|
552
|
+
async function enableRepo(ctx, repo) {
|
|
553
|
+
const result = await ctx.database.set("github_trusted_repos", { repo }, {
|
|
554
|
+
enabled: true,
|
|
555
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
556
|
+
});
|
|
557
|
+
return (result.modified ?? 0) > 0;
|
|
558
|
+
}
|
|
559
|
+
__name(enableRepo, "enableRepo");
|
|
560
|
+
async function disableRepo(ctx, repo) {
|
|
561
|
+
const result = await ctx.database.set("github_trusted_repos", { repo }, {
|
|
562
|
+
enabled: false,
|
|
563
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
564
|
+
});
|
|
565
|
+
return (result.modified ?? 0) > 0;
|
|
566
|
+
}
|
|
567
|
+
__name(disableRepo, "disableRepo");
|
|
568
|
+
|
|
569
|
+
// src/repository/delivery.ts
|
|
570
|
+
async function recordDelivery(ctx, deliveryId, repo, event) {
|
|
571
|
+
const now = /* @__PURE__ */ new Date();
|
|
572
|
+
await ctx.database.create("github_deliveries", {
|
|
573
|
+
deliveryId,
|
|
574
|
+
repo,
|
|
575
|
+
event,
|
|
576
|
+
receivedAt: now
|
|
577
|
+
});
|
|
578
|
+
const [created] = await ctx.database.get("github_deliveries", { deliveryId });
|
|
579
|
+
return created;
|
|
580
|
+
}
|
|
581
|
+
__name(recordDelivery, "recordDelivery");
|
|
582
|
+
async function isDelivered(ctx, deliveryId) {
|
|
583
|
+
const deliveries = await ctx.database.get("github_deliveries", { deliveryId });
|
|
584
|
+
return deliveries.length > 0;
|
|
585
|
+
}
|
|
586
|
+
__name(isDelivered, "isDelivered");
|
|
587
|
+
|
|
588
|
+
// src/webhook.ts
|
|
589
|
+
var logger2 = new import_koishi4.Logger("github-webhook");
|
|
590
|
+
function registerWebhook(ctx, config) {
|
|
591
|
+
const path = config.path || "/github/webhook";
|
|
592
|
+
logger2.info(`注册 Webhook 处理器: ${path}`);
|
|
593
|
+
ctx.server.post(path, async (koaCtx) => {
|
|
594
|
+
const startTime = Date.now();
|
|
595
|
+
try {
|
|
596
|
+
const signature = koaCtx.get("X-Hub-Signature-256");
|
|
597
|
+
const eventName = koaCtx.get("X-GitHub-Event");
|
|
598
|
+
const deliveryId = koaCtx.get("X-GitHub-Delivery");
|
|
599
|
+
const rawBody = await getRawBody(koaCtx);
|
|
600
|
+
if (config.debug) {
|
|
601
|
+
logger2.debug(`收到 Webhook 请求: event=${eventName}, delivery=${deliveryId}`);
|
|
602
|
+
}
|
|
603
|
+
if (config.secret) {
|
|
604
|
+
if (!signature) {
|
|
605
|
+
logger2.warn("请求缺少 X-Hub-Signature-256 头");
|
|
606
|
+
koaCtx.status = 401;
|
|
607
|
+
koaCtx.body = { error: "Missing signature" };
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
if (!verifySignature(rawBody, signature, config.secret)) {
|
|
611
|
+
logger2.warn("签名验证失败");
|
|
612
|
+
koaCtx.status = 401;
|
|
613
|
+
koaCtx.body = { error: "Invalid signature" };
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
let payload;
|
|
618
|
+
try {
|
|
619
|
+
payload = JSON.parse(rawBody);
|
|
620
|
+
} catch (e) {
|
|
621
|
+
logger2.warn("无法解析 JSON 负载");
|
|
622
|
+
koaCtx.status = 400;
|
|
623
|
+
koaCtx.body = { error: "Invalid JSON payload" };
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
const repo = payload.repository?.full_name;
|
|
627
|
+
if (!repo) {
|
|
628
|
+
logger2.warn("负载中缺少仓库信息");
|
|
629
|
+
koaCtx.status = 400;
|
|
630
|
+
koaCtx.body = { error: "Missing repository information" };
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
logger2.info(`收到 Webhook: [${repo}] ${eventName} (${deliveryId})`);
|
|
634
|
+
if (deliveryId) {
|
|
635
|
+
const isDuplicate = await isDelivered(ctx, deliveryId);
|
|
636
|
+
if (isDuplicate) {
|
|
637
|
+
logger2.info(`跳过重复请求: ${deliveryId}`);
|
|
638
|
+
koaCtx.status = 200;
|
|
639
|
+
koaCtx.body = { status: "duplicate" };
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
if (!config.allowUntrusted) {
|
|
644
|
+
const trusted = await isTrusted(ctx, repo);
|
|
645
|
+
if (!trusted) {
|
|
646
|
+
logger2.info(`忽略非信任仓库事件: ${repo}`);
|
|
647
|
+
koaCtx.status = 200;
|
|
648
|
+
koaCtx.body = { status: "ignored", reason: "untrusted" };
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
const event = parseEvent(eventName, payload);
|
|
653
|
+
if (!event) {
|
|
654
|
+
logger2.info(`不支持的事件类型: ${eventName}`);
|
|
655
|
+
koaCtx.status = 200;
|
|
656
|
+
koaCtx.body = { status: "ignored", reason: "unsupported" };
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
if (deliveryId) {
|
|
660
|
+
await recordDelivery(ctx, deliveryId, repo, eventName);
|
|
661
|
+
}
|
|
662
|
+
const result = await pushEvent(ctx, event, config.concurrency);
|
|
663
|
+
const elapsed = Date.now() - startTime;
|
|
664
|
+
logger2.info(`处理完成: 推送 ${result.pushed} 成功, ${result.failed} 失败 (${elapsed}ms)`);
|
|
665
|
+
koaCtx.status = 200;
|
|
666
|
+
koaCtx.body = { status: "ok", pushed: result.pushed };
|
|
667
|
+
} catch (error) {
|
|
668
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
669
|
+
logger2.error(`处理 Webhook 时发生错误: ${errorMessage}`);
|
|
670
|
+
if (config.debug && error instanceof Error) {
|
|
671
|
+
logger2.error(error.stack || "");
|
|
672
|
+
}
|
|
673
|
+
koaCtx.status = 500;
|
|
674
|
+
koaCtx.body = { status: "error", error: "Internal server error" };
|
|
675
|
+
}
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
__name(registerWebhook, "registerWebhook");
|
|
679
|
+
async function getRawBody(koaCtx) {
|
|
680
|
+
if (koaCtx.request.body) {
|
|
681
|
+
return JSON.stringify(koaCtx.request.body);
|
|
682
|
+
}
|
|
683
|
+
return new Promise((resolve, reject) => {
|
|
684
|
+
let data = "";
|
|
685
|
+
koaCtx.req.on("data", (chunk) => {
|
|
686
|
+
data += chunk.toString();
|
|
687
|
+
});
|
|
688
|
+
koaCtx.req.on("end", () => {
|
|
689
|
+
resolve(data);
|
|
690
|
+
});
|
|
691
|
+
koaCtx.req.on("error", reject);
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
__name(getRawBody, "getRawBody");
|
|
695
|
+
|
|
696
|
+
// src/commands/trust.ts
|
|
697
|
+
var ADMIN_AUTHORITY = 3;
|
|
698
|
+
function registerTrustCommands(ctx) {
|
|
699
|
+
const trust = ctx.command("gh.trust", "管理信任的 GitHub 仓库").usage("gh.trust <add|remove|list|enable|disable> [repo]");
|
|
700
|
+
trust.subcommand(".add <repo:string>", "添加信任仓库").usage("gh.trust.add owner/repo").example("gh.trust.add koishijs/koishi").action(async ({ session }, repo) => {
|
|
701
|
+
const user = session?.user;
|
|
702
|
+
if ((user?.authority ?? 0) < ADMIN_AUTHORITY) {
|
|
703
|
+
return "❌ 权限不足,需要管理员权限";
|
|
704
|
+
}
|
|
705
|
+
if (!repo) {
|
|
706
|
+
return "❌ 请指定仓库名,格式: owner/repo";
|
|
707
|
+
}
|
|
708
|
+
if (!isValidRepoFormat(repo)) {
|
|
709
|
+
return "❌ 仓库格式错误,请使用 owner/repo 格式";
|
|
710
|
+
}
|
|
711
|
+
const result = await addTrustedRepo(ctx, repo);
|
|
712
|
+
if (result) {
|
|
713
|
+
return `✅ 已添加信任仓库: ${repo}`;
|
|
714
|
+
}
|
|
715
|
+
return "❌ 添加失败";
|
|
716
|
+
});
|
|
717
|
+
trust.subcommand(".remove <repo:string>", "移除信任仓库").usage("gh.trust.remove owner/repo").example("gh.trust.remove koishijs/koishi").action(async ({ session }, repo) => {
|
|
718
|
+
const user = session?.user;
|
|
719
|
+
if ((user?.authority ?? 0) < ADMIN_AUTHORITY) {
|
|
720
|
+
return "❌ 权限不足,需要管理员权限";
|
|
721
|
+
}
|
|
722
|
+
if (!repo) {
|
|
723
|
+
return "❌ 请指定仓库名";
|
|
724
|
+
}
|
|
725
|
+
const success = await removeTrustedRepo(ctx, repo);
|
|
726
|
+
if (success) {
|
|
727
|
+
return `✅ 已移除信任仓库: ${repo}`;
|
|
728
|
+
}
|
|
729
|
+
return `❌ 仓库 ${repo} 不在信任列表中`;
|
|
730
|
+
});
|
|
731
|
+
trust.subcommand(".list", "列出所有信任仓库").usage("gh.trust.list").action(async ({ session }) => {
|
|
732
|
+
const user = session?.user;
|
|
733
|
+
if ((user?.authority ?? 0) < ADMIN_AUTHORITY) {
|
|
734
|
+
return "❌ 权限不足,需要管理员权限";
|
|
735
|
+
}
|
|
736
|
+
const repos = await listTrustedRepos(ctx);
|
|
737
|
+
if (repos.length === 0) {
|
|
738
|
+
return "📋 信任仓库列表为空";
|
|
739
|
+
}
|
|
740
|
+
const lines = ["📋 信任仓库列表:"];
|
|
741
|
+
for (const repo of repos) {
|
|
742
|
+
const status = repo.enabled ? "✅" : "⏸️";
|
|
743
|
+
lines.push(`${status} ${repo.repo}`);
|
|
744
|
+
}
|
|
745
|
+
return lines.join("\n");
|
|
746
|
+
});
|
|
747
|
+
trust.subcommand(".enable <repo:string>", "启用信任仓库").usage("gh.trust.enable owner/repo").example("gh.trust.enable koishijs/koishi").action(async ({ session }, repo) => {
|
|
748
|
+
const user = session?.user;
|
|
749
|
+
if ((user?.authority ?? 0) < ADMIN_AUTHORITY) {
|
|
750
|
+
return "❌ 权限不足,需要管理员权限";
|
|
751
|
+
}
|
|
752
|
+
if (!repo) {
|
|
753
|
+
return "❌ 请指定仓库名";
|
|
754
|
+
}
|
|
755
|
+
const success = await enableRepo(ctx, repo);
|
|
756
|
+
if (success) {
|
|
757
|
+
return `✅ 已启用仓库: ${repo}`;
|
|
758
|
+
}
|
|
759
|
+
return `❌ 仓库 ${repo} 不在信任列表中`;
|
|
760
|
+
});
|
|
761
|
+
trust.subcommand(".disable <repo:string>", "禁用信任仓库").usage("gh.trust.disable owner/repo").example("gh.trust.disable koishijs/koishi").action(async ({ session }, repo) => {
|
|
762
|
+
const user = session?.user;
|
|
763
|
+
if ((user?.authority ?? 0) < ADMIN_AUTHORITY) {
|
|
764
|
+
return "❌ 权限不足,需要管理员权限";
|
|
765
|
+
}
|
|
766
|
+
if (!repo) {
|
|
767
|
+
return "❌ 请指定仓库名";
|
|
768
|
+
}
|
|
769
|
+
const success = await disableRepo(ctx, repo);
|
|
770
|
+
if (success) {
|
|
771
|
+
return `⏸️ 已禁用仓库: ${repo}`;
|
|
772
|
+
}
|
|
773
|
+
return `❌ 仓库 ${repo} 不在信任列表中`;
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
__name(registerTrustCommands, "registerTrustCommands");
|
|
777
|
+
|
|
778
|
+
// src/commands/subscription.ts
|
|
779
|
+
var ALL_EVENT_TYPES = ["issues", "release", "push", "pull_request", "star"];
|
|
780
|
+
function getSessionIdentifier(session) {
|
|
781
|
+
return {
|
|
782
|
+
platform: session.platform,
|
|
783
|
+
channelId: session.channelId,
|
|
784
|
+
guildId: session.guildId,
|
|
785
|
+
userId: session.userId
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
__name(getSessionIdentifier, "getSessionIdentifier");
|
|
789
|
+
function parseEventChanges(changes, currentEvents) {
|
|
790
|
+
const events = new Set(currentEvents);
|
|
791
|
+
for (const change of changes) {
|
|
792
|
+
if (!change) continue;
|
|
793
|
+
const prefix = change[0];
|
|
794
|
+
const eventName = change.slice(1);
|
|
795
|
+
if (!ALL_EVENT_TYPES.includes(eventName)) {
|
|
796
|
+
continue;
|
|
797
|
+
}
|
|
798
|
+
if (prefix === "+") {
|
|
799
|
+
events.add(eventName);
|
|
800
|
+
} else if (prefix === "-") {
|
|
801
|
+
events.delete(eventName);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
return Array.from(events);
|
|
805
|
+
}
|
|
806
|
+
__name(parseEventChanges, "parseEventChanges");
|
|
807
|
+
function registerSubscriptionCommands(ctx, config) {
|
|
808
|
+
ctx.command("gh.sub <repo:string>", "订阅 GitHub 仓库事件").usage("gh.sub owner/repo").example("gh.sub koishijs/koishi").action(async ({ session }, repo) => {
|
|
809
|
+
if (!session) return "❌ 无法获取会话信息";
|
|
810
|
+
if (!repo) {
|
|
811
|
+
return "❌ 请指定仓库名,格式: owner/repo";
|
|
812
|
+
}
|
|
813
|
+
const trusted = await isInTrustList(ctx, repo);
|
|
814
|
+
if (!trusted) {
|
|
815
|
+
return "❌ 该仓库不在信任列表中";
|
|
816
|
+
}
|
|
817
|
+
const sessionId = getSessionIdentifier(session);
|
|
818
|
+
const subscription = await createSubscription(ctx, sessionId, repo, config.defaultEvents);
|
|
819
|
+
if (subscription) {
|
|
820
|
+
const eventList = subscription.events.join(", ");
|
|
821
|
+
return `✅ 已订阅仓库: ${repo}
|
|
822
|
+
📋 订阅事件: ${eventList}`;
|
|
823
|
+
}
|
|
824
|
+
return "❌ 订阅失败";
|
|
825
|
+
});
|
|
826
|
+
ctx.command("gh.unsub <repo:string>", "取消订阅 GitHub 仓库").usage("gh.unsub owner/repo").example("gh.unsub koishijs/koishi").action(async ({ session }, repo) => {
|
|
827
|
+
if (!session) return "❌ 无法获取会话信息";
|
|
828
|
+
if (!repo) {
|
|
829
|
+
return "❌ 请指定仓库名";
|
|
830
|
+
}
|
|
831
|
+
const sessionId = getSessionIdentifier(session);
|
|
832
|
+
const success = await removeSubscription(ctx, sessionId, repo);
|
|
833
|
+
if (success) {
|
|
834
|
+
return `✅ 已取消订阅: ${repo}`;
|
|
835
|
+
}
|
|
836
|
+
return `❌ 未找到仓库 ${repo} 的订阅`;
|
|
837
|
+
});
|
|
838
|
+
ctx.command("gh.list", "列出当前会话的所有订阅").usage("gh.list").action(async ({ session }) => {
|
|
839
|
+
if (!session) return "❌ 无法获取会话信息";
|
|
840
|
+
const sessionId = getSessionIdentifier(session);
|
|
841
|
+
const subscriptions = await listSubscriptions(ctx, sessionId);
|
|
842
|
+
if (subscriptions.length === 0) {
|
|
843
|
+
return "📋 当前会话没有订阅任何仓库";
|
|
844
|
+
}
|
|
845
|
+
const lines = ["📋 订阅列表:"];
|
|
846
|
+
for (const sub of subscriptions) {
|
|
847
|
+
const status = sub.enabled ? "✅" : "⏸️";
|
|
848
|
+
const events = sub.events.join(", ");
|
|
849
|
+
lines.push(`${status} ${sub.repo}`);
|
|
850
|
+
lines.push(` 事件: ${events}`);
|
|
851
|
+
}
|
|
852
|
+
return lines.join("\n");
|
|
853
|
+
});
|
|
854
|
+
ctx.command("gh.events <repo:string> [...changes:string]", "查看或修改订阅的事件类型").usage("gh.events owner/repo [+event] [-event]").example("gh.events koishijs/koishi").example("gh.events koishijs/koishi +issues -star +release").action(async ({ session }, repo, ...changes) => {
|
|
855
|
+
if (!session) return "❌ 无法获取会话信息";
|
|
856
|
+
if (!repo) {
|
|
857
|
+
const lines = ["📋 可用事件类型:"];
|
|
858
|
+
for (const [type, info] of Object.entries(EVENT_DISPLAY_MAP)) {
|
|
859
|
+
lines.push(`${info.emoji} ${type} - ${info.name}`);
|
|
860
|
+
}
|
|
861
|
+
return lines.join("\n");
|
|
862
|
+
}
|
|
863
|
+
const sessionId = getSessionIdentifier(session);
|
|
864
|
+
const subscription = await getSubscription(ctx, sessionId, repo);
|
|
865
|
+
if (!subscription) {
|
|
866
|
+
return `❌ 未找到仓库 ${repo} 的订阅`;
|
|
867
|
+
}
|
|
868
|
+
if (!changes || changes.length === 0) {
|
|
869
|
+
const events = subscription.events.join(", ");
|
|
870
|
+
return `📋 ${repo} 订阅的事件:
|
|
871
|
+
${events}`;
|
|
872
|
+
}
|
|
873
|
+
const newEvents = parseEventChanges(changes, subscription.events);
|
|
874
|
+
const success = await updateEvents(ctx, sessionId, repo, newEvents);
|
|
875
|
+
if (success) {
|
|
876
|
+
const eventList = newEvents.join(", ");
|
|
877
|
+
return `✅ 已更新 ${repo} 的订阅事件:
|
|
878
|
+
${eventList}`;
|
|
879
|
+
}
|
|
880
|
+
return "❌ 更新失败";
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
__name(registerSubscriptionCommands, "registerSubscriptionCommands");
|
|
884
|
+
|
|
885
|
+
// src/commands/utils.ts
|
|
886
|
+
var PLUGIN_NAME = "github-webhook-pusher";
|
|
887
|
+
var ADMIN_AUTHORITY2 = 3;
|
|
888
|
+
var ALL_EVENT_TYPES2 = ["issues", "release", "push", "pull_request", "star"];
|
|
889
|
+
function generateTestEvent(repo, eventType) {
|
|
890
|
+
const baseEvent = {
|
|
891
|
+
type: eventType,
|
|
892
|
+
displayType: getDisplayType(eventType),
|
|
893
|
+
repo,
|
|
894
|
+
actor: "test-user",
|
|
895
|
+
url: `https://github.com/${repo}`
|
|
896
|
+
};
|
|
897
|
+
switch (eventType) {
|
|
898
|
+
case "issues":
|
|
899
|
+
return {
|
|
900
|
+
...baseEvent,
|
|
901
|
+
action: "opened",
|
|
902
|
+
title: "测试 Issue 标题",
|
|
903
|
+
number: 123
|
|
904
|
+
};
|
|
905
|
+
case "release":
|
|
906
|
+
return {
|
|
907
|
+
...baseEvent,
|
|
908
|
+
action: "published",
|
|
909
|
+
title: "v1.0.0",
|
|
910
|
+
tagName: "v1.0.0"
|
|
911
|
+
};
|
|
912
|
+
case "push":
|
|
913
|
+
return {
|
|
914
|
+
...baseEvent,
|
|
915
|
+
ref: "main",
|
|
916
|
+
commits: [
|
|
917
|
+
{ sha: "abc1234", message: "测试提交 1", author: "test-user", url: "" },
|
|
918
|
+
{ sha: "def5678", message: "测试提交 2", author: "test-user", url: "" }
|
|
919
|
+
],
|
|
920
|
+
totalCommits: 2
|
|
921
|
+
};
|
|
922
|
+
case "pull_request":
|
|
923
|
+
return {
|
|
924
|
+
...baseEvent,
|
|
925
|
+
action: "opened",
|
|
926
|
+
title: "测试 PR 标题",
|
|
927
|
+
number: 456
|
|
928
|
+
};
|
|
929
|
+
case "star":
|
|
930
|
+
return {
|
|
931
|
+
...baseEvent,
|
|
932
|
+
action: "created",
|
|
933
|
+
starCount: 1234
|
|
934
|
+
};
|
|
935
|
+
default:
|
|
936
|
+
return baseEvent;
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
__name(generateTestEvent, "generateTestEvent");
|
|
940
|
+
function registerUtilCommands(ctx, config) {
|
|
941
|
+
ctx.command("gh.ping", "查看插件状态").usage("gh.ping").action(async () => {
|
|
942
|
+
const repos = await listTrustedRepos(ctx);
|
|
943
|
+
const enabledCount = repos.filter((r) => r.enabled).length;
|
|
944
|
+
const lines = [
|
|
945
|
+
"🏓 GitHub Webhook 推送插件",
|
|
946
|
+
`📦 插件名称: ${PLUGIN_NAME}`,
|
|
947
|
+
`🔗 Webhook 路径: ${config.path}`,
|
|
948
|
+
`📋 信任仓库: ${repos.length} 个 (${enabledCount} 个已启用)`,
|
|
949
|
+
`🔧 调试模式: ${config.debug ? "开启" : "关闭"}`
|
|
950
|
+
];
|
|
951
|
+
return lines.join("\n");
|
|
952
|
+
});
|
|
953
|
+
ctx.command("gh.test <repo:string> <event:string>", "生成测试消息").usage("gh.test owner/repo event").example("gh.test koishijs/koishi issues").example("gh.test koishijs/koishi push").action(async ({ session }, repo, event) => {
|
|
954
|
+
const user = session?.user;
|
|
955
|
+
if ((user?.authority ?? 0) < ADMIN_AUTHORITY2) {
|
|
956
|
+
return "❌ 权限不足,需要管理员权限";
|
|
957
|
+
}
|
|
958
|
+
if (!repo) {
|
|
959
|
+
return "❌ 请指定仓库名,格式: owner/repo";
|
|
960
|
+
}
|
|
961
|
+
if (!event) {
|
|
962
|
+
const eventList = ALL_EVENT_TYPES2.map((e) => {
|
|
963
|
+
const info = EVENT_DISPLAY_MAP[e];
|
|
964
|
+
return `${info.emoji} ${e}`;
|
|
965
|
+
}).join(", ");
|
|
966
|
+
return `❌ 请指定事件类型
|
|
967
|
+
可用类型: ${eventList}`;
|
|
968
|
+
}
|
|
969
|
+
if (!ALL_EVENT_TYPES2.includes(event)) {
|
|
970
|
+
const eventList = ALL_EVENT_TYPES2.join(", ");
|
|
971
|
+
return `❌ 不支持的事件类型: ${event}
|
|
972
|
+
可用类型: ${eventList}`;
|
|
973
|
+
}
|
|
974
|
+
const testEvent = generateTestEvent(repo, event);
|
|
975
|
+
const message = buildMessage(testEvent);
|
|
976
|
+
await session?.send(message);
|
|
977
|
+
return;
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
__name(registerUtilCommands, "registerUtilCommands");
|
|
981
|
+
|
|
982
|
+
// src/index.ts
|
|
983
|
+
var name = "github-webhook-pusher";
|
|
984
|
+
var inject = ["server", "database"];
|
|
985
|
+
var logger3 = new import_koishi5.Logger("github-webhook-pusher");
|
|
986
|
+
function apply(ctx, config) {
|
|
987
|
+
logger3.info(`正在加载 GitHub Webhook 插件...`);
|
|
988
|
+
extendDatabase(ctx);
|
|
989
|
+
logger3.debug("数据库模型已注册");
|
|
990
|
+
registerWebhook(ctx, config);
|
|
991
|
+
logger3.debug(`Webhook 处理器已注册: ${config.path}`);
|
|
992
|
+
registerTrustCommands(ctx);
|
|
993
|
+
logger3.debug("信任仓库管理命令已注册");
|
|
994
|
+
registerSubscriptionCommands(ctx, config);
|
|
995
|
+
logger3.debug("订阅管理命令已注册");
|
|
996
|
+
registerUtilCommands(ctx, config);
|
|
997
|
+
logger3.debug("工具命令已注册");
|
|
998
|
+
logger3.info(`GitHub Webhook 插件已加载`);
|
|
999
|
+
logger3.info(`Webhook 路径: ${config.path}`);
|
|
1000
|
+
logger3.info(`默认订阅事件: ${config.defaultEvents.join(", ")}`);
|
|
1001
|
+
if (config.debug) {
|
|
1002
|
+
logger3.info("调试模式已启用");
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
__name(apply, "apply");
|
|
1006
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1007
|
+
0 && (module.exports = {
|
|
1008
|
+
Config,
|
|
1009
|
+
apply,
|
|
1010
|
+
inject,
|
|
1011
|
+
name
|
|
1012
|
+
});
|