@bunizao/contracts 0.4.0 → 0.6.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/dist/admin.d.ts +457 -0
- package/dist/comments.d.ts +167 -1
- package/dist/comments.js +12 -0
- package/dist/content.d.ts +11 -0
- package/dist/index.js +22 -0
- package/dist/messages.d.ts +6 -5
- package/dist/mood.d.ts +25 -0
- package/dist/mood.js +2 -0
- package/dist/telegram-ops.d.ts +18 -0
- package/dist/telegram-ops.js +8 -0
- package/package.json +1 -1
package/dist/admin.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ClientFingerprint, CommentStatus, Interaction } from './comments';
|
|
1
2
|
import type { DeliveryMode, NotifyAuditEventType, NotifyChannel, SubscriberRecord, SubscriberStatus } from './notify';
|
|
2
3
|
export interface SubscriberChannelCount {
|
|
3
4
|
total: number;
|
|
@@ -85,3 +86,459 @@ export interface BroadcastSendResult {
|
|
|
85
86
|
failedCount: number;
|
|
86
87
|
status: BroadcastRecord['status'];
|
|
87
88
|
}
|
|
89
|
+
/** The queue's status filter; the same four values as `CommentStatus`, in
|
|
90
|
+
the order the portal tabs show them. */
|
|
91
|
+
export type AdminCommentStatus = CommentStatus;
|
|
92
|
+
/** One row of the comment queue. The public `Comment` never carries the
|
|
93
|
+
writer's standing or the moderation verdict; this does, and adds the
|
|
94
|
+
actor block that says where the row came from. */
|
|
95
|
+
export interface AdminCommentRecord {
|
|
96
|
+
id: string;
|
|
97
|
+
postId: string;
|
|
98
|
+
parentId: string | null;
|
|
99
|
+
author: string;
|
|
100
|
+
/** True when the writer had a confirmed identity at the time of writing. */
|
|
101
|
+
verified: boolean;
|
|
102
|
+
body: string;
|
|
103
|
+
status: AdminCommentStatus;
|
|
104
|
+
moderationAction: string | null;
|
|
105
|
+
moderationReason: string | null;
|
|
106
|
+
moderationNote: string | null;
|
|
107
|
+
moderationModel: string | null;
|
|
108
|
+
country: string | null;
|
|
109
|
+
createdAt: string;
|
|
110
|
+
editedAt: string | null;
|
|
111
|
+
/** Filled from the post registry; null when it could not say. */
|
|
112
|
+
postTitle: string | null;
|
|
113
|
+
postSlug: string | null;
|
|
114
|
+
actor: AdminCommentActor;
|
|
115
|
+
}
|
|
116
|
+
/** One page of the moderation queue. */
|
|
117
|
+
export interface AdminCommentListResult {
|
|
118
|
+
comments: AdminCommentRecord[];
|
|
119
|
+
/** Rows matching the filter, ignoring the page window. */
|
|
120
|
+
total: number;
|
|
121
|
+
/** `offset + comments.length`, or null on the last page. */
|
|
122
|
+
nextOffset: number | null;
|
|
123
|
+
}
|
|
124
|
+
/** What a ban can hold onto. Values are hashes (or the ASN as text) for
|
|
125
|
+
every kind but the two domain kinds, which are stored raw -- a domain is
|
|
126
|
+
not personal data. `client_fp` matches either the exact or the stable
|
|
127
|
+
device hash. */
|
|
128
|
+
export type AdminBanKeyType = 'email' | 'session' | 'ip' | 'ip24' | 'fp' | 'asn' | 'client_fp' | 'domain' | 'email_domain';
|
|
129
|
+
export type AdminBanSource = 'portal' | 'telegram' | 'script';
|
|
130
|
+
/** One key a queue row, a reaction, or a source profile can be pivoted on:
|
|
131
|
+
`GET /admin/comments?key=&value=`, `GET /admin/reactions?key=&value=`
|
|
132
|
+
and `GET /admin/sources/:type/:value` all take one of these. Every ban
|
|
133
|
+
key type is also a source key type; the rest are cluster keys that can be
|
|
134
|
+
looked at but not banned. */
|
|
135
|
+
export type AdminSourceKeyType = AdminBanKeyType | 'client_fp_stable' | 'storage_id' | 'body_hash';
|
|
136
|
+
/** The `signals` blob on a row: the request's network and header set,
|
|
137
|
+
serialised once at insert and nulled by the 90-day sweep. */
|
|
138
|
+
export interface ActorDetail {
|
|
139
|
+
colo: string | null;
|
|
140
|
+
region: string | null;
|
|
141
|
+
timezone: string | null;
|
|
142
|
+
httpProtocol: string | null;
|
|
143
|
+
tlsVersion: string | null;
|
|
144
|
+
tlsCipher: string | null;
|
|
145
|
+
tlsCiphersSha1: string | null;
|
|
146
|
+
tlsExtensionsSha1: string | null;
|
|
147
|
+
tlsHelloLength: number | null;
|
|
148
|
+
rttMs: number | null;
|
|
149
|
+
/** From the static hosting-ASN list; a vpn hint, not a bot hint. */
|
|
150
|
+
asnKind: 'hosting' | 'other' | null;
|
|
151
|
+
acceptLanguage: string | null;
|
|
152
|
+
acceptEncoding: string | null;
|
|
153
|
+
chUa: string | null;
|
|
154
|
+
chPlatform: string | null;
|
|
155
|
+
chMobile: string | null;
|
|
156
|
+
/** `sec-fetch-dest/mode/site` joined with `/`. */
|
|
157
|
+
secFetch: string | null;
|
|
158
|
+
priority: string | null;
|
|
159
|
+
referer: string | null;
|
|
160
|
+
origin: string | null;
|
|
161
|
+
/** `cf-worker` header present: the request came out of another Worker. */
|
|
162
|
+
viaWorker: boolean;
|
|
163
|
+
}
|
|
164
|
+
/** The `client` blob on a row: the fingerprint components and interaction
|
|
165
|
+
aggregates exactly as the browser sent them (bounded), plus the hint
|
|
166
|
+
lists the server derived from them and the headers together. */
|
|
167
|
+
export interface ActorClient {
|
|
168
|
+
components: ClientFingerprint | null;
|
|
169
|
+
interaction: Interaction | null;
|
|
170
|
+
/** Contradictions that count into the row's `botHints` total. */
|
|
171
|
+
botHints: string[];
|
|
172
|
+
/** Contradictions shown and not counted: what a VPN looks like. */
|
|
173
|
+
vpnHints: string[];
|
|
174
|
+
}
|
|
175
|
+
/** Behavioural integers, kept past the sweep. Comments carry dwell, the
|
|
176
|
+
Turnstile age, the link count and the MX result; reactions carry the
|
|
177
|
+
Turnstile age and which door the heart came through. */
|
|
178
|
+
export interface AdminActorBehaviour {
|
|
179
|
+
dwellMs?: number | null;
|
|
180
|
+
turnstileAgeMs?: number | null;
|
|
181
|
+
linkCount?: number | null;
|
|
182
|
+
emailMx?: boolean | null;
|
|
183
|
+
emailGravatar?: boolean | null;
|
|
184
|
+
auth?: 'turnstile' | 'pass' | 'verified' | null;
|
|
185
|
+
}
|
|
186
|
+
/** The row's keys, at full length: the portal shortens a hash to its first
|
|
187
|
+
eight characters for the eye and keeps the whole value for the pivot
|
|
188
|
+
link. Domains are raw, and `session` is '' when the row predates
|
|
189
|
+
sessions. */
|
|
190
|
+
export interface AdminActorKeys {
|
|
191
|
+
session: string;
|
|
192
|
+
ip: string | null;
|
|
193
|
+
ip24: string | null;
|
|
194
|
+
fp: string | null;
|
|
195
|
+
email: string | null;
|
|
196
|
+
clientFp: string | null;
|
|
197
|
+
clientFpStable: string | null;
|
|
198
|
+
storageId: string | null;
|
|
199
|
+
emailDomain: string | null;
|
|
200
|
+
bodyHash: string | null;
|
|
201
|
+
linkDomains: string[];
|
|
202
|
+
}
|
|
203
|
+
export type AdminClusterKey = 'session' | 'ip' | 'ip24' | 'fp' | 'email' | 'clientFp' | 'clientFpStable' | 'storageId' | 'emailDomain' | 'bodyHash';
|
|
204
|
+
export interface AdminClusterCount {
|
|
205
|
+
comments: number;
|
|
206
|
+
held: number;
|
|
207
|
+
reactions: number;
|
|
208
|
+
}
|
|
209
|
+
/** Where a row came from, as far as the write path could tell. Shared by
|
|
210
|
+
comments and reactions; a reaction's block has no email, no link domains
|
|
211
|
+
and no body hash. */
|
|
212
|
+
export interface AdminCommentActor {
|
|
213
|
+
readerId: string | null;
|
|
214
|
+
/** Plaintext while the row still holds it (verified: with the row;
|
|
215
|
+
unverified: seven days). */
|
|
216
|
+
email: string | null;
|
|
217
|
+
/** Plaintext while inside the 90-day window. */
|
|
218
|
+
ip: string | null;
|
|
219
|
+
ua: string | null;
|
|
220
|
+
browser: string | null;
|
|
221
|
+
os: string | null;
|
|
222
|
+
country: string | null;
|
|
223
|
+
city: string | null;
|
|
224
|
+
asn: number | null;
|
|
225
|
+
asOrg: string | null;
|
|
226
|
+
/** This write minted the anonymous session cookie. */
|
|
227
|
+
sessionNew: boolean;
|
|
228
|
+
/** Count of tripped bot hints; the names are in `client.botHints`. */
|
|
229
|
+
botHints: number;
|
|
230
|
+
/** The `signals` blob, parsed. Null once the sweep has run. */
|
|
231
|
+
detail: ActorDetail | null;
|
|
232
|
+
/** The `client` blob, parsed. Null when the write carried neither
|
|
233
|
+
fingerprint nor interaction, or once the sweep has run. */
|
|
234
|
+
client: ActorClient | null;
|
|
235
|
+
behaviour: AdminActorBehaviour;
|
|
236
|
+
keys: AdminActorKeys;
|
|
237
|
+
/** Which of this row's keys are on the ban list right now. */
|
|
238
|
+
banned: AdminBanKeyType[];
|
|
239
|
+
/** Other rows sharing each key in the last 90 days, excluding this one. */
|
|
240
|
+
cluster: Record<AdminClusterKey, AdminClusterCount>;
|
|
241
|
+
/** Same, per link domain on this row. */
|
|
242
|
+
domainCluster: Array<{
|
|
243
|
+
domain: string;
|
|
244
|
+
comments: number;
|
|
245
|
+
held: number;
|
|
246
|
+
banned: boolean;
|
|
247
|
+
}>;
|
|
248
|
+
/** Published comments at this email domain in the last 90 days.
|
|
249
|
+
Missing or null means the broad domain ban cannot be evaluated. */
|
|
250
|
+
emailDomainPublishedComments?: number | null;
|
|
251
|
+
}
|
|
252
|
+
/** One reaction row as the reactions list shows it. */
|
|
253
|
+
export interface AdminReactionRecord {
|
|
254
|
+
/** ULID, like a comment id. */
|
|
255
|
+
id: string;
|
|
256
|
+
targetType: 'post' | 'comment';
|
|
257
|
+
targetId: string;
|
|
258
|
+
postId: string | null;
|
|
259
|
+
postTitle: string | null;
|
|
260
|
+
postSlug: string | null;
|
|
261
|
+
emoji: string;
|
|
262
|
+
createdAt: string;
|
|
263
|
+
actor: AdminCommentActor;
|
|
264
|
+
}
|
|
265
|
+
export interface AdminReactionListResult {
|
|
266
|
+
reactions: AdminReactionRecord[];
|
|
267
|
+
total: number;
|
|
268
|
+
nextOffset: number | null;
|
|
269
|
+
}
|
|
270
|
+
export interface AdminBan {
|
|
271
|
+
keyType: AdminBanKeyType;
|
|
272
|
+
keyValue: string;
|
|
273
|
+
note: string | null;
|
|
274
|
+
source: AdminBanSource;
|
|
275
|
+
createdAt: string;
|
|
276
|
+
expiresAt: string | null;
|
|
277
|
+
/** Rows in the last 90 days, both tables, matching this key. */
|
|
278
|
+
hits: number;
|
|
279
|
+
}
|
|
280
|
+
/** `POST /admin/bans`: every key in one write. With `purge`, the source's
|
|
281
|
+
comments from the last 90 days are soft-deleted and its reaction rows
|
|
282
|
+
removed, each logged. `revokeReaderId` flips `notify_subscribers.banned`
|
|
283
|
+
for a verified writer -- the account lever, offered on verified rows. */
|
|
284
|
+
export interface AdminBanInput {
|
|
285
|
+
keys: Array<{
|
|
286
|
+
type: AdminBanKeyType;
|
|
287
|
+
value: string;
|
|
288
|
+
}>;
|
|
289
|
+
note?: string;
|
|
290
|
+
expiresAt?: string | null;
|
|
291
|
+
purge?: boolean;
|
|
292
|
+
revokeReaderId?: string | null;
|
|
293
|
+
}
|
|
294
|
+
export interface AdminBanResult {
|
|
295
|
+
bans: AdminBan[];
|
|
296
|
+
purged: {
|
|
297
|
+
comments: number;
|
|
298
|
+
reactions: number;
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
export interface AdminBanListResult {
|
|
302
|
+
bans: AdminBan[];
|
|
303
|
+
}
|
|
304
|
+
/** Everything about one key in one response --
|
|
305
|
+
`GET /admin/sources/:type/:value`. */
|
|
306
|
+
export interface AdminSourceProfile {
|
|
307
|
+
key: {
|
|
308
|
+
type: AdminSourceKeyType;
|
|
309
|
+
value: string;
|
|
310
|
+
};
|
|
311
|
+
firstSeenAt: string | null;
|
|
312
|
+
lastSeenAt: string | null;
|
|
313
|
+
comments: {
|
|
314
|
+
total: number;
|
|
315
|
+
byStatus: Record<AdminCommentStatus, number>;
|
|
316
|
+
/** Newest 50. */
|
|
317
|
+
rows: AdminCommentRecord[];
|
|
318
|
+
};
|
|
319
|
+
reactions: {
|
|
320
|
+
total: number;
|
|
321
|
+
byTarget: Array<{
|
|
322
|
+
targetType: 'post' | 'comment';
|
|
323
|
+
targetId: string;
|
|
324
|
+
postTitle: string | null;
|
|
325
|
+
count: number;
|
|
326
|
+
}>;
|
|
327
|
+
/** Newest 50. */
|
|
328
|
+
rows: AdminReactionRecord[];
|
|
329
|
+
};
|
|
330
|
+
/** Distinct values of every other key this source has used. The spread is
|
|
331
|
+
the churn: one fingerprint over 40 sessions and 12 IPs is a bot; one
|
|
332
|
+
session over 3 IPs is a phone that changed networks. */
|
|
333
|
+
spread: Record<'session' | 'ip' | 'ip24' | 'fp' | 'clientFp' | 'clientFpStable' | 'storageId' | 'email' | 'asn' | 'ua', number>;
|
|
334
|
+
/** Every bot and vpn hint this source has tripped, with how often. */
|
|
335
|
+
hints: Array<{
|
|
336
|
+
hint: string;
|
|
337
|
+
kind: 'bot' | 'vpn';
|
|
338
|
+
count: number;
|
|
339
|
+
}>;
|
|
340
|
+
/** Two hops over strong keys only (session, email, clientFpStable,
|
|
341
|
+
storageId): the sessions this source links to, and what those sessions
|
|
342
|
+
carried. Never `ip24`, `asn` or `ua` as a hop. */
|
|
343
|
+
linked: {
|
|
344
|
+
sessions: number;
|
|
345
|
+
via: Record<'email' | 'clientFpStable' | 'storageId', number>;
|
|
346
|
+
carried: Record<'ip24' | 'clientFpStable' | 'email' | 'storageId', number>;
|
|
347
|
+
comments: number;
|
|
348
|
+
held: number;
|
|
349
|
+
reactions: number;
|
|
350
|
+
};
|
|
351
|
+
/** Writes per hour over the source's last 7 days. */
|
|
352
|
+
hourly: Array<{
|
|
353
|
+
hour: string;
|
|
354
|
+
comments: number;
|
|
355
|
+
reactions: number;
|
|
356
|
+
}>;
|
|
357
|
+
behaviour: {
|
|
358
|
+
dwellMsMedian: number | null;
|
|
359
|
+
turnstileAgeMsMedian: number | null;
|
|
360
|
+
/** 0..1 */
|
|
361
|
+
newSessionShare: number;
|
|
362
|
+
authMix: Record<'turnstile' | 'pass' | 'verified', number>;
|
|
363
|
+
};
|
|
364
|
+
bans: AdminBan[];
|
|
365
|
+
}
|
|
366
|
+
/** One grouped row in an insights table. `held` and `heldRate` (0..1) are
|
|
367
|
+
null on the reaction tables, where nothing is held. */
|
|
368
|
+
export interface AdminInsightRow {
|
|
369
|
+
count: number;
|
|
370
|
+
held: number | null;
|
|
371
|
+
heldRate: number | null;
|
|
372
|
+
/** Distinct sessions behind the count. */
|
|
373
|
+
sessions: number;
|
|
374
|
+
}
|
|
375
|
+
export type AdminCommentInsightsWindow = '7d' | '30d' | '90d';
|
|
376
|
+
export type AdminReactionInsightsWindow = '48h' | '7d';
|
|
377
|
+
/** `GET /admin/comments/insights?window=`. Every table carries a held rate,
|
|
378
|
+
because a source is only interesting relative to how the automatic pass
|
|
379
|
+
treats it. NULL groups show as their own row, never dropped. */
|
|
380
|
+
export interface AdminCommentInsights {
|
|
381
|
+
window: AdminCommentInsightsWindow;
|
|
382
|
+
since: string;
|
|
383
|
+
networks: Array<AdminInsightRow & {
|
|
384
|
+
asn: number | null;
|
|
385
|
+
asOrg: string | null;
|
|
386
|
+
}>;
|
|
387
|
+
countries: Array<AdminInsightRow & {
|
|
388
|
+
country: string | null;
|
|
389
|
+
}>;
|
|
390
|
+
subnets: Array<AdminInsightRow & {
|
|
391
|
+
ip24: string | null;
|
|
392
|
+
sampleIp: string | null;
|
|
393
|
+
}>;
|
|
394
|
+
browsers: Array<AdminInsightRow & {
|
|
395
|
+
browser: string | null;
|
|
396
|
+
os: string | null;
|
|
397
|
+
}>;
|
|
398
|
+
devices: Array<AdminInsightRow & {
|
|
399
|
+
clientFp: string | null;
|
|
400
|
+
renderer: string | null;
|
|
401
|
+
screen: string | null;
|
|
402
|
+
platform: string | null;
|
|
403
|
+
subnets: number;
|
|
404
|
+
}>;
|
|
405
|
+
botHints: Array<{
|
|
406
|
+
hint: string;
|
|
407
|
+
count: number;
|
|
408
|
+
held: number;
|
|
409
|
+
heldRate: number;
|
|
410
|
+
}>;
|
|
411
|
+
vpnHints: Array<{
|
|
412
|
+
hint: string;
|
|
413
|
+
count: number;
|
|
414
|
+
held: number;
|
|
415
|
+
heldRate: number;
|
|
416
|
+
}>;
|
|
417
|
+
linkDomains: Array<AdminInsightRow & {
|
|
418
|
+
domain: string;
|
|
419
|
+
banned: boolean;
|
|
420
|
+
}>;
|
|
421
|
+
duplicates: Array<AdminInsightRow & {
|
|
422
|
+
bodyHash: string;
|
|
423
|
+
sample: string;
|
|
424
|
+
}>;
|
|
425
|
+
emailDomains: Array<AdminInsightRow & {
|
|
426
|
+
domain: string;
|
|
427
|
+
mxShare: number | null;
|
|
428
|
+
banned: boolean;
|
|
429
|
+
}>;
|
|
430
|
+
tlsStacks: Array<{
|
|
431
|
+
browser: string | null;
|
|
432
|
+
ciphersSha1: string | null;
|
|
433
|
+
count: number;
|
|
434
|
+
held: number;
|
|
435
|
+
heldRate: number;
|
|
436
|
+
}>;
|
|
437
|
+
typing: Array<{
|
|
438
|
+
bucket: string;
|
|
439
|
+
count: number;
|
|
440
|
+
held: number;
|
|
441
|
+
heldRate: number;
|
|
442
|
+
}>;
|
|
443
|
+
dailyByStatus: Array<{
|
|
444
|
+
day: string;
|
|
445
|
+
published: number;
|
|
446
|
+
held: number;
|
|
447
|
+
rejected: number;
|
|
448
|
+
deleted: number;
|
|
449
|
+
}>;
|
|
450
|
+
dwell: Array<{
|
|
451
|
+
bucket: string;
|
|
452
|
+
count: number;
|
|
453
|
+
held: number;
|
|
454
|
+
heldRate: number;
|
|
455
|
+
}>;
|
|
456
|
+
sessionAge: Array<{
|
|
457
|
+
day: string;
|
|
458
|
+
writes: number;
|
|
459
|
+
newShare: number;
|
|
460
|
+
}>;
|
|
461
|
+
email: Array<{
|
|
462
|
+
kind: 'without' | 'verified' | 'unverified' | 'disposable';
|
|
463
|
+
count: number;
|
|
464
|
+
held: number;
|
|
465
|
+
heldRate: number;
|
|
466
|
+
}>;
|
|
467
|
+
overturns: Array<{
|
|
468
|
+
week: string;
|
|
469
|
+
falsePositives: number;
|
|
470
|
+
falseNegatives: number;
|
|
471
|
+
}>;
|
|
472
|
+
banHits: Array<{
|
|
473
|
+
keyType: AdminBanKeyType;
|
|
474
|
+
keyValue: string;
|
|
475
|
+
note: string | null;
|
|
476
|
+
hits: number;
|
|
477
|
+
}>;
|
|
478
|
+
}
|
|
479
|
+
/** `GET /admin/reactions/insights?window=`. */
|
|
480
|
+
export interface AdminReactionInsights {
|
|
481
|
+
window: AdminReactionInsightsWindow;
|
|
482
|
+
since: string;
|
|
483
|
+
hourly: Array<{
|
|
484
|
+
hour: string;
|
|
485
|
+
reactions: number;
|
|
486
|
+
sessions: number;
|
|
487
|
+
subnets: number;
|
|
488
|
+
}>;
|
|
489
|
+
authMix: Array<{
|
|
490
|
+
day: string;
|
|
491
|
+
turnstile: number;
|
|
492
|
+
pass: number;
|
|
493
|
+
verified: number;
|
|
494
|
+
}>;
|
|
495
|
+
targets: Array<{
|
|
496
|
+
targetType: 'post' | 'comment';
|
|
497
|
+
targetId: string;
|
|
498
|
+
postTitle: string | null;
|
|
499
|
+
reactions: number;
|
|
500
|
+
subnets: number;
|
|
501
|
+
fps: number;
|
|
502
|
+
}>;
|
|
503
|
+
networks: Array<AdminInsightRow & {
|
|
504
|
+
asn: number | null;
|
|
505
|
+
asOrg: string | null;
|
|
506
|
+
}>;
|
|
507
|
+
countries: Array<AdminInsightRow & {
|
|
508
|
+
country: string | null;
|
|
509
|
+
}>;
|
|
510
|
+
subnets: Array<AdminInsightRow & {
|
|
511
|
+
ip24: string | null;
|
|
512
|
+
sampleIp: string | null;
|
|
513
|
+
}>;
|
|
514
|
+
browsers: Array<AdminInsightRow & {
|
|
515
|
+
browser: string | null;
|
|
516
|
+
os: string | null;
|
|
517
|
+
}>;
|
|
518
|
+
devices: Array<AdminInsightRow & {
|
|
519
|
+
clientFp: string | null;
|
|
520
|
+
renderer: string | null;
|
|
521
|
+
screen: string | null;
|
|
522
|
+
platform: string | null;
|
|
523
|
+
subnets: number;
|
|
524
|
+
}>;
|
|
525
|
+
botHints: Array<{
|
|
526
|
+
hint: string;
|
|
527
|
+
count: number;
|
|
528
|
+
}>;
|
|
529
|
+
tlsStacks: Array<{
|
|
530
|
+
browser: string | null;
|
|
531
|
+
ciphersSha1: string | null;
|
|
532
|
+
count: number;
|
|
533
|
+
}>;
|
|
534
|
+
sessionAge: Array<{
|
|
535
|
+
day: string;
|
|
536
|
+
writes: number;
|
|
537
|
+
newShare: number;
|
|
538
|
+
}>;
|
|
539
|
+
timeToTap: Array<{
|
|
540
|
+
bucket: string;
|
|
541
|
+
count: number;
|
|
542
|
+
sessions: number;
|
|
543
|
+
}>;
|
|
544
|
+
}
|
package/dist/comments.d.ts
CHANGED
|
@@ -21,6 +21,25 @@ export declare const READER_GRADES: readonly ['l0', 'l1', 'l2'];
|
|
|
21
21
|
export type ReaderGrade = (typeof READER_GRADES)[number];
|
|
22
22
|
export declare const COMMENT_STATUSES: readonly ['published', 'held', 'rejected', 'deleted'];
|
|
23
23
|
export type CommentStatus = (typeof COMMENT_STATUSES)[number];
|
|
24
|
+
/** Where a comment was written. `blog` rows key on Ghost post ids; `mood`
|
|
25
|
+
rows key on the Telegram channel message id and are bridged into the
|
|
26
|
+
channel's discussion group by the ops bot — plans/mood-comments-bridge.md. */
|
|
27
|
+
export declare const COMMENT_SURFACES: readonly ['blog', 'mood'];
|
|
28
|
+
export type CommentSurface = (typeof COMMENT_SURFACES)[number];
|
|
29
|
+
/** Length of the anchor token derived from a comment id. */
|
|
30
|
+
export declare const COMMENT_ANCHOR_TOKEN_LENGTH = 12;
|
|
31
|
+
/**
|
|
32
|
+
* Stable, unguessable-enough anchor for a comment: the first 12 hex chars of
|
|
33
|
+
* sha256(commentId). It rides in the bridged Telegram message's link
|
|
34
|
+
* (`/mood/<postId>#c-<token>`), in the owner card's View URL, and as the
|
|
35
|
+
* `id="c-<token>"` of the rendered row. The read path matches bridged
|
|
36
|
+
* messages back to their rows by this string, so it must be computed the
|
|
37
|
+
* same way everywhere — hence one helper here. Async because it uses Web
|
|
38
|
+
* Crypto, which is what Workers, Node >= 20, and browsers share.
|
|
39
|
+
*/
|
|
40
|
+
export declare function commentAnchorToken(commentId: string): Promise<string>;
|
|
41
|
+
/** Matches `#c-<token>` anywhere in a URL or text; group 1 is the token. */
|
|
42
|
+
export declare const COMMENT_ANCHOR_PATTERN: RegExp;
|
|
24
43
|
export declare const MODERATION_ACTIONS: readonly ['publish', 'hold', 'reject', 'unsure'];
|
|
25
44
|
export type ModerationAction = (typeof MODERATION_ACTIONS)[number];
|
|
26
45
|
export declare const MODERATION_REASONS: readonly ['ok', 'spam', 'promotional', 'abuse', 'off_topic', 'personal_info'];
|
|
@@ -126,6 +145,131 @@ export type ReactionTargetKey = string;
|
|
|
126
145
|
export interface ReactionBatchResult {
|
|
127
146
|
reactions: Record<ReactionTargetKey, ReactionSummary[]>;
|
|
128
147
|
}
|
|
148
|
+
/** What the browser says about itself, collected by the lazily loaded
|
|
149
|
+
`fingerprint.ts` module on the first interaction with a compose box or a
|
|
150
|
+
reaction bar and sent as `clientFp` on both write bodies. Every field is
|
|
151
|
+
optional; the server treats a missing or malformed object as no object
|
|
152
|
+
and never refuses the write for it -- these are evidence, not a door.
|
|
153
|
+
Bounds the server enforces: strings at most 128 characters, `fonts` at
|
|
154
|
+
most 32 entries, every number a bounded integer (`timezoneOffset` is the
|
|
155
|
+
one that may be negative), the whole object under 4 KiB. The server
|
|
156
|
+
hashes the canonical component JSON itself; the client never names its
|
|
157
|
+
own hash. See plans/comment-actor-identity.md "Client fingerprint". */
|
|
158
|
+
export interface ClientFingerprint {
|
|
159
|
+
navigator?: {
|
|
160
|
+
platform?: string;
|
|
161
|
+
languages?: string[];
|
|
162
|
+
hardwareConcurrency?: number;
|
|
163
|
+
deviceMemory?: number;
|
|
164
|
+
maxTouchPoints?: number;
|
|
165
|
+
webdriver?: boolean;
|
|
166
|
+
pdfViewerEnabled?: boolean;
|
|
167
|
+
/** `navigator.plugins.length`. */
|
|
168
|
+
plugins?: number;
|
|
169
|
+
cookieEnabled?: boolean;
|
|
170
|
+
/** `userAgentData.brands` as `"Chromium 128"` strings. */
|
|
171
|
+
uaBrands?: string[];
|
|
172
|
+
uaPlatform?: string;
|
|
173
|
+
uaMobile?: boolean;
|
|
174
|
+
uaPlatformVersion?: string;
|
|
175
|
+
uaArchitecture?: string;
|
|
176
|
+
uaModel?: string;
|
|
177
|
+
};
|
|
178
|
+
screen?: {
|
|
179
|
+
width?: number;
|
|
180
|
+
height?: number;
|
|
181
|
+
availWidth?: number;
|
|
182
|
+
availHeight?: number;
|
|
183
|
+
colorDepth?: number;
|
|
184
|
+
/** `devicePixelRatio` in percent (200 = 2x), so it stays an integer. */
|
|
185
|
+
dprPct?: number;
|
|
186
|
+
outerWidth?: number;
|
|
187
|
+
outerHeight?: number;
|
|
188
|
+
};
|
|
189
|
+
/** `Intl.DateTimeFormat().resolvedOptions().timeZone`. */
|
|
190
|
+
timezone?: string;
|
|
191
|
+
/** `Date#getTimezoneOffset()`, minutes, negative east of UTC. */
|
|
192
|
+
timezoneOffset?: number;
|
|
193
|
+
/** SHA-256 hex of an offscreen canvas `toDataURL()`. */
|
|
194
|
+
canvas?: string;
|
|
195
|
+
webgl?: {
|
|
196
|
+
vendor?: string;
|
|
197
|
+
renderer?: string;
|
|
198
|
+
maxTextureSize?: number;
|
|
199
|
+
maxViewport?: number;
|
|
200
|
+
};
|
|
201
|
+
/** Sum of `OfflineAudioContext` samples, formatted to a fixed precision. */
|
|
202
|
+
audio?: string;
|
|
203
|
+
/** Families detected by a width probe, in the probe list's order. */
|
|
204
|
+
fonts?: string[];
|
|
205
|
+
media?: {
|
|
206
|
+
colorScheme?: string;
|
|
207
|
+
reducedMotion?: string;
|
|
208
|
+
pointer?: string;
|
|
209
|
+
hover?: string;
|
|
210
|
+
colorGamut?: string;
|
|
211
|
+
dynamicRange?: string;
|
|
212
|
+
};
|
|
213
|
+
presence?: {
|
|
214
|
+
chrome?: boolean;
|
|
215
|
+
/** `Notification.permission`, or absent when the API is missing. */
|
|
216
|
+
notification?: string;
|
|
217
|
+
performanceMemory?: boolean;
|
|
218
|
+
indexedDb?: boolean;
|
|
219
|
+
localStorage?: boolean;
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
/** How the form was filled, as aggregates only: counts and one spread
|
|
223
|
+
figure, never the key sequence or the intervals themselves. Collected by
|
|
224
|
+
the same lazy module from listeners on the compose box and the reaction
|
|
225
|
+
bar, sent as `interaction` beside `clientFp`. Same bounds and the same
|
|
226
|
+
never-a-gate rule as `ClientFingerprint`. Comment-only and reaction-only
|
|
227
|
+
fields are marked; the rest apply to both. */
|
|
228
|
+
export interface Interaction {
|
|
229
|
+
/** `performance.now()` at first compose focus (comments). */
|
|
230
|
+
loadToFocusMs?: number;
|
|
231
|
+
/** `performance.now()` at first heart tap (reactions). */
|
|
232
|
+
loadToTapMs?: number;
|
|
233
|
+
/** First focus to submit (comments). */
|
|
234
|
+
composeMs?: number;
|
|
235
|
+
/** Counts on the body field (comments). */
|
|
236
|
+
keyEvents?: number;
|
|
237
|
+
inputEvents?: number;
|
|
238
|
+
pasteEvents?: number;
|
|
239
|
+
/** Coefficient of variation of inter-key intervals, per mille (comments). */
|
|
240
|
+
keyIntervalCv?: number;
|
|
241
|
+
/** The submit or tap's `pointerType`. */
|
|
242
|
+
pointerType?: string;
|
|
243
|
+
/** `pointermove` count on the page before the submit or tap. */
|
|
244
|
+
pointerMoves?: number;
|
|
245
|
+
/** Distance from the button's centre, CSS px, of the submit click or tap. */
|
|
246
|
+
clickOffset?: number;
|
|
247
|
+
scrollEvents?: number;
|
|
248
|
+
/** Max `scrollY / (docHeight - innerHeight)` as a percentage. */
|
|
249
|
+
scrollDepth?: number;
|
|
250
|
+
/** `visibilitychange` to hidden before the submit or tap. */
|
|
251
|
+
hiddenCount?: number;
|
|
252
|
+
/** `document.hasFocus()` at submit. */
|
|
253
|
+
hasFocus?: boolean;
|
|
254
|
+
historyLength?: number;
|
|
255
|
+
/** Client-side rejections before the successful submit (comments). */
|
|
256
|
+
validationErrors?: number;
|
|
257
|
+
/** Widget render to token callback. */
|
|
258
|
+
turnstileSolveMs?: number;
|
|
259
|
+
/** Whether `before-interactive-callback` fired. */
|
|
260
|
+
turnstileInteractive?: boolean;
|
|
261
|
+
/** Reaction taps on this page so far (reactions). */
|
|
262
|
+
tapsThisPage?: number;
|
|
263
|
+
}
|
|
264
|
+
/** The optional client evidence both write bodies may carry. `storageId` is
|
|
265
|
+
a random 32-hex value the lazy module reads or creates in IndexedDB; the
|
|
266
|
+
server stores only its HMAC and never uses it to set, restore or extend
|
|
267
|
+
a cookie. */
|
|
268
|
+
export interface ClientEvidence {
|
|
269
|
+
clientFp?: ClientFingerprint;
|
|
270
|
+
interaction?: Interaction;
|
|
271
|
+
storageId?: string;
|
|
272
|
+
}
|
|
129
273
|
export interface ReactionToggleInput {
|
|
130
274
|
targetType: ReactionTargetType;
|
|
131
275
|
targetId: string;
|
|
@@ -136,9 +280,20 @@ export interface ReactionToggleInput {
|
|
|
136
280
|
stack" step 2. The widget solves invisibly (managed mode), so this
|
|
137
281
|
never costs the reader a prompt or a round trip of their own. */
|
|
138
282
|
turnstileToken: string;
|
|
283
|
+
/** Client evidence, optional and never a gate -- see `ClientEvidence`. */
|
|
284
|
+
clientFp?: ClientFingerprint;
|
|
285
|
+
interaction?: Interaction;
|
|
286
|
+
storageId?: string;
|
|
139
287
|
}
|
|
140
288
|
export interface ReactionToggleResult {
|
|
141
289
|
reaction: ReactionSummary;
|
|
290
|
+
/** Epoch ms until which this browser holds a reader pass: the server has
|
|
291
|
+
set a session-bound cookie that stands in for `turnstileToken` on
|
|
292
|
+
later reactions. A client that sees this can stop minting tokens until
|
|
293
|
+
then and send `turnstileToken: ''`; a `400 turnstile_failed` on such a
|
|
294
|
+
request means the pass is gone and a token is needed again. Absent
|
|
295
|
+
only when the server has no session secret to sign one. */
|
|
296
|
+
passUntil?: number;
|
|
142
297
|
}
|
|
143
298
|
/** Public author view on a comment row. Never includes email or email_hash. */
|
|
144
299
|
export interface CommentAuthor {
|
|
@@ -154,8 +309,12 @@ export interface CommentAuthor {
|
|
|
154
309
|
}
|
|
155
310
|
export interface Comment {
|
|
156
311
|
id: string;
|
|
157
|
-
|
|
312
|
+
surface: CommentSurface;
|
|
313
|
+
/** `blog`: Ghost's post.id, stable across slug renames. `mood`: the
|
|
314
|
+
Telegram channel message id, the same value `/mood/[id]` routes on. */
|
|
158
315
|
postId: string;
|
|
316
|
+
/** `commentAnchorToken(id)`, precomputed so clients never hash. */
|
|
317
|
+
anchorToken: string;
|
|
159
318
|
/** Always a root comment id, or null. Threading is one level deep. */
|
|
160
319
|
parentId: string | null;
|
|
161
320
|
author: CommentAuthor;
|
|
@@ -189,6 +348,9 @@ export interface CommentListResult {
|
|
|
189
348
|
total: number;
|
|
190
349
|
}
|
|
191
350
|
export interface CommentCreateInput {
|
|
351
|
+
/** Defaults to `blog`. `mood` requires the post to have a linked
|
|
352
|
+
discussion thread (`MoodContentDocument.discussionLinked`). */
|
|
353
|
+
surface?: CommentSurface;
|
|
192
354
|
postId: string;
|
|
193
355
|
body: string;
|
|
194
356
|
/** Root comment id this replies to. Omitted or null for a root comment. */
|
|
@@ -209,6 +371,10 @@ export interface CommentCreateInput {
|
|
|
209
371
|
once the address is verified. */
|
|
210
372
|
notifyReplies?: boolean;
|
|
211
373
|
locale?: CommentLocale;
|
|
374
|
+
/** Client evidence, optional and never a gate -- see `ClientEvidence`. */
|
|
375
|
+
clientFp?: ClientFingerprint;
|
|
376
|
+
interaction?: Interaction;
|
|
377
|
+
storageId?: string;
|
|
212
378
|
}
|
|
213
379
|
export declare const COMMENT_CREATE_OUTCOMES: readonly ['published', 'held'];
|
|
214
380
|
export type CommentCreateOutcome = (typeof COMMENT_CREATE_OUTCOMES)[number];
|
package/dist/comments.js
CHANGED
|
@@ -3,6 +3,14 @@ var READER_PROVIDERS = ["email", "github", "google"];
|
|
|
3
3
|
var COMMENT_LOCALES = ["zh", "en"];
|
|
4
4
|
var READER_GRADES = ["l0", "l1", "l2"];
|
|
5
5
|
var COMMENT_STATUSES = ["published", "held", "rejected", "deleted"];
|
|
6
|
+
var COMMENT_SURFACES = ["blog", "mood"];
|
|
7
|
+
var COMMENT_ANCHOR_TOKEN_LENGTH = 12;
|
|
8
|
+
async function commentAnchorToken(commentId) {
|
|
9
|
+
const bytes = new TextEncoder().encode(commentId);
|
|
10
|
+
const digest = await crypto.subtle.digest("SHA-256", bytes);
|
|
11
|
+
return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("").slice(0, COMMENT_ANCHOR_TOKEN_LENGTH);
|
|
12
|
+
}
|
|
13
|
+
var COMMENT_ANCHOR_PATTERN = /#c-([0-9a-f]{12})\b/;
|
|
6
14
|
var MODERATION_ACTIONS = ["publish", "hold", "reject", "unsure"];
|
|
7
15
|
var MODERATION_REASONS = [
|
|
8
16
|
"ok",
|
|
@@ -54,10 +62,13 @@ function commentPolicyFromTags(tags, base = DEFAULT_COMMENT_POLICY) {
|
|
|
54
62
|
var acceptsComments = (policy) => policy.mode === "open";
|
|
55
63
|
export {
|
|
56
64
|
COMMENTS_MODES,
|
|
65
|
+
COMMENT_ANCHOR_PATTERN,
|
|
66
|
+
COMMENT_ANCHOR_TOKEN_LENGTH,
|
|
57
67
|
COMMENT_CREATE_OUTCOMES,
|
|
58
68
|
COMMENT_LOCALES,
|
|
59
69
|
COMMENT_POLICY_TAGS,
|
|
60
70
|
COMMENT_STATUSES,
|
|
71
|
+
COMMENT_SURFACES,
|
|
61
72
|
DEFAULT_COMMENT_POLICY,
|
|
62
73
|
DEFAULT_REACTION_EMOJI,
|
|
63
74
|
MODERATION_ACTIONS,
|
|
@@ -68,5 +79,6 @@ export {
|
|
|
68
79
|
READER_PROVIDERS,
|
|
69
80
|
READER_VERIFY_OUTCOMES,
|
|
70
81
|
acceptsComments,
|
|
82
|
+
commentAnchorToken,
|
|
71
83
|
commentPolicyFromTags
|
|
72
84
|
};
|
package/dist/content.d.ts
CHANGED
|
@@ -79,6 +79,17 @@ export interface MoodContentDocument extends ContentDocument {
|
|
|
79
79
|
source: 'mood';
|
|
80
80
|
groupIds?: string[];
|
|
81
81
|
channel?: ContentChannelSummary;
|
|
82
|
+
/** True when the post's copy in the Telegram discussion group is known
|
|
83
|
+
(`mood_posts.discussion_message_id`) and mood comments are enabled, so
|
|
84
|
+
the compose box can post into the thread. False or absent: the page
|
|
85
|
+
keeps the "Leave a comment on Telegram" link instead. */
|
|
86
|
+
discussionLinked?: boolean;
|
|
87
|
+
/** True once the read path has verified, from live traffic, that the
|
|
88
|
+
embed's comment ids are the group's message ids, which is what a
|
|
89
|
+
web reply to a Telegram-origin comment needs to thread correctly.
|
|
90
|
+
Gates reply-to on `telegram` items only; replies to `web` items are
|
|
91
|
+
always allowed. */
|
|
92
|
+
discussionRepliesEnabled?: boolean;
|
|
82
93
|
}
|
|
83
94
|
export interface PostContentDocument extends ContentDocument {
|
|
84
95
|
source: 'post';
|
package/dist/index.js
CHANGED
|
@@ -16,6 +16,14 @@ var READER_PROVIDERS = ["email", "github", "google"];
|
|
|
16
16
|
var COMMENT_LOCALES = ["zh", "en"];
|
|
17
17
|
var READER_GRADES = ["l0", "l1", "l2"];
|
|
18
18
|
var COMMENT_STATUSES = ["published", "held", "rejected", "deleted"];
|
|
19
|
+
var COMMENT_SURFACES = ["blog", "mood"];
|
|
20
|
+
var COMMENT_ANCHOR_TOKEN_LENGTH = 12;
|
|
21
|
+
async function commentAnchorToken(commentId) {
|
|
22
|
+
const bytes = new TextEncoder().encode(commentId);
|
|
23
|
+
const digest = await crypto.subtle.digest("SHA-256", bytes);
|
|
24
|
+
return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("").slice(0, COMMENT_ANCHOR_TOKEN_LENGTH);
|
|
25
|
+
}
|
|
26
|
+
var COMMENT_ANCHOR_PATTERN = /#c-([0-9a-f]{12})\b/;
|
|
19
27
|
var MODERATION_ACTIONS = ["publish", "hold", "reject", "unsure"];
|
|
20
28
|
var MODERATION_REASONS = [
|
|
21
29
|
"ok",
|
|
@@ -95,6 +103,7 @@ var MESSAGE_STATES = ["new", "read", "replied", "archived", "spam"];
|
|
|
95
103
|
// src/mood.ts
|
|
96
104
|
var MOOD_SENTIMENT_LABELS = ["joy", "calm", "melancholy", "anger", "anxiety", "neutral"];
|
|
97
105
|
var MOOD_AI_MODELS = ["gpt-5.5", "gpt-5", "claude-sonnet-4.6"];
|
|
106
|
+
var MOOD_COMMENT_ORIGINS = ["telegram", "web"];
|
|
98
107
|
|
|
99
108
|
// src/notify.ts
|
|
100
109
|
var NOTIFY_CHANNELS = ["mood", "blog", "privacy", "announcement"];
|
|
@@ -141,6 +150,13 @@ var LEGACY_HEALTH_PATH = "/v2/health";
|
|
|
141
150
|
|
|
142
151
|
// src/telegram-ops.ts
|
|
143
152
|
var TELEGRAM_OPS_WEBHOOK_PATH = "/webhooks/telegram-ops";
|
|
153
|
+
var TELEGRAM_OPS_COMMENT_CALLBACK_PREFIXES = {
|
|
154
|
+
reply: "comment:reply:",
|
|
155
|
+
approve: "comment:approve:",
|
|
156
|
+
hide: "comment:hide:",
|
|
157
|
+
delete: "comment:delete:",
|
|
158
|
+
ban: "comment:ban:"
|
|
159
|
+
};
|
|
144
160
|
var NOTIFY_GATE_PATH = "/admin/notify-gate";
|
|
145
161
|
var NOTIFY_GATE_RELEASE_PATH = `${NOTIFY_GATE_PATH}/release`;
|
|
146
162
|
var NOTIFY_GATE_STATES = ["open", "held"];
|
|
@@ -202,11 +218,14 @@ export {
|
|
|
202
218
|
BLOG_ANALYTICS_SUMMARY_ENDPOINT,
|
|
203
219
|
COMMENTS_MODES,
|
|
204
220
|
COMMENTS_PATH,
|
|
221
|
+
COMMENT_ANCHOR_PATTERN,
|
|
222
|
+
COMMENT_ANCHOR_TOKEN_LENGTH,
|
|
205
223
|
COMMENT_CREATE_OUTCOMES,
|
|
206
224
|
COMMENT_LOCALES,
|
|
207
225
|
COMMENT_PATH_PREFIX,
|
|
208
226
|
COMMENT_POLICY_TAGS,
|
|
209
227
|
COMMENT_STATUSES,
|
|
228
|
+
COMMENT_SURFACES,
|
|
210
229
|
CONTENT_DOCUMENT_SOURCES,
|
|
211
230
|
DEFAULT_COMMENT_POLICY,
|
|
212
231
|
DEFAULT_REACTION_EMOJI,
|
|
@@ -230,6 +249,7 @@ export {
|
|
|
230
249
|
MOOD_AI_MODELS,
|
|
231
250
|
MOOD_ARCHIVE_FEED_PATH,
|
|
232
251
|
MOOD_ARCHIVE_STATS_PATH,
|
|
252
|
+
MOOD_COMMENT_ORIGINS,
|
|
233
253
|
MOOD_IMAGE_PROXY_BASE_PATH,
|
|
234
254
|
MOOD_LIVE_COUNTS_PATH,
|
|
235
255
|
MOOD_LIVE_FEED_PATH,
|
|
@@ -273,6 +293,7 @@ export {
|
|
|
273
293
|
TELEGRAM_OPS_BROADCAST_PREVIEW_PATH,
|
|
274
294
|
TELEGRAM_OPS_BROADCAST_SEND_PATH,
|
|
275
295
|
TELEGRAM_OPS_BROADCAST_STATUSES,
|
|
296
|
+
TELEGRAM_OPS_COMMENT_CALLBACK_PREFIXES,
|
|
276
297
|
TELEGRAM_OPS_EVENTS_PATH,
|
|
277
298
|
TELEGRAM_OPS_OVERVIEW_PATH,
|
|
278
299
|
TELEGRAM_OPS_REMINDERS_DUE_PATH,
|
|
@@ -282,6 +303,7 @@ export {
|
|
|
282
303
|
TELEGRAM_OPS_WEBHOOK_PATH,
|
|
283
304
|
TELEGRAM_WEBHOOK_PATH,
|
|
284
305
|
acceptsComments,
|
|
306
|
+
commentAnchorToken,
|
|
285
307
|
commentPolicyFromTags,
|
|
286
308
|
parsePostLocaleTag,
|
|
287
309
|
telegramOpsEventActionPath,
|
package/dist/messages.d.ts
CHANGED
|
@@ -36,12 +36,13 @@ export interface OwnerMessageCreateInput {
|
|
|
36
36
|
body: string;
|
|
37
37
|
displayName: string;
|
|
38
38
|
/**
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* receive the
|
|
39
|
+
* Required. The comment box channels rather than blocks, because a comment
|
|
40
|
+
* that nobody can answer is still worth publishing; a private message that
|
|
41
|
+
* nobody can answer is a dead letter. Supplying one triggers the shared
|
|
42
|
+
* lazy-verification mail, and only a verified address can ever receive the
|
|
43
|
+
* owner's reply — so this is necessary for a reply, not sufficient.
|
|
43
44
|
*/
|
|
44
|
-
email
|
|
45
|
+
email: string;
|
|
45
46
|
turnstileToken: string;
|
|
46
47
|
/** Minted by the form on load; proves the submit was not instant. */
|
|
47
48
|
dwellToken: string;
|
package/dist/mood.d.ts
CHANGED
|
@@ -188,13 +188,38 @@ export interface MoodLiveCount {
|
|
|
188
188
|
export interface MoodLiveCountsResponse {
|
|
189
189
|
counts: Record<string, MoodLiveCount>;
|
|
190
190
|
}
|
|
191
|
+
/**
|
|
192
|
+
* The parent a comment replies to, read from the t.me reply block.
|
|
193
|
+
* `id` is the parent comment id; `text` is a plain-text preview, not HTML.
|
|
194
|
+
*/
|
|
195
|
+
export interface MoodCommentReplyTo {
|
|
196
|
+
id: string;
|
|
197
|
+
author: string;
|
|
198
|
+
text: string;
|
|
199
|
+
}
|
|
200
|
+
/** Where a thread item came from. `telegram`: a group member wrote it in
|
|
201
|
+
the discussion thread. `web`: written on /mood/[id]; the group holds the
|
|
202
|
+
ops bot's bridged copy, and the read path re-attributes that copy to the
|
|
203
|
+
reader — plans/mood-comments-bridge.md "Read path: scrape plus overlay". */
|
|
204
|
+
export declare const MOOD_COMMENT_ORIGINS: readonly ['telegram', 'web'];
|
|
205
|
+
export type MoodCommentOrigin = (typeof MOOD_COMMENT_ORIGINS)[number];
|
|
191
206
|
export interface MoodComment {
|
|
207
|
+
/** Discussion-group message id as the t.me embed reports it. */
|
|
192
208
|
id: string;
|
|
193
209
|
author: string;
|
|
194
210
|
authorAvatar?: string;
|
|
195
211
|
datetime: string;
|
|
196
212
|
content: string;
|
|
197
213
|
reactions: MoodReaction[];
|
|
214
|
+
replyTo?: MoodCommentReplyTo;
|
|
215
|
+
/** Omitted means `telegram`. */
|
|
216
|
+
origin?: MoodCommentOrigin;
|
|
217
|
+
/** The site comment row behind a `web` item (`Comment.id`). Lets the
|
|
218
|
+
browser mark rows it wrote and offer edit/delete through /v2/comments. */
|
|
219
|
+
commentId?: string;
|
|
220
|
+
/** `commentAnchorToken(commentId)`; the row renders as `id="c-<token>"`
|
|
221
|
+
so the bridged Telegram link and the owner card land on it. */
|
|
222
|
+
anchorToken?: string;
|
|
198
223
|
}
|
|
199
224
|
export interface MoodCommentsPage {
|
|
200
225
|
comments: MoodComment[];
|
package/dist/mood.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
// src/mood.ts
|
|
2
2
|
var MOOD_SENTIMENT_LABELS = ["joy", "calm", "melancholy", "anger", "anxiety", "neutral"];
|
|
3
3
|
var MOOD_AI_MODELS = ["gpt-5.5", "gpt-5", "claude-sonnet-4.6"];
|
|
4
|
+
var MOOD_COMMENT_ORIGINS = ["telegram", "web"];
|
|
4
5
|
export {
|
|
5
6
|
MOOD_AI_MODELS,
|
|
7
|
+
MOOD_COMMENT_ORIGINS,
|
|
6
8
|
MOOD_SENTIMENT_LABELS
|
|
7
9
|
};
|
package/dist/telegram-ops.d.ts
CHANGED
|
@@ -1,6 +1,24 @@
|
|
|
1
1
|
import type { AdminSubscriberPatch, BroadcastInput, BroadcastPreviewResult, BroadcastRecord, BroadcastSendResult } from './admin';
|
|
2
2
|
import type { DeliveryMode, NotifyChannel, SubscriberRecord, SubscriberStatus } from './notify';
|
|
3
3
|
export declare const TELEGRAM_OPS_WEBHOOK_PATH: '/webhooks/telegram-ops';
|
|
4
|
+
/**
|
|
5
|
+
* Callback data and message tokens the ops bot uses for comment cards. The
|
|
6
|
+
* comment id no longer lives in an expiring pending-action token: every
|
|
7
|
+
* card and every reply prompt carries `#c-<anchorToken>` in its View URL
|
|
8
|
+
* and as a text link, so replying to the card resolves the comment for as
|
|
9
|
+
* long as the card exists — plans/mood-comments-bridge.md "Owner reply
|
|
10
|
+
* flow". Group messages (the discussion group's automatic forwards and
|
|
11
|
+
* replies to the bot's bridged comments) are expected on this webhook too.
|
|
12
|
+
*/
|
|
13
|
+
export declare const TELEGRAM_OPS_COMMENT_CALLBACK_PREFIXES: {
|
|
14
|
+
readonly reply: 'comment:reply:';
|
|
15
|
+
readonly approve: 'comment:approve:';
|
|
16
|
+
readonly hide: 'comment:hide:';
|
|
17
|
+
readonly delete: 'comment:delete:';
|
|
18
|
+
/** Bans the comment's pre-ticked source keys, no purge -- see
|
|
19
|
+
plans/comment-actor-identity.md "Actions by source". */
|
|
20
|
+
readonly ban: 'comment:ban:';
|
|
21
|
+
};
|
|
4
22
|
export declare const NOTIFY_GATE_PATH: '/admin/notify-gate';
|
|
5
23
|
export declare const NOTIFY_GATE_RELEASE_PATH: "/admin/notify-gate/release";
|
|
6
24
|
export declare const NOTIFY_GATE_STATES: readonly ['open', 'held'];
|
package/dist/telegram-ops.js
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
// src/telegram-ops.ts
|
|
2
2
|
var TELEGRAM_OPS_WEBHOOK_PATH = "/webhooks/telegram-ops";
|
|
3
|
+
var TELEGRAM_OPS_COMMENT_CALLBACK_PREFIXES = {
|
|
4
|
+
reply: "comment:reply:",
|
|
5
|
+
approve: "comment:approve:",
|
|
6
|
+
hide: "comment:hide:",
|
|
7
|
+
delete: "comment:delete:",
|
|
8
|
+
ban: "comment:ban:"
|
|
9
|
+
};
|
|
3
10
|
var NOTIFY_GATE_PATH = "/admin/notify-gate";
|
|
4
11
|
var NOTIFY_GATE_RELEASE_PATH = `${NOTIFY_GATE_PATH}/release`;
|
|
5
12
|
var NOTIFY_GATE_STATES = ["open", "held"];
|
|
@@ -61,6 +68,7 @@ export {
|
|
|
61
68
|
TELEGRAM_OPS_BROADCAST_PREVIEW_PATH,
|
|
62
69
|
TELEGRAM_OPS_BROADCAST_SEND_PATH,
|
|
63
70
|
TELEGRAM_OPS_BROADCAST_STATUSES,
|
|
71
|
+
TELEGRAM_OPS_COMMENT_CALLBACK_PREFIXES,
|
|
64
72
|
TELEGRAM_OPS_EVENTS_PATH,
|
|
65
73
|
TELEGRAM_OPS_OVERVIEW_PATH,
|
|
66
74
|
TELEGRAM_OPS_REMINDERS_DUE_PATH,
|