@mastra/github-signals 0.1.0 → 0.1.1-alpha.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/CHANGELOG.md +16 -0
- package/dist/index.cjs +235 -38
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +34 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +235 -38
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# @mastra/github-signals
|
|
2
|
+
|
|
3
|
+
## 0.1.1-alpha.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Gate GitHub signal notifications behind author permission checks to guard against prompt injection from random commenters. Only comments from users with write access (admin, maintain, write) trigger notifications. Bot comments are opt-in via an allowlist that defaults to CodeRabbit and Devin, with `ignoredBots` still available as an explicit blocklist. Unauthorized latest comments are excluded before notification classification so noisy bot edits do not render in notification metadata or mask the latest authorized comment. Scheduled polls now include comments and detect latest-comment timestamp changes so comment notifications are not lost behind stale or unchanged thread hashes. Comment activity notifications render the latest authorized comment author and excerpt as high-priority GitHub signal updates. ([#17590](https://github.com/mastra-ai/mastra/pull/17590))
|
|
8
|
+
|
|
9
|
+
New options on `GithubSignalsOptions`:
|
|
10
|
+
- `authorizedPermissions` — permission levels that authorize human commenters (default: `['admin', 'maintain', 'write']`)
|
|
11
|
+
- `authorizedBots` — bot logins authorized to trigger notifications (default: `['coderabbitai[bot]', 'devin-ai-integration[bot]']`)
|
|
12
|
+
- `ignoredBots` — bot logins whose comments should NOT trigger notifications, even if authorized
|
|
13
|
+
- `permissionResolver` — injectable resolver for looking up collaborator permissions (default: `gh api`)
|
|
14
|
+
|
|
15
|
+
- Updated dependencies [[`2bccba4`](https://github.com/mastra-ai/mastra/commit/2bccba4c03cadc815c2d54cbf4dd43a922140a8d), [`2bccba4`](https://github.com/mastra-ai/mastra/commit/2bccba4c03cadc815c2d54cbf4dd43a922140a8d), [`f2ab060`](https://github.com/mastra-ai/mastra/commit/f2ab060162bea81505fda553e2cee29c1979fd04), [`5d302c8`](https://github.com/mastra-ai/mastra/commit/5d302c8eda1a6ac74eab5e442c4f64db6cc97a06)]:
|
|
16
|
+
- @mastra/core@1.42.0-alpha.1
|
package/dist/index.cjs
CHANGED
|
@@ -26,6 +26,10 @@ var GITHUB_SUBSCRIBE_PR_TAG = "github-subscribe-pr";
|
|
|
26
26
|
var GITHUB_UNSUBSCRIBE_PR_TAG = "github-unsubscribe-pr";
|
|
27
27
|
var GITHUB_SYNC_STATUS_TAG = "github-sync-status";
|
|
28
28
|
var GITHUB_SIGNALS_METADATA_KEY = "githubSignals";
|
|
29
|
+
var DEFAULT_AUTHORIZED_PERMISSIONS = ["admin", "maintain", "write"];
|
|
30
|
+
var DEFAULT_AUTHORIZED_BOTS = ["coderabbitai[bot]", "devin-ai-integration[bot]"];
|
|
31
|
+
var PERMISSION_CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
32
|
+
var AUTHOR_GATED_NOTIFICATION_KINDS = /* @__PURE__ */ new Set(["pull-request-activity", "pull-request-review-activity"]);
|
|
29
33
|
var createGithubTool = tools.createTool;
|
|
30
34
|
function isPlainObject(value) {
|
|
31
35
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -146,6 +150,36 @@ function getPrLabel(subscription, snapshot) {
|
|
|
146
150
|
function getMergedNotificationSummary(label) {
|
|
147
151
|
return `${label} was merged. This thread has been automatically unsubscribed from this PR. Resubscribe if you still need updates.`;
|
|
148
152
|
}
|
|
153
|
+
function getCommentExcerpt(body) {
|
|
154
|
+
const excerpt = body.replace(/\s+/g, " ").trim();
|
|
155
|
+
return excerpt.length > 240 ? `${excerpt.slice(0, 237)}...` : excerpt;
|
|
156
|
+
}
|
|
157
|
+
function getCommentNotificationSummary(pr, snapshot) {
|
|
158
|
+
if (!snapshot.latestCommentAuthor || !snapshot.latestCommentBody) return void 0;
|
|
159
|
+
return `${snapshot.latestCommentAuthor} commented on ${pr}: ${getCommentExcerpt(snapshot.latestCommentBody)}`;
|
|
160
|
+
}
|
|
161
|
+
var githubActivityNotificationPriority = {
|
|
162
|
+
high: 0,
|
|
163
|
+
medium: 1
|
|
164
|
+
};
|
|
165
|
+
function getGithubActivityNotificationRank(notification) {
|
|
166
|
+
return notification.kind === "pull-request-activity" ? 0 : 1;
|
|
167
|
+
}
|
|
168
|
+
function compareGithubActivityNotifications(a, b) {
|
|
169
|
+
if (!a && !b) return 0;
|
|
170
|
+
if (!a) return 1;
|
|
171
|
+
if (!b) return -1;
|
|
172
|
+
const priorityComparison = githubActivityNotificationPriority[a.priority] - githubActivityNotificationPriority[b.priority];
|
|
173
|
+
if (priorityComparison !== 0) return priorityComparison;
|
|
174
|
+
return getGithubActivityNotificationRank(a) - getGithubActivityNotificationRank(b);
|
|
175
|
+
}
|
|
176
|
+
function classifyGithubCommentActivityNotification(input) {
|
|
177
|
+
if (isBotOnlyActivity(input.snapshot)) return void 0;
|
|
178
|
+
const pr = `${input.subscription.owner}/${input.subscription.repo}#${input.subscription.number}`;
|
|
179
|
+
const summary = getCommentNotificationSummary(pr, input.snapshot);
|
|
180
|
+
if (!summary) return void 0;
|
|
181
|
+
return { kind: "pull-request-activity", priority: "high", summary };
|
|
182
|
+
}
|
|
149
183
|
function getCheckUpdatedTime(check) {
|
|
150
184
|
const value = check.updatedAt ? Date.parse(check.updatedAt) : Number.NaN;
|
|
151
185
|
return Number.isFinite(value) ? value : 0;
|
|
@@ -269,10 +303,11 @@ function classifyGithubActivityNotification(input) {
|
|
|
269
303
|
}
|
|
270
304
|
if (input.snapshot.ciState === "pending" && input.subscription.lastObservedCiState === "pending") return void 0;
|
|
271
305
|
if (isBotOnlyActivity(input.snapshot)) return void 0;
|
|
306
|
+
const commentSummary = getCommentNotificationSummary(pr, input.snapshot);
|
|
272
307
|
return {
|
|
273
308
|
kind: "pull-request-activity",
|
|
274
|
-
priority: "medium",
|
|
275
|
-
summary: `${pr} has new activity${input.snapshot.title ? `: ${input.snapshot.title}` : ""}`
|
|
309
|
+
priority: commentSummary ? "high" : "medium",
|
|
310
|
+
summary: commentSummary ?? `${pr} has new activity${input.snapshot.title ? `: ${input.snapshot.title}` : ""}`
|
|
276
311
|
};
|
|
277
312
|
}
|
|
278
313
|
function classifyGithubBaselineNotification(input) {
|
|
@@ -397,13 +432,15 @@ var GitcrawlSyncClient = class {
|
|
|
397
432
|
join threads t on t.id=rt.thread_id
|
|
398
433
|
join repositories r on r.id=t.repo_id
|
|
399
434
|
where r.owner=${owner} and r.name=${repo} and t.number=${number} and rt.is_resolved=0`);
|
|
400
|
-
const
|
|
435
|
+
const latestComments = await queryGitcrawlDb(`select c.author_login, c.author_type, c.is_bot, c.body, json_extract(c.raw_json, '$.html_url') as html_url,
|
|
436
|
+
coalesce(c.updated_at_gh, c.created_at_gh) as updated_at
|
|
401
437
|
from comments c
|
|
402
438
|
join threads t on t.id=c.thread_id
|
|
403
439
|
join repositories r on r.id=t.repo_id
|
|
404
440
|
where r.owner=${owner} and r.name=${repo} and t.number=${number}
|
|
405
441
|
order by coalesce(c.updated_at_gh, c.created_at_gh) desc
|
|
406
|
-
limit
|
|
442
|
+
limit 20`);
|
|
443
|
+
const latestComment = latestComments[0];
|
|
407
444
|
const checks = normalizeGithubChecksForSnapshot({
|
|
408
445
|
checkRows: checkRows.map((row) => ({
|
|
409
446
|
source: "check",
|
|
@@ -465,7 +502,18 @@ var GitcrawlSyncClient = class {
|
|
|
465
502
|
latestReviewThreadAt: readString(reviewState?.latest_review_thread_at),
|
|
466
503
|
latestCommentAuthor: readString(latestComment?.author_login),
|
|
467
504
|
latestCommentAuthorType: readString(latestComment?.author_type),
|
|
468
|
-
latestCommentIsBot: latestComment?.is_bot === 1
|
|
505
|
+
latestCommentIsBot: latestComment?.is_bot === 1,
|
|
506
|
+
latestCommentBody: readString(latestComment?.body),
|
|
507
|
+
latestCommentUrl: readString(latestComment?.html_url),
|
|
508
|
+
latestCommentUpdatedAt: readString(latestComment?.updated_at),
|
|
509
|
+
latestComments: latestComments.map((comment) => ({
|
|
510
|
+
author: readString(comment.author_login),
|
|
511
|
+
authorType: readString(comment.author_type),
|
|
512
|
+
isBot: comment.is_bot === 1,
|
|
513
|
+
body: readString(comment.body),
|
|
514
|
+
url: readString(comment.html_url),
|
|
515
|
+
updatedAt: readString(comment.updated_at)
|
|
516
|
+
}))
|
|
469
517
|
};
|
|
470
518
|
} catch {
|
|
471
519
|
return void 0;
|
|
@@ -520,9 +568,11 @@ var GithubSignals = class extends signals.SignalProvider {
|
|
|
520
568
|
#syncClient;
|
|
521
569
|
#repositoryResolver;
|
|
522
570
|
#polling = /* @__PURE__ */ new Map();
|
|
571
|
+
#permissionCache = /* @__PURE__ */ new Map();
|
|
523
572
|
#agent;
|
|
524
573
|
#agentOptions = {};
|
|
525
574
|
#subscriptionsChangedHandler;
|
|
575
|
+
#pollingChangedHandler;
|
|
526
576
|
constructor(options = {}) {
|
|
527
577
|
super();
|
|
528
578
|
this.#options = options;
|
|
@@ -557,6 +607,9 @@ var GithubSignals = class extends signals.SignalProvider {
|
|
|
557
607
|
onSubscriptionsChanged(handler) {
|
|
558
608
|
this.#subscriptionsChangedHandler = handler;
|
|
559
609
|
}
|
|
610
|
+
onPollingChanged(handler) {
|
|
611
|
+
this.#pollingChangedHandler = handler;
|
|
612
|
+
}
|
|
560
613
|
__registerMastra(mastra) {
|
|
561
614
|
super.__registerMastra(mastra);
|
|
562
615
|
this.#ghMastra = mastra;
|
|
@@ -595,15 +648,13 @@ var GithubSignals = class extends signals.SignalProvider {
|
|
|
595
648
|
this.#polling.delete(pollingKey);
|
|
596
649
|
}
|
|
597
650
|
if (this.#polling.has(key)) return true;
|
|
598
|
-
let scheduledPollCount = options.pollImmediately ? 1 : 0;
|
|
599
651
|
const runPoll = (pollOptions = {}) => {
|
|
600
652
|
void this.#pollThread(input, pollOptions).catch((error) => {
|
|
601
653
|
console.warn("GitHub PR polling failed:", error);
|
|
602
654
|
});
|
|
603
655
|
};
|
|
604
656
|
const timer = setInterval(() => {
|
|
605
|
-
|
|
606
|
-
runPoll({ includeComments: scheduledPollCount % 2 === 1 });
|
|
657
|
+
runPoll({ includeComments: true });
|
|
607
658
|
}, this.#options.pollIntervalMs ?? 3e5);
|
|
608
659
|
if (options.pollImmediately) runPoll({ includeComments: true });
|
|
609
660
|
timer.unref?.();
|
|
@@ -620,6 +671,9 @@ var GithubSignals = class extends signals.SignalProvider {
|
|
|
620
671
|
isPollingThread(input) {
|
|
621
672
|
return this.#polling.has(this.#pollingKey(input));
|
|
622
673
|
}
|
|
674
|
+
isPollingThreadRunning(input) {
|
|
675
|
+
return this.#polling.get(this.#pollingKey(input))?.running ?? false;
|
|
676
|
+
}
|
|
623
677
|
getPollIntervalMs() {
|
|
624
678
|
return this.#options.pollIntervalMs ?? 3e5;
|
|
625
679
|
}
|
|
@@ -805,6 +859,9 @@ var GithubSignals = class extends signals.SignalProvider {
|
|
|
805
859
|
#notifySubscriptionsChanged(input) {
|
|
806
860
|
this.#subscriptionsChangedHandler?.(input);
|
|
807
861
|
}
|
|
862
|
+
#notifyPollingChanged(input) {
|
|
863
|
+
this.#pollingChangedHandler?.(input);
|
|
864
|
+
}
|
|
808
865
|
async #pollThread(input, options = {}) {
|
|
809
866
|
const key = this.#pollingKey(input);
|
|
810
867
|
const state = this.#polling.get(key);
|
|
@@ -812,6 +869,7 @@ var GithubSignals = class extends signals.SignalProvider {
|
|
|
812
869
|
return 0;
|
|
813
870
|
}
|
|
814
871
|
if (state) state.running = true;
|
|
872
|
+
this.#notifyPollingChanged({ threadId: input.threadId, resourceId: input.resourceId, running: true });
|
|
815
873
|
try {
|
|
816
874
|
const { threadStore, loadedThread } = await this.#loadThread(input);
|
|
817
875
|
const githubMetadata = getGithubMetadata(loadedThread.metadata);
|
|
@@ -830,7 +888,9 @@ var GithubSignals = class extends signals.SignalProvider {
|
|
|
830
888
|
includeComments: options.includeComments
|
|
831
889
|
};
|
|
832
890
|
const syncResult = await this.#syncClient.syncPullRequest(syncInput);
|
|
833
|
-
|
|
891
|
+
let snapshot = syncResult.ok ? await this.#syncClient.getPullRequestSnapshot?.(syncInput) : void 0;
|
|
892
|
+
if (snapshot)
|
|
893
|
+
snapshot = await this.#filterUnauthorizedLatestComment(subscription.owner, subscription.repo, snapshot);
|
|
834
894
|
const nextSubscription = {
|
|
835
895
|
...subscription,
|
|
836
896
|
updatedAt: now,
|
|
@@ -843,25 +903,28 @@ var GithubSignals = class extends signals.SignalProvider {
|
|
|
843
903
|
const previousContentHash = subscription.lastObservedContentHash;
|
|
844
904
|
const previousThreadContentHash = subscription.lastObservedThreadContentHash;
|
|
845
905
|
const previousHeadSha = subscription.lastObservedHeadSha;
|
|
906
|
+
const latestCommentChanged = !!previousGithubUpdatedAt && !!snapshot?.latestCommentUpdatedAt && Date.parse(snapshot.latestCommentUpdatedAt) > Date.parse(previousGithubUpdatedAt);
|
|
846
907
|
if (snapshot) applySnapshotCursor(nextSubscription, snapshot);
|
|
847
908
|
const isFirstObservation = syncResult.ok && snapshot && !previousGithubUpdatedAt && !previousContentHash;
|
|
848
909
|
const legacyAggregateChanged = previousContentHash && snapshot?.contentHash && previousContentHash !== snapshot.contentHash && !previousThreadContentHash && !previousHeadSha;
|
|
849
|
-
const changed = isFirstObservation || syncResult.ok && snapshot && (legacyAggregateChanged || previousThreadContentHash && snapshot.threadContentHash && previousThreadContentHash !== snapshot.threadContentHash || previousHeadSha && snapshot.headSha && previousHeadSha !== snapshot.headSha || subscription.lastObservedState && snapshot.state && subscription.lastObservedState !== snapshot.state || subscription.lastObservedMergeableState && snapshot.mergeableState && subscription.lastObservedMergeableState !== snapshot.mergeableState || subscription.lastObservedCiState && snapshot.ciState && subscription.lastObservedCiState !== snapshot.ciState || subscription.lastObservedReviewStateHash && snapshot.reviewStateHash && subscription.lastObservedReviewStateHash !== snapshot.reviewStateHash);
|
|
910
|
+
const changed = isFirstObservation || syncResult.ok && snapshot && (legacyAggregateChanged || latestCommentChanged || previousThreadContentHash && snapshot.threadContentHash && previousThreadContentHash !== snapshot.threadContentHash || previousHeadSha && snapshot.headSha && previousHeadSha !== snapshot.headSha || subscription.lastObservedState && snapshot.state && subscription.lastObservedState !== snapshot.state || subscription.lastObservedMergeableState && snapshot.mergeableState && subscription.lastObservedMergeableState !== snapshot.mergeableState || subscription.lastObservedCiState && snapshot.ciState && subscription.lastObservedCiState !== snapshot.ciState || subscription.lastObservedReviewStateHash && snapshot.reviewStateHash && subscription.lastObservedReviewStateHash !== snapshot.reviewStateHash);
|
|
850
911
|
let shouldKeepSubscription = true;
|
|
851
|
-
if (changed) {
|
|
852
|
-
const
|
|
912
|
+
if (changed && snapshot) {
|
|
913
|
+
const notifications = await this.#sendActivityNotifications({
|
|
853
914
|
polling: input,
|
|
854
915
|
subscription,
|
|
855
916
|
snapshot,
|
|
856
917
|
previousGithubUpdatedAt,
|
|
857
|
-
previousContentHash
|
|
918
|
+
previousContentHash,
|
|
919
|
+
latestCommentChanged
|
|
858
920
|
});
|
|
859
|
-
|
|
921
|
+
const primaryNotification = notifications[0];
|
|
922
|
+
if (primaryNotification) {
|
|
860
923
|
nextSubscription.lastNotificationAt = now;
|
|
861
|
-
nextSubscription.lastNotificationKind =
|
|
862
|
-
nextSubscription.lastNotificationPriority =
|
|
863
|
-
nextSubscription.lastNotificationSummary =
|
|
864
|
-
shouldKeepSubscription = notification.kind !== "pull-request-merged";
|
|
924
|
+
nextSubscription.lastNotificationKind = primaryNotification.kind;
|
|
925
|
+
nextSubscription.lastNotificationPriority = primaryNotification.priority;
|
|
926
|
+
nextSubscription.lastNotificationSummary = primaryNotification.summary;
|
|
927
|
+
shouldKeepSubscription = notifications.every((notification) => notification.kind !== "pull-request-merged");
|
|
865
928
|
}
|
|
866
929
|
}
|
|
867
930
|
if (shouldKeepSubscription) subscriptions.push(nextSubscription);
|
|
@@ -884,17 +947,20 @@ var GithubSignals = class extends signals.SignalProvider {
|
|
|
884
947
|
} finally {
|
|
885
948
|
const latestState = this.#polling.get(key);
|
|
886
949
|
if (latestState) latestState.running = false;
|
|
950
|
+
this.#notifyPollingChanged({ threadId: input.threadId, resourceId: input.resourceId, running: false });
|
|
887
951
|
}
|
|
888
952
|
}
|
|
889
|
-
|
|
953
|
+
#createGithubNotificationInput(input) {
|
|
890
954
|
const failingChecks = getFailingChecks(input.snapshot);
|
|
891
955
|
const pendingChecks = getPendingChecks(input.snapshot);
|
|
956
|
+
const latestCommentExcerpt = input.snapshot.latestCommentBody ? getCommentExcerpt(input.snapshot.latestCommentBody) : void 0;
|
|
957
|
+
const latestCommentDedupeSuffix = input.notification.kind === "pull-request-activity" && input.snapshot.latestCommentUrl ? `comment:${input.snapshot.latestCommentUrl}:${input.snapshot.latestCommentUpdatedAt ?? ""}` : input.dedupeSuffix;
|
|
892
958
|
const notificationInput = {
|
|
893
959
|
source: "github",
|
|
894
960
|
kind: input.notification.kind,
|
|
895
961
|
priority: input.notification.priority,
|
|
896
962
|
summary: input.notification.summary,
|
|
897
|
-
dedupeKey: `github:${input.subscription.owner}/${input.subscription.repo}#${input.subscription.number}:${
|
|
963
|
+
dedupeKey: `github:${input.subscription.owner}/${input.subscription.repo}#${input.subscription.number}:${latestCommentDedupeSuffix}`,
|
|
898
964
|
coalesceKey: `github:${input.subscription.owner}/${input.subscription.repo}#${input.subscription.number}:${input.notification.kind}`,
|
|
899
965
|
attributes: {
|
|
900
966
|
owner: input.subscription.owner,
|
|
@@ -908,6 +974,10 @@ var GithubSignals = class extends signals.SignalProvider {
|
|
|
908
974
|
...input.snapshot.mergeableState ? { mergeableState: input.snapshot.mergeableState } : {},
|
|
909
975
|
...input.snapshot.ciState ? { ciState: input.snapshot.ciState } : {},
|
|
910
976
|
...input.snapshot.unresolvedReviewThreads !== void 0 ? { unresolvedReviewThreads: input.snapshot.unresolvedReviewThreads } : {},
|
|
977
|
+
...input.snapshot.latestCommentAuthor ? { latestCommentAuthor: input.snapshot.latestCommentAuthor } : {},
|
|
978
|
+
...latestCommentExcerpt ? { latestCommentExcerpt } : {},
|
|
979
|
+
...input.snapshot.latestCommentUrl ? { latestCommentUrl: input.snapshot.latestCommentUrl } : {},
|
|
980
|
+
...input.snapshot.latestCommentUpdatedAt ? { latestCommentUpdatedAt: input.snapshot.latestCommentUpdatedAt } : {},
|
|
911
981
|
...failingChecks.length > 0 ? { failingChecks: failingChecks.map((check) => check.name).join(", ") } : {},
|
|
912
982
|
...pendingChecks.length > 0 ? { pendingChecks: pendingChecks.map((check) => check.name).join(", ") } : {}
|
|
913
983
|
},
|
|
@@ -936,11 +1006,19 @@ var GithubSignals = class extends signals.SignalProvider {
|
|
|
936
1006
|
latestCommentAuthor: input.snapshot.latestCommentAuthor,
|
|
937
1007
|
latestCommentAuthorType: input.snapshot.latestCommentAuthorType,
|
|
938
1008
|
latestCommentIsBot: input.snapshot.latestCommentIsBot,
|
|
1009
|
+
latestCommentBody: input.snapshot.latestCommentBody,
|
|
1010
|
+
latestCommentExcerpt,
|
|
1011
|
+
latestCommentUrl: input.snapshot.latestCommentUrl,
|
|
1012
|
+
latestCommentUpdatedAt: input.snapshot.latestCommentUpdatedAt,
|
|
939
1013
|
failingChecks,
|
|
940
1014
|
pendingChecks
|
|
941
1015
|
}
|
|
942
1016
|
}
|
|
943
1017
|
};
|
|
1018
|
+
return notificationInput;
|
|
1019
|
+
}
|
|
1020
|
+
async #sendGithubNotification(input) {
|
|
1021
|
+
const notificationInput = this.#createGithubNotificationInput(input);
|
|
944
1022
|
const streamOptions = await this.#agentOptions.getNotificationStreamOptions?.(input.target);
|
|
945
1023
|
await input.agent?.sendNotificationSignal?.(
|
|
946
1024
|
notificationInput,
|
|
@@ -959,25 +1037,144 @@ var GithubSignals = class extends signals.SignalProvider {
|
|
|
959
1037
|
dedupeSuffix: `baseline:${input.subscription.lastSubscribeSignalId}`
|
|
960
1038
|
});
|
|
961
1039
|
}
|
|
962
|
-
async #
|
|
1040
|
+
async #isAuthorizedAuthor(owner, repo, user, metadata = {}) {
|
|
1041
|
+
if (!user) return false;
|
|
1042
|
+
const normalizedUser = user.toLowerCase();
|
|
1043
|
+
const isBot = metadata.isBot === true || metadata.authorType?.toLowerCase() === "bot" || normalizedUser.endsWith("[bot]");
|
|
1044
|
+
if (isBot) {
|
|
1045
|
+
const ignoredBots = this.#options.ignoredBots ?? [];
|
|
1046
|
+
if (ignoredBots.some((bot) => bot.toLowerCase() === normalizedUser)) return false;
|
|
1047
|
+
const authorizedBots = this.#options.authorizedBots ?? DEFAULT_AUTHORIZED_BOTS;
|
|
1048
|
+
return authorizedBots.some((bot) => bot.toLowerCase() === normalizedUser);
|
|
1049
|
+
}
|
|
1050
|
+
const permission = await this.#loadAuthorPermission(owner, repo, user);
|
|
1051
|
+
const authorizedPermissions = this.#options.authorizedPermissions ?? DEFAULT_AUTHORIZED_PERMISSIONS;
|
|
1052
|
+
return !!permission && authorizedPermissions.includes(permission);
|
|
1053
|
+
}
|
|
1054
|
+
async #filterUnauthorizedLatestComment(owner, repo, snapshot) {
|
|
1055
|
+
const comments = snapshot.latestComments?.length ? snapshot.latestComments : [
|
|
1056
|
+
{
|
|
1057
|
+
author: snapshot.latestCommentAuthor,
|
|
1058
|
+
authorType: snapshot.latestCommentAuthorType,
|
|
1059
|
+
isBot: snapshot.latestCommentIsBot,
|
|
1060
|
+
body: snapshot.latestCommentBody,
|
|
1061
|
+
url: snapshot.latestCommentUrl,
|
|
1062
|
+
updatedAt: snapshot.latestCommentUpdatedAt
|
|
1063
|
+
}
|
|
1064
|
+
];
|
|
1065
|
+
if (!comments.some((comment) => comment.author)) return snapshot;
|
|
1066
|
+
if (!comments.some((comment) => comment.body || comment.url || comment.updatedAt)) return snapshot;
|
|
1067
|
+
for (const comment of comments) {
|
|
1068
|
+
if (!await this.#isAuthorizedAuthor(owner, repo, comment.author, {
|
|
1069
|
+
authorType: comment.authorType,
|
|
1070
|
+
isBot: comment.isBot
|
|
1071
|
+
})) {
|
|
1072
|
+
continue;
|
|
1073
|
+
}
|
|
1074
|
+
return {
|
|
1075
|
+
...snapshot,
|
|
1076
|
+
latestCommentAuthor: comment.author,
|
|
1077
|
+
latestCommentAuthorType: comment.authorType,
|
|
1078
|
+
latestCommentIsBot: comment.isBot,
|
|
1079
|
+
latestCommentBody: comment.body,
|
|
1080
|
+
latestCommentUrl: comment.url,
|
|
1081
|
+
latestCommentUpdatedAt: comment.updatedAt
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
return {
|
|
1085
|
+
...snapshot,
|
|
1086
|
+
latestCommentAuthor: void 0,
|
|
1087
|
+
latestCommentAuthorType: void 0,
|
|
1088
|
+
latestCommentIsBot: void 0,
|
|
1089
|
+
latestCommentBody: void 0,
|
|
1090
|
+
latestCommentUrl: void 0,
|
|
1091
|
+
latestCommentUpdatedAt: void 0
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
async #loadAuthorPermission(owner, repo, user) {
|
|
1095
|
+
const cacheKey = `${owner}/${repo}:${user.toLowerCase()}`;
|
|
1096
|
+
const cached = this.#permissionCache.get(cacheKey);
|
|
1097
|
+
if (cached && cached.expiresAt > Date.now()) return cached.permission;
|
|
1098
|
+
if (cached) this.#permissionCache.delete(cacheKey);
|
|
1099
|
+
try {
|
|
1100
|
+
let permission;
|
|
1101
|
+
if (this.#options.permissionResolver) {
|
|
1102
|
+
permission = await this.#options.permissionResolver.getPermission(owner, repo, user);
|
|
1103
|
+
} else {
|
|
1104
|
+
const { stdout } = await execFileAsync("gh", [
|
|
1105
|
+
"api",
|
|
1106
|
+
`repos/${owner}/${repo}/collaborators/${user}/permission`,
|
|
1107
|
+
"--jq",
|
|
1108
|
+
".permission"
|
|
1109
|
+
]);
|
|
1110
|
+
const raw = stdout.trim();
|
|
1111
|
+
permission = ["admin", "maintain", "write", "triage", "read", "none"].includes(
|
|
1112
|
+
raw
|
|
1113
|
+
) ? raw : void 0;
|
|
1114
|
+
}
|
|
1115
|
+
if (permission) {
|
|
1116
|
+
this.#permissionCache.set(cacheKey, { permission, expiresAt: Date.now() + PERMISSION_CACHE_TTL_MS });
|
|
1117
|
+
}
|
|
1118
|
+
return permission;
|
|
1119
|
+
} catch {
|
|
1120
|
+
this.#permissionCache.delete(cacheKey);
|
|
1121
|
+
return void 0;
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
async #sendActivityNotifications(input) {
|
|
963
1125
|
const agent = this.#getNotificationAgent(input.polling);
|
|
964
|
-
if (!agent?.sendNotificationSignal) return
|
|
965
|
-
const
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
1126
|
+
if (!agent?.sendNotificationSignal) return [];
|
|
1127
|
+
const notifications = [
|
|
1128
|
+
classifyGithubActivityNotification({
|
|
1129
|
+
subscription: input.subscription,
|
|
1130
|
+
snapshot: input.snapshot
|
|
1131
|
+
})
|
|
1132
|
+
];
|
|
1133
|
+
if (input.latestCommentChanged && notifications[0]?.kind !== "pull-request-activity") {
|
|
1134
|
+
notifications.push(
|
|
1135
|
+
classifyGithubCommentActivityNotification({
|
|
1136
|
+
subscription: input.subscription,
|
|
1137
|
+
snapshot: input.snapshot
|
|
1138
|
+
})
|
|
1139
|
+
);
|
|
1140
|
+
}
|
|
1141
|
+
const sent = [];
|
|
1142
|
+
const notificationInputs = [];
|
|
1143
|
+
for (const notification of notifications.sort(compareGithubActivityNotifications)) {
|
|
1144
|
+
if (!notification) continue;
|
|
1145
|
+
if (AUTHOR_GATED_NOTIFICATION_KINDS.has(notification.kind)) {
|
|
1146
|
+
const authorized = await this.#isAuthorizedAuthor(
|
|
1147
|
+
input.subscription.owner,
|
|
1148
|
+
input.subscription.repo,
|
|
1149
|
+
input.snapshot.latestCommentAuthor,
|
|
1150
|
+
{
|
|
1151
|
+
authorType: input.snapshot.latestCommentAuthorType,
|
|
1152
|
+
isBot: input.snapshot.latestCommentIsBot
|
|
1153
|
+
}
|
|
1154
|
+
);
|
|
1155
|
+
if (!authorized) continue;
|
|
1156
|
+
}
|
|
1157
|
+
notificationInputs.push(
|
|
1158
|
+
this.#createGithubNotificationInput({
|
|
1159
|
+
subscription: input.subscription,
|
|
1160
|
+
snapshot: input.snapshot,
|
|
1161
|
+
notification,
|
|
1162
|
+
dedupeSuffix: input.snapshot.contentHash ?? input.snapshot.githubUpdatedAt ?? String(Date.now()),
|
|
1163
|
+
previousGithubUpdatedAt: input.previousGithubUpdatedAt,
|
|
1164
|
+
previousContentHash: input.previousContentHash
|
|
1165
|
+
})
|
|
1166
|
+
);
|
|
1167
|
+
sent.push(notification);
|
|
1168
|
+
}
|
|
1169
|
+
if (notificationInputs.length > 0) {
|
|
1170
|
+
const target = { resourceId: input.polling.resourceId, threadId: input.polling.threadId };
|
|
1171
|
+
const streamOptions = await this.#agentOptions.getNotificationStreamOptions?.(target);
|
|
1172
|
+
await agent.sendNotificationSignal(
|
|
1173
|
+
notificationInputs,
|
|
1174
|
+
streamOptions ? { ...target, ifIdle: { streamOptions } } : target
|
|
1175
|
+
);
|
|
1176
|
+
}
|
|
1177
|
+
return sent;
|
|
981
1178
|
}
|
|
982
1179
|
async #subscribe(input) {
|
|
983
1180
|
const { owner, repo } = await this.#resolveRepository(input);
|