@bunizao/contracts 0.1.0 → 0.4.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/dist/analytics.js +10 -10
- package/dist/comments.d.ts +327 -0
- package/dist/comments.js +72 -0
- package/dist/content.js +2 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +173 -62
- package/dist/messages.d.ts +68 -0
- package/dist/messages.js +13 -0
- package/dist/mood.js +2 -2
- package/dist/routes.d.ts +14 -0
- package/dist/routes.js +51 -23
- package/dist/telegram-ops.js +22 -22
- package/package.json +11 -1
package/dist/analytics.js
CHANGED
|
@@ -11,15 +11,15 @@ var BLOG_ANALYTICS_READ_THRESHOLD_MS = 5000;
|
|
|
11
11
|
var BLOG_ANALYTICS_COMPLETION_SCROLL_DEPTH = 0.9;
|
|
12
12
|
var BLOG_ANALYTICS_RANGE_OPTIONS = [7, 30, 90];
|
|
13
13
|
export {
|
|
14
|
-
|
|
15
|
-
NEWSLETTER_ANALYTICS_CLICK_ENDPOINT,
|
|
16
|
-
LISTENING_ANALYTICS_EVENT_ENDPOINT,
|
|
17
|
-
BLOG_ANALYTICS_SUMMARY_ENDPOINT,
|
|
18
|
-
BLOG_ANALYTICS_READ_THRESHOLD_MS,
|
|
19
|
-
BLOG_ANALYTICS_RANGE_OPTIONS,
|
|
20
|
-
BLOG_ANALYTICS_EVENT_ENDPOINT,
|
|
21
|
-
BLOG_ANALYTICS_EVENTS_ENDPOINT,
|
|
22
|
-
BLOG_ANALYTICS_EVENTS_DEFAULT_LIMIT,
|
|
14
|
+
BLOG_ANALYTICS_ARTICLE_ENDPOINT,
|
|
23
15
|
BLOG_ANALYTICS_COMPLETION_SCROLL_DEPTH,
|
|
24
|
-
|
|
16
|
+
BLOG_ANALYTICS_EVENTS_DEFAULT_LIMIT,
|
|
17
|
+
BLOG_ANALYTICS_EVENTS_ENDPOINT,
|
|
18
|
+
BLOG_ANALYTICS_EVENT_ENDPOINT,
|
|
19
|
+
BLOG_ANALYTICS_RANGE_OPTIONS,
|
|
20
|
+
BLOG_ANALYTICS_READ_THRESHOLD_MS,
|
|
21
|
+
BLOG_ANALYTICS_SUMMARY_ENDPOINT,
|
|
22
|
+
LISTENING_ANALYTICS_EVENT_ENDPOINT,
|
|
23
|
+
NEWSLETTER_ANALYTICS_CLICK_ENDPOINT,
|
|
24
|
+
NEWSLETTER_ANALYTICS_OPEN_ENDPOINT
|
|
25
25
|
};
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Blog comments, reactions, and reader identity on /blog/[slug]. v2 shape —
|
|
3
|
+
* see plans/blog-comments.md "Decisions taken (v2)" for the model this
|
|
4
|
+
* follows: anonymous-first participation through a risk stack, lazy and
|
|
5
|
+
* stateless email verification, one reader table shared with the newsletter.
|
|
6
|
+
*
|
|
7
|
+
* Reading is open to everyone. Posting a comment or reacting needs only a
|
|
8
|
+
* name, with an optional email — no session, no verification gate. Email verification
|
|
9
|
+
* is an upgrade path (grade L1/L2 in the PRD), never a door charge; the
|
|
10
|
+
* email address itself never crosses this boundary in a response body.
|
|
11
|
+
*/
|
|
12
|
+
export declare const READER_PROVIDERS: readonly ['email', 'github', 'google'];
|
|
13
|
+
export type ReaderProvider = (typeof READER_PROVIDERS)[number];
|
|
14
|
+
export declare const COMMENT_LOCALES: readonly ['zh', 'en'];
|
|
15
|
+
export type CommentLocale = (typeof COMMENT_LOCALES)[number];
|
|
16
|
+
/** The reader's standing, from "Identity: three grades, one table":
|
|
17
|
+
* l0 — `reader_anon` cookie only, nothing verified
|
|
18
|
+
* l1 — verified email (lazy verification link)
|
|
19
|
+
* l2 — OAuth (GitHub or Google), phase 3 */
|
|
20
|
+
export declare const READER_GRADES: readonly ['l0', 'l1', 'l2'];
|
|
21
|
+
export type ReaderGrade = (typeof READER_GRADES)[number];
|
|
22
|
+
export declare const COMMENT_STATUSES: readonly ['published', 'held', 'rejected', 'deleted'];
|
|
23
|
+
export type CommentStatus = (typeof COMMENT_STATUSES)[number];
|
|
24
|
+
export declare const MODERATION_ACTIONS: readonly ['publish', 'hold', 'reject', 'unsure'];
|
|
25
|
+
export type ModerationAction = (typeof MODERATION_ACTIONS)[number];
|
|
26
|
+
export declare const MODERATION_REASONS: readonly ['ok', 'spam', 'promotional', 'abuse', 'off_topic', 'personal_info'];
|
|
27
|
+
export type ModerationReason = (typeof MODERATION_REASONS)[number];
|
|
28
|
+
/** Never sent to a reader. Rides along on the owner's Telegram notification. */
|
|
29
|
+
export interface ModerationVerdict {
|
|
30
|
+
action: ModerationAction;
|
|
31
|
+
reason: ModerationReason;
|
|
32
|
+
/** One short line explaining the call, in the language of the comment. */
|
|
33
|
+
note: string;
|
|
34
|
+
}
|
|
35
|
+
/** The calling browser's verified reader standing (L1/L2 only — an L0-only
|
|
36
|
+
visitor has no reader row and this is null). Never carries email or
|
|
37
|
+
email_hash; `avatarUrl` is always a same-origin proxy path. */
|
|
38
|
+
export interface ReaderMe {
|
|
39
|
+
readerId: string;
|
|
40
|
+
grade: ReaderGrade;
|
|
41
|
+
provider: ReaderProvider;
|
|
42
|
+
displayName: string;
|
|
43
|
+
avatarUrl: string;
|
|
44
|
+
notifyReplies: boolean;
|
|
45
|
+
/** Whether this address holds an active newsletter subscription. */
|
|
46
|
+
subscribed: boolean;
|
|
47
|
+
}
|
|
48
|
+
export interface ReaderMeResult {
|
|
49
|
+
reader: ReaderMe | null;
|
|
50
|
+
}
|
|
51
|
+
/** The confirm button's POST — plans/blog-comments.md "Lazy verification". */
|
|
52
|
+
export interface ReaderVerifyInput {
|
|
53
|
+
token: string;
|
|
54
|
+
/** Ticked "also subscribe" in the post-comment nudge. Activates the
|
|
55
|
+
subscription in the same POST — one email, one click, both confirmations. */
|
|
56
|
+
subscribe?: boolean;
|
|
57
|
+
}
|
|
58
|
+
export declare const READER_VERIFY_OUTCOMES: readonly ['confirmed', 'already_confirmed', 'expired', 'invalid'];
|
|
59
|
+
export type ReaderVerifyOutcome = (typeof READER_VERIFY_OUTCOMES)[number];
|
|
60
|
+
export interface ReaderVerifyResult {
|
|
61
|
+
outcome: ReaderVerifyOutcome;
|
|
62
|
+
reader: ReaderMe | null;
|
|
63
|
+
}
|
|
64
|
+
/** One conversation, quieted from the reply mail itself. The token is the
|
|
65
|
+
whole authentication: the reader is usually holding that mail on a device
|
|
66
|
+
that has never signed in here, and an off switch gated behind a sign-in is
|
|
67
|
+
one people reach by marking the sender spam instead. */
|
|
68
|
+
export interface ReaderMuteInput {
|
|
69
|
+
token: string;
|
|
70
|
+
/** Omitted or true mutes; false is the undo the landing page offers. */
|
|
71
|
+
muted?: boolean;
|
|
72
|
+
}
|
|
73
|
+
export declare const READER_MUTE_OUTCOMES: readonly ['muted', 'unmuted', 'invalid'];
|
|
74
|
+
export type ReaderMuteOutcome = (typeof READER_MUTE_OUTCOMES)[number];
|
|
75
|
+
export interface ReaderMuteResult {
|
|
76
|
+
outcome: ReaderMuteOutcome;
|
|
77
|
+
/** Carried so the page can offer the wider switches without another call. */
|
|
78
|
+
postId?: string;
|
|
79
|
+
}
|
|
80
|
+
/** Re-send the verification mail. Rate-limited per address; always answers
|
|
81
|
+
the same shape regardless of whether the address has a comment on file,
|
|
82
|
+
so this can never be used to probe which addresses have commented. */
|
|
83
|
+
export interface ReaderResendInput {
|
|
84
|
+
email: string;
|
|
85
|
+
/** Carries the original "notify me of replies" intent into the fresh
|
|
86
|
+
verification link, recovered from the stale token's payload. Optional;
|
|
87
|
+
a bare resend defaults to false. */
|
|
88
|
+
notifyReplies?: boolean;
|
|
89
|
+
locale?: CommentLocale;
|
|
90
|
+
}
|
|
91
|
+
export interface ReaderResendResult {
|
|
92
|
+
ok: boolean;
|
|
93
|
+
}
|
|
94
|
+
/** The two switches on the confirm page, and the only place a reader can move
|
|
95
|
+
them without an account. Session-authenticated (the cookie the verify POST
|
|
96
|
+
just set), so nothing here names an address. Both fields are optional: the
|
|
97
|
+
page sends only the one that was just toggled. */
|
|
98
|
+
export interface ReaderPreferencesInput {
|
|
99
|
+
notifyReplies?: boolean;
|
|
100
|
+
subscribed?: boolean;
|
|
101
|
+
}
|
|
102
|
+
export interface ReaderPreferencesResult {
|
|
103
|
+
reader: ReaderMe | null;
|
|
104
|
+
}
|
|
105
|
+
export declare const REACTION_TARGET_TYPES: readonly ['post', 'comment'];
|
|
106
|
+
export type ReactionTargetType = (typeof REACTION_TARGET_TYPES)[number];
|
|
107
|
+
/** The only reaction shipped at launch. The storage layer is emoji-keyed anyway. */
|
|
108
|
+
export declare const DEFAULT_REACTION_EMOJI = "\u2764\uFE0F";
|
|
109
|
+
/** A face in the avatar stack beside a reaction count. Only identified
|
|
110
|
+
reactors (L1/L2, or an L0 reactor whose claimed email resolves) ever
|
|
111
|
+
appear here — see "Reactions: anonymous counts, identified faces". */
|
|
112
|
+
export interface ReactorChip {
|
|
113
|
+
name: string;
|
|
114
|
+
avatarUrl: string | null;
|
|
115
|
+
}
|
|
116
|
+
export interface ReactionSummary {
|
|
117
|
+
emoji: string;
|
|
118
|
+
count: number;
|
|
119
|
+
/** Whether the calling browser (session or reader) holds this reaction. */
|
|
120
|
+
reacted: boolean;
|
|
121
|
+
/** Most recent identified reactors, newest first, capped server-side. */
|
|
122
|
+
reactors: ReactorChip[];
|
|
123
|
+
}
|
|
124
|
+
/** `${targetType}:${targetId}`, e.g. `post:abc123` or `comment:xyz789`. */
|
|
125
|
+
export type ReactionTargetKey = string;
|
|
126
|
+
export interface ReactionBatchResult {
|
|
127
|
+
reactions: Record<ReactionTargetKey, ReactionSummary[]>;
|
|
128
|
+
}
|
|
129
|
+
export interface ReactionToggleInput {
|
|
130
|
+
targetType: ReactionTargetType;
|
|
131
|
+
targetId: string;
|
|
132
|
+
emoji?: string;
|
|
133
|
+
/** Desired final state. Repeating the same request is safe. */
|
|
134
|
+
reacted: boolean;
|
|
135
|
+
/** `expectedAction: 'blog_reaction'` -- plans/blog-comments.md "The risk
|
|
136
|
+
stack" step 2. The widget solves invisibly (managed mode), so this
|
|
137
|
+
never costs the reader a prompt or a round trip of their own. */
|
|
138
|
+
turnstileToken: string;
|
|
139
|
+
}
|
|
140
|
+
export interface ReactionToggleResult {
|
|
141
|
+
reaction: ReactionSummary;
|
|
142
|
+
}
|
|
143
|
+
/** Public author view on a comment row. Never includes email or email_hash. */
|
|
144
|
+
export interface CommentAuthor {
|
|
145
|
+
name: string;
|
|
146
|
+
/** Same-origin proxy path to the writer's cached avatar, or empty when no
|
|
147
|
+
avatar has resolved for their address (or they left none). Empty means
|
|
148
|
+
"draw your own": the endpoint behind this path answers an identicon for
|
|
149
|
+
any key it does not know, so a path emitted for every address would make
|
|
150
|
+
every face an identicon. */
|
|
151
|
+
avatarUrl: string;
|
|
152
|
+
/** True when this row's writer is the blog owner. */
|
|
153
|
+
byAuthor: boolean;
|
|
154
|
+
}
|
|
155
|
+
export interface Comment {
|
|
156
|
+
id: string;
|
|
157
|
+
/** Ghost's post.id, stable across slug renames. */
|
|
158
|
+
postId: string;
|
|
159
|
+
/** Always a root comment id, or null. Threading is one level deep. */
|
|
160
|
+
parentId: string | null;
|
|
161
|
+
author: CommentAuthor;
|
|
162
|
+
/** Plain text, 1-2000 chars. Escaping and autolinking happen at render. */
|
|
163
|
+
body: string;
|
|
164
|
+
status: CommentStatus;
|
|
165
|
+
createdAt: string;
|
|
166
|
+
editedAt: string | null;
|
|
167
|
+
/** True when this browser wrote the row (verified reader match, or the
|
|
168
|
+
`reader_anon` session matches). Highlights the row as yours; mutation
|
|
169
|
+
rights are signalled by `editableUntil`/`deletable`, not by this. */
|
|
170
|
+
mine: boolean;
|
|
171
|
+
/** Server clock deadline for the 15-minute edit window, ms since epoch.
|
|
172
|
+
Present only when the viewer is the verified reader who owns the row
|
|
173
|
+
and the window has not closed. Always null for session-owned rows --
|
|
174
|
+
anonymous comments cannot be edited. */
|
|
175
|
+
editableUntil: number | null;
|
|
176
|
+
/** True when the viewer may delete this row: verified reader owns it and
|
|
177
|
+
it is not already a tombstone. Always false for session-owned rows. */
|
|
178
|
+
deletable: boolean;
|
|
179
|
+
/** Soft-deleted but kept as a shape-preserving placeholder because a reply
|
|
180
|
+
hangs underneath it. `body`/`author` are empty on a tombstone. */
|
|
181
|
+
tombstone: boolean;
|
|
182
|
+
}
|
|
183
|
+
export interface CommentListResult {
|
|
184
|
+
comments: Comment[];
|
|
185
|
+
hasMore: boolean;
|
|
186
|
+
/** Cursor for the next page, or null when `hasMore` is false. */
|
|
187
|
+
nextBefore: string | null;
|
|
188
|
+
/** Published comments on the post (excludes held/rejected/deleted). */
|
|
189
|
+
total: number;
|
|
190
|
+
}
|
|
191
|
+
export interface CommentCreateInput {
|
|
192
|
+
postId: string;
|
|
193
|
+
body: string;
|
|
194
|
+
/** Root comment id this replies to. Omitted or null for a root comment. */
|
|
195
|
+
parentId?: string | null;
|
|
196
|
+
displayName: string;
|
|
197
|
+
/** Optional. Supplied: must be valid; triggers lazy verification and
|
|
198
|
+
enables claiming and a Gravatar-backed avatar.
|
|
199
|
+
Omitted or empty: the comment is owned by its anon session only and
|
|
200
|
+
the client renders an identicon. */
|
|
201
|
+
email?: string;
|
|
202
|
+
turnstileToken: string;
|
|
203
|
+
/** Visually-hidden honeypot field. Must arrive empty. */
|
|
204
|
+
website?: string;
|
|
205
|
+
/** Signed server timestamp minted at first interaction with the compose
|
|
206
|
+
box — see "The risk stack" step 4 (dwell time). */
|
|
207
|
+
dwellToken: string;
|
|
208
|
+
/** Reader accepted the post-comment subscribe offer. Only takes effect
|
|
209
|
+
once the address is verified. */
|
|
210
|
+
notifyReplies?: boolean;
|
|
211
|
+
locale?: CommentLocale;
|
|
212
|
+
}
|
|
213
|
+
export declare const COMMENT_CREATE_OUTCOMES: readonly ['published', 'held'];
|
|
214
|
+
export type CommentCreateOutcome = (typeof COMMENT_CREATE_OUTCOMES)[number];
|
|
215
|
+
export interface CommentCreateResult {
|
|
216
|
+
outcome: CommentCreateOutcome;
|
|
217
|
+
comment: Comment;
|
|
218
|
+
/** True when a supplied `email` was not already a verified reader — the
|
|
219
|
+
client shows the verification nudge (and, when accepted, the subscribe
|
|
220
|
+
offer). Always false when no email was supplied; the add-an-email
|
|
221
|
+
nudge is driven client-side by the missing address, not by this flag. */
|
|
222
|
+
unverifiedEmail: boolean;
|
|
223
|
+
}
|
|
224
|
+
export interface CommentEditInput {
|
|
225
|
+
body: string;
|
|
226
|
+
}
|
|
227
|
+
export interface CommentEditResult {
|
|
228
|
+
comment: Comment;
|
|
229
|
+
}
|
|
230
|
+
export interface CommentDeleteResult {
|
|
231
|
+
ok: true;
|
|
232
|
+
/** True when the row became a tombstone instead of disappearing, because
|
|
233
|
+
a reply hangs underneath it. */
|
|
234
|
+
tombstone: boolean;
|
|
235
|
+
}
|
|
236
|
+
/** What the comment section does on a post.
|
|
237
|
+
*
|
|
238
|
+
* open — the ordinary thread: read, write, reply.
|
|
239
|
+
* readonly — everything already written stays readable; nothing new is
|
|
240
|
+
* accepted. The section says so instead of offering a box.
|
|
241
|
+
* off — no comment section at all.
|
|
242
|
+
*
|
|
243
|
+
* `off` and `readonly` differ only in what the page draws; to the API both
|
|
244
|
+
* mean "this post takes no writes" (see `acceptsComments`). */
|
|
245
|
+
export declare const COMMENTS_MODES: readonly ['open', 'readonly', 'off'];
|
|
246
|
+
export type CommentsMode = (typeof COMMENTS_MODES)[number];
|
|
247
|
+
export interface CommentPolicy {
|
|
248
|
+
mode: CommentsMode;
|
|
249
|
+
/** Whether the heart can be pressed — on the post and on its comments.
|
|
250
|
+
Independent of `mode`: a post can take reactions with comments off, and
|
|
251
|
+
an open thread can refuse them. */
|
|
252
|
+
reactions: boolean;
|
|
253
|
+
/** Accept a comment only from an address that has been verified. Anonymous
|
|
254
|
+
and unverified-email writers are refused rather than held. */
|
|
255
|
+
requireVerifiedEmail: boolean;
|
|
256
|
+
}
|
|
257
|
+
export declare const DEFAULT_COMMENT_POLICY: CommentPolicy;
|
|
258
|
+
/** Ghost internal tags that override the site-wide default, one knob each.
|
|
259
|
+
*
|
|
260
|
+
* Internal tags are the switch because they are the only per-post field the
|
|
261
|
+
* author already edits in Ghost, and both halves of the system can read them:
|
|
262
|
+
* the site sees them through the Admin API at build time, site-api through the
|
|
263
|
+
* Content API at request time (`include=tags` returns internal tags). One
|
|
264
|
+
* source of truth, no settings table, no admin screen to keep in sync.
|
|
265
|
+
*
|
|
266
|
+
* `#no-comments` predates the others and is kept: it always meant "this post
|
|
267
|
+
* takes no more comments", which is `readonly`. */
|
|
268
|
+
export declare const COMMENT_POLICY_TAGS: {
|
|
269
|
+
readonly 'comments-off': (policy: CommentPolicy) => {
|
|
270
|
+
/** Whether the heart can be pressed — on the post and on its comments.
|
|
271
|
+
Independent of `mode`: a post can take reactions with comments off, and
|
|
272
|
+
an open thread can refuse them. */
|
|
273
|
+
reactions: boolean;
|
|
274
|
+
/** Accept a comment only from an address that has been verified. Anonymous
|
|
275
|
+
and unverified-email writers are refused rather than held. */
|
|
276
|
+
requireVerifiedEmail: boolean;
|
|
277
|
+
mode: CommentsMode;
|
|
278
|
+
};
|
|
279
|
+
readonly 'comments-readonly': (policy: CommentPolicy) => {
|
|
280
|
+
/** Whether the heart can be pressed — on the post and on its comments.
|
|
281
|
+
Independent of `mode`: a post can take reactions with comments off, and
|
|
282
|
+
an open thread can refuse them. */
|
|
283
|
+
reactions: boolean;
|
|
284
|
+
/** Accept a comment only from an address that has been verified. Anonymous
|
|
285
|
+
and unverified-email writers are refused rather than held. */
|
|
286
|
+
requireVerifiedEmail: boolean;
|
|
287
|
+
mode: CommentsMode;
|
|
288
|
+
};
|
|
289
|
+
readonly 'no-comments': (policy: CommentPolicy) => {
|
|
290
|
+
/** Whether the heart can be pressed — on the post and on its comments.
|
|
291
|
+
Independent of `mode`: a post can take reactions with comments off, and
|
|
292
|
+
an open thread can refuse them. */
|
|
293
|
+
reactions: boolean;
|
|
294
|
+
/** Accept a comment only from an address that has been verified. Anonymous
|
|
295
|
+
and unverified-email writers are refused rather than held. */
|
|
296
|
+
requireVerifiedEmail: boolean;
|
|
297
|
+
mode: CommentsMode;
|
|
298
|
+
};
|
|
299
|
+
readonly 'reactions-off': (policy: CommentPolicy) => {
|
|
300
|
+
mode: CommentsMode;
|
|
301
|
+
/** Accept a comment only from an address that has been verified. Anonymous
|
|
302
|
+
and unverified-email writers are refused rather than held. */
|
|
303
|
+
requireVerifiedEmail: boolean;
|
|
304
|
+
reactions: false;
|
|
305
|
+
};
|
|
306
|
+
readonly 'comments-verified': (policy: CommentPolicy) => {
|
|
307
|
+
mode: CommentsMode;
|
|
308
|
+
/** Whether the heart can be pressed — on the post and on its comments.
|
|
309
|
+
Independent of `mode`: a post can take reactions with comments off, and
|
|
310
|
+
an open thread can refuse them. */
|
|
311
|
+
reactions: boolean;
|
|
312
|
+
requireVerifiedEmail: true;
|
|
313
|
+
};
|
|
314
|
+
};
|
|
315
|
+
/** A tag as either half of Ghost's pair: `#no-comments` is the name a person
|
|
316
|
+
types, `hash-no-comments` the slug the API returns beside it. */
|
|
317
|
+
export interface CommentPolicyTagLike {
|
|
318
|
+
name?: string | null;
|
|
319
|
+
slug?: string | null;
|
|
320
|
+
}
|
|
321
|
+
/** Fold a post's tags onto the site-wide default. Order does not matter:
|
|
322
|
+
every tag sets one field, and a tag the map does not know is ignored. */
|
|
323
|
+
export declare function commentPolicyFromTags(tags: readonly CommentPolicyTagLike[] | null | undefined, base?: CommentPolicy): CommentPolicy;
|
|
324
|
+
/** Whether the post takes new comments — the one question the API asks. Both
|
|
325
|
+
`readonly` and `off` answer no; the difference between them is drawn, not
|
|
326
|
+
enforced. */
|
|
327
|
+
export declare const acceptsComments: (policy: CommentPolicy) => boolean;
|
package/dist/comments.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// src/comments.ts
|
|
2
|
+
var READER_PROVIDERS = ["email", "github", "google"];
|
|
3
|
+
var COMMENT_LOCALES = ["zh", "en"];
|
|
4
|
+
var READER_GRADES = ["l0", "l1", "l2"];
|
|
5
|
+
var COMMENT_STATUSES = ["published", "held", "rejected", "deleted"];
|
|
6
|
+
var MODERATION_ACTIONS = ["publish", "hold", "reject", "unsure"];
|
|
7
|
+
var MODERATION_REASONS = [
|
|
8
|
+
"ok",
|
|
9
|
+
"spam",
|
|
10
|
+
"promotional",
|
|
11
|
+
"abuse",
|
|
12
|
+
"off_topic",
|
|
13
|
+
"personal_info"
|
|
14
|
+
];
|
|
15
|
+
var READER_VERIFY_OUTCOMES = ["confirmed", "already_confirmed", "expired", "invalid"];
|
|
16
|
+
var READER_MUTE_OUTCOMES = ["muted", "unmuted", "invalid"];
|
|
17
|
+
var REACTION_TARGET_TYPES = ["post", "comment"];
|
|
18
|
+
var DEFAULT_REACTION_EMOJI = "❤️";
|
|
19
|
+
var COMMENT_CREATE_OUTCOMES = ["published", "held"];
|
|
20
|
+
var COMMENTS_MODES = ["open", "readonly", "off"];
|
|
21
|
+
var DEFAULT_COMMENT_POLICY = {
|
|
22
|
+
mode: "open",
|
|
23
|
+
reactions: true,
|
|
24
|
+
requireVerifiedEmail: false
|
|
25
|
+
};
|
|
26
|
+
var COMMENT_POLICY_TAGS = {
|
|
27
|
+
"comments-off": (policy) => ({ ...policy, mode: "off" }),
|
|
28
|
+
"comments-readonly": (policy) => ({ ...policy, mode: "readonly" }),
|
|
29
|
+
"no-comments": (policy) => ({ ...policy, mode: "readonly" }),
|
|
30
|
+
"reactions-off": (policy) => ({ ...policy, reactions: false }),
|
|
31
|
+
"comments-verified": (policy) => ({ ...policy, requireVerifiedEmail: true })
|
|
32
|
+
};
|
|
33
|
+
var policyKeyOf = (tag) => {
|
|
34
|
+
const name = tag.name?.trim();
|
|
35
|
+
if (name?.startsWith("#"))
|
|
36
|
+
return name.slice(1).toLowerCase();
|
|
37
|
+
const slug = tag.slug?.trim().toLowerCase();
|
|
38
|
+
if (slug?.startsWith("hash-"))
|
|
39
|
+
return slug.slice(5);
|
|
40
|
+
return null;
|
|
41
|
+
};
|
|
42
|
+
function commentPolicyFromTags(tags, base = DEFAULT_COMMENT_POLICY) {
|
|
43
|
+
let policy = { ...base };
|
|
44
|
+
for (const tag of tags ?? []) {
|
|
45
|
+
const key = policyKeyOf(tag);
|
|
46
|
+
if (!key)
|
|
47
|
+
continue;
|
|
48
|
+
const apply = COMMENT_POLICY_TAGS[key];
|
|
49
|
+
if (apply)
|
|
50
|
+
policy = apply(policy);
|
|
51
|
+
}
|
|
52
|
+
return policy;
|
|
53
|
+
}
|
|
54
|
+
var acceptsComments = (policy) => policy.mode === "open";
|
|
55
|
+
export {
|
|
56
|
+
COMMENTS_MODES,
|
|
57
|
+
COMMENT_CREATE_OUTCOMES,
|
|
58
|
+
COMMENT_LOCALES,
|
|
59
|
+
COMMENT_POLICY_TAGS,
|
|
60
|
+
COMMENT_STATUSES,
|
|
61
|
+
DEFAULT_COMMENT_POLICY,
|
|
62
|
+
DEFAULT_REACTION_EMOJI,
|
|
63
|
+
MODERATION_ACTIONS,
|
|
64
|
+
MODERATION_REASONS,
|
|
65
|
+
REACTION_TARGET_TYPES,
|
|
66
|
+
READER_GRADES,
|
|
67
|
+
READER_MUTE_OUTCOMES,
|
|
68
|
+
READER_PROVIDERS,
|
|
69
|
+
READER_VERIFY_OUTCOMES,
|
|
70
|
+
acceptsComments,
|
|
71
|
+
commentPolicyFromTags
|
|
72
|
+
};
|
package/dist/content.js
CHANGED
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -11,6 +11,61 @@ var BLOG_ANALYTICS_READ_THRESHOLD_MS = 5000;
|
|
|
11
11
|
var BLOG_ANALYTICS_COMPLETION_SCROLL_DEPTH = 0.9;
|
|
12
12
|
var BLOG_ANALYTICS_RANGE_OPTIONS = [7, 30, 90];
|
|
13
13
|
|
|
14
|
+
// src/comments.ts
|
|
15
|
+
var READER_PROVIDERS = ["email", "github", "google"];
|
|
16
|
+
var COMMENT_LOCALES = ["zh", "en"];
|
|
17
|
+
var READER_GRADES = ["l0", "l1", "l2"];
|
|
18
|
+
var COMMENT_STATUSES = ["published", "held", "rejected", "deleted"];
|
|
19
|
+
var MODERATION_ACTIONS = ["publish", "hold", "reject", "unsure"];
|
|
20
|
+
var MODERATION_REASONS = [
|
|
21
|
+
"ok",
|
|
22
|
+
"spam",
|
|
23
|
+
"promotional",
|
|
24
|
+
"abuse",
|
|
25
|
+
"off_topic",
|
|
26
|
+
"personal_info"
|
|
27
|
+
];
|
|
28
|
+
var READER_VERIFY_OUTCOMES = ["confirmed", "already_confirmed", "expired", "invalid"];
|
|
29
|
+
var READER_MUTE_OUTCOMES = ["muted", "unmuted", "invalid"];
|
|
30
|
+
var REACTION_TARGET_TYPES = ["post", "comment"];
|
|
31
|
+
var DEFAULT_REACTION_EMOJI = "❤️";
|
|
32
|
+
var COMMENT_CREATE_OUTCOMES = ["published", "held"];
|
|
33
|
+
var COMMENTS_MODES = ["open", "readonly", "off"];
|
|
34
|
+
var DEFAULT_COMMENT_POLICY = {
|
|
35
|
+
mode: "open",
|
|
36
|
+
reactions: true,
|
|
37
|
+
requireVerifiedEmail: false
|
|
38
|
+
};
|
|
39
|
+
var COMMENT_POLICY_TAGS = {
|
|
40
|
+
"comments-off": (policy) => ({ ...policy, mode: "off" }),
|
|
41
|
+
"comments-readonly": (policy) => ({ ...policy, mode: "readonly" }),
|
|
42
|
+
"no-comments": (policy) => ({ ...policy, mode: "readonly" }),
|
|
43
|
+
"reactions-off": (policy) => ({ ...policy, reactions: false }),
|
|
44
|
+
"comments-verified": (policy) => ({ ...policy, requireVerifiedEmail: true })
|
|
45
|
+
};
|
|
46
|
+
var policyKeyOf = (tag) => {
|
|
47
|
+
const name = tag.name?.trim();
|
|
48
|
+
if (name?.startsWith("#"))
|
|
49
|
+
return name.slice(1).toLowerCase();
|
|
50
|
+
const slug = tag.slug?.trim().toLowerCase();
|
|
51
|
+
if (slug?.startsWith("hash-"))
|
|
52
|
+
return slug.slice(5);
|
|
53
|
+
return null;
|
|
54
|
+
};
|
|
55
|
+
function commentPolicyFromTags(tags, base = DEFAULT_COMMENT_POLICY) {
|
|
56
|
+
let policy = { ...base };
|
|
57
|
+
for (const tag of tags ?? []) {
|
|
58
|
+
const key = policyKeyOf(tag);
|
|
59
|
+
if (!key)
|
|
60
|
+
continue;
|
|
61
|
+
const apply = COMMENT_POLICY_TAGS[key];
|
|
62
|
+
if (apply)
|
|
63
|
+
policy = apply(policy);
|
|
64
|
+
}
|
|
65
|
+
return policy;
|
|
66
|
+
}
|
|
67
|
+
var acceptsComments = (policy) => policy.mode === "open";
|
|
68
|
+
|
|
14
69
|
// src/content.ts
|
|
15
70
|
var CONTENT_DOCUMENT_SOURCES = ["mood", "post"];
|
|
16
71
|
var POST_LOCALE_RE = /^[a-z]{2,8}(?:-[a-z0-9]{1,8})*$/;
|
|
@@ -30,6 +85,13 @@ function parsePostLocaleTag(tagName) {
|
|
|
30
85
|
}
|
|
31
86
|
return canonicalSlug ? { locale, canonicalSlug } : { locale };
|
|
32
87
|
}
|
|
88
|
+
// src/messages.ts
|
|
89
|
+
var MESSAGE_LOCALES = ["zh", "en"];
|
|
90
|
+
var MESSAGE_MIN_BODY_LENGTH = 2;
|
|
91
|
+
var MESSAGE_MAX_BODY_LENGTH = 4000;
|
|
92
|
+
var MESSAGE_MAX_NAME_LENGTH = 32;
|
|
93
|
+
var MESSAGE_STATES = ["new", "read", "replied", "archived", "spam"];
|
|
94
|
+
|
|
33
95
|
// src/mood.ts
|
|
34
96
|
var MOOD_SENTIMENT_LABELS = ["joy", "calm", "melancholy", "anger", "anxiety", "neutral"];
|
|
35
97
|
var MOOD_AI_MODELS = ["gpt-5.5", "gpt-5", "claude-sonnet-4.6"];
|
|
@@ -52,6 +114,20 @@ var MOOD_SEARCH_PATH = "/v2/mood/search";
|
|
|
52
114
|
var MOOD_IMAGE_PROXY_BASE_PATH = "/v2/images";
|
|
53
115
|
var MOOD_MEDIA_PROXY_BASE_PATH = "/v2/media";
|
|
54
116
|
var LISTENING_PATH = "/v2/listening";
|
|
117
|
+
var COMMENTS_PATH = "/v2/comments";
|
|
118
|
+
var COMMENT_PATH_PREFIX = "/v2/comments/";
|
|
119
|
+
var OWNER_MESSAGES_PATH = "/v2/messages";
|
|
120
|
+
var REACTIONS_PATH = "/v2/reactions";
|
|
121
|
+
var REACTIONS_TOGGLE_PATH = "/v2/reactions/toggle";
|
|
122
|
+
var READER_ME_PATH = "/v2/reader/me";
|
|
123
|
+
var READER_VERIFY_PATH = "/v2/reader/verify";
|
|
124
|
+
var READER_RESEND_PATH = "/v2/reader/resend";
|
|
125
|
+
var READER_PREFERENCES_PATH = "/v2/reader/preferences";
|
|
126
|
+
var READER_MUTE_PATH = "/v2/reader/mute";
|
|
127
|
+
var READER_AVATAR_PATH_PREFIX = "/v2/reader/avatar/";
|
|
128
|
+
var READER_OAUTH_PATH_PREFIX = "/oauth/reader/";
|
|
129
|
+
var READER_CONFIRM_PATH = "/reader/confirm";
|
|
130
|
+
var READER_MUTE_PAGE_PATH = "/reader/mute";
|
|
55
131
|
var ADMIN_BASE_PATH = "/admin";
|
|
56
132
|
var NOTIFY_BASE_PATH = "/notify";
|
|
57
133
|
var GHOST_WEBHOOK_PATH = "/webhooks/ghost";
|
|
@@ -114,67 +190,102 @@ var TELEGRAM_OPS_BROADCAST_STATUSES = [
|
|
|
114
190
|
"failed"
|
|
115
191
|
];
|
|
116
192
|
export {
|
|
117
|
-
|
|
118
|
-
telegramOpsReminderDeliveredPath,
|
|
119
|
-
telegramOpsEventPath,
|
|
120
|
-
telegramOpsEventActionPath,
|
|
121
|
-
parsePostLocaleTag,
|
|
122
|
-
TELEGRAM_WEBHOOK_PATH,
|
|
123
|
-
TELEGRAM_OPS_WEBHOOK_PATH,
|
|
124
|
-
TELEGRAM_OPS_SUBSCRIBER_STATUSES,
|
|
125
|
-
TELEGRAM_OPS_SUBSCRIBERS_PATH,
|
|
126
|
-
TELEGRAM_OPS_REMINDERS_PATH,
|
|
127
|
-
TELEGRAM_OPS_REMINDERS_DUE_PATH,
|
|
128
|
-
TELEGRAM_OPS_OVERVIEW_PATH,
|
|
129
|
-
TELEGRAM_OPS_EVENTS_PATH,
|
|
130
|
-
TELEGRAM_OPS_BROADCAST_STATUSES,
|
|
131
|
-
TELEGRAM_OPS_BROADCAST_SEND_PATH,
|
|
132
|
-
TELEGRAM_OPS_BROADCAST_PREVIEW_PATH,
|
|
133
|
-
TELEGRAM_OPS_BROADCAST_DELIVERY_MODES,
|
|
134
|
-
TELEGRAM_OPS_BROADCAST_CHANNELS,
|
|
135
|
-
TELEGRAM_OPS_BROADCASTS_PATH,
|
|
136
|
-
TELEGRAM_OPS_BASE_PATH,
|
|
137
|
-
PING_PATH,
|
|
138
|
-
NOTIFY_GATE_STATES,
|
|
139
|
-
NOTIFY_GATE_RELEASE_PATH,
|
|
140
|
-
NOTIFY_GATE_PATH,
|
|
141
|
-
NOTIFY_GATE_DECISIONS,
|
|
142
|
-
NOTIFY_CHANNELS,
|
|
143
|
-
NOTIFY_BASE_PATH,
|
|
144
|
-
NEWSLETTER_ANALYTICS_OPEN_ENDPOINT,
|
|
145
|
-
NEWSLETTER_ANALYTICS_CLICK_ENDPOINT,
|
|
146
|
-
MUSICKIT_TOKEN_PATH,
|
|
147
|
-
MOOD_SENTIMENT_LABELS,
|
|
148
|
-
MOOD_SEARCH_PATH,
|
|
149
|
-
MOOD_PUBLIC_FEED_PATH,
|
|
150
|
-
MOOD_PUBLIC_COMMENTS_PATH,
|
|
151
|
-
MOOD_MEDIA_PROXY_BASE_PATH,
|
|
152
|
-
MOOD_LIVE_META_PATH,
|
|
153
|
-
MOOD_LIVE_FEED_PATH,
|
|
154
|
-
MOOD_LIVE_COUNTS_PATH,
|
|
155
|
-
MOOD_IMAGE_PROXY_BASE_PATH,
|
|
156
|
-
MOOD_ARCHIVE_STATS_PATH,
|
|
157
|
-
MOOD_ARCHIVE_FEED_PATH,
|
|
158
|
-
MOOD_AI_MODELS,
|
|
159
|
-
LISTENING_PATH,
|
|
160
|
-
LISTENING_ANALYTICS_EVENT_ENDPOINT,
|
|
161
|
-
LEGACY_NOTIFY_BASE_PATH,
|
|
162
|
-
LEGACY_MUSICKIT_TOKEN_PATH,
|
|
163
|
-
LEGACY_HEALTH_PATH,
|
|
164
|
-
LEGACY_GHOST_WEBHOOK_PATH,
|
|
165
|
-
LEGACY_ADMIN_BASE_PATH,
|
|
166
|
-
HEALTH_PATH,
|
|
167
|
-
GHOST_WEBHOOK_PATH,
|
|
168
|
-
EVENT_STATUSES,
|
|
169
|
-
CONTENT_DOCUMENT_SOURCES,
|
|
170
|
-
BLOG_ANALYTICS_SUMMARY_ENDPOINT,
|
|
171
|
-
BLOG_ANALYTICS_READ_THRESHOLD_MS,
|
|
172
|
-
BLOG_ANALYTICS_RANGE_OPTIONS,
|
|
173
|
-
BLOG_ANALYTICS_EVENT_ENDPOINT,
|
|
174
|
-
BLOG_ANALYTICS_EVENTS_ENDPOINT,
|
|
175
|
-
BLOG_ANALYTICS_EVENTS_DEFAULT_LIMIT,
|
|
176
|
-
BLOG_ANALYTICS_COMPLETION_SCROLL_DEPTH,
|
|
177
|
-
BLOG_ANALYTICS_ARTICLE_ENDPOINT,
|
|
193
|
+
ADMIN_BASE_PATH,
|
|
178
194
|
API_PREFIX,
|
|
179
|
-
|
|
195
|
+
BLOG_ANALYTICS_ARTICLE_ENDPOINT,
|
|
196
|
+
BLOG_ANALYTICS_COMPLETION_SCROLL_DEPTH,
|
|
197
|
+
BLOG_ANALYTICS_EVENTS_DEFAULT_LIMIT,
|
|
198
|
+
BLOG_ANALYTICS_EVENTS_ENDPOINT,
|
|
199
|
+
BLOG_ANALYTICS_EVENT_ENDPOINT,
|
|
200
|
+
BLOG_ANALYTICS_RANGE_OPTIONS,
|
|
201
|
+
BLOG_ANALYTICS_READ_THRESHOLD_MS,
|
|
202
|
+
BLOG_ANALYTICS_SUMMARY_ENDPOINT,
|
|
203
|
+
COMMENTS_MODES,
|
|
204
|
+
COMMENTS_PATH,
|
|
205
|
+
COMMENT_CREATE_OUTCOMES,
|
|
206
|
+
COMMENT_LOCALES,
|
|
207
|
+
COMMENT_PATH_PREFIX,
|
|
208
|
+
COMMENT_POLICY_TAGS,
|
|
209
|
+
COMMENT_STATUSES,
|
|
210
|
+
CONTENT_DOCUMENT_SOURCES,
|
|
211
|
+
DEFAULT_COMMENT_POLICY,
|
|
212
|
+
DEFAULT_REACTION_EMOJI,
|
|
213
|
+
EVENT_STATUSES,
|
|
214
|
+
GHOST_WEBHOOK_PATH,
|
|
215
|
+
HEALTH_PATH,
|
|
216
|
+
LEGACY_ADMIN_BASE_PATH,
|
|
217
|
+
LEGACY_GHOST_WEBHOOK_PATH,
|
|
218
|
+
LEGACY_HEALTH_PATH,
|
|
219
|
+
LEGACY_MUSICKIT_TOKEN_PATH,
|
|
220
|
+
LEGACY_NOTIFY_BASE_PATH,
|
|
221
|
+
LISTENING_ANALYTICS_EVENT_ENDPOINT,
|
|
222
|
+
LISTENING_PATH,
|
|
223
|
+
MESSAGE_LOCALES,
|
|
224
|
+
MESSAGE_MAX_BODY_LENGTH,
|
|
225
|
+
MESSAGE_MAX_NAME_LENGTH,
|
|
226
|
+
MESSAGE_MIN_BODY_LENGTH,
|
|
227
|
+
MESSAGE_STATES,
|
|
228
|
+
MODERATION_ACTIONS,
|
|
229
|
+
MODERATION_REASONS,
|
|
230
|
+
MOOD_AI_MODELS,
|
|
231
|
+
MOOD_ARCHIVE_FEED_PATH,
|
|
232
|
+
MOOD_ARCHIVE_STATS_PATH,
|
|
233
|
+
MOOD_IMAGE_PROXY_BASE_PATH,
|
|
234
|
+
MOOD_LIVE_COUNTS_PATH,
|
|
235
|
+
MOOD_LIVE_FEED_PATH,
|
|
236
|
+
MOOD_LIVE_META_PATH,
|
|
237
|
+
MOOD_MEDIA_PROXY_BASE_PATH,
|
|
238
|
+
MOOD_PUBLIC_COMMENTS_PATH,
|
|
239
|
+
MOOD_PUBLIC_FEED_PATH,
|
|
240
|
+
MOOD_SEARCH_PATH,
|
|
241
|
+
MOOD_SENTIMENT_LABELS,
|
|
242
|
+
MUSICKIT_TOKEN_PATH,
|
|
243
|
+
NEWSLETTER_ANALYTICS_CLICK_ENDPOINT,
|
|
244
|
+
NEWSLETTER_ANALYTICS_OPEN_ENDPOINT,
|
|
245
|
+
NOTIFY_BASE_PATH,
|
|
246
|
+
NOTIFY_CHANNELS,
|
|
247
|
+
NOTIFY_GATE_DECISIONS,
|
|
248
|
+
NOTIFY_GATE_PATH,
|
|
249
|
+
NOTIFY_GATE_RELEASE_PATH,
|
|
250
|
+
NOTIFY_GATE_STATES,
|
|
251
|
+
OWNER_MESSAGES_PATH,
|
|
252
|
+
PING_PATH,
|
|
253
|
+
REACTIONS_PATH,
|
|
254
|
+
REACTIONS_TOGGLE_PATH,
|
|
255
|
+
REACTION_TARGET_TYPES,
|
|
256
|
+
READER_AVATAR_PATH_PREFIX,
|
|
257
|
+
READER_CONFIRM_PATH,
|
|
258
|
+
READER_GRADES,
|
|
259
|
+
READER_ME_PATH,
|
|
260
|
+
READER_MUTE_OUTCOMES,
|
|
261
|
+
READER_MUTE_PAGE_PATH,
|
|
262
|
+
READER_MUTE_PATH,
|
|
263
|
+
READER_OAUTH_PATH_PREFIX,
|
|
264
|
+
READER_PREFERENCES_PATH,
|
|
265
|
+
READER_PROVIDERS,
|
|
266
|
+
READER_RESEND_PATH,
|
|
267
|
+
READER_VERIFY_OUTCOMES,
|
|
268
|
+
READER_VERIFY_PATH,
|
|
269
|
+
TELEGRAM_OPS_BASE_PATH,
|
|
270
|
+
TELEGRAM_OPS_BROADCASTS_PATH,
|
|
271
|
+
TELEGRAM_OPS_BROADCAST_CHANNELS,
|
|
272
|
+
TELEGRAM_OPS_BROADCAST_DELIVERY_MODES,
|
|
273
|
+
TELEGRAM_OPS_BROADCAST_PREVIEW_PATH,
|
|
274
|
+
TELEGRAM_OPS_BROADCAST_SEND_PATH,
|
|
275
|
+
TELEGRAM_OPS_BROADCAST_STATUSES,
|
|
276
|
+
TELEGRAM_OPS_EVENTS_PATH,
|
|
277
|
+
TELEGRAM_OPS_OVERVIEW_PATH,
|
|
278
|
+
TELEGRAM_OPS_REMINDERS_DUE_PATH,
|
|
279
|
+
TELEGRAM_OPS_REMINDERS_PATH,
|
|
280
|
+
TELEGRAM_OPS_SUBSCRIBERS_PATH,
|
|
281
|
+
TELEGRAM_OPS_SUBSCRIBER_STATUSES,
|
|
282
|
+
TELEGRAM_OPS_WEBHOOK_PATH,
|
|
283
|
+
TELEGRAM_WEBHOOK_PATH,
|
|
284
|
+
acceptsComments,
|
|
285
|
+
commentPolicyFromTags,
|
|
286
|
+
parsePostLocaleTag,
|
|
287
|
+
telegramOpsEventActionPath,
|
|
288
|
+
telegramOpsEventPath,
|
|
289
|
+
telegramOpsReminderDeliveredPath,
|
|
290
|
+
telegramOpsSubscriberPath
|
|
180
291
|
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Owner messages — the standalone form at /message.
|
|
3
|
+
*
|
|
4
|
+
* This is deliberately NOT the comments contract. A message is private by
|
|
5
|
+
* construction, not by moderation state: there is no `published`, no thread,
|
|
6
|
+
* no parent, no reactions, and no public read endpoint anywhere in this file.
|
|
7
|
+
* The comments stack answers "who may see this row"; here the answer is fixed
|
|
8
|
+
* at the storage layer, so a mis-written WHERE clause cannot leak one.
|
|
9
|
+
*
|
|
10
|
+
* What it does share with comments is everything that lives above the table:
|
|
11
|
+
* Turnstile, the honeypot and dwell tripwires, Akismet, the risk heuristics,
|
|
12
|
+
* reader identity, and the ops-bot notification. See site-api's
|
|
13
|
+
* features/messages/server/message-service.ts.
|
|
14
|
+
*/
|
|
15
|
+
export declare const MESSAGE_LOCALES: readonly ['zh', 'en'];
|
|
16
|
+
export type MessageLocale = (typeof MESSAGE_LOCALES)[number];
|
|
17
|
+
/** Body bounds, shared by the form's client-side validation and the service. */
|
|
18
|
+
export declare const MESSAGE_MIN_BODY_LENGTH = 2;
|
|
19
|
+
export declare const MESSAGE_MAX_BODY_LENGTH = 4000;
|
|
20
|
+
export declare const MESSAGE_MAX_NAME_LENGTH = 32;
|
|
21
|
+
/**
|
|
22
|
+
* Triage, not moderation. Every value here is private; none of them makes a
|
|
23
|
+
* message visible to anyone but the owner, which is why there is no
|
|
24
|
+
* `published` member to accidentally set.
|
|
25
|
+
*
|
|
26
|
+
* new — landed, not yet looked at
|
|
27
|
+
* read — the owner has seen it
|
|
28
|
+
* replied — answered from Telegram; `repliedAt` is set
|
|
29
|
+
* archived — filed away by the owner
|
|
30
|
+
* spam — the risk stack or Akismet called it; never notified loudly
|
|
31
|
+
*/
|
|
32
|
+
export declare const MESSAGE_STATES: readonly ['new', 'read', 'replied', 'archived', 'spam'];
|
|
33
|
+
export type MessageState = (typeof MESSAGE_STATES)[number];
|
|
34
|
+
export interface OwnerMessageCreateInput {
|
|
35
|
+
/** Plain text, MESSAGE_MIN_BODY_LENGTH–MESSAGE_MAX_BODY_LENGTH characters. */
|
|
36
|
+
body: string;
|
|
37
|
+
displayName: string;
|
|
38
|
+
/**
|
|
39
|
+
* Optional, and optional on purpose — the same "channeling beats blocking"
|
|
40
|
+
* call the comment box makes. Supplied, it must be valid: it triggers the
|
|
41
|
+
* shared lazy-verification mail, and only a verified address can ever
|
|
42
|
+
* receive the owner's reply.
|
|
43
|
+
*/
|
|
44
|
+
email?: string;
|
|
45
|
+
turnstileToken: string;
|
|
46
|
+
/** Minted by the form on load; proves the submit was not instant. */
|
|
47
|
+
dwellToken: string;
|
|
48
|
+
/** Honeypot. Any non-empty value is a silent drop. */
|
|
49
|
+
website?: string;
|
|
50
|
+
locale?: MessageLocale;
|
|
51
|
+
}
|
|
52
|
+
export interface OwnerMessageCreateResult {
|
|
53
|
+
id: string;
|
|
54
|
+
createdAt: string;
|
|
55
|
+
/**
|
|
56
|
+
* Whether the owner's reply can actually reach the sender. True only when
|
|
57
|
+
* the address belongs to a verified reader — an address typed into a public
|
|
58
|
+
* form is not proof that its owner sent anything, so an unverified one is
|
|
59
|
+
* never mailed. False also when no address was given at all; the form uses
|
|
60
|
+
* this to decide whether the receipt promises a reply or not.
|
|
61
|
+
*/
|
|
62
|
+
replyable: boolean;
|
|
63
|
+
/**
|
|
64
|
+
* A verification mail just went out to a not-yet-verified address. The
|
|
65
|
+
* receipt tells the sender to click it if they want an answer.
|
|
66
|
+
*/
|
|
67
|
+
verificationSent: boolean;
|
|
68
|
+
}
|
package/dist/messages.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// src/messages.ts
|
|
2
|
+
var MESSAGE_LOCALES = ["zh", "en"];
|
|
3
|
+
var MESSAGE_MIN_BODY_LENGTH = 2;
|
|
4
|
+
var MESSAGE_MAX_BODY_LENGTH = 4000;
|
|
5
|
+
var MESSAGE_MAX_NAME_LENGTH = 32;
|
|
6
|
+
var MESSAGE_STATES = ["new", "read", "replied", "archived", "spam"];
|
|
7
|
+
export {
|
|
8
|
+
MESSAGE_LOCALES,
|
|
9
|
+
MESSAGE_MAX_BODY_LENGTH,
|
|
10
|
+
MESSAGE_MAX_NAME_LENGTH,
|
|
11
|
+
MESSAGE_MIN_BODY_LENGTH,
|
|
12
|
+
MESSAGE_STATES
|
|
13
|
+
};
|
package/dist/mood.js
CHANGED
package/dist/routes.d.ts
CHANGED
|
@@ -12,6 +12,20 @@ export declare const MOOD_SEARCH_PATH: '/v2/mood/search';
|
|
|
12
12
|
export declare const MOOD_IMAGE_PROXY_BASE_PATH: '/v2/images';
|
|
13
13
|
export declare const MOOD_MEDIA_PROXY_BASE_PATH: '/v2/media';
|
|
14
14
|
export declare const LISTENING_PATH: '/v2/listening';
|
|
15
|
+
export declare const COMMENTS_PATH: '/v2/comments';
|
|
16
|
+
export declare const COMMENT_PATH_PREFIX: '/v2/comments/';
|
|
17
|
+
export declare const OWNER_MESSAGES_PATH: '/v2/messages';
|
|
18
|
+
export declare const REACTIONS_PATH: '/v2/reactions';
|
|
19
|
+
export declare const REACTIONS_TOGGLE_PATH: '/v2/reactions/toggle';
|
|
20
|
+
export declare const READER_ME_PATH: '/v2/reader/me';
|
|
21
|
+
export declare const READER_VERIFY_PATH: '/v2/reader/verify';
|
|
22
|
+
export declare const READER_RESEND_PATH: '/v2/reader/resend';
|
|
23
|
+
export declare const READER_PREFERENCES_PATH: '/v2/reader/preferences';
|
|
24
|
+
export declare const READER_MUTE_PATH: '/v2/reader/mute';
|
|
25
|
+
export declare const READER_AVATAR_PATH_PREFIX: '/v2/reader/avatar/';
|
|
26
|
+
export declare const READER_OAUTH_PATH_PREFIX: '/oauth/reader/';
|
|
27
|
+
export declare const READER_CONFIRM_PATH: '/reader/confirm';
|
|
28
|
+
export declare const READER_MUTE_PAGE_PATH: '/reader/mute';
|
|
15
29
|
export declare const ADMIN_BASE_PATH: '/admin';
|
|
16
30
|
export declare const NOTIFY_BASE_PATH: '/notify';
|
|
17
31
|
export declare const GHOST_WEBHOOK_PATH: '/webhooks/ghost';
|
package/dist/routes.js
CHANGED
|
@@ -13,6 +13,20 @@ var MOOD_SEARCH_PATH = "/v2/mood/search";
|
|
|
13
13
|
var MOOD_IMAGE_PROXY_BASE_PATH = "/v2/images";
|
|
14
14
|
var MOOD_MEDIA_PROXY_BASE_PATH = "/v2/media";
|
|
15
15
|
var LISTENING_PATH = "/v2/listening";
|
|
16
|
+
var COMMENTS_PATH = "/v2/comments";
|
|
17
|
+
var COMMENT_PATH_PREFIX = "/v2/comments/";
|
|
18
|
+
var OWNER_MESSAGES_PATH = "/v2/messages";
|
|
19
|
+
var REACTIONS_PATH = "/v2/reactions";
|
|
20
|
+
var REACTIONS_TOGGLE_PATH = "/v2/reactions/toggle";
|
|
21
|
+
var READER_ME_PATH = "/v2/reader/me";
|
|
22
|
+
var READER_VERIFY_PATH = "/v2/reader/verify";
|
|
23
|
+
var READER_RESEND_PATH = "/v2/reader/resend";
|
|
24
|
+
var READER_PREFERENCES_PATH = "/v2/reader/preferences";
|
|
25
|
+
var READER_MUTE_PATH = "/v2/reader/mute";
|
|
26
|
+
var READER_AVATAR_PATH_PREFIX = "/v2/reader/avatar/";
|
|
27
|
+
var READER_OAUTH_PATH_PREFIX = "/oauth/reader/";
|
|
28
|
+
var READER_CONFIRM_PATH = "/reader/confirm";
|
|
29
|
+
var READER_MUTE_PAGE_PATH = "/reader/mute";
|
|
16
30
|
var ADMIN_BASE_PATH = "/admin";
|
|
17
31
|
var NOTIFY_BASE_PATH = "/notify";
|
|
18
32
|
var GHOST_WEBHOOK_PATH = "/webhooks/ghost";
|
|
@@ -24,28 +38,42 @@ var LEGACY_GHOST_WEBHOOK_PATH = "/v2/ghost/webhook";
|
|
|
24
38
|
var LEGACY_MUSICKIT_TOKEN_PATH = "/v2/musickit/token";
|
|
25
39
|
var LEGACY_HEALTH_PATH = "/v2/health";
|
|
26
40
|
export {
|
|
27
|
-
|
|
28
|
-
PING_PATH,
|
|
29
|
-
NOTIFY_BASE_PATH,
|
|
30
|
-
MUSICKIT_TOKEN_PATH,
|
|
31
|
-
MOOD_SEARCH_PATH,
|
|
32
|
-
MOOD_PUBLIC_FEED_PATH,
|
|
33
|
-
MOOD_PUBLIC_COMMENTS_PATH,
|
|
34
|
-
MOOD_MEDIA_PROXY_BASE_PATH,
|
|
35
|
-
MOOD_LIVE_META_PATH,
|
|
36
|
-
MOOD_LIVE_FEED_PATH,
|
|
37
|
-
MOOD_LIVE_COUNTS_PATH,
|
|
38
|
-
MOOD_IMAGE_PROXY_BASE_PATH,
|
|
39
|
-
MOOD_ARCHIVE_STATS_PATH,
|
|
40
|
-
MOOD_ARCHIVE_FEED_PATH,
|
|
41
|
-
LISTENING_PATH,
|
|
42
|
-
LEGACY_NOTIFY_BASE_PATH,
|
|
43
|
-
LEGACY_MUSICKIT_TOKEN_PATH,
|
|
44
|
-
LEGACY_HEALTH_PATH,
|
|
45
|
-
LEGACY_GHOST_WEBHOOK_PATH,
|
|
46
|
-
LEGACY_ADMIN_BASE_PATH,
|
|
47
|
-
HEALTH_PATH,
|
|
48
|
-
GHOST_WEBHOOK_PATH,
|
|
41
|
+
ADMIN_BASE_PATH,
|
|
49
42
|
API_PREFIX,
|
|
50
|
-
|
|
43
|
+
COMMENTS_PATH,
|
|
44
|
+
COMMENT_PATH_PREFIX,
|
|
45
|
+
GHOST_WEBHOOK_PATH,
|
|
46
|
+
HEALTH_PATH,
|
|
47
|
+
LEGACY_ADMIN_BASE_PATH,
|
|
48
|
+
LEGACY_GHOST_WEBHOOK_PATH,
|
|
49
|
+
LEGACY_HEALTH_PATH,
|
|
50
|
+
LEGACY_MUSICKIT_TOKEN_PATH,
|
|
51
|
+
LEGACY_NOTIFY_BASE_PATH,
|
|
52
|
+
LISTENING_PATH,
|
|
53
|
+
MOOD_ARCHIVE_FEED_PATH,
|
|
54
|
+
MOOD_ARCHIVE_STATS_PATH,
|
|
55
|
+
MOOD_IMAGE_PROXY_BASE_PATH,
|
|
56
|
+
MOOD_LIVE_COUNTS_PATH,
|
|
57
|
+
MOOD_LIVE_FEED_PATH,
|
|
58
|
+
MOOD_LIVE_META_PATH,
|
|
59
|
+
MOOD_MEDIA_PROXY_BASE_PATH,
|
|
60
|
+
MOOD_PUBLIC_COMMENTS_PATH,
|
|
61
|
+
MOOD_PUBLIC_FEED_PATH,
|
|
62
|
+
MOOD_SEARCH_PATH,
|
|
63
|
+
MUSICKIT_TOKEN_PATH,
|
|
64
|
+
NOTIFY_BASE_PATH,
|
|
65
|
+
OWNER_MESSAGES_PATH,
|
|
66
|
+
PING_PATH,
|
|
67
|
+
REACTIONS_PATH,
|
|
68
|
+
REACTIONS_TOGGLE_PATH,
|
|
69
|
+
READER_AVATAR_PATH_PREFIX,
|
|
70
|
+
READER_CONFIRM_PATH,
|
|
71
|
+
READER_ME_PATH,
|
|
72
|
+
READER_MUTE_PAGE_PATH,
|
|
73
|
+
READER_MUTE_PATH,
|
|
74
|
+
READER_OAUTH_PATH_PREFIX,
|
|
75
|
+
READER_PREFERENCES_PATH,
|
|
76
|
+
READER_RESEND_PATH,
|
|
77
|
+
READER_VERIFY_PATH,
|
|
78
|
+
TELEGRAM_WEBHOOK_PATH
|
|
51
79
|
};
|
package/dist/telegram-ops.js
CHANGED
|
@@ -49,27 +49,27 @@ var TELEGRAM_OPS_BROADCAST_STATUSES = [
|
|
|
49
49
|
"failed"
|
|
50
50
|
];
|
|
51
51
|
export {
|
|
52
|
-
|
|
53
|
-
telegramOpsReminderDeliveredPath,
|
|
54
|
-
telegramOpsEventPath,
|
|
55
|
-
telegramOpsEventActionPath,
|
|
56
|
-
TELEGRAM_OPS_WEBHOOK_PATH,
|
|
57
|
-
TELEGRAM_OPS_SUBSCRIBER_STATUSES,
|
|
58
|
-
TELEGRAM_OPS_SUBSCRIBERS_PATH,
|
|
59
|
-
TELEGRAM_OPS_REMINDERS_PATH,
|
|
60
|
-
TELEGRAM_OPS_REMINDERS_DUE_PATH,
|
|
61
|
-
TELEGRAM_OPS_OVERVIEW_PATH,
|
|
62
|
-
TELEGRAM_OPS_EVENTS_PATH,
|
|
63
|
-
TELEGRAM_OPS_BROADCAST_STATUSES,
|
|
64
|
-
TELEGRAM_OPS_BROADCAST_SEND_PATH,
|
|
65
|
-
TELEGRAM_OPS_BROADCAST_PREVIEW_PATH,
|
|
66
|
-
TELEGRAM_OPS_BROADCAST_DELIVERY_MODES,
|
|
67
|
-
TELEGRAM_OPS_BROADCAST_CHANNELS,
|
|
68
|
-
TELEGRAM_OPS_BROADCASTS_PATH,
|
|
69
|
-
TELEGRAM_OPS_BASE_PATH,
|
|
70
|
-
NOTIFY_GATE_STATES,
|
|
71
|
-
NOTIFY_GATE_RELEASE_PATH,
|
|
72
|
-
NOTIFY_GATE_PATH,
|
|
52
|
+
EVENT_STATUSES,
|
|
73
53
|
NOTIFY_GATE_DECISIONS,
|
|
74
|
-
|
|
54
|
+
NOTIFY_GATE_PATH,
|
|
55
|
+
NOTIFY_GATE_RELEASE_PATH,
|
|
56
|
+
NOTIFY_GATE_STATES,
|
|
57
|
+
TELEGRAM_OPS_BASE_PATH,
|
|
58
|
+
TELEGRAM_OPS_BROADCASTS_PATH,
|
|
59
|
+
TELEGRAM_OPS_BROADCAST_CHANNELS,
|
|
60
|
+
TELEGRAM_OPS_BROADCAST_DELIVERY_MODES,
|
|
61
|
+
TELEGRAM_OPS_BROADCAST_PREVIEW_PATH,
|
|
62
|
+
TELEGRAM_OPS_BROADCAST_SEND_PATH,
|
|
63
|
+
TELEGRAM_OPS_BROADCAST_STATUSES,
|
|
64
|
+
TELEGRAM_OPS_EVENTS_PATH,
|
|
65
|
+
TELEGRAM_OPS_OVERVIEW_PATH,
|
|
66
|
+
TELEGRAM_OPS_REMINDERS_DUE_PATH,
|
|
67
|
+
TELEGRAM_OPS_REMINDERS_PATH,
|
|
68
|
+
TELEGRAM_OPS_SUBSCRIBERS_PATH,
|
|
69
|
+
TELEGRAM_OPS_SUBSCRIBER_STATUSES,
|
|
70
|
+
TELEGRAM_OPS_WEBHOOK_PATH,
|
|
71
|
+
telegramOpsEventActionPath,
|
|
72
|
+
telegramOpsEventPath,
|
|
73
|
+
telegramOpsReminderDeliveredPath,
|
|
74
|
+
telegramOpsSubscriberPath
|
|
75
75
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bunizao/contracts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Shared TypeScript contracts for buxx.me services and clients.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -38,6 +38,11 @@
|
|
|
38
38
|
"import": "./dist/admin.js",
|
|
39
39
|
"default": "./dist/admin.js"
|
|
40
40
|
},
|
|
41
|
+
"./comments": {
|
|
42
|
+
"types": "./dist/comments.d.ts",
|
|
43
|
+
"import": "./dist/comments.js",
|
|
44
|
+
"default": "./dist/comments.js"
|
|
45
|
+
},
|
|
41
46
|
"./content": {
|
|
42
47
|
"types": "./dist/content.d.ts",
|
|
43
48
|
"import": "./dist/content.js",
|
|
@@ -48,6 +53,11 @@
|
|
|
48
53
|
"import": "./dist/listening.js",
|
|
49
54
|
"default": "./dist/listening.js"
|
|
50
55
|
},
|
|
56
|
+
"./messages": {
|
|
57
|
+
"types": "./dist/messages.d.ts",
|
|
58
|
+
"import": "./dist/messages.js",
|
|
59
|
+
"default": "./dist/messages.js"
|
|
60
|
+
},
|
|
51
61
|
"./mood": {
|
|
52
62
|
"types": "./dist/mood.d.ts",
|
|
53
63
|
"import": "./dist/mood.js",
|