@symbiosis-lab/moss-plugin-matters 1.4.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.
Files changed (75) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/README.md +18 -0
  3. package/assets/icon.svg +1 -0
  4. package/assets/manifest.json +36 -0
  5. package/codegen.ts +26 -0
  6. package/e2e/moss-cli.test.ts +338 -0
  7. package/features/api/fetch-articles.feature +39 -0
  8. package/features/auth/wallet-auth.feature +27 -0
  9. package/features/download/retry-logic.feature +36 -0
  10. package/features/download/self-correcting.feature +83 -0
  11. package/features/download/worker-pool.feature +29 -0
  12. package/features/social/fetch-social-data.feature +40 -0
  13. package/features/steps/api.steps.ts +180 -0
  14. package/features/steps/download.steps.ts +423 -0
  15. package/features/steps/incremental-sync.steps.ts +105 -0
  16. package/features/steps/self-correcting.steps.ts +575 -0
  17. package/features/steps/social.steps.ts +257 -0
  18. package/features/steps/syndication.steps.ts +264 -0
  19. package/features/steps/wallet-auth.steps.ts +185 -0
  20. package/features/sync/article-sync.feature +49 -0
  21. package/features/sync/homepage-grid.feature +43 -0
  22. package/features/sync/incremental-sync.feature +28 -0
  23. package/features/syndication/create-draft.feature +35 -0
  24. package/package.json +58 -0
  25. package/src/__generated__/schema.graphql +4289 -0
  26. package/src/__generated__/types.ts +5355 -0
  27. package/src/__tests__/api.test.ts +678 -0
  28. package/src/__tests__/auth-route.test.ts +38 -0
  29. package/src/__tests__/auth-routing.test.ts +462 -0
  30. package/src/__tests__/auto-detect.test.ts +412 -0
  31. package/src/__tests__/binding-guard.test.ts +256 -0
  32. package/src/__tests__/config.test.ts +212 -0
  33. package/src/__tests__/converter.test.ts +289 -0
  34. package/src/__tests__/credential.test.ts +332 -0
  35. package/src/__tests__/domain.test.ts +341 -0
  36. package/src/__tests__/downloader.test.ts +679 -0
  37. package/src/__tests__/folder-detection.test.ts +289 -0
  38. package/src/__tests__/force-fresh-login.test.ts +236 -0
  39. package/src/__tests__/main.test.ts +2437 -0
  40. package/src/__tests__/progress.test.ts +93 -0
  41. package/src/__tests__/session.test.ts +375 -0
  42. package/src/__tests__/social-integration.test.ts +386 -0
  43. package/src/__tests__/social-sync-logic.test.ts +107 -0
  44. package/src/__tests__/social.test.ts +788 -0
  45. package/src/__tests__/sync.test.ts +1273 -0
  46. package/src/__tests__/syndication-toast-law.test.ts +649 -0
  47. package/src/__tests__/syndication.test.ts +125 -0
  48. package/src/__tests__/test-profile-escape.test.ts +209 -0
  49. package/src/__tests__/url-detect.test.ts +79 -0
  50. package/src/__tests__/utils.test.ts +226 -0
  51. package/src/api.ts +1366 -0
  52. package/src/auth-route.ts +38 -0
  53. package/src/config.ts +80 -0
  54. package/src/converter.ts +305 -0
  55. package/src/credential.ts +329 -0
  56. package/src/domain.ts +183 -0
  57. package/src/downloader.ts +761 -0
  58. package/src/main.ts +2092 -0
  59. package/src/progress.ts +89 -0
  60. package/src/queries/user.graphql +85 -0
  61. package/src/queries/viewer.graphql +104 -0
  62. package/src/social.ts +413 -0
  63. package/src/sync.ts +818 -0
  64. package/src/types.ts +477 -0
  65. package/src/url-detect.ts +49 -0
  66. package/src/utils.ts +305 -0
  67. package/test-fixtures/syndication-test-site/input/index.md +8 -0
  68. package/test-fixtures/syndication-test-site/input/posts/rich-test-article.md +90 -0
  69. package/test-helpers/TEST_ACCOUNT.md +151 -0
  70. package/test-helpers/api-client.ts +252 -0
  71. package/test-helpers/fixtures/articles.ts +147 -0
  72. package/test-helpers/wallet-auth.ts +305 -0
  73. package/test-setup/e2e.ts +93 -0
  74. package/tsconfig.json +23 -0
  75. package/vitest.config.ts +39 -0
