create-feedbax 0.2.0 → 0.2.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/README.md +17 -5
- package/dist/cli.js +2610 -291
- package/dist/index.js +2375 -200
- package/package.json +7 -7
- package/template/.feedbax-template.sha256 +1 -1
- package/template/.gitignore.template +1 -0
- package/template/.npmignore.template +1 -0
- package/template/.nvmrc +1 -0
- package/template/AGENTS.md +13 -0
- package/template/CONNECTED.md +418 -0
- package/template/README.md +97 -0
- package/template/components.json +1 -1
- package/template/eslint.config.mjs +23 -0
- package/template/feedbax-vite.ts +79 -0
- package/template/migrations/0001_connected.sql +6 -0
- package/template/migrations/0002_participation.sql +8 -0
- package/template/migrations/0003_images.sql +3 -0
- package/template/migrations/0004_followed_status.sql +9 -0
- package/template/migrations/0005_github.sql +4 -0
- package/template/migrations/0006_integration.sql +3 -0
- package/template/migrations/0007_ownership.sql +2 -0
- package/template/migrations/0008_integration_recovery.sql +5 -0
- package/template/migrations/0009_observed_history.sql +3 -0
- package/template/package.json +39 -34
- package/template/src/authorized-draft-post.tsx +1 -0
- package/template/src/changelog-page.tsx +63 -54
- package/template/src/changelog-runtime.ts +2 -1
- package/template/src/cloudflare-workers.d.ts +9 -0
- package/template/src/comment-creation.test.tsx +95 -7
- package/template/src/comment-thread-page.ts +11 -1
- package/template/src/components/command-palette.tsx +59 -30
- package/template/src/components/connected-identity-provider.tsx +135 -0
- package/template/src/components/device-profile-provider.tsx +41 -26
- package/template/src/components/follow-post-button.tsx +78 -0
- package/template/src/components/header.tsx +2 -0
- package/template/src/components/image-attachments.tsx +187 -0
- package/template/src/components/loader.tsx +48 -5
- package/template/src/components/notification-inbox.tsx +232 -0
- package/template/src/components/portal-footer.tsx +14 -0
- package/template/src/components/post-history.tsx +101 -0
- package/template/src/components/profile-menu.tsx +37 -0
- package/template/src/components/ui/components/command.tsx +30 -29
- package/template/src/components/ui/components/message-scroller.tsx +2 -2
- package/template/src/components/ui/components/scroll-area.tsx +0 -1
- package/template/src/components/ui/components/switch.tsx +29 -0
- package/template/src/components/ui/styles/globals.css +1 -1
- package/template/src/configured-feedback.test.ts +82 -0
- package/template/src/configured-feedback.ts +61 -0
- package/template/src/connected-identity.test.ts +148 -0
- package/template/src/connected-identity.ts +127 -0
- package/template/src/connected-participation.test.tsx +67 -0
- package/template/src/connected-runtime.test.ts +244 -0
- package/template/src/connected-runtime.ts +176 -0
- package/template/src/connected-social.test.tsx +158 -0
- package/template/src/feedback-cache-invalidation.ts +3 -0
- package/template/src/feedback-runtime.ts +123 -13
- package/template/src/feedbax/changelog/index.test.ts +35 -0
- package/template/src/feedbax/changelog/index.ts +5 -1
- package/template/src/feedbax/changelog/notion-changelog.test.ts +1 -1
- package/template/src/feedbax/changelog/notion-changelog.ts +1 -1
- package/template/src/feedbax/config/index.test.ts +73 -1
- package/template/src/feedbax/config/index.ts +205 -9
- package/template/src/feedbax/feedback/comment-storage.ts +38 -0
- package/template/src/feedbax/feedback/connected-feedback.ts +235 -0
- package/template/src/feedbax/feedback/connected.test.ts +272 -0
- package/template/src/feedbax/feedback/connected.ts +225 -0
- package/template/src/feedbax/feedback/email-delivery.test.ts +65 -0
- package/template/src/feedbax/feedback/email-delivery.ts +66 -0
- package/template/src/feedbax/feedback/feedback-storage.ts +1 -0
- package/template/src/feedbax/feedback/github-comments.ts +390 -0
- package/template/src/feedbax/feedback/github-feedback.test.ts +1138 -0
- package/template/src/feedbax/feedback/github-feedback.ts +581 -0
- package/template/src/feedbax/feedback/github-request.ts +120 -0
- package/template/src/feedbax/feedback/github-service-client.ts +56 -0
- package/template/src/feedbax/feedback/identity-handoff.test.ts +81 -0
- package/template/src/feedbax/feedback/identity-handoff.ts +119 -0
- package/template/src/feedbax/feedback/image-feedback.ts +114 -0
- package/template/src/feedbax/feedback/images.test.ts +223 -0
- package/template/src/feedbax/feedback/images.ts +350 -0
- package/template/src/feedbax/feedback/index.ts +226 -16
- package/template/src/feedbax/feedback/integration-auth.ts +138 -0
- package/template/src/feedbax/feedback/notion-comments.ts +2 -35
- package/template/src/feedbax/feedback/notion-image-review.ts +55 -0
- package/template/src/feedbax/feedback/observed-history.test.ts +150 -0
- package/template/src/feedbax/feedback/observed-history.ts +219 -0
- package/template/src/feedbax/feedback/ownership.ts +170 -0
- package/template/src/feedbax/feedback/participation.test.ts +653 -0
- package/template/src/feedbax/feedback/participation.ts +502 -0
- package/template/src/history-runtime.test.ts +80 -0
- package/template/src/history-runtime.ts +37 -0
- package/template/src/images-runtime.test.ts +133 -0
- package/template/src/images-runtime.ts +125 -0
- package/template/src/index.css +103 -440
- package/template/src/integration-runtime.test.ts +365 -0
- package/template/src/integration-runtime.ts +289 -0
- package/template/src/lazy-post-detail-overlay.tsx +13 -2
- package/template/src/my-feedback-page.test.tsx +152 -0
- package/template/src/my-feedback-page.tsx +398 -0
- package/template/src/my-feedback-runtime.test.ts +93 -0
- package/template/src/my-feedback-runtime.ts +154 -0
- package/template/src/overlay-post-detail.test.tsx +59 -8
- package/template/src/overlay-post-detail.tsx +20 -69
- package/template/src/participation-client.ts +63 -0
- package/template/src/participation-runtime.test.ts +35 -0
- package/template/src/participation-runtime.ts +255 -0
- package/template/src/portal-comments.ts +35 -5
- package/template/src/portal-feedback-form.tsx +29 -6
- package/template/src/portal-feedback-runtime.ts +3 -0
- package/template/src/portal-feedback-server-function.ts +6 -3
- package/template/src/portal-feedback-submission.ts +19 -0
- package/template/src/portal-server-entry.ts +13 -0
- package/template/src/post-creation-flow.tsx +2 -2
- package/template/src/post-detail-content.test.tsx +233 -0
- package/template/src/post-detail-content.tsx +162 -0
- package/template/src/post-detail-query.ts +34 -0
- package/template/src/post-results.tsx +1 -1
- package/template/src/public-cache-policy.test.ts +11 -4
- package/template/src/public-cache-policy.ts +1 -1
- package/template/src/public-feedback-index.tsx +9 -39
- package/template/src/public-feedback-page.test.tsx +65 -0
- package/template/src/public-feedback-page.ts +12 -1
- package/template/src/public-portal-error.tsx +1 -1
- package/template/src/public-post-detail-skeleton.tsx +54 -0
- package/template/src/public-post-detail.tsx +437 -271
- package/template/src/public-post-route.test.tsx +39 -3
- package/template/src/public-post-route.tsx +15 -28
- package/template/src/public-release-links.test.ts +120 -0
- package/template/src/public-release-links.ts +130 -0
- package/template/src/public-roadmap-route.test.tsx +38 -2
- package/template/src/public-roadmap-view.tsx +56 -23
- package/template/src/public-shell.test.tsx +26 -0
- package/template/src/routeTree.gen.ts +70 -3
- package/template/src/router.tsx +2 -0
- package/template/src/routes/__root.tsx +3 -5
- package/template/src/routes/email-preferences.tsx +77 -0
- package/template/src/routes/my-feedback.tsx +16 -0
- package/template/src/routes/submit.tsx +1 -1
- package/template/src/safe-public-failure.ts +3 -1
- package/template/src/server.ts +87 -2
- package/template/src/submission-intent.ts +26 -0
- package/template/src/trusted-feedback-handler.test.ts +1 -1
- package/template/src/vote-toggle.tsx +42 -9
- package/template/vite.config.ts +2 -0
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import type { CommentStorage, StoredComment } from "./comment-storage";
|
|
3
|
+
import type { FeedbackStorage } from "./feedback-storage";
|
|
4
|
+
import type { GitHubFeedbackOptions } from "./github-feedback";
|
|
5
|
+
import {
|
|
6
|
+
createGitHubClient,
|
|
7
|
+
GitHubRateLimitError,
|
|
8
|
+
isRecord,
|
|
9
|
+
} from "./github-request";
|
|
10
|
+
|
|
11
|
+
interface CommentMapping {
|
|
12
|
+
comment_id: string;
|
|
13
|
+
post_id: string;
|
|
14
|
+
discussion_id: string;
|
|
15
|
+
remote_id: number | null;
|
|
16
|
+
record: string;
|
|
17
|
+
state: "creating" | "ready" | "uncertain" | "deleted";
|
|
18
|
+
}
|
|
19
|
+
const start = "<!-- feedbax:comment:v1 -->";
|
|
20
|
+
const end = "<!-- /feedbax:comment -->";
|
|
21
|
+
|
|
22
|
+
export function createGitHubCommentStorage(
|
|
23
|
+
options: GitHubFeedbackOptions,
|
|
24
|
+
posts: FeedbackStorage,
|
|
25
|
+
): CommentStorage {
|
|
26
|
+
const { database, installationId, repository } = options;
|
|
27
|
+
const { request, authorize, scan } = createGitHubClient(options);
|
|
28
|
+
const rowFor = (id: string) =>
|
|
29
|
+
database
|
|
30
|
+
.prepare(
|
|
31
|
+
"SELECT * FROM github_comments WHERE installation_id=? AND repository_id=? AND comment_id=?",
|
|
32
|
+
)
|
|
33
|
+
.bind(installationId, repository.id, id)
|
|
34
|
+
.first<CommentMapping>();
|
|
35
|
+
const postFor = async (id: string, requirePublished = true) => {
|
|
36
|
+
const post = await posts.find(id);
|
|
37
|
+
const row = await database
|
|
38
|
+
.prepare(
|
|
39
|
+
"SELECT issue_number FROM github_posts WHERE installation_id=? AND repository_id=? AND post_id=? AND state='ready'",
|
|
40
|
+
)
|
|
41
|
+
.bind(installationId, repository.id, id)
|
|
42
|
+
.first<{ issue_number: number }>();
|
|
43
|
+
if (!post || (requirePublished && !post.published) || !row)
|
|
44
|
+
throw new Error("Post is unavailable.");
|
|
45
|
+
return row;
|
|
46
|
+
};
|
|
47
|
+
const project = (
|
|
48
|
+
row: CommentMapping,
|
|
49
|
+
value: Record<string, unknown>,
|
|
50
|
+
): StoredComment | undefined => {
|
|
51
|
+
if (
|
|
52
|
+
!isRecord(value.user) ||
|
|
53
|
+
value.user.id !== options.botUserId ||
|
|
54
|
+
typeof value.body !== "string" ||
|
|
55
|
+
(row.remote_id !== null && value.id !== row.remote_id) ||
|
|
56
|
+
!Number.isSafeInteger(value.id)
|
|
57
|
+
)
|
|
58
|
+
return;
|
|
59
|
+
const body = value.body;
|
|
60
|
+
if (
|
|
61
|
+
body.split(start).length !== 2 ||
|
|
62
|
+
body.split(end).length !== 2 ||
|
|
63
|
+
body.indexOf(end) < body.indexOf(start)
|
|
64
|
+
)
|
|
65
|
+
return;
|
|
66
|
+
try {
|
|
67
|
+
const content: unknown = JSON.parse(
|
|
68
|
+
body.slice(body.indexOf(start) + start.length, body.indexOf(end)),
|
|
69
|
+
);
|
|
70
|
+
const createdAt = new Date(String(value.created_at));
|
|
71
|
+
const original = JSON.parse(row.record) as { displayName: string };
|
|
72
|
+
if (
|
|
73
|
+
!isRecord(content) ||
|
|
74
|
+
content.id !== row.comment_id ||
|
|
75
|
+
content.discussionId !== row.discussion_id ||
|
|
76
|
+
typeof content.body !== "string" ||
|
|
77
|
+
!content.body.trim() ||
|
|
78
|
+
content.body.length > 5000 ||
|
|
79
|
+
content.displayName !== original.displayName ||
|
|
80
|
+
!Number.isFinite(createdAt.getTime())
|
|
81
|
+
)
|
|
82
|
+
return;
|
|
83
|
+
return {
|
|
84
|
+
id: row.comment_id,
|
|
85
|
+
postId: row.post_id,
|
|
86
|
+
discussionId: row.discussion_id,
|
|
87
|
+
body: content.body,
|
|
88
|
+
author: { kind: "participant", displayName: original.displayName },
|
|
89
|
+
createdAt,
|
|
90
|
+
resolved: false,
|
|
91
|
+
scope: "page",
|
|
92
|
+
};
|
|
93
|
+
} catch {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
const encode = (row: CommentMapping, body: string) =>
|
|
98
|
+
`${start}\n${JSON.stringify({
|
|
99
|
+
id: row.comment_id,
|
|
100
|
+
discussionId: row.discussion_id,
|
|
101
|
+
body,
|
|
102
|
+
displayName: (JSON.parse(row.record) as { displayName: string })
|
|
103
|
+
.displayName,
|
|
104
|
+
}).replaceAll("<", "\\u003c")}\n${end}`;
|
|
105
|
+
const complete = async (
|
|
106
|
+
row: CommentMapping,
|
|
107
|
+
value: Record<string, unknown>,
|
|
108
|
+
) => {
|
|
109
|
+
const comment = project(row, value);
|
|
110
|
+
if (!comment) throw new Error("GitHub returned an invalid portal Comment.");
|
|
111
|
+
const result = await database
|
|
112
|
+
.prepare(
|
|
113
|
+
"UPDATE github_comments SET remote_id=?,state='ready' WHERE installation_id=? AND repository_id=? AND comment_id=? AND state IN('creating','uncertain')",
|
|
114
|
+
)
|
|
115
|
+
.bind(value.id, installationId, repository.id, row.comment_id)
|
|
116
|
+
.run();
|
|
117
|
+
if (result.meta.changes === 0) {
|
|
118
|
+
const current = await rowFor(row.comment_id);
|
|
119
|
+
if (current?.state !== "ready" || current.remote_id !== value.id)
|
|
120
|
+
throw new Error("Comment is unavailable.");
|
|
121
|
+
}
|
|
122
|
+
return comment;
|
|
123
|
+
};
|
|
124
|
+
const list = async (postId: string) => {
|
|
125
|
+
const post = await postFor(postId);
|
|
126
|
+
const values = await scan(`/issues/${post.issue_number}/comments`);
|
|
127
|
+
const rows = await database
|
|
128
|
+
.prepare(
|
|
129
|
+
"SELECT * FROM github_comments WHERE installation_id=? AND repository_id=? AND post_id=? AND state!='deleted'",
|
|
130
|
+
)
|
|
131
|
+
.bind(installationId, repository.id, postId)
|
|
132
|
+
.all<CommentMapping>();
|
|
133
|
+
const result: StoredComment[] = [];
|
|
134
|
+
const remoteOrder = new Map<string, number>();
|
|
135
|
+
for (const row of rows.results) {
|
|
136
|
+
const matching = values.filter((value) => project(row, value));
|
|
137
|
+
if (matching.length !== 1) continue;
|
|
138
|
+
const value = matching[0]!;
|
|
139
|
+
remoteOrder.set(row.comment_id, Number(value.id));
|
|
140
|
+
result.push(
|
|
141
|
+
row.state === "ready"
|
|
142
|
+
? project(row, value)!
|
|
143
|
+
: await complete(row, value),
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
result.sort(
|
|
147
|
+
(a, b) =>
|
|
148
|
+
a.createdAt.getTime() - b.createdAt.getTime() ||
|
|
149
|
+
remoteOrder.get(a.id)! - remoteOrder.get(b.id)!,
|
|
150
|
+
);
|
|
151
|
+
return result;
|
|
152
|
+
};
|
|
153
|
+
const create = async (input: {
|
|
154
|
+
submissionKey?: string;
|
|
155
|
+
recoveryCommentId?: string;
|
|
156
|
+
postId: string;
|
|
157
|
+
discussionId?: string;
|
|
158
|
+
body: string;
|
|
159
|
+
displayName: string;
|
|
160
|
+
}) => {
|
|
161
|
+
const post = await postFor(input.postId);
|
|
162
|
+
if (
|
|
163
|
+
input.submissionKey !== undefined &&
|
|
164
|
+
!/^[A-Za-z0-9_-]{43}$/.test(input.submissionKey)
|
|
165
|
+
)
|
|
166
|
+
throw new Error("Comment intent is invalid.");
|
|
167
|
+
const digest = input.submissionKey
|
|
168
|
+
? createHash("sha256")
|
|
169
|
+
.update(
|
|
170
|
+
JSON.stringify([
|
|
171
|
+
"feedbax-comment-intent-v1",
|
|
172
|
+
installationId,
|
|
173
|
+
repository.id,
|
|
174
|
+
input.postId,
|
|
175
|
+
input.discussionId ?? null,
|
|
176
|
+
input.submissionKey,
|
|
177
|
+
]),
|
|
178
|
+
)
|
|
179
|
+
.digest("hex")
|
|
180
|
+
: undefined;
|
|
181
|
+
const id = digest
|
|
182
|
+
? `${digest.slice(0, 8)}-${digest.slice(8, 12)}-8${digest.slice(13, 16)}-8${digest.slice(17, 20)}-${digest.slice(20, 32)}`
|
|
183
|
+
: randomUUID();
|
|
184
|
+
if (input.recoveryCommentId && input.recoveryCommentId !== id)
|
|
185
|
+
throw new Error("Images belong to another Comment intent.");
|
|
186
|
+
let row: CommentMapping = {
|
|
187
|
+
comment_id: id,
|
|
188
|
+
post_id: input.postId,
|
|
189
|
+
discussion_id: input.discussionId ?? id,
|
|
190
|
+
remote_id: null,
|
|
191
|
+
record: JSON.stringify({
|
|
192
|
+
displayName: input.displayName,
|
|
193
|
+
body: input.body,
|
|
194
|
+
}),
|
|
195
|
+
state: "creating",
|
|
196
|
+
};
|
|
197
|
+
const claimed = await database
|
|
198
|
+
.prepare(
|
|
199
|
+
"INSERT INTO github_comments(installation_id,repository_id,comment_id,post_id,discussion_id,record,state) VALUES(?,?,?,?,?,?,'creating') ON CONFLICT DO NOTHING RETURNING comment_id",
|
|
200
|
+
)
|
|
201
|
+
.bind(
|
|
202
|
+
installationId,
|
|
203
|
+
repository.id,
|
|
204
|
+
id,
|
|
205
|
+
row.post_id,
|
|
206
|
+
row.discussion_id,
|
|
207
|
+
row.record,
|
|
208
|
+
)
|
|
209
|
+
.first();
|
|
210
|
+
if (!claimed) {
|
|
211
|
+
const original = await rowFor(id);
|
|
212
|
+
if (!original || original.state === "deleted")
|
|
213
|
+
throw new Error("Comment is unavailable.");
|
|
214
|
+
if (original.state === "ready") {
|
|
215
|
+
const response = await request(
|
|
216
|
+
`/issues/comments/${original.remote_id}`,
|
|
217
|
+
);
|
|
218
|
+
if (!response.ok) throw new Error("Comment is unavailable.");
|
|
219
|
+
const comment = project(
|
|
220
|
+
original,
|
|
221
|
+
(await response.json()) as Record<string, unknown>,
|
|
222
|
+
);
|
|
223
|
+
if (!comment) throw new Error("Comment is unavailable.");
|
|
224
|
+
return { ...comment, recoverable: true };
|
|
225
|
+
}
|
|
226
|
+
const retryAt = (JSON.parse(original.record) as { retryAt?: number })
|
|
227
|
+
.retryAt;
|
|
228
|
+
if (retryAt === undefined) {
|
|
229
|
+
const candidates = (
|
|
230
|
+
await scan(`/issues/${post.issue_number}/comments`)
|
|
231
|
+
).filter((value) => project(original, value));
|
|
232
|
+
if (candidates.length !== 1)
|
|
233
|
+
throw new Error(
|
|
234
|
+
"Comment creation is pending reconciliation; do not start another intent.",
|
|
235
|
+
);
|
|
236
|
+
return {
|
|
237
|
+
...(await complete(original, candidates[0]!)),
|
|
238
|
+
recoverable: true,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
if (retryAt > Date.now())
|
|
242
|
+
throw new GitHubRateLimitError(retryAt - Date.now());
|
|
243
|
+
const resumed = await database
|
|
244
|
+
.prepare(
|
|
245
|
+
"UPDATE github_comments SET state='creating',record=json_remove(record,'$.retryAt') WHERE installation_id=? AND repository_id=? AND comment_id=? AND state='uncertain' AND json_extract(record,'$.retryAt') <= ? RETURNING *",
|
|
246
|
+
)
|
|
247
|
+
.bind(installationId, repository.id, id, Date.now())
|
|
248
|
+
.first<CommentMapping>();
|
|
249
|
+
if (!resumed) throw new Error("Comment creation is already in progress.");
|
|
250
|
+
row = resumed;
|
|
251
|
+
}
|
|
252
|
+
try {
|
|
253
|
+
const response = await request(
|
|
254
|
+
`/issues/${post.issue_number}/comments`,
|
|
255
|
+
"POST",
|
|
256
|
+
{
|
|
257
|
+
body: encode(row, (JSON.parse(row.record) as { body: string }).body),
|
|
258
|
+
},
|
|
259
|
+
);
|
|
260
|
+
if (
|
|
261
|
+
response.status === 429 ||
|
|
262
|
+
(response.status === 403 &&
|
|
263
|
+
(response.headers.has("Retry-After") ||
|
|
264
|
+
response.headers.get("X-RateLimit-Remaining") === "0"))
|
|
265
|
+
) {
|
|
266
|
+
const after = response.headers.get("Retry-After");
|
|
267
|
+
const reset = response.headers.get("X-RateLimit-Reset");
|
|
268
|
+
const delay =
|
|
269
|
+
after !== null
|
|
270
|
+
? Number.isFinite(Number(after))
|
|
271
|
+
? Number(after) * 1000
|
|
272
|
+
: Date.parse(after) - Date.now()
|
|
273
|
+
: reset
|
|
274
|
+
? Number(reset) * 1000 - Date.now()
|
|
275
|
+
: 1000;
|
|
276
|
+
throw new GitHubRateLimitError(
|
|
277
|
+
Number.isFinite(delay) ? Math.max(0, delay) : 1000,
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
if (!response.ok)
|
|
281
|
+
throw new Error(`GitHub Comment creation failed (${response.status}).`);
|
|
282
|
+
return {
|
|
283
|
+
...(await complete(
|
|
284
|
+
row,
|
|
285
|
+
(await response.json()) as Record<string, unknown>,
|
|
286
|
+
)),
|
|
287
|
+
recoverable: Boolean(input.submissionKey),
|
|
288
|
+
};
|
|
289
|
+
} catch (error) {
|
|
290
|
+
await database
|
|
291
|
+
.prepare(
|
|
292
|
+
"UPDATE github_comments SET state='uncertain',record=CASE WHEN ? IS NULL THEN record ELSE json_set(record,'$.retryAt',?) END WHERE installation_id=? AND repository_id=? AND comment_id=? AND state='creating'",
|
|
293
|
+
)
|
|
294
|
+
.bind(
|
|
295
|
+
error instanceof GitHubRateLimitError
|
|
296
|
+
? Date.now() + error.retryAfterMs
|
|
297
|
+
: null,
|
|
298
|
+
error instanceof GitHubRateLimitError
|
|
299
|
+
? Date.now() + error.retryAfterMs
|
|
300
|
+
: null,
|
|
301
|
+
installationId,
|
|
302
|
+
repository.id,
|
|
303
|
+
id,
|
|
304
|
+
)
|
|
305
|
+
.run();
|
|
306
|
+
throw error;
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
return {
|
|
310
|
+
supportsIntentRecovery: true,
|
|
311
|
+
createPageComment: create,
|
|
312
|
+
createDiscussionReply: create,
|
|
313
|
+
async listPageComments(postId, cursor) {
|
|
314
|
+
const comments = await list(postId);
|
|
315
|
+
let offset = 0;
|
|
316
|
+
if (cursor) {
|
|
317
|
+
try {
|
|
318
|
+
const parsed = JSON.parse(
|
|
319
|
+
Buffer.from(cursor, "base64url").toString("utf8"),
|
|
320
|
+
);
|
|
321
|
+
if (
|
|
322
|
+
parsed.post !== postId ||
|
|
323
|
+
!Number.isSafeInteger(parsed.offset) ||
|
|
324
|
+
parsed.offset < 0
|
|
325
|
+
)
|
|
326
|
+
throw new Error();
|
|
327
|
+
offset = parsed.offset;
|
|
328
|
+
} catch {
|
|
329
|
+
throw new Error("Invalid GitHub Comment cursor.");
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return {
|
|
333
|
+
items: comments.slice(offset, offset + 50),
|
|
334
|
+
...(offset + 50 < comments.length
|
|
335
|
+
? {
|
|
336
|
+
nextCursor: Buffer.from(
|
|
337
|
+
JSON.stringify({ post: postId, offset: offset + 50 }),
|
|
338
|
+
).toString("base64url"),
|
|
339
|
+
}
|
|
340
|
+
: {}),
|
|
341
|
+
};
|
|
342
|
+
},
|
|
343
|
+
async updateComment(id, body) {
|
|
344
|
+
const row = await rowFor(id);
|
|
345
|
+
if (!row || row.state !== "ready")
|
|
346
|
+
throw new Error("Comment is unavailable.");
|
|
347
|
+
await postFor(row.post_id);
|
|
348
|
+
const response = await request(`/issues/comments/${row.remote_id}`);
|
|
349
|
+
if (!response.ok) throw new Error("Comment is unavailable.");
|
|
350
|
+
const value = (await response.json()) as Record<string, unknown>;
|
|
351
|
+
if (!project(row, value)) throw new Error("Comment is unavailable.");
|
|
352
|
+
const previous = String(value.body);
|
|
353
|
+
const replacement =
|
|
354
|
+
previous.slice(0, previous.indexOf(start)) +
|
|
355
|
+
encode(row, body) +
|
|
356
|
+
previous.slice(previous.indexOf(end) + end.length);
|
|
357
|
+
const updated = await request(
|
|
358
|
+
`/issues/comments/${row.remote_id}`,
|
|
359
|
+
"PATCH",
|
|
360
|
+
{ body: replacement },
|
|
361
|
+
);
|
|
362
|
+
if (!updated.ok)
|
|
363
|
+
throw new Error(`GitHub Comment edit failed (${updated.status}).`);
|
|
364
|
+
const projected = project(
|
|
365
|
+
row,
|
|
366
|
+
(await updated.json()) as Record<string, unknown>,
|
|
367
|
+
);
|
|
368
|
+
if (!projected)
|
|
369
|
+
throw new Error("GitHub returned an invalid portal Comment.");
|
|
370
|
+
return projected;
|
|
371
|
+
},
|
|
372
|
+
async deleteComment(id) {
|
|
373
|
+
const row = await rowFor(id);
|
|
374
|
+
if (!row || !row.remote_id) throw new Error("Comment is unavailable.");
|
|
375
|
+
await authorize();
|
|
376
|
+
await database
|
|
377
|
+
.prepare(
|
|
378
|
+
"UPDATE github_comments SET state='deleted' WHERE installation_id=? AND repository_id=? AND comment_id=?",
|
|
379
|
+
)
|
|
380
|
+
.bind(installationId, repository.id, id)
|
|
381
|
+
.run();
|
|
382
|
+
const response = await request(
|
|
383
|
+
`/issues/comments/${row.remote_id}`,
|
|
384
|
+
"DELETE",
|
|
385
|
+
);
|
|
386
|
+
if (!response.ok && response.status !== 404)
|
|
387
|
+
throw new Error(`GitHub Comment cleanup failed (${response.status}).`);
|
|
388
|
+
},
|
|
389
|
+
};
|
|
390
|
+
}
|