package/src/types.ts ADDED
@@ -0,0 +1,477 @@
1
+ /**
2
+ * Plugin-specific type definitions for the Matters Syndicator Plugin
3
+ *
4
+ * Common types (ProcessContext, SyndicateContext, HookResult, etc.) are
5
+ * imported from moss-plugin-sdk.
6
+ */
7
+
8
+ // Re-export SDK types for convenience
9
+ export type {
10
+ ProcessContext,
11
+ SyndicateContext,
12
+ HookResult,
13
+ DeploymentInfo,
14
+ ProjectInfo,
15
+ ArticleInfo,
16
+ PluginMessage,
17
+ } from "@symbiosis-lab/moss-api";
18
+
19
+ // ============================================================================
20
+ // Matters API Types
21
+ // ============================================================================
22
+
23
+ export interface PageInfo {
24
+ endCursor: string;
25
+ hasNextPage: boolean;
26
+ }
27
+
28
+ export interface MattersTag {
29
+ id: string;
30
+ content: string;
31
+ }
32
+
33
+ export interface MattersArticle {
34
+ id: string;
35
+ title: string;
36
+ slug: string;
37
+ shortHash: string;
38
+ content: string; // HTML content
39
+ summary: string;
40
+ createdAt: string;
41
+ revisedAt?: string;
42
+ tags: MattersTag[];
43
+ cover?: string;
44
+ language?: string; // e.g. "zh_hans" / "zh_hant" / "en" — public per-article language (G)
45
+ }
46
+
47
+ export interface MattersDraft {
48
+ id: string;
49
+ title: string;
50
+ content: string; // HTML content
51
+ summary?: string;
52
+ createdAt: string;
53
+ updatedAt?: string;
54
+ tags?: string[];
55
+ cover?: string;
56
+ }
57
+
58
+ export interface MattersCollectionArticle {
59
+ id: string;
60
+ shortHash: string;
61
+ title: string;
62
+ slug: string;
63
+ }
64
+
65
+ export interface MattersCollection {
66
+ id: string;
67
+ title: string;
68
+ description?: string;
69
+ cover?: string;
70
+ articles: MattersCollectionArticle[];
71
+ }
72
+
73
+ export interface MattersPinnedWork {
74
+ id: string;
75
+ type: "article" | "collection";
76
+ title: string;
77
+ slug?: string; // articles only
78
+ shortHash?: string; // articles only
79
+ cover?: string;
80
+ }
81
+
82
+ export interface MattersUserProfile {
83
+ userName: string;
84
+ displayName: string;
85
+ description?: string;
86
+ avatar?: string;
87
+ profileCover?: string;
88
+ language?: string; // e.g., "zh_hans", "zh_hant", "en"
89
+ pinnedWorks?: MattersPinnedWork[]; // optional for backwards compat
90
+ }
91
+
92
+ // ============================================================================
93
+ // Internal Types
94
+ // ============================================================================
95
+
96
+ export interface SyncResult {
97
+ created: number;
98
+ updated: number;
99
+ skipped: number;
100
+ errors: string[];
101
+ }
102
+
103
+ /**
104
+ * Extended sync result that includes the article path map for link rewriting.
105
+ * The articlePathMap maps Matters URLs and shortHashes to local file paths.
106
+ */
107
+ export interface SyncResultWithMap {
108
+ result: SyncResult;
109
+ articlePathMap: Map<string, string>;
110
+ }
111
+
112
+ export interface MediaDownloadResult {
113
+ filesProcessed: number;
114
+ imagesDownloaded: number;
115
+ imagesSkipped: number;
116
+ errors: string[];
117
+ }
118
+
119
+ export interface DownloadAndRewriteResult {
120
+ content: string;
121
+ downloadedCount: number;
122
+ errors: string[];
123
+ }
124
+
125
+ export interface ExtractedMedia {
126
+ url: string;
127
+ localFilename: string;
128
+ }
129
+
130
+ export interface ParsedFrontmatter {
131
+ frontmatter: Record<string, unknown>;
132
+ body: string;
133
+ }
134
+
135
+ // ============================================================================
136
+ // GraphQL Response Types
137
+ // ============================================================================
138
+
139
+ export interface ViewerArticlesResponse {
140
+ viewer: {
141
+ id: string;
142
+ userName: string;
143
+ articles: {
144
+ totalCount: number;
145
+ pageInfo: PageInfo;
146
+ edges: Array<{
147
+ node: MattersArticle;
148
+ }>;
149
+ };
150
+ };
151
+ }
152
+
153
+ export interface ViewerDraftsResponse {
154
+ viewer: {
155
+ id: string;
156
+ drafts: {
157
+ pageInfo: PageInfo;
158
+ edges: Array<{
159
+ node: MattersDraft;
160
+ }>;
161
+ };
162
+ };
163
+ }
164
+
165
+ export interface ViewerCollectionsResponse {
166
+ viewer: {
167
+ id: string;
168
+ collections: {
169
+ totalCount: number;
170
+ pageInfo: PageInfo;
171
+ edges: Array<{
172
+ node: {
173
+ id: string;
174
+ title: string;
175
+ description?: string;
176
+ cover?: string;
177
+ articles: {
178
+ edges: Array<{
179
+ node: MattersCollectionArticle;
180
+ }>;
181
+ };
182
+ };
183
+ }>;
184
+ };
185
+ };
186
+ }
187
+
188
+ export interface ViewerProfileResponse {
189
+ viewer: {
190
+ id: string;
191
+ userName: string;
192
+ displayName: string;
193
+ info: {
194
+ description?: string;
195
+ profileCover?: string;
196
+ };
197
+ avatar?: string;
198
+ settings: {
199
+ language?: string;
200
+ };
201
+ pinnedWorks?: Array<{
202
+ id: string;
203
+ pinned: boolean;
204
+ title: string;
205
+ cover?: string;
206
+ __typename?: string;
207
+ slug?: string;
208
+ shortHash?: string;
209
+ }>;
210
+ };
211
+ }
212
+
213
+ // ============================================================================
214
+ // Frontmatter Data Types
215
+ // ============================================================================
216
+
217
+ export interface FrontmatterData {
218
+ title: string;
219
+ /** Marks this file as its folder's home page (moss `home: true` marker). */
220
+ home?: boolean;
221
+ date?: string;
222
+ updated?: string;
223
+ tags?: string[];
224
+ cover?: string;
225
+ syndicated?: string[];
226
+ description?: string;
227
+ collections?: Record<string, number> | string[];
228
+ order?: string[]; // For ordered folders: list of article filenames
229
+ }
230
+
231
+ // ============================================================================
232
+ // Social Data Types (for .moss/data/social/matters.json)
233
+ // ============================================================================
234
+
235
+ /**
236
+ * User information for social interactions
237
+ */
238
+ export interface SocialUser {
239
+ id: string;
240
+ userName: string;
241
+ displayName: string;
242
+ avatar?: string;
243
+ }
244
+
245
+ /**
246
+ * Comment on an article
247
+ */
248
+ export interface MattersComment {
249
+ id: string;
250
+ content: string;
251
+ createdAt: string;
252
+ state: "active" | "archived" | "banned" | "collapsed";
253
+ upvotes: number;
254
+ author: SocialUser;
255
+ replyToId?: string;
256
+ replyToAuthor?: string;
257
+ }
258
+
259
+ /**
260
+ * Donation to an article
261
+ */
262
+ export interface MattersDonation {
263
+ id: string;
264
+ sender: SocialUser;
265
+ }
266
+
267
+ /**
268
+ * Appreciation (claps) for an article
269
+ */
270
+ export interface MattersAppreciation {
271
+ amount: number;
272
+ createdAt: string;
273
+ sender: SocialUser;
274
+ }
275
+
276
+ /**
277
+ * Social data for a single article
278
+ */
279
+ export interface ArticleSocialData {
280
+ comments: MattersComment[];
281
+ donations: MattersDonation[];
282
+ appreciations: MattersAppreciation[];
283
+ /**
284
+ * commentCount as reported by Matters at the last successful sync.
285
+ * Used to skip per-article fetches when the remote count hasn't changed.
286
+ * Undefined for entries written before this field existed (treated as
287
+ * "unknown" — sync will fetch as before, then populate this).
288
+ */
289
+ lastKnownCommentCount?: number;
290
+ }
291
+
292
+ /**
293
+ * Complete social data stored in .moss/data/social/matters.json
294
+ *
295
+ * Schema:
296
+ * - schemaVersion: Version string for future migrations (currently "1.0.0")
297
+ * - updatedAt: ISO timestamp of last update
298
+ * - articles: Map of source .md path (project-relative) to social data
299
+ *
300
+ * Merge strategy: Upsert by ID (add new, update existing, never delete)
301
+ */
302
+ export interface MattersSocialData {
303
+ /** Schema version for forward compatibility */
304
+ schemaVersion: string;
305
+ /** ISO timestamp of last update */
306
+ updatedAt: string;
307
+ /** Social data keyed by source .md path (project-relative) */
308
+ articles: Record<string, ArticleSocialData>;
309
+ }
310
+
311
+ // ============================================================================
312
+ // Lightweight Comment-Count Discovery Types
313
+ // ============================================================================
314
+
315
+ /**
316
+ * Minimal article node for the "which articles have new comments?" query.
317
+ * Just enough to join against local social data and compare counts.
318
+ */
319
+ export interface ArticleCommentCount {
320
+ shortHash: string;
321
+ commentCount: number;
322
+ }
323
+
324
+ export interface ViewerArticleCommentCountsResponse {
325
+ viewer: {
326
+ id: string;
327
+ articles: {
328
+ pageInfo: PageInfo;
329
+ edges: Array<{ node: ArticleCommentCount }>;
330
+ };
331
+ };
332
+ }
333
+
334
+ export interface UserArticleCommentCountsResponse {
335
+ user: {
336
+ id: string;
337
+ articles: {
338
+ pageInfo: PageInfo;
339
+ edges: Array<{ node: ArticleCommentCount }>;
340
+ } | null;
341
+ } | null;
342
+ }
343
+
344
+ // ============================================================================
345
+ // Social Data GraphQL Response Types
346
+ // ============================================================================
347
+
348
+ export interface ArticleCommentsResponse {
349
+ article: {
350
+ id: string;
351
+ shortHash: string;
352
+ comments: {
353
+ totalCount: number;
354
+ pageInfo: PageInfo;
355
+ edges: Array<{
356
+ node: {
357
+ id: string;
358
+ content: string;
359
+ createdAt: string;
360
+ state: string;
361
+ upvotes: number;
362
+ author: {
363
+ id: string;
364
+ userName: string;
365
+ displayName: string;
366
+ avatar?: string;
367
+ };
368
+ replyTo?: {
369
+ id: string;
370
+ author: {
371
+ userName: string;
372
+ };
373
+ };
374
+ };
375
+ }>;
376
+ };
377
+ };
378
+ }
379
+
380
+ export interface ArticleDonationsResponse {
381
+ article: {
382
+ id: string;
383
+ shortHash: string;
384
+ donations: {
385
+ totalCount: number;
386
+ pageInfo: PageInfo;
387
+ edges: Array<{
388
+ node: {
389
+ id: string;
390
+ sender: {
391
+ id: string;
392
+ userName: string;
393
+ displayName: string;
394
+ avatar?: string;
395
+ };
396
+ };
397
+ }>;
398
+ };
399
+ };
400
+ }
401
+
402
+ export interface ArticleAppreciationsResponse {
403
+ article: {
404
+ id: string;
405
+ shortHash: string;
406
+ appreciationsReceived: {
407
+ totalCount: number;
408
+ pageInfo: PageInfo;
409
+ edges: Array<{
410
+ node: {
411
+ amount: number;
412
+ createdAt: string;
413
+ sender: {
414
+ id: string;
415
+ userName: string;
416
+ displayName: string;
417
+ avatar?: string;
418
+ };
419
+ };
420
+ }>;
421
+ };
422
+ };
423
+ }
424
+
425
+ // ============================================================================
426
+ // Draft/Syndication Types
427
+ // ============================================================================
428
+
429
+ export interface MattersDraftWithArticle extends MattersDraft {
430
+ /** Present when draft has been published */
431
+ article?: {
432
+ id: string;
433
+ shortHash: string;
434
+ slug: string;
435
+ };
436
+ publishState: "unpublished" | "pending" | "published";
437
+ }
438
+
439
+ export interface PutDraftInput {
440
+ id?: string;
441
+ title?: string;
442
+ content?: string;
443
+ summary?: string;
444
+ tags?: string[];
445
+ cover?: string;
446
+ collections?: string[];
447
+ }
448
+
449
+ export interface PutDraftResponse {
450
+ putDraft: MattersDraftWithArticle;
451
+ }
452
+
453
+ export interface PublishArticleResponse {
454
+ publishArticle: {
455
+ id: string;
456
+ article: {
457
+ id: string;
458
+ shortHash: string;
459
+ slug: string;
460
+ };
461
+ };
462
+ }
463
+
464
+ export interface PutCollectionInput {
465
+ id?: string;
466
+ title?: string;
467
+ cover?: string;
468
+ description?: string;
469
+ pinned?: boolean;
470
+ }
471
+
472
+ export interface PutCollectionResponse {
473
+ putCollection: {
474
+ id: string;
475
+ title: string;
476
+ };
477
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Pure URL classifiers for the Matters browser-panel.
3
+ *
4
+ * The published-article URL is only a TRIGGER; the Matters API (draft.article)
5
+ * remains the source of truth for confirming a publish. These functions never
6
+ * confirm a publish on their own — they are hints to fire an immediate API
7
+ * verify instead of waiting for the next 5s poll cycle.
8
+ */
9
+
10
+ /**
11
+ * Extract the pathname from a URL string. Returns "" on parse failure.
12
+ */
13
+ function pathOf(url: string): string {
14
+ try {
15
+ return new URL(url).pathname;
16
+ } catch {
17
+ return "";
18
+ }
19
+ }
20
+
21
+ /**
22
+ * Returns true when the URL points to the Matters draft editor:
23
+ * `…/me/drafts/<id>`.
24
+ */
25
+ export function isDraftUrl(url: string): boolean {
26
+ return /^\/me\/drafts\/[^/]+/.test(pathOf(url));
27
+ }
28
+
29
+ /**
30
+ * Returns true when the URL looks like a published Matters article:
31
+ * `/@<user>/<slug>-<shortHash>`.
32
+ *
33
+ * The hash suffix requirement (`-[a-z0-9]{6,}` at the end of the slug) rejects
34
+ * profile sub-pages like `/@user/followers`, `/@user/settings`,
35
+ * `/@user/bookmarks`, and bare `/@user`.
36
+ *
37
+ * Accepted false-trigger: Matters sub-pages whose final path segment ends in
38
+ * `-<6+ alnum>` (e.g. `/@u/tags-abcdef`) will pass this check. This is
39
+ * harmless — the API `draft.article` verify rejects them as unrelated content.
40
+ *
41
+ * This is a HINT only — always verify publication via the API before acting on it.
42
+ */
43
+ export function looksLikePublishedArticleUrl(url: string): boolean {
44
+ const path = pathOf(url);
45
+ // Must be /@user/slug-<shortHash>
46
+ // - segment under @ handle is required (rejects bare /@user)
47
+ // - trailing segment must end with -[a-z0-9]{6,} (the Matters hash suffix)
48
+ return /^\/@[^/]+\/[^/]+-[a-z0-9]{6,}$/.test(path);
49
+ }