@pkcprotocol/pkc-js 0.0.35 → 0.0.37

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 (162) hide show
  1. package/README.md +18 -0
  2. package/dist/browser/community/remote-community.d.ts +3 -3
  3. package/dist/browser/community/remote-community.js.map +1 -1
  4. package/dist/browser/community/schema.d.ts +24 -0
  5. package/dist/browser/community/schema.js +14 -1
  6. package/dist/browser/community/schema.js.map +1 -1
  7. package/dist/browser/errors.d.ts +5 -1
  8. package/dist/browser/errors.js +4 -0
  9. package/dist/browser/errors.js.map +1 -1
  10. package/dist/browser/generated-version.d.ts +1 -1
  11. package/dist/browser/generated-version.js +1 -1
  12. package/dist/browser/index.d.ts +12 -0
  13. package/dist/browser/pkc/pkc.js +3 -3
  14. package/dist/browser/pkc/pkc.js.map +1 -1
  15. package/dist/browser/pkc-error.d.ts +1 -1
  16. package/dist/browser/publications/comment/schema.d.ts +2 -0
  17. package/dist/browser/publications/comment/schema.js +32 -0
  18. package/dist/browser/publications/comment/schema.js.map +1 -1
  19. package/dist/browser/rpc/src/lib/pkc-js/index.d.ts +6 -0
  20. package/dist/browser/rpc/src/schema.d.ts +36 -0
  21. package/dist/browser/runtime/browser/community/local-community.d.ts +2 -0
  22. package/dist/browser/runtime/browser/community/local-community.js +6 -0
  23. package/dist/browser/runtime/browser/community/local-community.js.map +1 -1
  24. package/dist/browser/runtime/node/community/challenges/index.d.ts +11 -5
  25. package/dist/browser/runtime/node/community/challenges/index.js +40 -5
  26. package/dist/browser/runtime/node/community/challenges/index.js.map +1 -1
  27. package/dist/browser/runtime/node/community/challenges/validate-challenge-result.d.ts +5 -0
  28. package/dist/browser/runtime/node/community/challenges/validate-challenge-result.js +70 -0
  29. package/dist/browser/runtime/node/community/challenges/validate-challenge-result.js.map +1 -0
  30. package/dist/browser/runtime/node/community/db-handler.d.ts +3 -1
  31. package/dist/browser/runtime/node/community/db-handler.js +24 -9
  32. package/dist/browser/runtime/node/community/db-handler.js.map +1 -1
  33. package/dist/browser/runtime/node/community/local-community/challenges.d.ts +27 -0
  34. package/dist/browser/runtime/node/community/local-community/challenges.js +534 -0
  35. package/dist/browser/runtime/node/community/local-community/challenges.js.map +1 -0
  36. package/dist/browser/runtime/node/community/local-community/cleanup.d.ts +9 -0
  37. package/dist/browser/runtime/node/community/local-community/cleanup.js +165 -0
  38. package/dist/browser/runtime/node/community/local-community/cleanup.js.map +1 -0
  39. package/dist/browser/runtime/node/community/local-community/comment-updates.d.ts +11 -0
  40. package/dist/browser/runtime/node/community/local-community/comment-updates.js +218 -0
  41. package/dist/browser/runtime/node/community/local-community/comment-updates.js.map +1 -0
  42. package/dist/browser/runtime/node/community/local-community/db-state.d.ts +15 -0
  43. package/dist/browser/runtime/node/community/local-community/db-state.js +323 -0
  44. package/dist/browser/runtime/node/community/local-community/db-state.js.map +1 -0
  45. package/dist/browser/runtime/node/community/local-community/defaults.d.ts +12 -0
  46. package/dist/browser/runtime/node/community/local-community/defaults.js +29 -0
  47. package/dist/browser/runtime/node/community/local-community/defaults.js.map +1 -0
  48. package/dist/browser/runtime/node/community/local-community/editing.d.ts +10 -0
  49. package/dist/browser/runtime/node/community/local-community/editing.js +188 -0
  50. package/dist/browser/runtime/node/community/local-community/editing.js.map +1 -0
  51. package/dist/browser/runtime/node/community/local-community/ipns-publishing.d.ts +12 -0
  52. package/dist/browser/runtime/node/community/local-community/ipns-publishing.js +340 -0
  53. package/dist/browser/runtime/node/community/local-community/ipns-publishing.js.map +1 -0
  54. package/dist/browser/runtime/node/community/local-community/lifecycle.d.ts +11 -0
  55. package/dist/browser/runtime/node/community/local-community/lifecycle.js +428 -0
  56. package/dist/browser/runtime/node/community/local-community/lifecycle.js.map +1 -0
  57. package/dist/browser/runtime/node/community/local-community/publication-store.d.ts +102 -0
  58. package/dist/browser/runtime/node/community/local-community/publication-store.js +403 -0
  59. package/dist/browser/runtime/node/community/local-community/publication-store.js.map +1 -0
  60. package/dist/browser/runtime/node/community/local-community/publication-validation.d.ts +9 -0
  61. package/dist/browser/runtime/node/community/local-community/publication-validation.js +493 -0
  62. package/dist/browser/runtime/node/community/local-community/publication-validation.js.map +1 -0
  63. package/dist/browser/runtime/node/community/local-community/pubsub.d.ts +3 -0
  64. package/dist/browser/runtime/node/community/local-community/pubsub.js +43 -0
  65. package/dist/browser/runtime/node/community/local-community/pubsub.js.map +1 -0
  66. package/dist/browser/runtime/node/community/local-community/registry.d.ts +3 -0
  67. package/dist/browser/runtime/node/community/local-community/registry.js +4 -0
  68. package/dist/browser/runtime/node/community/local-community/registry.js.map +1 -0
  69. package/dist/browser/runtime/node/community/local-community.d.ts +68 -105
  70. package/dist/browser/runtime/node/community/local-community.js +127 -2927
  71. package/dist/browser/runtime/node/community/local-community.js.map +1 -1
  72. package/dist/browser/schema/schema.d.ts +1 -0
  73. package/dist/browser/schema/schema.js +17 -0
  74. package/dist/browser/schema/schema.js.map +1 -1
  75. package/dist/browser/schema.d.ts +48 -0
  76. package/dist/browser/signer/signatures.d.ts +1 -1
  77. package/dist/browser/signer/signatures.js +9 -2
  78. package/dist/browser/signer/signatures.js.map +1 -1
  79. package/dist/browser/test/test-util.js +0 -1
  80. package/dist/browser/test/test-util.js.map +1 -1
  81. package/dist/browser/version.js +1 -1
  82. package/dist/node/community/remote-community.d.ts +3 -3
  83. package/dist/node/community/remote-community.js.map +1 -1
  84. package/dist/node/community/schema.d.ts +24 -0
  85. package/dist/node/community/schema.js +14 -1
  86. package/dist/node/community/schema.js.map +1 -1
  87. package/dist/node/errors.d.ts +5 -1
  88. package/dist/node/errors.js +4 -0
  89. package/dist/node/errors.js.map +1 -1
  90. package/dist/node/generated-version.d.ts +1 -1
  91. package/dist/node/generated-version.js +1 -1
  92. package/dist/node/index.d.ts +12 -0
  93. package/dist/node/pkc/pkc.js +3 -3
  94. package/dist/node/pkc/pkc.js.map +1 -1
  95. package/dist/node/pkc-error.d.ts +1 -1
  96. package/dist/node/publications/comment/schema.d.ts +2 -0
  97. package/dist/node/publications/comment/schema.js +32 -0
  98. package/dist/node/publications/comment/schema.js.map +1 -1
  99. package/dist/node/rpc/src/lib/pkc-js/index.d.ts +6 -0
  100. package/dist/node/rpc/src/schema.d.ts +36 -0
  101. package/dist/node/runtime/browser/community/local-community.d.ts +2 -0
  102. package/dist/node/runtime/browser/community/local-community.js +6 -0
  103. package/dist/node/runtime/browser/community/local-community.js.map +1 -1
  104. package/dist/node/runtime/node/community/challenges/index.d.ts +11 -5
  105. package/dist/node/runtime/node/community/challenges/index.js +40 -5
  106. package/dist/node/runtime/node/community/challenges/index.js.map +1 -1
  107. package/dist/node/runtime/node/community/challenges/validate-challenge-result.d.ts +5 -0
  108. package/dist/node/runtime/node/community/challenges/validate-challenge-result.js +70 -0
  109. package/dist/node/runtime/node/community/challenges/validate-challenge-result.js.map +1 -0
  110. package/dist/node/runtime/node/community/db-handler.d.ts +3 -1
  111. package/dist/node/runtime/node/community/db-handler.js +24 -9
  112. package/dist/node/runtime/node/community/db-handler.js.map +1 -1
  113. package/dist/node/runtime/node/community/local-community/challenges.d.ts +27 -0
  114. package/dist/node/runtime/node/community/local-community/challenges.js +534 -0
  115. package/dist/node/runtime/node/community/local-community/challenges.js.map +1 -0
  116. package/dist/node/runtime/node/community/local-community/cleanup.d.ts +9 -0
  117. package/dist/node/runtime/node/community/local-community/cleanup.js +165 -0
  118. package/dist/node/runtime/node/community/local-community/cleanup.js.map +1 -0
  119. package/dist/node/runtime/node/community/local-community/comment-updates.d.ts +11 -0
  120. package/dist/node/runtime/node/community/local-community/comment-updates.js +218 -0
  121. package/dist/node/runtime/node/community/local-community/comment-updates.js.map +1 -0
  122. package/dist/node/runtime/node/community/local-community/db-state.d.ts +15 -0
  123. package/dist/node/runtime/node/community/local-community/db-state.js +323 -0
  124. package/dist/node/runtime/node/community/local-community/db-state.js.map +1 -0
  125. package/dist/node/runtime/node/community/local-community/defaults.d.ts +12 -0
  126. package/dist/node/runtime/node/community/local-community/defaults.js +29 -0
  127. package/dist/node/runtime/node/community/local-community/defaults.js.map +1 -0
  128. package/dist/node/runtime/node/community/local-community/editing.d.ts +10 -0
  129. package/dist/node/runtime/node/community/local-community/editing.js +188 -0
  130. package/dist/node/runtime/node/community/local-community/editing.js.map +1 -0
  131. package/dist/node/runtime/node/community/local-community/ipns-publishing.d.ts +12 -0
  132. package/dist/node/runtime/node/community/local-community/ipns-publishing.js +340 -0
  133. package/dist/node/runtime/node/community/local-community/ipns-publishing.js.map +1 -0
  134. package/dist/node/runtime/node/community/local-community/lifecycle.d.ts +11 -0
  135. package/dist/node/runtime/node/community/local-community/lifecycle.js +428 -0
  136. package/dist/node/runtime/node/community/local-community/lifecycle.js.map +1 -0
  137. package/dist/node/runtime/node/community/local-community/publication-store.d.ts +102 -0
  138. package/dist/node/runtime/node/community/local-community/publication-store.js +403 -0
  139. package/dist/node/runtime/node/community/local-community/publication-store.js.map +1 -0
  140. package/dist/node/runtime/node/community/local-community/publication-validation.d.ts +9 -0
  141. package/dist/node/runtime/node/community/local-community/publication-validation.js +493 -0
  142. package/dist/node/runtime/node/community/local-community/publication-validation.js.map +1 -0
  143. package/dist/node/runtime/node/community/local-community/pubsub.d.ts +3 -0
  144. package/dist/node/runtime/node/community/local-community/pubsub.js +43 -0
  145. package/dist/node/runtime/node/community/local-community/pubsub.js.map +1 -0
  146. package/dist/node/runtime/node/community/local-community/registry.d.ts +3 -0
  147. package/dist/node/runtime/node/community/local-community/registry.js +4 -0
  148. package/dist/node/runtime/node/community/local-community/registry.js.map +1 -0
  149. package/dist/node/runtime/node/community/local-community.d.ts +68 -105
  150. package/dist/node/runtime/node/community/local-community.js +127 -2927
  151. package/dist/node/runtime/node/community/local-community.js.map +1 -1
  152. package/dist/node/schema/schema.d.ts +1 -0
  153. package/dist/node/schema/schema.js +17 -0
  154. package/dist/node/schema/schema.js.map +1 -1
  155. package/dist/node/schema.d.ts +48 -0
  156. package/dist/node/signer/signatures.d.ts +1 -1
  157. package/dist/node/signer/signatures.js +9 -2
  158. package/dist/node/signer/signatures.js.map +1 -1
  159. package/dist/node/test/test-util.js +0 -1
  160. package/dist/node/test/test-util.js.map +1 -1
  161. package/dist/node/version.js +1 -1
  162. package/package.json +3 -2
@@ -1,76 +1,35 @@
1
1
  import Logger from "../../../logger.js";
2
- import { LRUCache } from "lru-cache";
3
- import { PageGenerator } from "./page-generator.js";
4
- import { DbHandler } from "./db-handler.js";
5
- import { deriveDbReplies, deriveDbPosts, resolveDbPostsCidRefs } from "../util.js";
6
2
  import { of as calculateIpfsHash } from "typestub-ipfs-only-hash";
7
- import { derivePublicationFromChallengeRequest, doesDomainAddressHaveCapitalLetter, genToArray, hideClassPrivateProps, ipnsNameToIpnsOverPubsubTopic, isLinkOfMedia, isLinkOfImage, isLinkOfVideo, isLinkOfAnimatedImage, isLinkValid, isStringDomain, pubsubTopicToDhtKey, timestamp, getErrorCodeFromMessage, removeMfsFilesSafely, removeBlocksFromKuboNode, writeKuboFilesWithTimeout, retryKuboIpfsAddAndProvide, retryKuboBlockPutPinAndProvidePubsubTopic, calculateIpfsCidV0, calculateStringSizeSameAsIpfsAddCidV0, getIpnsRecordInLocalKuboNode, contentContainsMarkdownImages, contentContainsMarkdownVideos, isLinkOfAudio, contentContainsMarkdownAudio, areEquivalentCommunityAddresses } from "../../../util.js";
8
- import { STORAGE_KEYS } from "../../../constants.js";
3
+ import { calculateStringSizeSameAsIpfsAddCidV0, hideClassPrivateProps, isStringDomain, retryKuboIpfsAddAndProvide } from "../../../util.js";
9
4
  import { stringify as deterministicStringify } from "safe-stable-stringify";
10
5
  import { PKCError } from "../../../pkc-error.js";
11
- import { cleanUpBeforePublishing, signChallengeMessage, signChallengeVerification, signComment, signCommentEdit, signCommentUpdate, signCommentUpdateForChallengeVerification, signCommunity, verifyChallengeAnswer, verifyChallengeRequest, verifyCommentEdit, verifyCommentModeration, verifyCommentUpdate, verifyCommunityEdit } from "../../../signer/signatures.js";
12
- import { calculateExpectedSignatureSize, calculateInlineRepliesBudget, deriveCommentIpfsFromCommentTableRow, getThumbnailPropsOfLink, importSignerIntoKuboNode, moveCommunityDbToDeletedDirectory } from "../util.js";
13
- import { SignerWithPublicKeyAddress, decryptEd25519AesGcmPublicKeyBuffer, verifyCommentPubsubMessage, verifyCommunity, verifyVote } from "../../../signer/index.js";
14
- import { encryptEd25519AesGcmPublicKeyBuffer } from "../../../signer/encryption.js";
15
- import { messages } from "../../../errors.js";
16
- import { getChallengeVerification, getCommunityChallengeFromCommunityChallengeSettings } from "./challenges/index.js";
17
- import * as cborg from "cborg";
18
- import env from "../../../version.js";
19
- import { getIpfsKeyFromPrivateKey, getPKCAddressFromPublicKey, getPublicKeyFromPrivateKey } from "../../../signer/util.js";
6
+ import { storePublication } from "./local-community/publication-store.js";
7
+ import { verifyCommunity } from "../../../signer/signatures.js";
8
+ import { deriveCommentIpfsFromCommentTableRow } from "../util.js";
20
9
  import { RpcLocalCommunity } from "../../../community/rpc-local-community.js";
21
10
  import * as remeda from "remeda";
22
- import { buildRuntimeAuthor, cleanWireAuthor, getAuthorNameFromWire } from "../../../publications/publication-author.js";
23
- import { getCommunityPublicKeyFromWire, getCommunityNameFromWire } from "../../../publications/publication-community.js";
24
- import { CommentEditPubsubMessagePublicationSchema, CommentEditPubsubMessagePublicationWithFlexibleAuthorSchema, CommentEditReservedFields } from "../../../publications/comment-edit/schema.js";
25
- import { CommunityIpfsSchema, CommunitySignedPropertyNames } from "../../../community/schema.js";
26
- import { ChallengeAnswerMessageSchema, ChallengeMessageSchema, ChallengeRequestMessageSchema, ChallengeVerificationMessageSchema, DecryptedChallengeRequestPublicationSchema, DecryptedChallengeRequestSchema } from "../../../pubsub-messages/schema.js";
27
- import { parseDecryptedChallengeAnswerWithPKCErrorIfItFails, parseJsonWithPKCErrorIfFails, parseCommunityEditOptionsSchemaWithPKCErrorIfItFails, parseCommunityIpfsSchemaPassthroughWithPKCErrorIfItFails } from "../../../schema/schema-util.js";
28
- import { CommentIpfsSchema, CommentPubsubMessageReservedFields, CommentPubsubMessagePublicationSchema } from "../../../publications/comment/schema.js";
29
- import { VotePubsubMessagePublicationSchema, VotePubsubReservedFields } from "../../../publications/vote/schema.js";
30
- import { v4 as uuidV4 } from "uuid";
31
- import { AuthorReservedFields } from "../../../schema/schema.js";
32
- import { CommentModerationPubsubMessagePublicationSchema, CommentModerationReservedFields } from "../../../publications/comment-moderation/schema.js";
33
- import { CommunityEditPublicationPubsubReservedFields } from "../../../publications/community-edit/schema.js";
34
- import { default as lodashDeepMerge } from "lodash.merge"; // Importing only the `merge` function
11
+ import { CommunityIpfsSchema } from "../../../community/schema.js";
35
12
  import { MAX_FILE_SIZE_BYTES_FOR_COMMUNITY_IPFS } from "../../../community/community-client-manager.js";
36
- import pLimit from "p-limit";
37
13
  import { sha256 } from "js-sha256";
38
- import { iterateOverPageCidsToFindAllCids } from "../../../pages/util.js";
39
- import { TrackedInstanceRegistry } from "../../../pkc/tracked-instance-registry.js";
40
- import { findStartedCommunity, findCommunityInRegistry, findUpdatingCommunity, syncCommunityRegistryEntry, trackStartedCommunity, trackUpdatingCommunity, untrackStartedCommunity, untrackUpdatingCommunity } from "../../../pkc/tracked-instance-registry-util.js";
41
- const processStartedCommunities = new TrackedInstanceRegistry(); // A global registry on process level to track started communities
42
- const DUPLICATE_PUBLICATION_ERRORS = new Set([
43
- messages.ERR_DUPLICATE_COMMENT,
44
- messages.ERR_DUPLICATE_COMMENT_EDIT,
45
- messages.ERR_DUPLICATE_COMMENT_MODERATION
46
- ]);
14
+ import { generateDefaultChallenges } from "./local-community/defaults.js";
15
+ import { createNewLocalCommunityDb, getDbInternalState, initDbHandlerIfNeeded, initInternalCommunityAfterFirstUpdateNoMerge, initInternalCommunityBeforeFirstUpdateNoMerge, initNewLocalCommunityPropsNoMerge, updateDbInternalState, updateInstancePropsWithStartedCommunityOrDb } from "./local-community/db-state.js";
16
+ export { createNewLocalCommunityDb, updateInstancePropsWithStartedCommunityOrDb };
17
+ import { listenToIncomingRequests } from "./local-community/pubsub.js";
18
+ import { repinCommentsIPFSIfNeeded } from "./local-community/cleanup.js";
19
+ import { handleChallengeAnswer as handleChallengeAnswerFreeFunction, handleChallengeExchange as handleChallengeExchangeFreeFunction, handleChallengeRequest as handleChallengeRequestFreeFunction, publishChallengeVerification, publishIdempotentDuplicateVerification } from "./local-community/challenges.js";
20
+ import { checkPublicationValidity } from "./local-community/publication-validation.js";
21
+ import { calculateLocalMfsPathForCommentUpdate, updateCommentsThatNeedToBeUpdated } from "./local-community/comment-updates.js";
22
+ import { purgeDisapprovedCommentsOlderThan } from "./local-community/cleanup.js";
23
+ import { addOldPageCidsToCidsToUnpin, calculateLatestUpdateTrigger, calculateNewPostUpdates, resolveIpnsAndLogIfPotentialProblematicSequence, shouldResolveDomainForVerification, updateCommunityIpnsIfNeeded } from "./local-community/ipns-publishing.js";
24
+ import { deleteCommunity, start as lifecycleStart, stop as lifecycleStop, update as lifecycleUpdate } from "./local-community/lifecycle.js";
25
+ import { edit as editCommunity } from "./local-community/editing.js";
47
26
  // This is a sub we have locally in our pkc datapath, in a NodeJS environment
48
27
  export class LocalCommunity extends RpcLocalCommunity {
49
- static _generateDefaultChallenges(answer) {
50
- return [
51
- {
52
- name: "question",
53
- options: {
54
- question: LocalCommunity._defaultChallengeQuestionText,
55
- answer: answer ?? uuidV4()
56
- }
57
- }
58
- ];
59
- }
60
- static _isDefaultChallengeStructure(challenges) {
61
- if (!challenges || challenges.length !== 1)
62
- return false;
63
- const c = challenges[0];
64
- return (c.name === "question" &&
65
- c.options?.question === LocalCommunity._defaultChallengeQuestionText &&
66
- typeof c.options?.answer === "string" &&
67
- c.options.answer.length > 0);
68
- }
69
28
  constructor(pkc) {
70
29
  super(pkc);
71
30
  this.raw = {};
72
31
  this._postUpdatesBuckets = [86400, 604800, 2592000, 3153600000]; // 1 day, 1 week, 1 month, 100 years. Expecting to be sorted from smallest to largest
73
- this._defaultCommunityChallenges = LocalCommunity._generateDefaultChallenges();
32
+ this._defaultCommunityChallenges = generateDefaultChallenges();
74
33
  this._challengeExchangesFromLocalPublishers = {}; // key is stringified challengeRequestId and value is true if the challenge exchange is ongoing
75
34
  this._cidsToUnPin = new Set();
76
35
  this._mfsPathsToRemove = new Set();
@@ -155,575 +114,16 @@ export class LocalCommunity extends RpcLocalCommunity {
155
114
  this.started = await this._dbHandler.isCommunityStartLocked(this.address);
156
115
  }
157
116
  async initNewLocalCommunityPropsNoMerge(newProps) {
158
- await this._initSignerProps(newProps.signer);
159
- this.title = newProps.title;
160
- this.description = newProps.description;
161
- this.setAddress(newProps.address);
162
- this.pubsubTopic = newProps.pubsubTopic;
163
- this.roles = newProps.roles;
164
- this.features = newProps.features;
165
- this.suggested = newProps.suggested;
166
- this.rules = newProps.rules;
167
- this.flairs = newProps.flairs;
168
- if (newProps.settings)
169
- this.settings = newProps.settings;
117
+ return initNewLocalCommunityPropsNoMerge(this, newProps);
170
118
  }
171
119
  async initInternalCommunityAfterFirstUpdateNoMerge(newProps) {
172
- // Detect CID-ref format posts from DB: wire format always has 'pages' key, CID-ref format doesn't
173
- if (newProps.posts && !("pages" in newProps.posts)) {
174
- const dbPosts = newProps.posts;
175
- // Extract allPageCids for future unpinning
176
- const allPageCids = {};
177
- for (const [sortName, entry] of Object.entries(dbPosts)) {
178
- if (entry?.allPageCids?.length)
179
- allPageCids[sortName] = entry.allPageCids;
180
- }
181
- this._postsAllPageCids = Object.keys(allPageCids).length > 0 ? allPageCids : undefined;
182
- // Lightweight conversion: just pageCids from allPageCids[0], preloaded pages regenerated on next update.
183
- // Never use resolveDbPostsCidRefs here — this method is called from many code paths
184
- // where _dbHandler._db may not be initialized (e.g. updateListener from a mirrored community).
185
- const pageCids = {};
186
- for (const [sortName, entry] of Object.entries(dbPosts)) {
187
- if (entry?.allPageCids?.[0])
188
- pageCids[sortName] = entry.allPageCids[0];
189
- }
190
- newProps = {
191
- ...newProps,
192
- posts: { pages: {}, ...(Object.keys(pageCids).length > 0 ? { pageCids } : {}) }
193
- };
194
- }
195
- const keysOfCommunityIpfs = [...CommunitySignedPropertyNames, "signature"];
196
- this.initRpcInternalCommunityAfterFirstUpdateNoMerge({
197
- community: remeda.pick(newProps, keysOfCommunityIpfs),
198
- localCommunity: {
199
- signer: remeda.pick(newProps.signer, ["publicKey", "address", "shortAddress", "type"]),
200
- settings: newProps.settings,
201
- _usingDefaultChallenge: newProps._usingDefaultChallenge,
202
- address: newProps.address,
203
- started: this.started,
204
- startedState: this.startedState
205
- },
206
- runtimeFields: { updateCid: newProps.updateCid }
207
- });
208
- await this._initSignerProps(newProps.signer);
209
- this._internalStateUpdateId = newProps._internalStateUpdateId;
210
- if (Array.isArray(newProps._cidsToUnPin))
211
- newProps._cidsToUnPin.forEach((cid) => this._cidsToUnPin.add(cid));
212
- if (Array.isArray(newProps._mfsPathsToRemove))
213
- newProps._mfsPathsToRemove.forEach((path) => this._mfsPathsToRemove.add(path));
214
- this._updateIpnsPubsubPropsIfNeeded(newProps);
215
- if (processStartedCommunities.has(this))
216
- syncCommunityRegistryEntry(processStartedCommunities, this);
217
- if (this.updateCid)
218
- this.raw.localCommunity = this.toJSONInternalRpcAfterFirstUpdate();
120
+ return initInternalCommunityAfterFirstUpdateNoMerge(this, newProps);
219
121
  }
220
122
  async initInternalCommunityBeforeFirstUpdateNoMerge(newProps) {
221
- this.initRpcInternalCommunityBeforeFirstUpdateNoMerge({
222
- localCommunity: {
223
- ...remeda.omit(newProps, ["signer", "_internalStateUpdateId", "_pendingEditProps"]),
224
- signer: remeda.pick(newProps.signer, ["publicKey", "address", "shortAddress", "type"]),
225
- started: this.started,
226
- startedState: this.startedState
227
- }
228
- });
229
- await this._initSignerProps(newProps.signer);
230
- this._internalStateUpdateId = newProps._internalStateUpdateId;
231
- this._updateIpnsPubsubPropsIfNeeded(newProps);
232
- this.ipnsName = newProps.signer.address;
233
- this.ipnsPubsubTopic = ipnsNameToIpnsOverPubsubTopic(this.ipnsName);
234
- this.ipnsPubsubTopicRoutingCid = pubsubTopicToDhtKey(this.ipnsPubsubTopic);
235
- if (processStartedCommunities.has(this))
236
- syncCommunityRegistryEntry(processStartedCommunities, this);
237
- this.raw.localCommunity = this.toJSONInternalRpcBeforeFirstUpdate();
123
+ return initInternalCommunityBeforeFirstUpdateNoMerge(this, newProps);
238
124
  }
239
125
  async initDbHandlerIfNeeded() {
240
- if (!this._dbHandler) {
241
- this._dbHandler = new DbHandler(this);
242
- await this._dbHandler.initDbConfigIfNeeded();
243
- this._pageGenerator = new PageGenerator(this);
244
- }
245
- }
246
- async _updateInstancePropsWithStartedCommunityOrDb() {
247
- // if it's started in the same pkc instance, we will load it from the started community instance
248
- // if it's started in another process, we will throw an error
249
- // if community is not started, load the InternalCommunity props from the local db
250
- const log = Logger("pkc-js:local-community:_updateInstancePropsWithStartedCommunityOrDb");
251
- const startedCommunity = ((findStartedCommunity(this._pkc, { publicKey: this.publicKey, name: this.name }) ||
252
- findCommunityInRegistry(processStartedCommunities, { publicKey: this.publicKey, name: this.name })));
253
- if (startedCommunity) {
254
- log("Loading local community", this.address, "from started community instance");
255
- if (startedCommunity.updatedAt)
256
- await this.initInternalCommunityAfterFirstUpdateNoMerge(startedCommunity.toJSONInternalAfterFirstUpdate());
257
- else
258
- await this.initInternalCommunityBeforeFirstUpdateNoMerge(startedCommunity.toJSONInternalBeforeFirstUpdate());
259
- this.started = true;
260
- }
261
- else {
262
- await this.initDbHandlerIfNeeded();
263
- try {
264
- await this._updateStartedValue();
265
- const communityDbExists = this._dbHandler.communityDbExists();
266
- if (!communityDbExists)
267
- throw new PKCError("CAN_NOT_LOAD_LOCAL_COMMUNITY_IF_DB_DOES_NOT_EXIST", {
268
- address: this.address,
269
- dataPath: this._pkc.dataPath
270
- });
271
- const dbConfig = this.state === "updating" ? { readonly: true } : undefined;
272
- await this._dbHandler.initDbIfNeeded(dbConfig);
273
- await this._updateInstanceStateWithDbState(); // Load InternalCommunity from DB here
274
- if (!this.signer)
275
- throw new PKCError("ERR_LOCAL_COMMUNITY_HAS_NO_SIGNER_IN_INTERNAL_STATE", { address: this.address });
276
- await this._updateStartedValue();
277
- log("Loaded local community", this.address, "from db");
278
- }
279
- catch (e) {
280
- throw e;
281
- }
282
- finally {
283
- this._dbHandler.destoryConnection(); // Need to destory connection so process wouldn't hang
284
- }
285
- }
286
- // need to validate schema of Community IPFS
287
- if (this.raw.communityIpfs)
288
- try {
289
- parseCommunityIpfsSchemaPassthroughWithPKCErrorIfItFails(this.raw.communityIpfs);
290
- }
291
- catch (e) {
292
- if (e instanceof Error) {
293
- log("Local community", this.address, "has an invalid communityIpfs schema from DB, clearing for re-generation after migration:", e.message);
294
- this.raw.communityIpfs = undefined;
295
- }
296
- }
297
- }
298
- async _importCommunitySignerIntoIpfsIfNeeded() {
299
- if (!this.signer.ipnsKeyName)
300
- throw Error("community.signer.ipnsKeyName is not defined");
301
- if (!this.signer.ipfsKey)
302
- throw Error("community.signer.ipfsKey is not defined");
303
- await importSignerIntoKuboNode(this.signer.ipnsKeyName, this.signer.ipfsKey, {
304
- url: this._pkc.kuboRpcClientsOptions[0].url.toString(),
305
- headers: this._pkc.kuboRpcClientsOptions[0].headers
306
- });
307
- }
308
- async _updateDbInternalState(props) {
309
- const log = Logger("pkc-js:local-community:_updateDbInternalState");
310
- if (remeda.isEmpty(props))
311
- throw Error("props to update DB internal state should not be empty");
312
- await this._dbHandler.initDbIfNeeded();
313
- props._internalStateUpdateId = uuidV4();
314
- let lockedIt = false;
315
- try {
316
- await this._dbHandler.lockCommunityState();
317
- lockedIt = true;
318
- const internalStateBefore = await this._getDbInternalState(false);
319
- // Convert posts to CID-ref format for compact DB storage (strip preloaded page data)
320
- const propsToStore = "posts" in props && props.posts
321
- ? {
322
- ...props,
323
- posts: deriveDbPosts({
324
- posts: props.posts,
325
- allPageCids: this._postsAllPageCids
326
- })
327
- }
328
- : props;
329
- const mergedInternalState = { ...internalStateBefore, ...propsToStore };
330
- await this._dbHandler.keyvSet(STORAGE_KEYS[STORAGE_KEYS.INTERNAL_COMMUNITY], mergedInternalState);
331
- this._internalStateUpdateId = props._internalStateUpdateId;
332
- log.trace("Updated community", this.address, "internal state in db with new props", Object.keys(props));
333
- if (this.updateCid && this.raw.communityIpfs) {
334
- this.raw.localCommunity = this.toJSONInternalRpcAfterFirstUpdate();
335
- }
336
- else if (this.settings) {
337
- this.raw.localCommunity = this.toJSONInternalRpcBeforeFirstUpdate();
338
- }
339
- return mergedInternalState;
340
- }
341
- catch (e) {
342
- log.error("Failed to update community", this.address, "internal state in db with new props", Object.keys(props), e);
343
- throw e;
344
- }
345
- finally {
346
- if (lockedIt)
347
- await this._dbHandler.unlockCommunityState();
348
- }
349
- }
350
- async _getDbInternalState(lock) {
351
- const log = Logger("pkc-js:local-community:_getDbInternalState");
352
- if (!this._dbHandler.keyvHas(STORAGE_KEYS[STORAGE_KEYS.INTERNAL_COMMUNITY]))
353
- throw new PKCError("ERR_COMMUNITY_HAS_NO_INTERNAL_STATE", { address: this.address, dataPath: this._pkc.dataPath });
354
- let lockedIt = false;
355
- try {
356
- if (lock) {
357
- await this._dbHandler.lockCommunityState();
358
- lockedIt = true;
359
- }
360
- const internalState = await this._dbHandler.keyvGet(STORAGE_KEYS[STORAGE_KEYS.INTERNAL_COMMUNITY]);
361
- if (!internalState)
362
- throw new PKCError("ERR_COMMUNITY_HAS_NO_INTERNAL_STATE", { address: this.address, dataPath: this._pkc.dataPath });
363
- return internalState;
364
- }
365
- catch (e) {
366
- log.error("Failed to get community", this.address, "internal state from db", e);
367
- throw e;
368
- }
369
- finally {
370
- if (lockedIt)
371
- await this._dbHandler.unlockCommunityState();
372
- }
373
- }
374
- async _updateInstanceStateWithDbState() {
375
- const currentDbState = await this._getDbInternalState(false);
376
- if ("updatedAt" in currentDbState) {
377
- // Resolve CID-ref posts from DB back to full wire format with preloaded pages.
378
- // DB stores posts in compact CID-ref format (no preloaded page data).
379
- // _dbHandler is guaranteed to be initialized here since we're loading from DB.
380
- if (currentDbState.posts && !("pages" in currentDbState.posts)) {
381
- const dbPosts = currentDbState.posts;
382
- currentDbState.posts = resolveDbPostsCidRefs({ dbPosts, dbHandler: this._dbHandler });
383
- }
384
- await this.initInternalCommunityAfterFirstUpdateNoMerge(currentDbState);
385
- }
386
- else
387
- await this.initInternalCommunityBeforeFirstUpdateNoMerge(currentDbState);
388
- }
389
- async _setChallengesToDefaultIfNotDefined(log) {
390
- if (this._usingDefaultChallenge !== false &&
391
- (!this.settings?.challenges || LocalCommunity._isDefaultChallengeStructure(this.settings?.challenges)))
392
- this._usingDefaultChallenge = true;
393
- if (this._usingDefaultChallenge) {
394
- const currentAnswer = this.settings?.challenges?.[0]?.options?.answer;
395
- if (currentAnswer && LocalCommunity._isDefaultChallengeStructure(this._defaultCommunityChallenges)) {
396
- // Preserve the existing per-community random answer in the template
397
- this._defaultCommunityChallenges = LocalCommunity._generateDefaultChallenges(currentAnswer);
398
- }
399
- if (!remeda.isDeepEqual(this.settings?.challenges, this._defaultCommunityChallenges)) {
400
- await this.edit({ settings: { ...this.settings, challenges: this._defaultCommunityChallenges } });
401
- // edit() recalculates _usingDefaultChallenge via _isDefaultChallengeStructure,
402
- // which may return false for non-standard defaults (e.g. []).
403
- // Re-assert true since we know this is still a default-driven upgrade.
404
- this._usingDefaultChallenge = true;
405
- log(`Upgraded default challenge for community (${this.address})`, this._defaultCommunityChallenges[0]?.options?.answer
406
- ? `with answer: ${this._defaultCommunityChallenges[0].options.answer}`
407
- : `to ${this._defaultCommunityChallenges.length} challenge(s)`);
408
- }
409
- }
410
- }
411
- async _createNewLocalCommunityDb() {
412
- // We're creating a totally new community here with a new db
413
- // This function should be called only once per community
414
- const log = Logger("pkc-js:local-community:_createNewLocalCommunityDb");
415
- await this.initDbHandlerIfNeeded();
416
- await this._dbHandler.initDbIfNeeded({ fileMustExist: false });
417
- await this._dbHandler.createOrMigrateTablesIfNeeded();
418
- await this._initSignerProps(this.signer); // init this.encryption as well
419
- if (!this.pubsubTopic)
420
- this.pubsubTopic = remeda.clone(this.signer.address);
421
- if (typeof this.createdAt !== "number")
422
- this.createdAt = timestamp();
423
- if (!this.protocolVersion)
424
- this.protocolVersion = env.PROTOCOL_VERSION;
425
- if (!this.settings?.maxPendingApprovalCount)
426
- this.settings = { ...this.settings, maxPendingApprovalCount: 500 };
427
- if (!this.settings?.challenges) {
428
- this.settings = { ...this.settings, challenges: this._defaultCommunityChallenges };
429
- this._usingDefaultChallenge = true;
430
- log(`Generated default challenge for community (${this.address}) with answer:`, this._defaultCommunityChallenges[0].options.answer);
431
- }
432
- if (typeof this.settings?.purgeDisapprovedCommentsOlderThan !== "number") {
433
- this.settings = { ...this.settings, purgeDisapprovedCommentsOlderThan: 1.21e6 }; // two weeks
434
- }
435
- this.challenges = await Promise.all(this.settings.challenges.map(async (cs) => (await getCommunityChallengeFromCommunityChallengeSettings({ communityChallengeSettings: cs, pkc: this._pkc }))
436
- .communityChallenge));
437
- if (this._dbHandler.keyvHas(STORAGE_KEYS[STORAGE_KEYS.INTERNAL_COMMUNITY]))
438
- throw Error("Internal state exists already");
439
- await this._dbHandler.keyvSet(STORAGE_KEYS[STORAGE_KEYS.INTERNAL_COMMUNITY], this.toJSONInternalBeforeFirstUpdate());
440
- await this._updateStartedValue();
441
- this._dbHandler.destoryConnection(); // Need to destory connection so process wouldn't hang
442
- this._updateIpnsPubsubPropsIfNeeded({
443
- ...this.toJSONInternalBeforeFirstUpdate(), //@ts-expect-error
444
- signature: { publicKey: this.signer.publicKey }
445
- });
446
- }
447
- async _calculateNewPostUpdates() {
448
- const postUpdates = {};
449
- const kuboRpcClient = this._clientsManager.getDefaultKuboRpcClient()._client;
450
- for (const timeBucket of this._postUpdatesBuckets) {
451
- try {
452
- const statRes = await kuboRpcClient.files.stat(`/${this.address}/postUpdates/${timeBucket}`);
453
- if (statRes.blocks !== 0)
454
- postUpdates[String(timeBucket)] = String(statRes.cid);
455
- }
456
- catch { }
457
- }
458
- if (remeda.isEmpty(postUpdates))
459
- return undefined;
460
- return postUpdates;
461
- }
462
- _calculateLatestUpdateTrigger() {
463
- const lastPublishTooOld = (this.updatedAt || 0) < timestamp() - 60 * 15; // Publish a community record every 15 minutes at least
464
- // these two checks below are for rare cases where a purged comments or post is not forcing community for a new update
465
- const lastPostCidChanged = this.lastPostCid !== this._dbHandler.queryLatestPostCid()?.cid;
466
- const lastCommentCidChanged = this.lastCommentCid !== this._dbHandler.queryLatestCommentCid()?.cid;
467
- this._communityUpdateTrigger =
468
- this._communityUpdateTrigger ||
469
- lastPublishTooOld ||
470
- this._pendingEditProps.length > 0 ||
471
- this._blocksToRm.length > 0 ||
472
- lastCommentCidChanged ||
473
- lastPostCidChanged; // we have at least one edit to include in new ipns
474
- }
475
- _requireCommunityUpdateIfModQueueChanged() {
476
- const combinedHashOfAllQueuedComments = this._dbHandler.queryCombinedHashOfPendingComments();
477
- if (this._combinedHashOfPendingCommentsCids !== combinedHashOfAllQueuedComments)
478
- this._communityUpdateTrigger = true;
479
- }
480
- async _resolveIpnsAndLogIfPotentialProblematicSequence() {
481
- const log = Logger("pkc-js:local-community:_resolveIpnsAndLogIfPotentialProblematicSequence");
482
- if (!this.signer.ipnsKeyName)
483
- throw Error("IPNS key name is not defined");
484
- if (!this.updateCid)
485
- return;
486
- try {
487
- const ipnsCid = await this._clientsManager.resolveIpnsToCidP2P(this.signer.ipnsKeyName, { timeoutMs: 120000 });
488
- log.trace("Resolved community", this.address, "IPNS key", this.signer.ipnsKeyName, "to", ipnsCid);
489
- if (ipnsCid && this.updateCid && ipnsCid !== this.updateCid) {
490
- log.error("community", this.address, "IPNS key", this.signer.ipnsKeyName, "points to", ipnsCid, "but we expected it to point to", this.updateCid, "This could result an IPNS record with invalid sequence number");
491
- }
492
- }
493
- catch (e) {
494
- log.trace("Failed to resolve community before publishing", this.address, "IPNS key", this.signer.ipnsKeyName, e);
495
- }
496
- }
497
- async _addOldPageCidsToCidsToUnpin(curPages, newPages, addToBlockRm) {
498
- if (!curPages && !newPages)
499
- return;
500
- else if (curPages && !newPages) {
501
- // we had to reset our community pages, maybe because we purged all comments or changed community address
502
- const allPageCidsUnderCurPages = await iterateOverPageCidsToFindAllCids({
503
- pages: curPages,
504
- clientManager: this._clientsManager
505
- });
506
- allPageCidsUnderCurPages.forEach((cid) => {
507
- this._cidsToUnPin.add(cid);
508
- if (addToBlockRm)
509
- this._blocksToRm.push(cid);
510
- });
511
- }
512
- else if (curPages && newPages) {
513
- // need to find cids for both, and compare them and only keep ones in newPages
514
- const allPageCidsUnderCurPages = await iterateOverPageCidsToFindAllCids({
515
- pages: curPages,
516
- clientManager: this._clientsManager
517
- });
518
- const allPageCidsUnderNewPages = await iterateOverPageCidsToFindAllCids({
519
- pages: newPages,
520
- clientManager: this._clientsManager
521
- });
522
- const cidsToUnpin = remeda.difference(allPageCidsUnderCurPages, allPageCidsUnderNewPages);
523
- cidsToUnpin.forEach((cid) => {
524
- this._cidsToUnPin.add(cid);
525
- if (addToBlockRm)
526
- this._blocksToRm.push(cid);
527
- });
528
- }
529
- }
530
- async updateCommunityIpnsIfNeeded(commentUpdateRowsToPublishToIpfs) {
531
- const log = Logger("pkc-js:local-community:start:updateCommunityIpnsIfNeeded");
532
- this._calculateLatestUpdateTrigger();
533
- if (!this._communityUpdateTrigger)
534
- return; // No reason to update
535
- this._dbHandler.createTransaction();
536
- const latestPost = this._dbHandler.queryLatestPostCid();
537
- const latestComment = this._dbHandler.queryLatestCommentCid();
538
- this._dbHandler.commitTransaction();
539
- const stats = this._dbHandler.queryCommunityStats();
540
- if (commentUpdateRowsToPublishToIpfs.length > 0) {
541
- try {
542
- await this._syncPostUpdatesWithIpfs(commentUpdateRowsToPublishToIpfs);
543
- }
544
- catch (e) {
545
- const err = e;
546
- const isMfsTimeout = err.message.includes("Timed out writing to MFS path") || err.message.includes("Timed out removing MFS paths");
547
- if (isMfsTimeout) {
548
- // Workaround for ipfs/kubo#10842: deeply nested MFS paths hang, but rm of the community root is fast.
549
- log.error(`MFS sync stuck for community ${this.address} - auto-nuking /${this.address} and forcing a full republish. See https://github.com/ipfs/kubo/issues/10842 for upstream context.`);
550
- const kuboRpc = this._clientsManager.getDefaultKuboRpcClient();
551
- try {
552
- await kuboRpc._client.files.rm("/" + this.address, {
553
- recursive: true,
554
- //@ts-expect-error force is not in FilesRmOptions
555
- force: true
556
- });
557
- }
558
- catch (rmErr) {
559
- log.error(`Auto-nuke files.rm of /${this.address} failed:`, rmErr);
560
- }
561
- this._dbHandler.forceUpdateOnAllComments();
562
- }
563
- throw e;
564
- }
565
- }
566
- const newPostUpdates = await this._calculateNewPostUpdates();
567
- const newModQueue = await this._pageGenerator.generateModQueuePages();
568
- const kuboRpcClient = this._clientsManager.getDefaultKuboRpcClient();
569
- const statsCid = (await retryKuboIpfsAddAndProvide({
570
- ipfsClient: kuboRpcClient._client,
571
- log,
572
- content: deterministicStringify(stats),
573
- addOptions: { pin: true },
574
- provideOptions: { recursive: true },
575
- provideInBackground: true
576
- })).path;
577
- if (this.statsCid && statsCid !== this.statsCid)
578
- this._cidsToUnPin.add(this.statsCid);
579
- const currentTimestamp = timestamp();
580
- const updatedAt = typeof this?.updatedAt === "number" && this.updatedAt >= currentTimestamp ? this.updatedAt + 1 : currentTimestamp;
581
- const editIdsToIncludeInNextUpdate = this._pendingEditProps.map((editProps) => editProps.editId);
582
- const pendingCommunityIpfsEditProps = Object.assign({}, //@ts-expect-error
583
- ...this._pendingEditProps.map((editProps) => remeda.pick(editProps, remeda.keys.strict(CommunityIpfsSchema.shape))));
584
- if (this._pendingEditProps.length > 0)
585
- log("Including edit props in next IPNS update", this._pendingEditProps);
586
- const newIpns = {
587
- ...cleanUpBeforePublishing({
588
- ...remeda.omit(this._toJSONIpfsBaseNoPosts(), ["signature"]),
589
- ...pendingCommunityIpfsEditProps,
590
- lastPostCid: latestPost?.cid,
591
- lastCommentCid: latestComment?.cid,
592
- statsCid,
593
- updatedAt,
594
- postUpdates: newPostUpdates,
595
- protocolVersion: env.PROTOCOL_VERSION
596
- })
597
- };
598
- const preloadedPostsPages = "hot";
599
- // Calculate size taken by community without posts and signature
600
- const communityWithoutPostsSignatureSize = Buffer.byteLength(JSON.stringify(newIpns), "utf8");
601
- // Calculate expected signature size
602
- const expectedSignatureSize = calculateExpectedSignatureSize(newIpns);
603
- // Calculate remaining space for posts
604
- const availablePostsSize = MAX_FILE_SIZE_BYTES_FOR_COMMUNITY_IPFS - communityWithoutPostsSignatureSize - expectedSignatureSize - 1000;
605
- const generatedPosts = await this._pageGenerator.generateCommunityPosts(preloadedPostsPages, availablePostsSize);
606
- // posts should not be cleaned up because we want to make sure not to modify authors' posts
607
- // Extract allPageCids from generation result for DB CID-ref storage and unpinning
608
- const newPostsAllPageCids = generatedPosts && !("singlePreloadedPage" in generatedPosts) ? generatedPosts.allPageCids : undefined;
609
- if (generatedPosts) {
610
- if ("singlePreloadedPage" in generatedPosts)
611
- newIpns.posts = { pages: generatedPosts.singlePreloadedPage };
612
- else if (generatedPosts.pageCids) {
613
- // multiple pages
614
- newIpns.posts = {
615
- pageCids: generatedPosts.pageCids,
616
- pages: remeda.pick(generatedPosts.pages, [preloadedPostsPages])
617
- };
618
- }
619
- }
620
- else {
621
- await this._updateDbInternalState({ posts: undefined }); // make sure db resets posts as well
622
- }
623
- // Unpin old posts page CIDs using direct allPageCids comparison (no IPFS fetches needed)
624
- {
625
- const oldCids = new Set(this._postsAllPageCids ? Object.values(this._postsAllPageCids).flat() : []);
626
- const newCids = new Set(newPostsAllPageCids ? Object.values(newPostsAllPageCids).flat() : []);
627
- for (const cid of oldCids) {
628
- if (!newCids.has(cid))
629
- this._cidsToUnPin.add(cid);
630
- }
631
- }
632
- this._postsAllPageCids = newPostsAllPageCids;
633
- if (newModQueue) {
634
- newIpns.modQueue = { pageCids: newModQueue.pageCids };
635
- }
636
- else {
637
- await this._updateDbInternalState({ modQueue: undefined });
638
- this.modQueue.resetPages();
639
- }
640
- const signature = await signCommunity({ community: newIpns, signer: this.signer });
641
- const newCommunityRecord = { ...newIpns, signature };
642
- await this._validateCommunitySizeSchemaAndSignatureBeforePublishing(newCommunityRecord);
643
- const contentToPublish = deterministicStringify(newCommunityRecord);
644
- const file = await retryKuboIpfsAddAndProvide({
645
- ipfsClient: kuboRpcClient._client,
646
- log,
647
- content: contentToPublish, // you need to do deterministic here or otherwise cids in commentUpdate.replies won't match up correctly
648
- addOptions: { pin: true },
649
- provideOptions: { recursive: true },
650
- provideInBackground: false
651
- });
652
- if (file.size > MAX_FILE_SIZE_BYTES_FOR_COMMUNITY_IPFS) {
653
- throw new PKCError("ERR_LOCAL_COMMUNITY_RECORD_TOO_LARGE", {
654
- calculatedSizeOfNewCommunityRecord: file.size,
655
- maxSize: MAX_FILE_SIZE_BYTES_FOR_COMMUNITY_IPFS,
656
- newCommunityRecord,
657
- address: this.address
658
- });
659
- }
660
- if (!this.signer.ipnsKeyName)
661
- throw Error("IPNS key name is not defined");
662
- // after kubo 0.40 implements fetching IPNS record from local blockstore, we don't need line below anymore
663
- if (this._firstUpdateAfterStart)
664
- await this._resolveIpnsAndLogIfPotentialProblematicSequence();
665
- const ttl = `${this._pkc.publishInterval * 3}ms`; // default publish interval is 20s, so default ttl is 60s
666
- const lastPublishedIpnsRecordData = await this._dbHandler.keyvGet(STORAGE_KEYS[STORAGE_KEYS.LAST_IPNS_RECORD]);
667
- const decodedIpnsRecord = lastPublishedIpnsRecordData
668
- ? cborg.decode(new Uint8Array(Object.values(lastPublishedIpnsRecordData)))
669
- : undefined;
670
- const ipnsSequence = decodedIpnsRecord ? BigInt(decodedIpnsRecord.sequence) + 1n : undefined;
671
- const publishRes = await kuboRpcClient._client.name.publish(file.path, {
672
- key: this.signer.ipnsKeyName,
673
- allowOffline: true,
674
- resolve: true,
675
- ttl
676
- // enable below line after kubo fixes their problems with fetching IPNS records from local blockstore
677
- // ...(ipnsSequence ? { sequence: ipnsSequence } : undefined)
678
- });
679
- log(`Published a new IPNS record for community(${this.address}) on IPNS (${publishRes.name}) that points to file (${publishRes.value}) with updatedAt (${newCommunityRecord.updatedAt}) and TTL (${ttl})`);
680
- this._clientsManager.updateKuboRpcState("stopped", kuboRpcClient.url);
681
- this._addOldPageCidsToCidsToUnpin(this.raw.communityIpfs?.modQueue, newIpns.modQueue).catch((err) => log.error("Failed to add old page cids of community.modQueue to _cidsToUnpin", err));
682
- await this._unpinStaleCids();
683
- if (this._blocksToRm.length > 0) {
684
- const removedBlocks = await removeBlocksFromKuboNode({
685
- ipfsClient: this._clientsManager.getDefaultKuboRpcClient()._client,
686
- log,
687
- cids: this._blocksToRm,
688
- options: { force: true }
689
- });
690
- log("Removed blocks", removedBlocks, "from kubo node");
691
- this._blocksToRm = this._blocksToRm.filter((blockCid) => !removedBlocks.includes(blockCid));
692
- }
693
- if (this.updateCid)
694
- this._cidsToUnPin.add(this.updateCid); // add old cid of community to be unpinned
695
- this.initCommunityIpfsPropsNoMerge(newCommunityRecord);
696
- this.updateCid = file.path;
697
- this._pendingEditProps = this._pendingEditProps.filter((editProps) => !editIdsToIncludeInNextUpdate.includes(editProps.editId));
698
- // Re-apply remaining pending edits to in-memory state.
699
- // initCommunityIpfsPropsNoMerge above overwrites all CommunityIpfs properties from the
700
- // published IPNS record. If edit() was called during the long IPNS publish await,
701
- // those edits are still in _pendingEditProps but their in-memory values were overwritten.
702
- if (this._pendingEditProps.length > 0) {
703
- const remainingEditProps = Object.assign({}, //@ts-expect-error
704
- ...this._pendingEditProps.map((editProps) => remeda.pick(editProps, remeda.keys.strict(CommunityIpfsSchema.shape))));
705
- Object.assign(this, remainingEditProps);
706
- }
707
- this._communityUpdateTrigger = false;
708
- this._firstUpdateAfterStart = false;
709
- try {
710
- // this call will fail if we have http routers + kubo 0.38 and earlier
711
- const ipnsRecord = await getIpnsRecordInLocalKuboNode(kuboRpcClient, this.signer.address);
712
- await this._dbHandler.keyvSet(STORAGE_KEYS[STORAGE_KEYS.LAST_IPNS_RECORD], cborg.encode(ipnsRecord));
713
- }
714
- catch (e) {
715
- log.trace("Failed to update IPNS record in sqlite record, not a critical error and will most likely be fixed by kubo past 0.38", e);
716
- }
717
- this._combinedHashOfPendingCommentsCids = newModQueue?.combinedHashOfCids || sha256("");
718
- log.trace("Updated combined hash of pending comments to", this._combinedHashOfPendingCommentsCids);
719
- await this._updateDbInternalState(this.toJSONInternalAfterFirstUpdate());
720
- this._changeStateEmitEventEmitStateChangeEvent({
721
- newStartedState: "succeeded",
722
- event: { name: "update", args: [this] }
723
- });
724
- }
725
- shouldResolveDomainForVerification() {
726
- return this.address.includes(".") && Math.random() < 0.005; // Resolving domain should be a rare process because default rpcs throttle if we resolve too much
126
+ return initDbHandlerIfNeeded(this);
727
127
  }
728
128
  async _validateCommunitySizeSchemaAndSignatureBeforePublishing(recordToPublishRaw) {
729
129
  const log = Logger("pkc-js:local-community:_validateCommunitySchemaAndSignatureBeforePublishing");
@@ -795,2337 +195,137 @@ export class LocalCommunity extends RpcLocalCommunity {
795
195
  }
796
196
  }
797
197
  }
798
- async storeCommentEdit(commentEditRaw, challengeRequestId) {
799
- const log = Logger("pkc-js:local-community:storeCommentEdit");
800
- const strippedOutEditPublication = CommentEditPubsubMessagePublicationWithFlexibleAuthorSchema.strip().parse(commentEditRaw); // we strip out here so we don't store any extra props in commentedits table
801
- strippedOutEditPublication.author = cleanWireAuthor(strippedOutEditPublication.author); // strip runtime-only author fields (address, publicKey, etc.)
802
- // Normalize to new wire format: ensure communityPublicKey/communityName for DB columns
803
- if (!strippedOutEditPublication.communityPublicKey)
804
- strippedOutEditPublication.communityPublicKey = this.signer.address;
805
- if (!strippedOutEditPublication.communityName && isStringDomain(this.address))
806
- strippedOutEditPublication.communityName = this.address;
807
- const commentToBeEdited = this._dbHandler.queryComment(commentEditRaw.commentCid); // We assume commentToBeEdited to be defined because we already tested for its existence above
808
- if (!commentToBeEdited)
809
- throw Error("The comment to edit doesn't exist"); // unlikely error to happen, but always a good idea to verify
810
- const editSignedByOriginalAuthor = commentEditRaw.signature.publicKey === commentToBeEdited.signature.publicKey;
811
- const authorSignerAddress = await getPKCAddressFromPublicKey(commentEditRaw.signature.publicKey);
812
- const editTableRow = {
813
- ...strippedOutEditPublication,
814
- isAuthorEdit: editSignedByOriginalAuthor,
815
- authorSignerAddress,
816
- insertedAt: timestamp()
817
- };
818
- const extraPropsInEdit = remeda
819
- .difference(remeda.keys.strict(commentEditRaw), remeda.keys.strict(CommentEditPubsubMessagePublicationSchema.shape))
820
- .filter((key) => key !== "communityAddress"); // communityAddress is excluded because it's been converted to communityPublicKey/communityName above
821
- if (extraPropsInEdit.length > 0) {
822
- log("Found extra props on CommentEdit", extraPropsInEdit, "Will be adding them to extraProps column");
823
- editTableRow.extraProps = remeda.pick(commentEditRaw, extraPropsInEdit);
824
- }
825
- const isEditDuplicate = this._dbHandler.hasCommentEditWithSignatureEncoded(editTableRow.signature.signature);
826
- if (isEditDuplicate) {
827
- throw new PKCError("ERR_DUPLICATE_COMMENT_EDIT", { editTableRow });
828
- }
829
- this._dbHandler.insertCommentEdits([editTableRow]);
830
- // If author is deleting a pending or disapproved comment, purge it immediately from the database
831
- if (commentEditRaw.deleted === true) {
832
- const isPending = commentToBeEdited.pendingApproval;
833
- const disapprovalResult = this._dbHandler._queryIsCommentApproved(commentToBeEdited);
834
- const isDisapproved = disapprovalResult && !disapprovalResult.approved;
835
- if (isPending || isDisapproved) {
836
- log("Author deleted a pending/disapproved comment, purging immediately", commentEditRaw.commentCid);
837
- this._dbHandler.purgeComment(commentEditRaw.commentCid);
838
- this._communityUpdateTrigger = true;
839
- }
840
- }
198
+ async handleChallengeRequest(request, isLocalPublisher) {
199
+ return handleChallengeRequestFreeFunction(this, request, isLocalPublisher);
841
200
  }
842
- async storeCommentModeration(commentModRaw, challengeRequestId) {
843
- const log = Logger("pkc-js:local-community:storeCommentModeration");
844
- const strippedOutModPublication = CommentModerationPubsubMessagePublicationSchema.strip().parse(commentModRaw); // we strip out here so we don't store any extra props in commentedits table
845
- strippedOutModPublication.author = cleanWireAuthor(strippedOutModPublication.author); // strip runtime-only author fields (address, publicKey, etc.)
846
- // Normalize to new wire format: ensure communityPublicKey/communityName for DB columns
847
- if (!strippedOutModPublication.communityPublicKey)
848
- strippedOutModPublication.communityPublicKey = this.signer.address;
849
- if (!strippedOutModPublication.communityName && isStringDomain(this.address))
850
- strippedOutModPublication.communityName = this.address;
851
- const commentToBeEdited = this._dbHandler.queryComment(commentModRaw.commentCid); // We assume commentToBeEdited to be defined because we already tested for its existence above
852
- if (!commentToBeEdited)
853
- throw Error("The comment to edit doesn't exist"); // unlikely error to happen, but always a good idea to verify
854
- const modSignerAddress = await getPKCAddressFromPublicKey(commentModRaw.signature.publicKey);
855
- // Determine the target author signer address and domain if this moderation affects the author (ban/flair)
856
- let targetAuthorSignerAddress;
857
- let targetAuthorDomain;
858
- if (strippedOutModPublication.commentModeration.author) {
859
- // Check if the comment was published with pseudonymity - if so, get the original author address/domain
860
- const aliasInfo = this._dbHandler.queryPseudonymityAliasByCommentCid(commentModRaw.commentCid);
861
- if (aliasInfo) {
862
- targetAuthorSignerAddress = await getPKCAddressFromPublicKey(aliasInfo.originalAuthorPublicKey);
863
- targetAuthorDomain = aliasInfo.originalAuthorName || undefined;
864
- }
865
- else {
866
- targetAuthorSignerAddress = commentToBeEdited.authorSignerAddress;
867
- targetAuthorDomain = getAuthorNameFromWire(commentToBeEdited.author);
868
- }
869
- }
870
- const modTableRow = {
871
- ...strippedOutModPublication,
872
- modSignerAddress,
873
- insertedAt: timestamp(),
874
- targetAuthorSignerAddress,
875
- targetAuthorDomain
876
- };
877
- const isCommentModDuplicate = this._dbHandler.hasCommentModerationWithSignatureEncoded(modTableRow.signature.signature);
878
- if (isCommentModDuplicate) {
879
- throw new PKCError("ERR_DUPLICATE_COMMENT_MODERATION", { modTableRow });
880
- }
881
- const extraPropsInMod = remeda
882
- .difference(remeda.keys.strict(commentModRaw), remeda.keys.strict(CommentModerationPubsubMessagePublicationSchema.shape))
883
- .filter((key) => key !== "communityAddress"); // communityAddress is excluded because it's been converted to communityPublicKey/communityName above
884
- if (extraPropsInMod.length > 0) {
885
- log("Found extra props on CommentModeration", extraPropsInMod, "Will be adding them to extraProps column");
886
- modTableRow.extraProps = remeda.pick(commentModRaw, extraPropsInMod);
887
- }
888
- if (modTableRow.commentModeration.purged) {
889
- log("commentModeration.purged=true, and therefore will delete the post/comment and all its reply tree from the db as well as unpin the cids from ipfs", "comment cid is", modTableRow.commentCid);
890
- const commentToPurge = this._dbHandler.queryComment(modTableRow.commentCid);
891
- if (!commentToPurge)
892
- throw Error("Comment to purge not found");
893
- const purgedTableRows = this._dbHandler.purgeComment(modTableRow.commentCid);
894
- for (const purgedTableRow of purgedTableRows)
895
- await this._addAllCidsUnderPurgedCommentToBeRemoved(purgedTableRow);
896
- log("Purged comment", modTableRow.commentCid, "and its comment and comment update children", "out of DB and IPFS");
897
- await this._rmUnneededMfsPaths(); // not sure if needed here
898
- if (this.updateCid) {
899
- // need to remove any update cids with reference to purged comment
900
- this._blocksToRm.push(this.updateCid);
901
- this._cidsToUnPin.add(this.updateCid);
902
- }
903
- }
904
- else if ("approved" in modTableRow.commentModeration) {
905
- if (modTableRow.commentModeration.approved) {
906
- log("commentModeration.approved=true, and therefore move comment from pending approval and add it to IPFS", "comment cid is", modTableRow.commentCid);
907
- await this._addCommentRowToIPFS(commentToBeEdited, Logger("pkc-js:local-community:storeCommentModeration:_addCommentRowToIPFS"));
908
- this._dbHandler.approvePendingComment({ cid: modTableRow.commentCid });
909
- }
910
- else {
911
- const shouldPurgeDisapprovedComment = Object.keys(modTableRow.commentModeration).length === 1; // no other props were included, if so purge the comment
912
- log("commentModeration.approved=false, and therefore this comment will be removed entirely from DB", "should we purge this comment? = ", shouldPurgeDisapprovedComment, "comment cid is", modTableRow.commentCid);
913
- if (shouldPurgeDisapprovedComment)
914
- this._dbHandler.purgeComment(modTableRow.commentCid);
915
- else
916
- this._dbHandler.removeCommentFromPendingApproval({ cid: modTableRow.commentCid });
917
- }
201
+ async handleChallengeAnswer(challengeAnswer) {
202
+ return handleChallengeAnswerFreeFunction(this, challengeAnswer);
203
+ }
204
+ async handleChallengeExchange(pubsubMsg) {
205
+ return handleChallengeExchangeFreeFunction(this, pubsubMsg);
206
+ }
207
+ async _addCommentRowToIPFS(unpinnedCommentRow, log) {
208
+ const ipfsClient = this._clientsManager.getDefaultKuboRpcClient();
209
+ const finalCommentIpfsJson = deriveCommentIpfsFromCommentTableRow(unpinnedCommentRow);
210
+ const commentIpfsContent = deterministicStringify(finalCommentIpfsJson);
211
+ const contentHash = await calculateIpfsHash(commentIpfsContent);
212
+ if (contentHash !== unpinnedCommentRow.cid) {
213
+ throw Error("Unable to recreate the CommentIpfs. This is a critical error");
918
214
  }
919
- this._dbHandler.insertCommentModerations([modTableRow]);
920
- this._communityUpdateTrigger = true;
921
- log("Inserted comment moderation", "of comment", modTableRow.commentCid, "into db", "with props", modTableRow);
215
+ const addRes = await retryKuboIpfsAddAndProvide({
216
+ ipfsClient: ipfsClient._client,
217
+ log,
218
+ content: commentIpfsContent,
219
+ addOptions: { pin: true },
220
+ provideOptions: { recursive: true },
221
+ provideInBackground: false
222
+ });
223
+ if (addRes.path !== unpinnedCommentRow.cid)
224
+ throw Error("Unable to recreate the CommentIpfs. This is a critical error");
225
+ log.trace("Pinned comment", unpinnedCommentRow.cid, "of community", this.address, "to IPFS node");
922
226
  }
923
- async storeVote(newVoteProps, challengeRequestId) {
924
- const log = Logger("pkc-js:local-community:storeVote");
925
- const authorSignerAddress = await getPKCAddressFromPublicKey(newVoteProps.signature.publicKey);
926
- this._dbHandler.deleteVote(authorSignerAddress, newVoteProps.commentCid);
927
- const voteTableRow = {
928
- ...remeda.pick(newVoteProps, ["vote", "commentCid", "protocolVersion", "timestamp"]),
929
- authorSignerAddress,
930
- insertedAt: timestamp()
931
- };
932
- const extraPropsInVote = remeda.difference(remeda.keys.strict(newVoteProps), remeda.keys.strict(VotePubsubMessagePublicationSchema.shape));
933
- if (extraPropsInVote.length > 0) {
934
- log("Found extra props on Vote", extraPropsInVote, "Will be adding them to extraProps column");
935
- voteTableRow.extraProps = remeda.pick(newVoteProps, extraPropsInVote);
227
+ async _assertDomainResolvesCorrectly(newAddressAsDomain) {
228
+ if (isStringDomain(newAddressAsDomain)) {
229
+ const resolvedIpnsFromNewDomain = await this._clientsManager.resolveCommunityNameIfNeeded({
230
+ communityName: newAddressAsDomain,
231
+ // Admin domain edits don't need second-fresh data.
232
+ cache: { maxAge: 600 }
233
+ });
234
+ if (resolvedIpnsFromNewDomain !== this.signer.address)
235
+ throw new PKCError("ERR_DOMAIN_COMMUNITY_ADDRESS_TXT_RECORD_POINT_TO_DIFFERENT_ADDRESS", {
236
+ currentCommunityAddress: this.address,
237
+ newAddressAsDomain,
238
+ resolvedIpnsFromNewDomain,
239
+ signerAddress: this.signer.address,
240
+ started: this.started
241
+ });
936
242
  }
937
- this._dbHandler.insertVotes([voteTableRow]);
938
- log("Inserted vote", "of comment", voteTableRow.commentCid, "into db", "with props", voteTableRow);
939
- return undefined;
940
243
  }
941
- async storeCommunityEditPublication(editProps, challengeRequestId) {
942
- const log = Logger("pkc-js:local-community:storeCommunityEdit");
943
- const authorSignerAddress = await getPKCAddressFromPublicKey(editProps.signature.publicKey);
944
- const authorIdentity = getAuthorNameFromWire(editProps.author) || authorSignerAddress;
945
- log("Received community edit", editProps.communityEdit, "from author", authorIdentity, "with signer address", authorSignerAddress, "Will be using these props to edit the community props");
946
- const propsAfterEdit = remeda.pick(this, remeda.keys.strict(editProps.communityEdit));
947
- log("Current props from community edit (not edited yet)", propsAfterEdit);
948
- lodashDeepMerge(propsAfterEdit, editProps.communityEdit);
949
- await this.edit(propsAfterEdit);
950
- return undefined;
244
+ async start() {
245
+ return lifecycleStart(this);
951
246
  }
952
- isPublicationReply(publication) {
953
- return Boolean(publication.parentCid);
247
+ async update() {
248
+ return lifecycleUpdate(this);
954
249
  }
955
- isPublicationPost(publication) {
956
- return !publication.parentCid;
250
+ async stop() {
251
+ return lifecycleStop(this);
957
252
  }
958
- async _calculateLinkProps(link) {
959
- if (!link || !this.settings?.fetchThumbnailUrls)
960
- return undefined;
961
- return getThumbnailPropsOfLink(link, this, this.settings.fetchThumbnailUrlsProxyUrl);
253
+ async delete() {
254
+ return deleteCommunity(this);
962
255
  }
963
- async _calculateLatestPostProps() {
964
- this._dbHandler.createTransaction();
965
- const previousCid = this._dbHandler.queryLatestPostCid()?.cid;
966
- this._dbHandler.commitTransaction();
967
- return { depth: 0, previousCid };
256
+ async edit(newCommunityOptions) {
257
+ return (await editCommunity(this, newCommunityOptions));
968
258
  }
969
- async _calculateReplyProps(comment) {
970
- if (!comment.parentCid)
971
- throw Error("Reply has to have parentCid");
972
- this._dbHandler.createTransaction();
973
- const commentsUnderParent = this._dbHandler.queryCommentsUnderComment(comment.parentCid);
974
- const parent = this._dbHandler.queryComment(comment.parentCid);
975
- this._dbHandler.commitTransaction();
976
- if (!parent)
977
- throw Error("Failed to find parent of reply");
978
- return {
979
- depth: parent.depth + 1,
980
- postCid: parent.postCid,
981
- previousCid: commentsUnderParent[0]?.cid
982
- };
259
+ // The three helpers below stay as methods (in addition to being free functions in their
260
+ // respective modules) because integration tests in test/node/community/ monkey-patch
261
+ // community._xxx = async () => { throw ... } to inject failures into the start/publish
262
+ // loops. Production callers in lifecycle.ts/db-state.ts/etc. go through these methods
263
+ // (not the bare imports) so the patches still take effect.
264
+ async _getDbInternalState(includeMutable = false) {
265
+ return getDbInternalState(this, includeMutable);
983
266
  }
984
- async _resolveAliasPrivateKeyForCommentPublication(opts) {
985
- if (opts.mode === "per-post") {
986
- // For a new post (no postCid yet), always generate a fresh alias; once stored the postCid will be used for reuse.
987
- if (opts.postCid) {
988
- const existing = this._dbHandler.queryPseudonymityAliasForPost(opts.originalAuthorPublicKey, opts.postCid);
989
- if (existing?.aliasPrivateKey)
990
- return existing.aliasPrivateKey;
991
- }
992
- return (await this._pkc.createSigner()).privateKey;
993
- }
994
- else if (opts.mode === "per-reply") {
995
- const signer = await this._pkc.createSigner();
996
- return signer.privateKey;
997
- }
998
- else if (opts.mode === "per-author") {
999
- const existing = this._dbHandler.queryPseudonymityAliasForAuthor(opts.originalAuthorPublicKey);
1000
- if (existing?.aliasPrivateKey)
1001
- return existing.aliasPrivateKey;
1002
- const signer = await this._pkc.createSigner();
1003
- return signer.privateKey;
1004
- }
1005
- else
1006
- throw Error(`Unsupported pseudonymityMode (${opts.mode})`);
267
+ async _listenToIncomingRequests() {
268
+ return listenToIncomingRequests(this);
1007
269
  }
1008
- async _prepareCommentWithAnonymity(originalComment) {
1009
- const mode = this.features?.pseudonymityMode;
1010
- if (!mode)
1011
- return { publication: originalComment };
1012
- // Mods (owner, admin, moderator) are never pseudonymized
1013
- const isAuthorMod = await this._isPublicationAuthorPartOfRoles(originalComment, ["owner", "admin", "moderator"]);
1014
- if (isAuthorMod)
1015
- return { publication: originalComment };
1016
- const originalAuthorPublicKey = originalComment.signature.publicKey;
1017
- const postCid = originalComment.postCid;
1018
- const aliasPrivateKey = await this._resolveAliasPrivateKeyForCommentPublication({
1019
- mode,
1020
- originalAuthorPublicKey,
1021
- postCid
1022
- });
1023
- const aliasSigner = await this._pkc.createSigner({ privateKey: aliasPrivateKey, type: "ed25519" });
1024
- const displayName = originalComment.author?.displayName;
1025
- const sanitizedAuthor = cleanWireAuthor(displayName !== undefined ? { displayName } : undefined);
1026
- const anonymizedComment = remeda.clone(originalComment);
1027
- if (sanitizedAuthor !== undefined) {
1028
- anonymizedComment.author = sanitizedAuthor;
1029
- }
1030
- else {
1031
- delete anonymizedComment.author;
1032
- }
1033
- anonymizedComment.signature = await signComment({
1034
- comment: { ...anonymizedComment, signer: aliasSigner, communityAddress: this.address },
1035
- pkc: this._pkc
1036
- });
1037
- return {
1038
- publication: anonymizedComment,
1039
- anonymity: {
1040
- aliasPrivateKey,
1041
- originalAuthorPublicKey,
1042
- mode,
1043
- originalComment
1044
- }
1045
- };
270
+ async _repinCommentsIPFSIfNeeded() {
271
+ return repinCommentsIPFSIfNeeded(this);
1046
272
  }
1047
- async _prepareCommentEditWithAlias(originalEdit) {
1048
- const aliasSignerOfComment = this._dbHandler.queryPseudonymityAliasByCommentCid(originalEdit.commentCid);
1049
- if (!aliasSignerOfComment)
1050
- return originalEdit;
1051
- const aliasSigner = await this._pkc.createSigner({
1052
- privateKey: aliasSignerOfComment.aliasPrivateKey,
1053
- type: "ed25519"
1054
- });
1055
- const commentEditSignedByAlias = remeda.clone(originalEdit);
1056
- delete commentEditSignedByAlias.author;
1057
- commentEditSignedByAlias.signature = await signCommentEdit({
1058
- edit: { ...commentEditSignedByAlias, signer: aliasSigner, communityAddress: this.address },
1059
- pkc: this._pkc
1060
- });
1061
- return commentEditSignedByAlias;
273
+ // Kept as a method (in addition to the free function) because the pseudonymityMode
274
+ // integration tests in test/node/community/features/ invoke it directly on the
275
+ // community instance to set up parent comments. Not on master's public API surface,
276
+ // but treated as one by those tests.
277
+ async storePublication(request, pendingApproval) {
278
+ return storePublication(this, request, pendingApproval);
1062
279
  }
1063
- async storeComment(opts) {
1064
- const { commentPubsub, pendingApproval, pseudonymityMode, originalCommentSignatureEncoded } = opts;
1065
- const log = Logger("pkc-js:local-community:handleChallengeExchange:storeComment");
1066
- const commentIpfs = {
1067
- ...commentPubsub,
1068
- ...(await this._calculateLinkProps(commentPubsub.link)),
1069
- ...(this.isPublicationPost(commentPubsub) && (await this._calculateLatestPostProps())),
1070
- ...(this.isPublicationReply(commentPubsub) && (await this._calculateReplyProps(commentPubsub))),
1071
- ...(pseudonymityMode ? { pseudonymityMode } : {})
1072
- };
1073
- // Normalize to new wire format: ensure communityPublicKey/communityName, remove old communityAddress
1074
- commentIpfs.communityPublicKey = this.signer.address;
1075
- if (isStringDomain(this.address))
1076
- commentIpfs.communityName = this.address;
1077
- delete commentIpfs.communityAddress;
1078
- // Strip runtime-only author fields (nameResolved, address, publicKey, etc.) before IPFS storage
1079
- commentIpfs.author = cleanWireAuthor(commentIpfs.author);
1080
- const ipfsClient = this._clientsManager.getDefaultKuboRpcClient();
1081
- const file = pendingApproval
1082
- ? undefined
1083
- : await retryKuboIpfsAddAndProvide({
1084
- ipfsClient: ipfsClient._client,
1085
- log,
1086
- content: deterministicStringify(commentIpfs),
1087
- addOptions: { pin: true },
1088
- provideOptions: { recursive: true },
1089
- provideInBackground: false
1090
- });
1091
- const commentCid = file?.path || (await calculateIpfsCidV0(deterministicStringify(commentIpfs)));
1092
- const postCid = commentIpfs.postCid || commentCid; // if postCid is not defined, then we're adding a post to IPFS, so its own cid is the postCid
1093
- const authorSignerAddress = await getPKCAddressFromPublicKey(commentPubsub.signature.publicKey);
1094
- const strippedOutCommentIpfs = CommentIpfsSchema.strip().parse(commentIpfs); // remove unknown props
1095
- strippedOutCommentIpfs.author = cleanWireAuthor(strippedOutCommentIpfs.author); // strip runtime-only author fields (address, publicKey, etc.)
1096
- const signaturesToCheck = Array.from(new Set([commentPubsub.signature.signature, originalCommentSignatureEncoded].filter((sig) => typeof sig === "string")));
1097
- const isCommentDuplicate = signaturesToCheck.some((signatureEncoded) => this._dbHandler.hasCommentWithSignatureEncoded(signatureEncoded));
1098
- if (isCommentDuplicate) {
1099
- this._cidsToUnPin.add(commentCid);
1100
- throw new PKCError("ERR_DUPLICATE_COMMENT", { file, commentIpfs, commentPubsub });
1101
- }
1102
- const commentRow = {
1103
- ...strippedOutCommentIpfs,
1104
- cid: commentCid,
1105
- postCid,
1106
- authorSignerAddress,
1107
- insertedAt: timestamp(),
1108
- pendingApproval
1109
- };
1110
- const unknownProps = remeda
1111
- .difference(remeda.keys.strict(commentPubsub), remeda.keys.strict(CommentPubsubMessagePublicationSchema.shape))
1112
- .filter((key) => key !== "communityAddress"); // communityAddress is excluded because it's been converted to communityPublicKey/communityName above
1113
- if (unknownProps.length > 0) {
1114
- log("Found extra props on Comment", unknownProps, "Will be adding them to extraProps column");
1115
- commentRow.extraProps = remeda.pick(commentPubsub, unknownProps);
1116
- }
1117
- if (originalCommentSignatureEncoded)
1118
- commentRow.originalCommentSignatureEncoded = originalCommentSignatureEncoded;
1119
- // we may need to query comment and verify its signature
1120
- this._dbHandler.createTransaction();
1121
- try {
1122
- if (!pendingApproval) {
1123
- const { number, postNumber } = this._dbHandler.getNextCommentNumbers(commentRow.depth);
1124
- commentRow.number = number;
1125
- if (typeof postNumber === "number")
1126
- commentRow.postNumber = postNumber;
1127
- }
1128
- this._dbHandler.insertComments([commentRow]);
1129
- if (typeof this.settings?.maxPendingApprovalCount === "number")
1130
- this._dbHandler.removeOldestPendingCommentIfWeHitMaxPendingCount(this.settings.maxPendingApprovalCount);
1131
- this._dbHandler.commitTransaction();
1132
- }
1133
- catch (e) {
1134
- this._dbHandler.rollbackTransaction();
1135
- throw e;
1136
- }
1137
- log("Inserted comment", commentRow.cid, "into db", "with props", commentRow);
1138
- return { comment: commentIpfs, cid: commentCid };
280
+ // Method facades for helpers stubbed/called as methods by garbage.collection and
281
+ // start.community integration tests. Internal production callers in lifecycle.ts
282
+ // and the facade above go through these methods (not the bare imports) so test
283
+ // stubs intercept correctly.
284
+ async updateCommunityIpnsIfNeeded(args) {
285
+ return updateCommunityIpnsIfNeeded(this, args.commentUpdateRowsToPublishToIpfs);
1139
286
  }
1140
- async storePublication(request, pendingApproval) {
1141
- if (request.vote)
1142
- return this.storeVote(request.vote, request.challengeRequestId);
1143
- else if (request.commentEdit) {
1144
- const commentEditWithAlias = await this._prepareCommentEditWithAlias(request.commentEdit);
1145
- return this.storeCommentEdit(commentEditWithAlias, request.challengeRequestId);
1146
- }
1147
- else if (request.commentModeration)
1148
- return this.storeCommentModeration(request.commentModeration, request.challengeRequestId);
1149
- else if (request.comment) {
1150
- const originalCommentSignatureEncoded = request.comment.signature.signature;
1151
- const { publication, anonymity } = await this._prepareCommentWithAnonymity(request.comment);
1152
- const storedComment = await this.storeComment({
1153
- commentPubsub: publication,
1154
- pendingApproval,
1155
- pseudonymityMode: anonymity?.mode,
1156
- originalCommentSignatureEncoded: anonymity ? originalCommentSignatureEncoded : undefined
1157
- });
1158
- if (anonymity)
1159
- this._dbHandler.insertPseudonymityAliases([
1160
- {
1161
- commentCid: storedComment.cid,
1162
- aliasPrivateKey: anonymity.aliasPrivateKey,
1163
- originalAuthorPublicKey: anonymity.originalAuthorPublicKey,
1164
- originalAuthorName: getAuthorNameFromWire(anonymity.originalComment.author) || null,
1165
- mode: anonymity.mode,
1166
- insertedAt: timestamp()
1167
- }
1168
- ]);
1169
- return storedComment;
1170
- }
1171
- else if (request.communityEdit)
1172
- return this.storeCommunityEditPublication(request.communityEdit, request.challengeRequestId);
1173
- else
1174
- throw Error("Don't know how to store this publication" + request);
287
+ async _addOldPageCidsToCidsToUnpin(curPages, newPages, addToBlockRm) {
288
+ return addOldPageCidsToCidsToUnpin(this, curPages, newPages, addToBlockRm);
1175
289
  }
1176
- async _decryptOrRespondWithFailure(request) {
1177
- const log = Logger("pkc-js:local-community:_decryptOrRespondWithFailure");
1178
- try {
1179
- return await decryptEd25519AesGcmPublicKeyBuffer(request.encrypted, this.signer.privateKey, request.signature.publicKey);
1180
- }
1181
- catch (e) {
1182
- log.error(`Failed to decrypt request (${request.challengeRequestId.toString()}) due to error`, e);
1183
- await this._publishFailedChallengeVerification({ reason: messages.ERR_COMMUNITY_FAILED_TO_DECRYPT_PUBSUB_MSG }, request.challengeRequestId);
1184
- throw e;
1185
- }
290
+ shouldResolveDomainForVerification() {
291
+ return shouldResolveDomainForVerification(this);
1186
292
  }
1187
- async _respondWithErrorIfSignatureOfPublicationIsInvalid(request) {
1188
- let validity;
1189
- if (request.comment)
1190
- validity = await verifyCommentPubsubMessage({
1191
- comment: request.comment,
1192
- resolveAuthorNames: this._pkc.resolveAuthorNames,
1193
- clientsManager: this._clientsManager
1194
- });
1195
- else if (request.commentEdit)
1196
- validity = await verifyCommentEdit({
1197
- edit: request.commentEdit,
1198
- resolveAuthorNames: this._pkc.resolveAuthorNames,
1199
- clientsManager: this._clientsManager
1200
- });
1201
- else if (request.vote)
1202
- validity = await verifyVote({
1203
- vote: request.vote,
1204
- resolveAuthorNames: this._pkc.resolveAuthorNames,
1205
- clientsManager: this._clientsManager
1206
- });
1207
- else if (request.commentModeration)
1208
- validity = await verifyCommentModeration({
1209
- moderation: request.commentModeration,
1210
- resolveAuthorNames: this._pkc.resolveAuthorNames,
1211
- clientsManager: this._clientsManager
1212
- });
1213
- else if (request.communityEdit)
1214
- validity = await verifyCommunityEdit({
1215
- communityEdit: request.communityEdit,
1216
- resolveAuthorNames: this._pkc.resolveAuthorNames,
1217
- clientsManager: this._clientsManager
1218
- });
1219
- else
1220
- throw Error("Can't detect the type of publication");
1221
- if (!validity.valid) {
1222
- await this._publishFailedChallengeVerification({ reason: validity.reason }, request.challengeRequestId);
1223
- throw new PKCError(getErrorCodeFromMessage(validity.reason), { request, validity });
1224
- }
293
+ // Method facades for helpers stubbed by the garbage.collection.community test's
294
+ // mock community (it relies on these being instance methods for vi.fn() overrides).
295
+ // Production callers in ipns-publishing.ts/lifecycle.ts/editing.ts go through the
296
+ // methods so the stubs intercept.
297
+ async _calculateNewPostUpdates() {
298
+ return calculateNewPostUpdates(this);
1225
299
  }
1226
- async _publishChallenges(challenges, request) {
1227
- const log = Logger("pkc-js:local-community:_publishChallenges");
1228
- const toEncryptChallenge = { challenges };
1229
- const toSignChallenge = cleanUpBeforePublishing({
1230
- type: "CHALLENGE",
1231
- protocolVersion: env.PROTOCOL_VERSION,
1232
- userAgent: this._pkc.userAgent,
1233
- challengeRequestId: request.challengeRequestId,
1234
- encrypted: await encryptEd25519AesGcmPublicKeyBuffer(deterministicStringify(toEncryptChallenge), this.signer.privateKey, request.signature.publicKey),
1235
- timestamp: timestamp()
1236
- });
1237
- const challengeMessage = {
1238
- ...toSignChallenge,
1239
- signature: await signChallengeMessage({ challengeMessage: toSignChallenge, signer: this.signer })
1240
- };
1241
- const pubsubClient = this._clientsManager.getDefaultKuboPubsubClient();
1242
- this._clientsManager.updateKuboRpcPubsubState("publishing-challenge", pubsubClient.url);
1243
- // we only publish over pubsub if the challenge exchange is not ongoing for local publishers
1244
- if (!this._challengeExchangesFromLocalPublishers[request.challengeRequestId.toString()])
1245
- await this._clientsManager.pubsubPublish(this.pubsubTopicWithfallback(), challengeMessage);
1246
- log(`Community ${this.address} with pubsub topic ${this.pubsubTopicWithfallback()} published ${challengeMessage.type} over pubsub: `, remeda.pick(toSignChallenge, ["timestamp"]), toEncryptChallenge.challenges.map((challenge) => challenge.type));
1247
- this._clientsManager.updateKuboRpcPubsubState("waiting-challenge-answers", pubsubClient.url);
1248
- this.emit("challenge", {
1249
- ...challengeMessage,
1250
- challenges
1251
- });
300
+ _calculateLatestUpdateTrigger() {
301
+ return calculateLatestUpdateTrigger(this);
1252
302
  }
1253
- async _publishFailedChallengeVerification(result, challengeRequestId) {
1254
- // challengeSucess=false
1255
- const log = Logger("pkc-js:local-community:_publishFailedChallengeVerification");
1256
- const toSignVerification = cleanUpBeforePublishing({
1257
- type: "CHALLENGEVERIFICATION",
1258
- challengeRequestId: challengeRequestId,
1259
- challengeSuccess: false,
1260
- challengeErrors: result.challengeErrors,
1261
- reason: result.reason,
1262
- userAgent: this._pkc.userAgent,
1263
- protocolVersion: env.PROTOCOL_VERSION,
1264
- timestamp: timestamp()
1265
- });
1266
- const challengeVerification = {
1267
- ...toSignVerification,
1268
- signature: await signChallengeVerification({ challengeVerification: toSignVerification, signer: this.signer })
1269
- };
1270
- const pubsubClient = this._clientsManager.getDefaultKuboPubsubClient();
1271
- this._clientsManager.updateKuboRpcPubsubState("publishing-challenge-verification", pubsubClient.url);
1272
- log(`Will publish ${challengeVerification.type} over pubsub topic ${this.pubsubTopicWithfallback()} on community ${this.address}:`, remeda.omit(toSignVerification, ["challengeRequestId"]));
1273
- if (!this._challengeExchangesFromLocalPublishers[challengeRequestId.toString()])
1274
- await this._clientsManager.pubsubPublish(this.pubsubTopicWithfallback(), challengeVerification);
1275
- this._clientsManager.updateKuboRpcPubsubState("waiting-challenge-requests", pubsubClient.url);
1276
- this.emit("challengeverification", challengeVerification);
1277
- this._ongoingChallengeExchanges.delete(challengeRequestId.toString());
1278
- delete this._challengeExchangesFromLocalPublishers[challengeRequestId.toString()];
1279
- this._cleanUpChallengeAnswerPromise(challengeRequestId.toString());
303
+ async _resolveIpnsAndLogIfPotentialProblematicSequence() {
304
+ return resolveIpnsAndLogIfPotentialProblematicSequence(this);
1280
305
  }
1281
- async _publishIdempotentDuplicateVerification(request, challengeRequestId, duplicateReason) {
1282
- const log = Logger("pkc-js:local-community:_publishIdempotentDuplicateVerification");
1283
- let encrypted;
1284
- let toEncryptDecrypted;
1285
- // For comments, include the existing comment data in the encrypted response
1286
- if (duplicateReason === messages.ERR_DUPLICATE_COMMENT && request.comment) {
1287
- const existingComment = this._dbHandler.queryCommentBySignatureEncoded(request.comment.signature.signature);
1288
- if (!existingComment) {
1289
- return this._publishFailedChallengeVerification({ reason: duplicateReason }, challengeRequestId);
1290
- }
1291
- log("Returning idempotent success for duplicate comment", existingComment.cid);
1292
- const authorSignerAddress = await getPKCAddressFromPublicKey(existingComment.signature.publicKey);
1293
- const authorDomain = getAuthorNameFromWire(existingComment.author);
1294
- const authorCommunity = this._dbHandler.queryCommunityAuthor(authorSignerAddress, authorDomain);
1295
- if (!authorCommunity) {
1296
- return this._publishFailedChallengeVerification({ reason: duplicateReason }, challengeRequestId);
1297
- }
1298
- const commentNumberPostNumber = this._dbHandler._assignNumbersForComment(existingComment.cid);
1299
- const commentUpdateNoSig = cleanUpBeforePublishing({
1300
- author: { community: authorCommunity },
1301
- cid: existingComment.cid,
1302
- protocolVersion: env.PROTOCOL_VERSION,
1303
- ...commentNumberPostNumber
1304
- });
1305
- const commentUpdate = {
1306
- ...commentUpdateNoSig,
1307
- signature: await signCommentUpdateForChallengeVerification({
1308
- update: commentUpdateNoSig,
1309
- signer: this.signer
1310
- })
1311
- };
1312
- const commentIpfs = CommentIpfsSchema.strip().parse(existingComment);
1313
- toEncryptDecrypted = { comment: commentIpfs, commentUpdate };
1314
- encrypted = await encryptEd25519AesGcmPublicKeyBuffer(deterministicStringify(toEncryptDecrypted), this.signer.privateKey, request.signature.publicKey);
1315
- }
1316
- else {
1317
- // For edits/moderations: success has no encrypted data (same as normal success)
1318
- log("Returning idempotent success for duplicate", duplicateReason);
1319
- }
1320
- const toSignMsg = cleanUpBeforePublishing({
1321
- type: "CHALLENGEVERIFICATION",
1322
- challengeRequestId,
1323
- encrypted,
1324
- challengeSuccess: true,
1325
- reason: undefined,
1326
- userAgent: this._pkc.userAgent,
1327
- protocolVersion: env.PROTOCOL_VERSION,
1328
- timestamp: timestamp()
1329
- });
1330
- const challengeVerification = {
1331
- ...toSignMsg,
1332
- signature: await signChallengeVerification({ challengeVerification: toSignMsg, signer: this.signer })
1333
- };
1334
- const pubsubClient = this._clientsManager.getDefaultKuboPubsubClient();
1335
- this._clientsManager.updateKuboRpcPubsubState("publishing-challenge-verification", pubsubClient.url);
1336
- if (!this._challengeExchangesFromLocalPublishers[challengeRequestId.toString()])
1337
- await this._clientsManager.pubsubPublish(this.pubsubTopicWithfallback(), challengeVerification);
1338
- this._clientsManager.updateKuboRpcPubsubState("waiting-challenge-requests", pubsubClient.url);
1339
- const objectToEmit = { ...challengeVerification, ...toEncryptDecrypted };
1340
- this.emit("challengeverification", objectToEmit);
1341
- this._ongoingChallengeExchanges.delete(challengeRequestId.toString());
1342
- delete this._challengeExchangesFromLocalPublishers[challengeRequestId.toString()];
1343
- this._cleanUpChallengeAnswerPromise(challengeRequestId.toString());
306
+ async _updateDbInternalState(props) {
307
+ return updateDbInternalState(this, props);
1344
308
  }
1345
- async _storePublicationAndEncryptForChallengeVerification(request, pendingApproval) {
1346
- const commentAfterAddingToIpfs = await this.storePublication(request, pendingApproval);
1347
- if (!commentAfterAddingToIpfs)
1348
- return undefined;
1349
- const authorSignerAddress = await getPKCAddressFromPublicKey(commentAfterAddingToIpfs.comment.signature.publicKey);
1350
- const authorDomain = getAuthorNameFromWire(commentAfterAddingToIpfs.comment.author);
1351
- const authorCommunity = this._dbHandler.queryCommunityAuthor(authorSignerAddress, authorDomain);
1352
- if (!authorCommunity)
1353
- throw Error("author.community can never be undefined after adding a comment");
1354
- const commentNumberPostNumber = this._dbHandler._assignNumbersForComment(commentAfterAddingToIpfs.cid);
1355
- const commentUpdateOfVerificationNoSignature = (cleanUpBeforePublishing({
1356
- author: { community: authorCommunity },
1357
- cid: commentAfterAddingToIpfs.cid,
1358
- protocolVersion: env.PROTOCOL_VERSION,
1359
- pendingApproval,
1360
- ...commentNumberPostNumber
1361
- }));
1362
- const commentUpdate = {
1363
- ...commentUpdateOfVerificationNoSignature,
1364
- signature: await signCommentUpdateForChallengeVerification({
1365
- update: commentUpdateOfVerificationNoSignature,
1366
- signer: this.signer
1367
- })
1368
- };
1369
- const toEncrypt = { comment: commentAfterAddingToIpfs.comment, commentUpdate };
1370
- const encrypted = await encryptEd25519AesGcmPublicKeyBuffer(deterministicStringify(toEncrypt), this.signer.privateKey, request.signature.publicKey);
1371
- return { ...toEncrypt, encrypted };
309
+ // Method facades for additional private helpers that integration tests
310
+ // (unique.publishing, purge.expire.rejection, edgecases.page.generation)
311
+ // invoke directly on the instance via `(ctx.community as LocalCommunity)._foo(...)`.
312
+ async _updateCommentsThatNeedToBeUpdated() {
313
+ return updateCommentsThatNeedToBeUpdated(this);
1372
314
  }
1373
- async _publishChallengeVerification(challengeResult, request, pendingApproval) {
1374
- const log = Logger("pkc-js:local-community:_publishChallengeVerification");
1375
- if (!challengeResult.challengeSuccess)
1376
- return this._publishFailedChallengeVerification(challengeResult, request.challengeRequestId);
1377
- else {
1378
- // Challenge has passed, we store the publication (except if there's an issue with the publication)
1379
- // call below could fail if the comment is duplicated
1380
- let failureReason;
1381
- let toEncrypt;
1382
- try {
1383
- toEncrypt = await this._storePublicationAndEncryptForChallengeVerification(request, pendingApproval);
1384
- }
1385
- catch (e) {
1386
- failureReason = e.message;
1387
- log.error("Failed to store store Publication And Encrypt For ChallengeVerification", e);
1388
- }
1389
- const toSignMsg = cleanUpBeforePublishing({
1390
- type: "CHALLENGEVERIFICATION",
1391
- challengeRequestId: request.challengeRequestId,
1392
- encrypted: toEncrypt?.encrypted, // could be undefined
1393
- challengeErrors: challengeResult.challengeErrors,
1394
- userAgent: this._pkc.userAgent,
1395
- protocolVersion: env.PROTOCOL_VERSION,
1396
- timestamp: timestamp(),
1397
- ...(failureReason ? { reason: failureReason, challengeSuccess: false } : { challengeSuccess: true, reason: undefined })
1398
- });
1399
- const challengeVerification = {
1400
- ...toSignMsg,
1401
- signature: await signChallengeVerification({ challengeVerification: toSignMsg, signer: this.signer })
1402
- };
1403
- const pubsubClient = this._clientsManager.getDefaultKuboPubsubClient();
1404
- this._clientsManager.updateKuboRpcPubsubState("publishing-challenge-verification", pubsubClient.url);
1405
- if (!this._challengeExchangesFromLocalPublishers[request.challengeRequestId.toString()])
1406
- await this._clientsManager.pubsubPublish(this.pubsubTopicWithfallback(), challengeVerification);
1407
- this._clientsManager.updateKuboRpcPubsubState("waiting-challenge-requests", pubsubClient.url);
1408
- const objectToEmit = { ...challengeVerification, ...toEncrypt };
1409
- this.emit("challengeverification", objectToEmit);
1410
- this._ongoingChallengeExchanges.delete(request.challengeRequestId.toString());
1411
- delete this._challengeExchangesFromLocalPublishers[request.challengeRequestId.toString()];
1412
- this._cleanUpChallengeAnswerPromise(request.challengeRequestId.toString());
1413
- log.trace(`Published ${challengeVerification.type} over pubsub topic ${this.pubsubTopicWithfallback()}:`, remeda.omit(objectToEmit, ["signature", "encrypted", "challengeRequestId"]));
1414
- }
315
+ async _purgeDisapprovedCommentsOlderThan() {
316
+ return purgeDisapprovedCommentsOlderThan(this);
1415
317
  }
1416
- async _isPublicationAuthorPartOfRoles(publication, rolesToCheckAgainst) {
1417
- if (!this.roles)
1418
- return false;
1419
- // is the author of publication a moderator?
1420
- const signerAddress = await getPKCAddressFromPublicKey(publication.signature.publicKey);
1421
- if (rolesToCheckAgainst.includes(this.roles[signerAddress]?.role))
1422
- return true;
1423
- const authorName = getAuthorNameFromWire(publication.author);
1424
- if (typeof authorName === "string") {
1425
- if (rolesToCheckAgainst.includes(this.roles[authorName]?.role))
1426
- return true;
1427
- if (this._pkc.resolveAuthorNames && isStringDomain(authorName)) {
1428
- const { resolvedAuthorName: resolvedSignerAddress } = await this._clientsManager.resolveAuthorNameIfNeeded({
1429
- authorName,
1430
- abortSignal: AbortSignal.timeout(this._pkc._timeouts["resolve-author-name"]),
1431
- // Mod authority must reflect current state — bypass cache.
1432
- cache: { maxAge: 0 }
1433
- });
1434
- if (resolvedSignerAddress !== signerAddress)
1435
- return false;
1436
- if (rolesToCheckAgainst.includes(this.roles[resolvedSignerAddress]?.role))
1437
- return true;
1438
- }
1439
- }
1440
- return false;
318
+ async _publishChallengeVerification(challengeResult, request) {
319
+ return publishChallengeVerification(this, challengeResult, request);
1441
320
  }
1442
- async _checkPublicationValidity(request, publication, authorCommunity) {
1443
- const log = Logger("pkc-js:local-community:handleChallengeRequest:checkPublicationValidity");
1444
- // Reject deprecated old wire format fields
1445
- if ("subplebbitAddress" in publication)
1446
- return messages.ERR_PUBLICATION_USES_DEPRECATED_SUBPLEBBIT_ADDRESS;
1447
- // reject run time field
1448
- if ("communityAddress" in publication)
1449
- return messages.ERR_PUBLICATION_USES_DEPRECATED_COMMUNITY_ADDRESS;
1450
- // communityPublicKey must be present and match this community's IPNS key
1451
- const pubCommunityPublicKey = getCommunityPublicKeyFromWire(publication);
1452
- if (!pubCommunityPublicKey || pubCommunityPublicKey !== this.signer.address)
1453
- return messages.ERR_PUBLICATION_INVALID_COMMUNITY_PUBLIC_KEY;
1454
- // communityName, if present, must match this community's address
1455
- const pubCommunityName = getCommunityNameFromWire(publication);
1456
- if (pubCommunityName && pubCommunityName !== this.address)
1457
- return messages.ERR_PUBLICATION_INVALID_COMMUNITY_NAME;
1458
- if (publication.timestamp <= timestamp() - 5 * 60 || publication.timestamp >= timestamp() + 5 * 60)
1459
- return messages.ERR_PUBLICATION_TIMESTAMP_IS_NOT_IN_PROPER_RANGE;
1460
- if (typeof authorCommunity?.banExpiresAt === "number" && authorCommunity.banExpiresAt > timestamp())
1461
- return messages.ERR_AUTHOR_IS_BANNED;
1462
- if (publication.author && remeda.intersection(remeda.keys.strict(publication.author), AuthorReservedFields).length > 0)
1463
- return messages.ERR_PUBLICATION_AUTHOR_HAS_RESERVED_FIELD;
1464
- // Reject publications with non-domain author.name — author.name must be a domain or absent
1465
- const authorName = getAuthorNameFromWire(publication.author);
1466
- if (authorName && !isStringDomain(authorName)) {
1467
- log("Rejecting publication: author.name is not a domain", authorName);
1468
- return messages.ERR_AUTHOR_NAME_MUST_BE_A_DOMAIN;
1469
- }
1470
- // Reject publications with author domains that can't be resolved or don't match the signer
1471
- // Use this._clientsManager (not this._pkc) so nameResolver state changes emit on the community's clients
1472
- if (authorName && isStringDomain(authorName) && this._pkc.resolveAuthorNames) {
1473
- let resolvedAddress;
1474
- try {
1475
- ({ resolvedAuthorName: resolvedAddress } = await this._clientsManager.resolveAuthorNameIfNeeded({
1476
- authorName,
1477
- abortSignal: AbortSignal.timeout(this._pkc._timeouts["resolve-author-name"]),
1478
- // Incoming pub validation: 30m staleness window is acceptable; domain transfers are rare.
1479
- cache: { maxAge: 1800 }
1480
- }));
1481
- }
1482
- catch (e) {
1483
- log("Rejecting publication with unresolvable author domain", authorName, e);
1484
- return messages.ERR_FAILED_TO_RESOLVE_AUTHOR_DOMAIN;
1485
- }
1486
- if (resolvedAddress === null) {
1487
- log("Rejecting publication: author domain could not be resolved", authorName);
1488
- return messages.ERR_FAILED_TO_RESOLVE_AUTHOR_DOMAIN;
1489
- }
1490
- const signerAddress = await getPKCAddressFromPublicKey(publication.signature.publicKey);
1491
- if (resolvedAddress !== signerAddress) {
1492
- log("Rejecting publication: author domain resolves to different signer", authorName, resolvedAddress, signerAddress);
1493
- return messages.ERR_AUTHOR_DOMAIN_RESOLVES_TO_DIFFERENT_SIGNER;
1494
- }
1495
- }
1496
- if ("commentCid" in publication || "parentCid" in publication) {
1497
- // vote or reply or commentEdit or commentModeration
1498
- // not post though
1499
- //@ts-expect-error
1500
- const parentCid = publication.parentCid || publication.commentCid;
1501
- if (typeof parentCid !== "string")
1502
- return messages.ERR_COMMUNITY_PUBLICATION_PARENT_CID_NOT_DEFINED;
1503
- const parent = this._dbHandler.queryComment(parentCid);
1504
- if (!parent)
1505
- return messages.ERR_PUBLICATION_PARENT_DOES_NOT_EXIST_IN_COMMUNITY;
1506
- const parentFlags = this._dbHandler.queryCommentFlagsSetByMod(parentCid);
1507
- if (parentFlags.removed && !request.commentModeration)
1508
- // not allowed to vote or reply under removed comments
1509
- return messages.ERR_COMMUNITY_PUBLICATION_PARENT_HAS_BEEN_REMOVED;
1510
- const isParentDeletedQueryRes = this._dbHandler.queryAuthorEditDeleted(parentCid);
1511
- if (isParentDeletedQueryRes?.deleted && !request.commentModeration)
1512
- return messages.ERR_COMMUNITY_PUBLICATION_PARENT_HAS_BEEN_DELETED; // not allowed to vote or reply under deleted comments
1513
- const postFlags = this._dbHandler.queryCommentFlagsSetByMod(parent.postCid);
1514
- if (postFlags.removed && !request.commentModeration)
1515
- return messages.ERR_COMMUNITY_PUBLICATION_POST_HAS_BEEN_REMOVED;
1516
- const isPostDeletedQueryRes = this._dbHandler.queryAuthorEditDeleted(parent.postCid);
1517
- if (isPostDeletedQueryRes?.deleted && !request.commentModeration)
1518
- return messages.ERR_COMMUNITY_PUBLICATION_POST_HAS_BEEN_DELETED;
1519
- if (postFlags.locked && !request.commentModeration)
1520
- return messages.ERR_COMMUNITY_PUBLICATION_POST_IS_LOCKED;
1521
- if (postFlags.archived && !request.commentModeration)
1522
- return messages.ERR_COMMUNITY_PUBLICATION_POST_IS_ARCHIVED;
1523
- if (parent.timestamp > publication.timestamp)
1524
- return messages.ERR_COMMUNITY_COMMENT_TIMESTAMP_IS_EARLIER_THAN_PARENT;
1525
- // if user publishes vote/reply/commentEdit under pending comment, it should fail
1526
- if (parent.pendingApproval && !("commentModeration" in request) && !(request.commentEdit?.deleted === true))
1527
- return messages.ERR_USER_PUBLISHED_UNDER_PENDING_COMMENT;
1528
- const isCommentDisapproved = this._dbHandler._queryIsCommentApproved(parent);
1529
- if (isCommentDisapproved &&
1530
- !isCommentDisapproved.approved &&
1531
- !("commentModeration" in request) &&
1532
- !(request.commentEdit?.deleted === true))
1533
- return messages.ERR_USER_PUBLISHED_UNDER_DISAPPROVED_COMMENT;
1534
- }
1535
- // Reject publications if their size is over 40kb
1536
- const publicationKilobyteSize = Buffer.byteLength(JSON.stringify(publication)) / 1000;
1537
- if (publicationKilobyteSize > 40)
1538
- return messages.ERR_REQUEST_PUBLICATION_OVER_ALLOWED_SIZE;
1539
- if (request.comment) {
1540
- const commentPublication = request.comment;
1541
- if (remeda.intersection(remeda.keys.strict(commentPublication), CommentPubsubMessageReservedFields).length > 0)
1542
- return messages.ERR_COMMENT_HAS_RESERVED_FIELD;
1543
- if (this.features?.requirePostLink &&
1544
- !commentPublication.parentCid &&
1545
- (!commentPublication.link || (!this.features?.requirePostLinkIsMedia && !isLinkValid(commentPublication.link))))
1546
- return messages.ERR_COMMENT_HAS_INVALID_LINK_FIELD;
1547
- if (this.features?.requirePostLinkIsMedia &&
1548
- commentPublication.link &&
1549
- (!isLinkValid(commentPublication.link) || !isLinkOfMedia(commentPublication.link)))
1550
- return messages.ERR_POST_LINK_IS_NOT_OF_MEDIA;
1551
- if (this.features?.requireReplyLink &&
1552
- commentPublication.parentCid &&
1553
- (!commentPublication.link || (!this.features?.requireReplyLinkIsMedia && !isLinkValid(commentPublication.link))))
1554
- return messages.ERR_REPLY_HAS_INVALID_LINK_FIELD;
1555
- if (this.features?.requireReplyLinkIsMedia &&
1556
- commentPublication.parentCid &&
1557
- commentPublication.link &&
1558
- (!isLinkValid(commentPublication.link) || !isLinkOfMedia(commentPublication.link)))
1559
- return messages.ERR_REPLY_LINK_IS_NOT_OF_MEDIA;
1560
- if (this.features?.noMarkdownImages && commentPublication.content && contentContainsMarkdownImages(commentPublication.content))
1561
- return messages.ERR_COMMENT_CONTENT_CONTAINS_MARKDOWN_IMAGE;
1562
- if (this.features?.noMarkdownVideos && commentPublication.content && contentContainsMarkdownVideos(commentPublication.content))
1563
- return messages.ERR_COMMENT_CONTENT_CONTAINS_MARKDOWN_VIDEO;
1564
- if (this.features?.noMarkdownAudio && commentPublication.content && contentContainsMarkdownAudio(commentPublication.content))
1565
- return messages.ERR_COMMENT_CONTENT_CONTAINS_MARKDOWN_AUDIO;
1566
- // noImages - block ALL comments with image links
1567
- if (this.features?.noImages && commentPublication.link && isLinkOfImage(commentPublication.link))
1568
- return messages.ERR_COMMENT_HAS_LINK_THAT_IS_IMAGE;
1569
- // noVideos - block ALL comments with video links (including animated images like GIF/APNG)
1570
- if (this.features?.noVideos &&
1571
- commentPublication.link &&
1572
- (isLinkOfVideo(commentPublication.link) || isLinkOfAnimatedImage(commentPublication.link)))
1573
- return messages.ERR_COMMENT_HAS_LINK_THAT_IS_VIDEO;
1574
- // noSpoilers - block ALL comments with spoiler=true
1575
- if (this.features?.noSpoilers && commentPublication.spoiler === true)
1576
- return messages.ERR_COMMENT_HAS_SPOILER_ENABLED;
1577
- // noImageReplies - block only replies with image links
1578
- if (this.features?.noImageReplies &&
1579
- commentPublication.parentCid &&
1580
- commentPublication.link &&
1581
- isLinkOfImage(commentPublication.link))
1582
- return messages.ERR_REPLY_HAS_LINK_THAT_IS_IMAGE;
1583
- // noVideoReplies - block only replies with video links (including animated images like GIF/APNG)
1584
- if (this.features?.noVideoReplies &&
1585
- commentPublication.parentCid &&
1586
- commentPublication.link &&
1587
- (isLinkOfVideo(commentPublication.link) || isLinkOfAnimatedImage(commentPublication.link)))
1588
- return messages.ERR_REPLY_HAS_LINK_THAT_IS_VIDEO;
1589
- // noAudio - block ALL comments with audio links
1590
- if (this.features?.noAudio && commentPublication.link && isLinkOfAudio(commentPublication.link))
1591
- return messages.ERR_COMMENT_HAS_LINK_THAT_IS_AUDIO;
1592
- // noAudioReplies - block only replies with audio links
1593
- if (this.features?.noAudioReplies &&
1594
- commentPublication.parentCid &&
1595
- commentPublication.link &&
1596
- isLinkOfAudio(commentPublication.link))
1597
- return messages.ERR_REPLY_HAS_LINK_THAT_IS_AUDIO;
1598
- // noSpoilerReplies - block only replies with spoiler=true
1599
- if (this.features?.noSpoilerReplies && commentPublication.parentCid && commentPublication.spoiler === true)
1600
- return messages.ERR_REPLY_HAS_SPOILER_ENABLED;
1601
- // noNestedReplies - block replies with depth > 1 (replies to replies)
1602
- if (this.features?.noNestedReplies && commentPublication.parentCid) {
1603
- const parent = this._dbHandler.queryComment(commentPublication.parentCid);
1604
- if (parent && parent.depth > 0) {
1605
- return messages.ERR_NESTED_REPLIES_NOT_ALLOWED;
1606
- }
1607
- }
1608
- // Post flairs validation (comment.flairs)
1609
- if (commentPublication.flairs && commentPublication.flairs.length > 0) {
1610
- if (!this.features?.postFlairs) {
1611
- return messages.ERR_POST_FLAIRS_NOT_ALLOWED;
1612
- }
1613
- const allowedPostFlairs = this.flairs?.["post"] || [];
1614
- for (const flair of commentPublication.flairs) {
1615
- if (!this._isFlairInAllowedList(flair, allowedPostFlairs)) {
1616
- return messages.ERR_POST_FLAIR_NOT_IN_ALLOWED_FLAIRS;
1617
- }
1618
- }
1619
- }
1620
- // requirePostFlairs - only for posts (depth=0)
1621
- if (this.features?.requirePostFlairs && !commentPublication.parentCid) {
1622
- if (!commentPublication.flairs || commentPublication.flairs.length === 0) {
1623
- return messages.ERR_POST_FLAIRS_REQUIRED;
1624
- }
1625
- }
1626
- // Author flairs validation (comment.author.flairs)
1627
- if (commentPublication.author?.flairs && commentPublication.author.flairs.length > 0 && !this.features?.pseudonymityMode) {
1628
- if (!this.features?.authorFlairs) {
1629
- return messages.ERR_AUTHOR_FLAIRS_NOT_ALLOWED;
1630
- }
1631
- const allowedAuthorFlairs = this.flairs?.["author"] || [];
1632
- for (const flair of commentPublication.author.flairs) {
1633
- if (!this._isFlairInAllowedList(flair, allowedAuthorFlairs)) {
1634
- return messages.ERR_AUTHOR_FLAIR_NOT_IN_ALLOWED_FLAIRS;
1635
- }
1636
- }
1637
- }
1638
- // requireAuthorFlairs - for all comments (posts and replies)
1639
- if (this.features?.requireAuthorFlairs && !this.features?.pseudonymityMode) {
1640
- if (!commentPublication.author?.flairs || commentPublication.author.flairs.length === 0) {
1641
- return messages.ERR_AUTHOR_FLAIRS_REQUIRED;
1642
- }
1643
- }
1644
- if (commentPublication.parentCid && !commentPublication.postCid)
1645
- return messages.ERR_REPLY_HAS_NOT_DEFINED_POST_CID;
1646
- if (commentPublication.parentCid) {
1647
- // query parents, and make sure commentPublication.postCid is the final parent
1648
- const parentsOfComment = this._dbHandler.queryParentsCids({ parentCid: commentPublication.parentCid });
1649
- if (parentsOfComment[parentsOfComment.length - 1].cid !== commentPublication.postCid)
1650
- return messages.ERR_REPLY_POST_CID_IS_NOT_PARENT_OF_REPLY;
1651
- }
1652
- // Validate quotedCids
1653
- if (commentPublication.quotedCids && commentPublication.quotedCids.length > 0) {
1654
- // Only replies can have quotedCids
1655
- if (!commentPublication.parentCid) {
1656
- return messages.ERR_POST_CANNOT_HAVE_QUOTED_CIDS;
1657
- }
1658
- const threadPostCid = commentPublication.postCid; // postCid is always defined for replies
1659
- for (const quotedCid of commentPublication.quotedCids) {
1660
- // 1. Check existence
1661
- const quotedComment = this._dbHandler.queryComment(quotedCid);
1662
- if (!quotedComment) {
1663
- return messages.ERR_QUOTED_CID_DOES_NOT_EXIST;
1664
- }
1665
- // 2. Check quoted comment is under the same post
1666
- const quotedPostCid = quotedComment.depth === 0 ? quotedComment.cid : quotedComment.postCid;
1667
- if (quotedPostCid !== threadPostCid) {
1668
- return messages.ERR_QUOTED_CID_NOT_UNDER_POST;
1669
- }
1670
- // 3. Check not pending approval
1671
- if (quotedComment.pendingApproval) {
1672
- return messages.ERR_QUOTED_CID_IS_PENDING_APPROVAL;
1673
- }
1674
- }
1675
- }
1676
- const isCommentDuplicate = this._dbHandler.hasCommentWithSignatureEncoded(commentPublication.signature.signature);
1677
- if (isCommentDuplicate)
1678
- return messages.ERR_DUPLICATE_COMMENT;
1679
- }
1680
- else if (request.vote) {
1681
- const votePublication = request.vote;
1682
- if (remeda.intersection(VotePubsubReservedFields, remeda.keys.strict(votePublication)).length > 0)
1683
- return messages.ERR_VOTE_HAS_RESERVED_FIELD;
1684
- if (this.features?.noUpvotes && votePublication.vote === 1)
1685
- return messages.ERR_NOT_ALLOWED_TO_PUBLISH_UPVOTES;
1686
- if (this.features?.noDownvotes && votePublication.vote === -1)
1687
- return messages.ERR_NOT_ALLOWED_TO_PUBLISH_DOWNVOTES;
1688
- const commentToVoteOn = this._dbHandler.queryComment(request.vote.commentCid);
1689
- if (this.features?.noPostDownvotes && commentToVoteOn.depth === 0 && votePublication.vote === -1)
1690
- return messages.ERR_NOT_ALLOWED_TO_PUBLISH_POST_DOWNVOTES;
1691
- if (this.features?.noPostUpvotes && commentToVoteOn.depth === 0 && votePublication.vote === 1)
1692
- return messages.ERR_NOT_ALLOWED_TO_PUBLISH_POST_UPVOTES;
1693
- if (this.features?.noReplyDownvotes && commentToVoteOn.depth > 0 && votePublication.vote === -1)
1694
- return messages.ERR_NOT_ALLOWED_TO_PUBLISH_REPLY_DOWNVOTES;
1695
- if (this.features?.noReplyUpvotes && commentToVoteOn.depth > 0 && votePublication.vote === 1)
1696
- return messages.ERR_NOT_ALLOWED_TO_PUBLISH_REPLY_UPVOTES;
1697
- const voteAuthorSignerAddress = await getPKCAddressFromPublicKey(votePublication.signature.publicKey);
1698
- const previousVote = this._dbHandler.queryVote(commentToVoteOn.cid, voteAuthorSignerAddress);
1699
- if (!previousVote && votePublication.vote === 0)
1700
- return messages.ERR_THERE_IS_NO_PREVIOUS_VOTE_TO_CANCEL;
1701
- }
1702
- else if (request.commentModeration) {
1703
- const commentModerationPublication = request.commentModeration;
1704
- if (remeda.intersection(CommentModerationReservedFields, remeda.keys.strict(commentModerationPublication)).length > 0)
1705
- return messages.ERR_COMMENT_MODERATION_HAS_RESERVED_FIELD;
1706
- const isAuthorMod = await this._isPublicationAuthorPartOfRoles(commentModerationPublication, ["owner", "moderator", "admin"]);
1707
- if (!isAuthorMod)
1708
- return messages.ERR_COMMENT_MODERATION_ATTEMPTED_WITHOUT_BEING_MODERATOR;
1709
- const commentToBeEdited = this._dbHandler.queryComment(commentModerationPublication.commentCid); // We assume commentToBeEdited to be defined because we already tested for its existence above
1710
- if (!commentToBeEdited)
1711
- return messages.ERR_COMMENT_MODERATION_NO_COMMENT_TO_EDIT;
1712
- if (isAuthorMod && commentModerationPublication.commentModeration.locked && commentToBeEdited.depth !== 0)
1713
- return messages.ERR_COMMUNITY_COMMENT_MOD_CAN_NOT_LOCK_REPLY;
1714
- if (isAuthorMod && commentModerationPublication.commentModeration.archived && commentToBeEdited.depth !== 0)
1715
- return messages.ERR_COMMUNITY_COMMENT_MOD_CAN_NOT_ARCHIVE_REPLY;
1716
- const commentModInDb = this._dbHandler.hasCommentModerationWithSignatureEncoded(commentModerationPublication.signature.signature);
1717
- if (commentModInDb)
1718
- return messages.ERR_DUPLICATE_COMMENT_MODERATION;
1719
- if ("approved" in commentModerationPublication.commentModeration && !commentToBeEdited.pendingApproval)
1720
- return messages.ERR_MOD_ATTEMPTING_TO_APPROVE_OR_DISAPPROVE_COMMENT_THAT_IS_NOT_PENDING;
1721
- }
1722
- else if (request.communityEdit) {
1723
- const communityEdit = request.communityEdit;
1724
- if (remeda.intersection(CommunityEditPublicationPubsubReservedFields, remeda.keys.strict(communityEdit)).length > 0)
1725
- return messages.ERR_COMMUNITY_EDIT_HAS_RESERVED_FIELD;
1726
- if (communityEdit.communityEdit.roles || communityEdit.communityEdit.address) {
1727
- const isAuthorOwner = await this._isPublicationAuthorPartOfRoles(communityEdit, ["owner"]);
1728
- if (!isAuthorOwner)
1729
- return messages.ERR_COMMUNITY_EDIT_ATTEMPTED_TO_MODIFY_OWNER_EXCLUSIVE_PROPS;
1730
- }
1731
- const isAuthorOwnerOrAdmin = await this._isPublicationAuthorPartOfRoles(communityEdit, ["owner", "admin"]);
1732
- if (!isAuthorOwnerOrAdmin) {
1733
- return messages.ERR_COMMUNITY_EDIT_ATTEMPTED_TO_MODIFY_COMMUNITY_WITHOUT_BEING_OWNER_OR_ADMIN;
1734
- }
1735
- const allowedCommunityEditKeys = [...remeda.keys.strict(CommunityIpfsSchema.shape), "address"];
1736
- if (remeda.difference(remeda.keys.strict(communityEdit.communityEdit), allowedCommunityEditKeys).length > 0) {
1737
- // should only be allowed to modify public props from CommunityIpfs
1738
- // shouldn't be able to modify settings for example
1739
- return messages.ERR_COMMUNITY_EDIT_ATTEMPTED_TO_NON_PUBLIC_PROPS;
1740
- }
1741
- }
1742
- else if (request.commentEdit) {
1743
- const commentEditPublication = request.commentEdit;
1744
- if (remeda.intersection(CommentEditReservedFields, remeda.keys.strict(commentEditPublication)).length > 0)
1745
- return messages.ERR_COMMENT_EDIT_HAS_RESERVED_FIELD;
1746
- const commentToBeEdited = this._dbHandler.queryComment(commentEditPublication.commentCid); // We assume commentToBeEdited to be defined because we already tested for its existence above
1747
- if (!commentToBeEdited)
1748
- return messages.ERR_COMMENT_EDIT_NO_COMMENT_TO_EDIT;
1749
- const commentEditInDb = this._dbHandler.hasCommentEditWithSignatureEncoded(commentEditPublication.signature.signature);
1750
- if (commentEditInDb)
1751
- return messages.ERR_DUPLICATE_COMMENT_EDIT;
1752
- const aliasSignerOfComment = this._dbHandler.queryPseudonymityAliasByCommentCid(commentToBeEdited.cid);
1753
- if (aliasSignerOfComment) {
1754
- const editSignedByOriginalAuthor = commentEditPublication.signature.publicKey === aliasSignerOfComment.originalAuthorPublicKey;
1755
- if (!editSignedByOriginalAuthor)
1756
- return messages.ERR_COMMENT_EDIT_CAN_NOT_EDIT_COMMENT_IF_NOT_ORIGINAL_AUTHOR;
1757
- }
1758
- else {
1759
- const editSignedByOriginalAuthor = commentEditPublication.signature.publicKey === commentToBeEdited.signature.publicKey;
1760
- if (!editSignedByOriginalAuthor)
1761
- return messages.ERR_COMMENT_EDIT_CAN_NOT_EDIT_COMMENT_IF_NOT_ORIGINAL_AUTHOR;
1762
- }
1763
- // Validate markdown content restrictions for comment edits
1764
- if (this.features?.noMarkdownImages &&
1765
- commentEditPublication.content &&
1766
- contentContainsMarkdownImages(commentEditPublication.content))
1767
- return messages.ERR_COMMENT_CONTENT_CONTAINS_MARKDOWN_IMAGE;
1768
- if (this.features?.noMarkdownVideos &&
1769
- commentEditPublication.content &&
1770
- contentContainsMarkdownVideos(commentEditPublication.content))
1771
- return messages.ERR_COMMENT_CONTENT_CONTAINS_MARKDOWN_VIDEO;
1772
- if (this.features?.noMarkdownAudio &&
1773
- commentEditPublication.content &&
1774
- contentContainsMarkdownAudio(commentEditPublication.content))
1775
- return messages.ERR_COMMENT_CONTENT_CONTAINS_MARKDOWN_AUDIO;
1776
- // noSpoilers - block ALL comment edits that set spoiler=true
1777
- if (this.features?.noSpoilers && commentEditPublication.spoiler === true)
1778
- return messages.ERR_COMMENT_HAS_SPOILER_ENABLED;
1779
- // noSpoilerReplies - block only reply edits that set spoiler=true
1780
- if (this.features?.noSpoilerReplies && commentToBeEdited.depth > 0 && commentEditPublication.spoiler === true)
1781
- return messages.ERR_REPLY_HAS_SPOILER_ENABLED;
1782
- // Post flairs validation for comment edits
1783
- if (commentEditPublication.flairs && commentEditPublication.flairs.length > 0) {
1784
- if (!this.features?.postFlairs) {
1785
- return messages.ERR_POST_FLAIRS_NOT_ALLOWED;
1786
- }
1787
- const allowedPostFlairs = this.flairs?.["post"] || [];
1788
- for (const flair of commentEditPublication.flairs) {
1789
- if (!this._isFlairInAllowedList(flair, allowedPostFlairs)) {
1790
- return messages.ERR_POST_FLAIR_NOT_IN_ALLOWED_FLAIRS;
1791
- }
1792
- }
1793
- }
1794
- }
1795
- return undefined;
1796
- }
1797
- async _parseChallengeRequestPublicationOrRespondWithFailure(request, decryptedRawString) {
1798
- let decryptedJson;
1799
- try {
1800
- decryptedJson = parseJsonWithPKCErrorIfFails(decryptedRawString);
1801
- }
1802
- catch (e) {
1803
- await this._publishFailedChallengeVerification({ reason: messages.ERR_REQUEST_ENCRYPTED_IS_INVALID_JSON_AFTER_DECRYPTION }, request.challengeRequestId);
1804
- throw e;
1805
- }
1806
- const parseRes = DecryptedChallengeRequestSchema.loose().safeParse(decryptedJson);
1807
- if (!parseRes.success) {
1808
- await this._publishFailedChallengeVerification({ reason: messages.ERR_REQUEST_ENCRYPTED_HAS_INVALID_SCHEMA_AFTER_DECRYPTING }, request.challengeRequestId);
1809
- throw new PKCError("ERR_REQUEST_ENCRYPTED_HAS_INVALID_SCHEMA_AFTER_DECRYPTING", {
1810
- decryptedJson,
1811
- schemaError: parseRes.error
1812
- });
1813
- }
1814
- return decryptedJson;
1815
- }
1816
- _buildRuntimeChallengeRequestPublication({ publication, authorCommunity }) {
1817
- return {
1818
- ...publication,
1819
- author: buildRuntimeAuthor({
1820
- author: publication.author,
1821
- signaturePublicKey: publication.signature.publicKey,
1822
- community: authorCommunity
1823
- })
1824
- };
1825
- }
1826
- _buildRuntimeChallengeRequest({ request, authorCommunity }) {
1827
- // This function needs to be updated everytime we add a new publication type
1828
- const runtimeRequest = remeda.clone(request);
1829
- if (request.comment)
1830
- runtimeRequest.comment = this._buildRuntimeChallengeRequestPublication({
1831
- publication: request.comment,
1832
- authorCommunity
1833
- });
1834
- if (request.vote)
1835
- runtimeRequest.vote = this._buildRuntimeChallengeRequestPublication({
1836
- publication: request.vote,
1837
- authorCommunity
1838
- });
1839
- if (request.commentEdit)
1840
- runtimeRequest.commentEdit = this._buildRuntimeChallengeRequestPublication({
1841
- publication: request.commentEdit,
1842
- authorCommunity
1843
- });
1844
- if (request.commentModeration)
1845
- runtimeRequest.commentModeration = this._buildRuntimeChallengeRequestPublication({
1846
- publication: request.commentModeration,
1847
- authorCommunity
1848
- });
1849
- if (request.communityEdit)
1850
- runtimeRequest.communityEdit = this._buildRuntimeChallengeRequestPublication({
1851
- publication: request.communityEdit,
1852
- authorCommunity
1853
- });
1854
- return runtimeRequest;
1855
- }
1856
- async handleChallengeRequest(request, isLocalPublisher) {
1857
- const log = Logger("pkc-js:local-community:handleChallengeRequest");
1858
- if (this._ongoingChallengeExchanges.has(request.challengeRequestId.toString())) {
1859
- log("Received a duplicate challenge request", request.challengeRequestId.toString());
1860
- return; // This is a duplicate challenge request
1861
- }
1862
- if (isLocalPublisher) {
1863
- // we need to mark the challenge exchange as ongoing for local publishers and skip publishing it over pubsub
1864
- log("Marking challenge exchange as ongoing for local publisher");
1865
- this._challengeExchangesFromLocalPublishers[request.challengeRequestId.toString()] = true;
1866
- }
1867
- this._ongoingChallengeExchanges.set(request.challengeRequestId.toString(), true);
1868
- const requestSignatureValidation = await verifyChallengeRequest({ request, validateTimestampRange: true });
1869
- if (!requestSignatureValidation.valid)
1870
- throw new PKCError(getErrorCodeFromMessage(requestSignatureValidation.reason), {
1871
- challengeRequest: remeda.omit(request, ["encrypted"])
1872
- });
1873
- const decryptedRawString = await this._decryptOrRespondWithFailure(request);
1874
- const decryptedRequest = await this._parseChallengeRequestPublicationOrRespondWithFailure(request, decryptedRawString);
1875
- const publicationFieldNames = remeda.keys.strict(DecryptedChallengeRequestPublicationSchema.shape);
1876
- let publication;
1877
- try {
1878
- publication = derivePublicationFromChallengeRequest(decryptedRequest);
1879
- }
1880
- catch {
1881
- return this._publishFailedChallengeVerification({ reason: messages.ERR_CHALLENGE_REQUEST_ENCRYPTED_HAS_NO_PUBLICATION_AFTER_DECRYPTING }, request.challengeRequestId);
1882
- }
1883
- let publicationCount = 0;
1884
- publicationFieldNames.forEach((pubField) => {
1885
- if (pubField in decryptedRequest)
1886
- publicationCount++;
1887
- });
1888
- if (publicationCount > 1)
1889
- return this._publishFailedChallengeVerification({ reason: messages.ERR_CHALLENGE_REQUEST_ENCRYPTED_HAS_MULTIPLE_PUBLICATIONS_AFTER_DECRYPTING }, request.challengeRequestId);
1890
- // Reject deprecated wire format fields early, before signature verification
1891
- // (these fields are never in signedPropertyNames and would otherwise fail with a generic error)
1892
- if ("subplebbitAddress" in publication) {
1893
- return this._publishFailedChallengeVerification({ reason: messages.ERR_PUBLICATION_USES_DEPRECATED_SUBPLEBBIT_ADDRESS }, request.challengeRequestId);
1894
- }
1895
- if ("communityAddress" in publication) {
1896
- return this._publishFailedChallengeVerification({ reason: messages.ERR_PUBLICATION_USES_DEPRECATED_COMMUNITY_ADDRESS }, request.challengeRequestId);
1897
- }
1898
- const authorSignerAddress = await getPKCAddressFromPublicKey(publication.signature.publicKey);
1899
- const authorDomain = getAuthorNameFromWire(publication.author);
1900
- // Check publication props validity
1901
- const communityAuthor = this._dbHandler.queryCommunityAuthor(authorSignerAddress, authorDomain);
1902
- const decryptedRequestMsg = { ...request, ...decryptedRequest };
1903
- const decryptedRequestWithCommunityAuthor = this._buildRuntimeChallengeRequest({
1904
- request: decryptedRequestMsg,
1905
- authorCommunity: communityAuthor
1906
- });
1907
- try {
1908
- await this._respondWithErrorIfSignatureOfPublicationIsInvalid(decryptedRequestMsg); // This function will throw an error if signature is invalid
1909
- }
1910
- catch (e) {
1911
- log.error("Signature of challengerequest.publication is invalid, emitting an error event and aborting the challenge exchange", e);
1912
- this.emit("challengerequest", decryptedRequestWithCommunityAuthor);
1913
- return;
1914
- }
1915
- log.trace("Received a valid challenge request", decryptedRequestWithCommunityAuthor);
1916
- this.emit("challengerequest", decryptedRequestWithCommunityAuthor);
1917
- const publicationInvalidityReason = await this._checkPublicationValidity(decryptedRequestMsg, publication, communityAuthor);
1918
- if (publicationInvalidityReason) {
1919
- if (DUPLICATE_PUBLICATION_ERRORS.has(publicationInvalidityReason)) {
1920
- const sig = publication.signature.signature;
1921
- const attempts = (this._duplicatePublicationAttempts.get(sig) || 0) + 1;
1922
- this._duplicatePublicationAttempts.set(sig, attempts);
1923
- if (attempts <= 1) {
1924
- return this._publishIdempotentDuplicateVerification(decryptedRequestMsg, request.challengeRequestId, publicationInvalidityReason);
1925
- }
1926
- }
1927
- return this._publishFailedChallengeVerification({ reason: publicationInvalidityReason }, request.challengeRequestId);
1928
- }
1929
- const answerPromiseKey = decryptedRequestWithCommunityAuthor.challengeRequestId.toString();
1930
- const getChallengeAnswers = async (challenges) => {
1931
- // ...get challenge answers from user. e.g.:
1932
- // step 1. community publishes challenge pubsub message with `challenges` provided in argument of `getChallengeAnswers`
1933
- // step 2. community waits for challenge answer pubsub message with `challengeAnswers` and then returns `challengeAnswers`
1934
- await this._publishChallenges(challenges, decryptedRequestWithCommunityAuthor);
1935
- const challengeAnswerPromise = new Promise((resolve, reject) => this._challengeAnswerResolveReject.set(answerPromiseKey, { resolve, reject }));
1936
- this._challengeAnswerPromises.set(answerPromiseKey, challengeAnswerPromise);
1937
- const challengeAnswers = await this._challengeAnswerPromises.get(answerPromiseKey);
1938
- if (!challengeAnswers)
1939
- throw Error("Failed to retrieve challenge answers from promise. This is a critical error");
1940
- this._cleanUpChallengeAnswerPromise(answerPromiseKey);
1941
- return challengeAnswers;
1942
- };
1943
- // NOTE: we try to get challenge verification immediately after receiving challenge request
1944
- // because some challenges are automatic and skip the challenge message
1945
- let challengeVerification;
1946
- try {
1947
- challengeVerification = await getChallengeVerification({
1948
- challengeRequestMessage: decryptedRequestWithCommunityAuthor,
1949
- community: this,
1950
- getChallengeAnswers
1951
- });
1952
- }
1953
- catch (e) {
1954
- // getChallengeVerification will throw if one of the getChallenge function throws, which indicates a bug with the challenge script
1955
- // notify the community owner that that one of his challenge is misconfigured via an error event
1956
- log.error("getChallenge failed, the community owner needs to check the challenge code. The error is: ", e);
1957
- this.emit("error", e);
1958
- // notify the author that his publication wasn't published because the community is misconfigured
1959
- challengeVerification = {
1960
- challengeSuccess: false,
1961
- reason: `One of the community challenges is misconfigured: ${e.message}`
1962
- };
1963
- }
1964
- await this._publishChallengeVerification(challengeVerification, decryptedRequestMsg, challengeVerification.pendingApproval);
1965
- }
1966
- _cleanUpChallengeAnswerPromise(challengeRequestIdString) {
1967
- this._challengeAnswerPromises.delete(challengeRequestIdString);
1968
- this._challengeAnswerResolveReject.delete(challengeRequestIdString);
1969
- delete this._challengeExchangesFromLocalPublishers[challengeRequestIdString];
1970
- }
1971
- _isFlairInAllowedList(flair, allowedFlairs) {
1972
- return allowedFlairs.some((allowed) => remeda.isDeepEqual(allowed, flair));
1973
- }
1974
- async _parseChallengeAnswerOrRespondWithFailure(challengeAnswer, decryptedRawString) {
1975
- let parsedJson;
1976
- try {
1977
- parsedJson = parseJsonWithPKCErrorIfFails(decryptedRawString);
1978
- }
1979
- catch (e) {
1980
- await this._publishFailedChallengeVerification({ reason: messages.ERR_CHALLENGE_ANSWER_IS_INVALID_JSON }, challengeAnswer.challengeRequestId);
1981
- throw e;
1982
- }
1983
- try {
1984
- return parseDecryptedChallengeAnswerWithPKCErrorIfItFails(parsedJson);
1985
- }
1986
- catch (e) {
1987
- await this._publishFailedChallengeVerification({ reason: messages.ERR_CHALLENGE_ANSWER_IS_INVALID_SCHEMA }, challengeAnswer.challengeRequestId);
1988
- throw e;
1989
- }
1990
- }
1991
- async handleChallengeAnswer(challengeAnswer) {
1992
- const log = Logger("pkc-js:local-community:handleChallengeAnswer");
1993
- if (!this._ongoingChallengeExchanges.has(challengeAnswer.challengeRequestId.toString()))
1994
- // Respond with error to answers without challenge request
1995
- return this._publishFailedChallengeVerification({ reason: messages.ERR_CHALLENGE_ANSWER_WITH_NO_CHALLENGE_REQUEST }, challengeAnswer.challengeRequestId);
1996
- const answerSignatureValidation = await verifyChallengeAnswer({ answer: challengeAnswer, validateTimestampRange: true });
1997
- if (!answerSignatureValidation.valid) {
1998
- this._cleanUpChallengeAnswerPromise(challengeAnswer.challengeRequestId.toString());
1999
- this._ongoingChallengeExchanges.delete(challengeAnswer.challengeRequestId.toString());
2000
- delete this._challengeExchangesFromLocalPublishers[challengeAnswer.challengeRequestId.toString()];
2001
- throw new PKCError(getErrorCodeFromMessage(answerSignatureValidation.reason), { challengeAnswer });
2002
- }
2003
- const decryptedRawString = await this._decryptOrRespondWithFailure(challengeAnswer);
2004
- const decryptedAnswers = await this._parseChallengeAnswerOrRespondWithFailure(challengeAnswer, decryptedRawString);
2005
- const decryptedChallengeAnswerPubsubMessage = { ...challengeAnswer, ...decryptedAnswers };
2006
- this.emit("challengeanswer", decryptedChallengeAnswerPubsubMessage);
2007
- const challengeAnswerPromise = this._challengeAnswerResolveReject.get(challengeAnswer.challengeRequestId.toString());
2008
- if (!challengeAnswerPromise)
2009
- throw Error("The challenge answer promise is undefined, there is an issue with challenge. This is a critical error");
2010
- challengeAnswerPromise.resolve(decryptedChallengeAnswerPubsubMessage.challengeAnswers);
2011
- }
2012
- async handleChallengeExchange(pubsubMsg) {
2013
- const log = Logger("pkc-js:local-community:handleChallengeExchange");
2014
- const timeReceived = timestamp();
2015
- const pubsubKilobyteSize = Buffer.byteLength(pubsubMsg.data) / 1000;
2016
- if (pubsubKilobyteSize > 80) {
2017
- log.error(`Received a pubsub message at (${timeReceived}) with size of ${pubsubKilobyteSize}. Silently dropping it`);
2018
- return;
2019
- }
2020
- let decodedMsg;
2021
- try {
2022
- decodedMsg = cborg.decode(pubsubMsg.data);
2023
- }
2024
- catch (e) {
2025
- log.error(`Failed to decode pubsub message received at (${timeReceived})`, e.toString());
2026
- return;
2027
- }
2028
- const pubsubSchemas = [
2029
- ChallengeRequestMessageSchema.loose(),
2030
- ChallengeMessageSchema.loose(),
2031
- ChallengeAnswerMessageSchema.loose(),
2032
- ChallengeVerificationMessageSchema.loose()
2033
- ];
2034
- let parsedPubsubMsg;
2035
- for (const pubsubSchema of pubsubSchemas) {
2036
- const parseRes = pubsubSchema.safeParse(decodedMsg);
2037
- if (parseRes.success) {
2038
- parsedPubsubMsg = parseRes.data;
2039
- break;
2040
- }
2041
- }
2042
- if (!parsedPubsubMsg) {
2043
- log.error(`Failed to parse the schema of pubsub message received at (${timeReceived})`, decodedMsg);
2044
- return;
2045
- }
2046
- if (parsedPubsubMsg.type === "CHALLENGE" || parsedPubsubMsg.type === "CHALLENGEVERIFICATION") {
2047
- log.trace(`Received a pubsub message that is not meant to by processed by the community - ${parsedPubsubMsg.type}. Will ignore it`);
2048
- return;
2049
- }
2050
- else if (parsedPubsubMsg.type === "CHALLENGEREQUEST") {
2051
- try {
2052
- await this.handleChallengeRequest(parsedPubsubMsg, false);
2053
- }
2054
- catch (e) {
2055
- log.error(`Failed to process challenge request message received at (${timeReceived})`, e);
2056
- this._dbHandler.rollbackTransaction();
2057
- }
2058
- }
2059
- else if (parsedPubsubMsg.type === "CHALLENGEANSWER") {
2060
- try {
2061
- await this.handleChallengeAnswer(parsedPubsubMsg);
2062
- }
2063
- catch (e) {
2064
- log.error(`Failed to process challenge answer message received at (${timeReceived})`, e);
2065
- this._dbHandler.rollbackTransaction();
2066
- }
2067
- }
2068
- }
2069
- _calculateLocalMfsPathForCommentUpdate(postDbComment, timestampRange) {
2070
- // TODO Can optimize the call below by only asking for timestamp field
2071
- return ["/" + this.address, "postUpdates", timestampRange, postDbComment.cid, "update"].join("/");
2072
- }
2073
- async _calculateNewCommentUpdate(comment) {
2074
- const log = Logger("pkc-js:local-community:_calculateNewCommentUpdate");
2075
- // If we're here that means we're gonna calculate the new update and publish it
2076
- log.trace(`Attempting to calculate new CommentUpdate for comment (${comment.cid}) on community`, this.address);
2077
- // This comment will have the local new CommentUpdate, which we will publish to IPFS fiels
2078
- // It includes new author.community as well as updated values in CommentUpdate (except for replies field)
2079
- const storedCommentUpdate = this._dbHandler.queryCommentUpdateTimestampBucketReplies({ cid: comment.cid });
2080
- const authorDomain = getAuthorNameFromWire(comment.author);
2081
- const calculatedCommentUpdate = this._dbHandler.queryCalculatedCommentUpdate({ comment, authorDomain });
2082
- log.trace("Calculated comment update for comment", comment.cid, "on community", this.address, "with reply count", calculatedCommentUpdate.replyCount);
2083
- const currentTimestamp = timestamp();
2084
- const newUpdatedAt = typeof storedCommentUpdate?.updatedAt === "number" && storedCommentUpdate.updatedAt >= currentTimestamp
2085
- ? storedCommentUpdate.updatedAt + 1
2086
- : currentTimestamp;
2087
- const commentUpdatePriorToSigning = {
2088
- ...cleanUpBeforePublishing({
2089
- ...calculatedCommentUpdate,
2090
- updatedAt: newUpdatedAt,
2091
- protocolVersion: env.PROTOCOL_VERSION
2092
- })
2093
- };
2094
- const preloadedRepliesPages = "best";
2095
- const inlineRepliesBudget = calculateInlineRepliesBudget({
2096
- comment,
2097
- commentUpdateWithoutReplies: commentUpdatePriorToSigning
2098
- });
2099
- const adjustedPreloadedRepliesPageSizeBytes = Math.max(inlineRepliesBudget, 1);
2100
- const generatedRepliesPages = comment.depth === 0
2101
- ? await this._pageGenerator.generatePostPages(comment, preloadedRepliesPages, adjustedPreloadedRepliesPageSizeBytes)
2102
- : await this._pageGenerator.generateReplyPages(comment, preloadedRepliesPages, adjustedPreloadedRepliesPageSizeBytes);
2103
- // we have to make sure not clean up submissions of authors by calling cleanUpBeforePublishing
2104
- if (generatedRepliesPages) {
2105
- if ("singlePreloadedPage" in generatedRepliesPages)
2106
- commentUpdatePriorToSigning.replies = { pages: generatedRepliesPages.singlePreloadedPage };
2107
- else if (generatedRepliesPages.pageCids) {
2108
- commentUpdatePriorToSigning.replies = {
2109
- pageCids: generatedRepliesPages.pageCids,
2110
- pages: remeda.pick(generatedRepliesPages.pages, [preloadedRepliesPages])
2111
- };
2112
- }
2113
- }
2114
- // Extract allPageCids from the generation result (not available for singlePreloadedPage case)
2115
- const allPageCids = generatedRepliesPages && !("singlePreloadedPage" in generatedRepliesPages) ? generatedRepliesPages.allPageCids : undefined;
2116
- // Unpin old page CIDs that are no longer in the new generation
2117
- {
2118
- const oldDbReplies = storedCommentUpdate?.replies;
2119
- const oldCids = new Set(oldDbReplies ? Object.values(oldDbReplies).flatMap((sort) => sort?.allPageCids ?? []) : []);
2120
- const newCids = new Set(allPageCids ? Object.values(allPageCids).flat() : []);
2121
- for (const cid of oldCids) {
2122
- if (!newCids.has(cid))
2123
- this._cidsToUnPin.add(cid);
2124
- }
2125
- }
2126
- const newCommentUpdate = {
2127
- ...commentUpdatePriorToSigning,
2128
- signature: await signCommentUpdate({ update: commentUpdatePriorToSigning, signer: this.signer })
2129
- };
2130
- await this._validateCommentUpdateSignature(newCommentUpdate, comment, log);
2131
- const newPostUpdateBucket = comment.depth === 0 ? this._postUpdatesBuckets.find((bucket) => timestamp() - bucket <= comment.timestamp) : undefined;
2132
- const newLocalMfsPath = typeof newPostUpdateBucket === "number" ? this._calculateLocalMfsPathForCommentUpdate(comment, newPostUpdateBucket) : undefined;
2133
- if (storedCommentUpdate?.postUpdatesBucket &&
2134
- newLocalMfsPath &&
2135
- newPostUpdateBucket &&
2136
- storedCommentUpdate.postUpdatesBucket !== newPostUpdateBucket) {
2137
- const oldPostUpdates = this._calculateLocalMfsPathForCommentUpdate(comment, storedCommentUpdate.postUpdatesBucket).replace("/update", "");
2138
- this._mfsPathsToRemove.add(oldPostUpdates);
2139
- }
2140
- const newCommentUpdateDbRecord = {
2141
- ...newCommentUpdate,
2142
- // Store CID refs instead of full inline page data — see deriveDbReplies()
2143
- replies: deriveDbReplies({ replies: newCommentUpdate.replies, allPageCids }),
2144
- postUpdatesBucket: newPostUpdateBucket,
2145
- publishedToPostUpdatesMFS: false,
2146
- insertedAt: timestamp()
2147
- };
2148
- return {
2149
- newCommentUpdate,
2150
- newCommentUpdateToWriteToDb: newCommentUpdateDbRecord,
2151
- localMfsPath: newLocalMfsPath,
2152
- pendingApproval: comment.pendingApproval
2153
- };
2154
- }
2155
- async _validateCommentUpdateSignature(newCommentUpdate, comment, log) {
2156
- // This function should be deleted at some point, once the protocol ossifies
2157
- const verificationOpts = {
2158
- update: newCommentUpdate,
2159
- resolveAuthorNames: false,
2160
- clientsManager: this._clientsManager,
2161
- community: this,
2162
- comment,
2163
- validatePages: this._pkc.validatePages,
2164
- validateUpdateSignature: true
2165
- };
2166
- const validation = await verifyCommentUpdate(verificationOpts);
2167
- if (!validation.valid) {
2168
- log.error(`CommentUpdate (${comment.cid}) signature is invalid due to (${validation.reason}). This is a critical error`);
2169
- throw new PKCError("ERR_COMMENT_UPDATE_SIGNATURE_IS_INVALID", { validation, verificationOpts });
2170
- }
2171
- }
2172
- async _listenToIncomingRequests() {
2173
- const log = Logger("pkc-js:local-community:sync:_listenToIncomingRequests");
2174
- // Make sure community listens to pubsub topic
2175
- // Code below is to handle in case the ipfs node restarted and the subscription got lost or something
2176
- const pubsubClient = this._clientsManager.getDefaultKuboPubsubClient();
2177
- const subscribedTopics = await pubsubClient._client.pubsub.ls();
2178
- if (!subscribedTopics.includes(this.pubsubTopicWithfallback())) {
2179
- await this._clientsManager.pubsubUnsubscribe(this.pubsubTopicWithfallback(), this.handleChallengeExchange); // Make sure it's not hanging
2180
- await this._clientsManager.pubsubSubscribe(this.pubsubTopicWithfallback(), this.handleChallengeExchange);
2181
- this._clientsManager.updateKuboRpcPubsubState("waiting-challenge-requests", pubsubClient.url);
2182
- log(`Waiting for publications on pubsub topic (${this.pubsubTopicWithfallback()})`);
2183
- }
2184
- }
2185
- async _movePostUpdatesFolderToNewAddress(oldAddress, newAddress) {
2186
- const log = Logger("pkc-js:local-community:_movePostUpdatesFolderToNewAddress");
2187
- const kuboRpc = this._clientsManager.getDefaultKuboRpcClient();
2188
- try {
2189
- await kuboRpc._client.files.mv(`/${oldAddress}`, `/${newAddress}`); // Could throw
2190
- }
2191
- catch (e) {
2192
- if (e instanceof Error && e.message !== "file does not exist") {
2193
- log.error("Failed to move directory of post updates in MFS", this.address, e);
2194
- throw e; // A critical error
2195
- }
2196
- }
2197
- }
2198
- async _updateCommentsThatNeedToBeUpdated() {
2199
- const log = Logger(`pkc-js:local-community:_updateCommentsThatNeedToBeUpdated`);
2200
- // Get all comments that need to be updated
2201
- const commentsToUpdate = this._dbHandler.queryCommentsToBeUpdated();
2202
- if (commentsToUpdate.length === 0)
2203
- return [];
2204
- this._communityUpdateTrigger = true;
2205
- log(`Will update ${commentsToUpdate.length} comments in this update loop for community (${this.address})`);
2206
- // Group by postCid
2207
- const commentsByPostCid = remeda.groupBy.strict(commentsToUpdate, (x) => x.postCid);
2208
- const allCommentUpdateRows = [];
2209
- // Process different post trees in parallel
2210
- const postLimit = pLimit(10); // Process up to 10 post trees concurrently
2211
- const postProcessingPromises = Object.entries(commentsByPostCid).map(([postCid, commentsForPost]) => postLimit(async () => {
2212
- try {
2213
- // Group by depth
2214
- const commentsByDepth = remeda.groupBy.strict(commentsForPost, (x) => x.depth);
2215
- const depthsKeySorted = remeda.keys.strict(commentsByDepth).sort((a, b) => Number(b) - Number(a)); // Sort depths from highest to lowest
2216
- const postUpdateRows = [];
2217
- // Process each depth level in sequence within this post tree
2218
- for (const depthKey of depthsKeySorted) {
2219
- const commentsAtDepth = commentsByDepth[depthKey];
2220
- // Process all comments at this depth in parallel
2221
- const depthLimit = pLimit(50);
2222
- // Calculate updates for all comments at this depth in parallel
2223
- const depthUpdatePromises = commentsAtDepth.map((comment) => depthLimit(async () => await this._calculateNewCommentUpdate(comment)));
2224
- // Wait for all comments at this depth to be calculated
2225
- const depthResults = await Promise.all(depthUpdatePromises);
2226
- // Batch write all updates for this depth to the database
2227
- this._dbHandler.upsertCommentUpdates(depthResults.map((r) => r.newCommentUpdateToWriteToDb));
2228
- // Add to our results
2229
- postUpdateRows.push(...depthResults);
2230
- }
2231
- return postUpdateRows;
2232
- }
2233
- catch (error) {
2234
- log.error(`Failed to process post tree ${postCid}:`, error);
2235
- throw error;
2236
- }
2237
- }));
2238
- // Wait for all post trees to be processed
2239
- const postResults = await Promise.all(postProcessingPromises);
2240
- // Collect all results
2241
- for (const result of postResults) {
2242
- allCommentUpdateRows.push(...result);
2243
- }
2244
- return allCommentUpdateRows;
2245
- }
2246
- async _addCommentRowToIPFS(unpinnedCommentRow, log) {
2247
- const ipfsClient = this._clientsManager.getDefaultKuboRpcClient();
2248
- const finalCommentIpfsJson = deriveCommentIpfsFromCommentTableRow(unpinnedCommentRow);
2249
- const commentIpfsContent = deterministicStringify(finalCommentIpfsJson);
2250
- const contentHash = await calculateIpfsHash(commentIpfsContent);
2251
- if (contentHash !== unpinnedCommentRow.cid) {
2252
- throw Error("Unable to recreate the CommentIpfs. This is a critical error");
2253
- }
2254
- const addRes = await retryKuboIpfsAddAndProvide({
2255
- ipfsClient: ipfsClient._client,
2256
- log,
2257
- content: commentIpfsContent,
2258
- addOptions: { pin: true },
2259
- provideOptions: { recursive: true },
2260
- provideInBackground: false
2261
- });
2262
- if (addRes.path !== unpinnedCommentRow.cid)
2263
- throw Error("Unable to recreate the CommentIpfs. This is a critical error");
2264
- log.trace("Pinned comment", unpinnedCommentRow.cid, "of community", this.address, "to IPFS node");
2265
- }
2266
- async _repinCommentsIPFSIfNeeded() {
2267
- const log = Logger("pkc-js:local-community:start:_repinCommentsIPFSIfNeeded");
2268
- const latestCommentCid = this._dbHandler.queryLatestCommentCid(); // latest comment ordered by id
2269
- if (!latestCommentCid)
2270
- return;
2271
- const kuboRpcOrHelia = this._clientsManager.getDefaultKuboRpcClient();
2272
- try {
2273
- await genToArray(kuboRpcOrHelia._client.pin.ls({ paths: latestCommentCid.cid }));
2274
- return; // the comment is already pinned, we assume the rest of the comments are so too
2275
- }
2276
- catch (e) {
2277
- if (!e.message.includes("is not pinned"))
2278
- throw e;
2279
- }
2280
- log("The latest comment is not pinned in the ipfs node, pkc-js will repin all existing comment ipfs for community", this.address);
2281
- // latestCommentCid should be the last in unpinnedCommentsFromDb array, in case we throw an error on a comment before it, it does not get pinned
2282
- const unpinnedCommentsFromDb = this._dbHandler.queryAllCommentsOrderedByIdAsc(); // we assume all comments are unpinned if latest comment is not pinned
2283
- // In the _repinCommentIpfs method:
2284
- const limit = pLimit(50);
2285
- const pinningPromises = unpinnedCommentsFromDb.map((unpinnedCommentRow) => limit(async () => {
2286
- if (unpinnedCommentRow.pendingApproval)
2287
- return; // we don't pin comments waiting to get approved
2288
- await this._addCommentRowToIPFS(unpinnedCommentRow, Logger("pkc-js:local-community:start:_repinCommentsIPFSIfNeeded:_addCommentRowToIPFS"));
2289
- }));
2290
- await Promise.all(pinningPromises);
2291
- this._dbHandler.forceUpdateOnAllComments(); // force pkc-js to republish all comment updates
2292
- log(`${unpinnedCommentsFromDb.length} comments' IPFS have been repinned`);
2293
- }
2294
- async _unpinStaleCids() {
2295
- const log = Logger("pkc-js:local-community:sync:unpinStaleCids");
2296
- if (this._cidsToUnPin.size > 0) {
2297
- const sizeBefore = this._cidsToUnPin.size;
2298
- // Create a concurrency limiter with a limit of 50
2299
- const limit = pLimit(50);
2300
- const kuboRpc = this._clientsManager.getDefaultKuboRpcClient();
2301
- // Process all unpinning in parallel with concurrency limit
2302
- await Promise.all(Array.from(this._cidsToUnPin.values()).map((cid) => limit(async () => {
2303
- try {
2304
- await kuboRpc._client.pin.rm(cid, { recursive: true });
2305
- this._cidsToUnPin.delete(cid);
2306
- }
2307
- catch (e) {
2308
- const error = e;
2309
- if (error.message.startsWith("not pinned")) {
2310
- this._cidsToUnPin.delete(cid);
2311
- }
2312
- else {
2313
- log.trace("Failed to unpin cid", cid, "on community", this.address, "due to error", error);
2314
- }
2315
- }
2316
- })));
2317
- log.trace(`unpinned ${sizeBefore - this._cidsToUnPin.size} stale cids from ipfs node for community (${this.address})`);
2318
- }
2319
- }
2320
- async _rmUnneededMfsPaths() {
2321
- const log = Logger("pkc-js:local-community:sync:_rmUnneededMfsPaths");
2322
- if (this._mfsPathsToRemove.size > 0) {
2323
- const toDeleteMfsPaths = Array.from(this._mfsPathsToRemove.values());
2324
- const kuboRpc = this._clientsManager.getDefaultKuboRpcClient();
2325
- try {
2326
- await removeMfsFilesSafely({
2327
- kuboRpcClient: kuboRpc,
2328
- paths: toDeleteMfsPaths,
2329
- log
2330
- });
2331
- toDeleteMfsPaths.forEach((path) => this._mfsPathsToRemove.delete(path));
2332
- return toDeleteMfsPaths;
2333
- }
2334
- catch (e) {
2335
- const error = e;
2336
- if (error.message.includes("file does not exist"))
2337
- return toDeleteMfsPaths; // file does not exist, we can return the paths that were not deleted
2338
- else {
2339
- log.error("Failed to remove paths from MFS", toDeleteMfsPaths, e);
2340
- throw error;
2341
- }
2342
- }
2343
- }
2344
- else
2345
- return [];
2346
- }
2347
- pubsubTopicWithfallback() {
2348
- return this.pubsubTopic || this.address;
2349
- }
2350
- async _repinCommentUpdateIfNeeded() {
2351
- const log = Logger("pkc-js:start:_repinCommentUpdateIfNeeded");
2352
- // iterating on all comment updates is not efficient, we should figure out a better way
2353
- // Most of the time we run this function, the comment updates are already written to ipfs rpeo
2354
- const kuboRpc = this._clientsManager.getDefaultKuboRpcClient();
2355
- try {
2356
- await kuboRpc._client.files.stat(`/${this.address}`, { hash: true });
2357
- return; // if the directory of this community exists, we assume all the comment updates are there
2358
- }
2359
- catch (e) {
2360
- if (!e.message.includes("file does not exist"))
2361
- throw e;
2362
- }
2363
- // community has no comment updates, we can return
2364
- if (!this.lastCommentCid)
2365
- return;
2366
- log(`CommentUpdate directory`, this.address, "will republish all comment updates");
2367
- this._dbHandler.forceUpdateOnAllComments(); // pkc-js will recalculate and publish all comment updates
2368
- }
2369
- async _syncPostUpdatesWithIpfs(commentUpdateRowsToPublishToIpfs) {
2370
- const log = Logger("pkc-js:local-community:sync:_syncPostUpdatesFilesystemWithIpfs");
2371
- const postUpdatesDirectory = `/${this.address}`;
2372
- const commentUpdatesWithLocalPath = commentUpdateRowsToPublishToIpfs.filter((row) => typeof row.localMfsPath === "string");
2373
- if (commentUpdatesWithLocalPath.length === 0)
2374
- throw Error("No comment updates of posts to publish to postUpdates directory. This is a critical bug");
2375
- const kuboRpc = this._clientsManager.getDefaultKuboRpcClient();
2376
- const removedMfsPaths = await this._rmUnneededMfsPaths();
2377
- let postUpdatesDirectoryCid;
2378
- const BATCH_SIZE = 50;
2379
- for (let index = 0; index < commentUpdatesWithLocalPath.length; index += BATCH_SIZE) {
2380
- const batch = commentUpdatesWithLocalPath.slice(index, index + BATCH_SIZE);
2381
- await Promise.all(batch.map(async (row) => {
2382
- const { localMfsPath, newCommentUpdate } = row;
2383
- const content = deterministicStringify(newCommentUpdate);
2384
- await writeKuboFilesWithTimeout({
2385
- ipfsClient: kuboRpc._client,
2386
- log,
2387
- path: localMfsPath,
2388
- content,
2389
- options: {
2390
- create: true,
2391
- truncate: true,
2392
- parents: true,
2393
- // flush: true to avoid Kubo's global Internal.MFSNoFlushLimit (default 256).
2394
- // Costs some throughput (each write self-flushes instead of batching) but
2395
- // is safe under multi-community concurrency, which the global counter is not.
2396
- flush: true
2397
- }
2398
- });
2399
- removedMfsPaths.push(localMfsPath);
2400
- }));
2401
- postUpdatesDirectoryCid = await kuboRpc._client.files.flush(postUpdatesDirectory);
2402
- }
2403
- const postUpdatesDirectoryCidString = postUpdatesDirectoryCid?.toString();
2404
- log("Community", this.address, "Synced", commentUpdatesWithLocalPath.length, "post CommentUpdates", "with MFS postUpdates directory", postUpdatesDirectoryCidString);
2405
- this._dbHandler.markCommentsAsPublishedToPostUpdates(commentUpdateRowsToPublishToIpfs.map((row) => row.newCommentUpdate.cid));
2406
- }
2407
- async _adjustPostUpdatesBucketsIfNeeded() {
2408
- if (!this.postUpdates)
2409
- return;
2410
- // Look for posts whose buckets should be changed
2411
- const log = Logger("pkc-js:local-community:start:_adjustPostUpdatesBucketsIfNeeded");
2412
- const postsWithOutdatedPostUpdateBucket = this._dbHandler.queryPostsWithOutdatedBuckets(this._postUpdatesBuckets);
2413
- if (postsWithOutdatedPostUpdateBucket.length === 0)
2414
- return;
2415
- this._dbHandler.forceUpdateOnAllCommentsWithCid(postsWithOutdatedPostUpdateBucket.map((post) => post.cid));
2416
- log(`Found ${postsWithOutdatedPostUpdateBucket.length} posts with outdated buckets and forced their updates`);
321
+ async _publishIdempotentDuplicateVerification(...args) {
322
+ return publishIdempotentDuplicateVerification(this, ...args);
2417
323
  }
2418
- async _cleanUpIpfsRepoRarely(force = false) {
2419
- const log = Logger("pkc-js:local-community:syncIpnsWithDb:_cleanUpIpfsRepoRarely");
2420
- if (Math.random() < 0.00001 || force) {
2421
- let gcCids = 0;
2422
- const kuboRpc = this._clientsManager.getDefaultKuboRpcClient();
2423
- try {
2424
- for await (const res of kuboRpc._client.repo.gc({ quiet: true })) {
2425
- if (res.cid)
2426
- gcCids++;
2427
- else
2428
- log.error("Failed to GC ipfs repo due to error", res.err);
2429
- }
2430
- }
2431
- catch (e) {
2432
- log.error("Failed to GC ipfs repo due to error", e);
2433
- }
2434
- log("GC cleaned", gcCids, "cids out of the IPFS node");
2435
- }
324
+ async _checkPublicationValidity(...args) {
325
+ return checkPublicationValidity(this, ...args);
2436
326
  }
2437
- async _providePubsubTopicRoutingCidsIfNeeded(force = false) {
2438
- const log = Logger("pkc-js:local-community:_providePubsubTopicRoutingCidsIfNeeded");
2439
- const reprovideIntervalMs = 6 * 60 * 60 * 1000;
2440
- const now = Date.now();
2441
- if (!force && this._lastPubsubTopicRoutingProvideAt && now - this._lastPubsubTopicRoutingProvideAt < reprovideIntervalMs)
2442
- return;
2443
- const pubsubTopic = this.pubsubTopicWithfallback();
2444
- const topics = [pubsubTopic, this.ipnsPubsubTopic].filter((topic) => typeof topic === "string");
2445
- if (topics.length === 0)
2446
- return;
2447
- this._lastPubsubTopicRoutingProvideAt = now;
2448
- const kuboRpcClient = this._clientsManager.getDefaultKuboRpcClient()._client;
2449
- for (const topic of topics) {
2450
- try {
2451
- await retryKuboBlockPutPinAndProvidePubsubTopic({
2452
- ipfsClient: kuboRpcClient,
2453
- log,
2454
- pubsubTopic: topic
2455
- });
2456
- }
2457
- catch (error) {
2458
- log.error("Failed to reprovide pubsub topic routing block", { topic, error });
2459
- }
2460
- }
2461
- }
2462
- async _addAllCidsUnderPurgedCommentToBeRemoved(purgedCommentAndCommentUpdate) {
2463
- this._cidsToUnPin.add(purgedCommentAndCommentUpdate.commentTableRow.cid);
2464
- this._blocksToRm.push(purgedCommentAndCommentUpdate.commentTableRow.cid);
2465
- if (typeof purgedCommentAndCommentUpdate.commentUpdateTableRow?.postUpdatesBucket === "number") {
2466
- const localCommentUpdatePath = this._calculateLocalMfsPathForCommentUpdate(purgedCommentAndCommentUpdate.commentTableRow, purgedCommentAndCommentUpdate.commentUpdateTableRow?.postUpdatesBucket);
2467
- this._mfsPathsToRemove.add(localCommentUpdatePath);
2468
- }
2469
- if (purgedCommentAndCommentUpdate?.commentUpdateTableRow?.replies) {
2470
- // replies is DbRepliesFormat — flat per-sort with allPageCids
2471
- const dbReplies = purgedCommentAndCommentUpdate.commentUpdateTableRow.replies;
2472
- for (const sortEntry of Object.values(dbReplies)) {
2473
- if (sortEntry?.allPageCids) {
2474
- for (const cid of sortEntry.allPageCids) {
2475
- this._cidsToUnPin.add(cid);
2476
- this._blocksToRm.push(cid);
2477
- }
2478
- }
2479
- }
2480
- }
2481
- }
2482
- async _purgeDisapprovedCommentsOlderThan() {
2483
- if (typeof this.settings?.purgeDisapprovedCommentsOlderThan !== "number")
2484
- return;
2485
- const log = Logger("pkc-js:local-community:_purgeDisapprovedCommentsOlderThan");
2486
- const purgedComments = this._dbHandler.purgeDisapprovedCommentsOlderThan(this.settings.purgeDisapprovedCommentsOlderThan);
2487
- if (!purgedComments || purgedComments.length === 0)
2488
- return;
2489
- log("Purged disapproved comments", purgedComments, "because retention time has passed and it's time to purge them from DB and pages");
2490
- // need to clear out any commentUpdate.postUpdatesBucket
2491
- // need to clear out any comment.cid
2492
- // need to clear out any commentUpdate.replies
2493
- for (const purgedComment of purgedComments)
2494
- for (const purgedCommentAndCommentUpdate of purgedComment.purgedTableRows)
2495
- await this._addAllCidsUnderPurgedCommentToBeRemoved(purgedCommentAndCommentUpdate);
2496
- if (this._mfsPathsToRemove.size > 0)
2497
- await this._rmUnneededMfsPaths();
2498
- if (this.updateCid) {
2499
- this._blocksToRm.push(this.updateCid); // we need to remove current updateCid which references purged comments
2500
- this._cidsToUnPin.add(this.updateCid);
2501
- }
2502
- }
2503
- async syncIpnsWithDb() {
2504
- const log = Logger("pkc-js:local-community:sync");
2505
- const kuboRpc = this._clientsManager.getDefaultKuboRpcClient();
2506
- try {
2507
- await this._listenToIncomingRequests();
2508
- await this._providePubsubTopicRoutingCidsIfNeeded();
2509
- await this._adjustPostUpdatesBucketsIfNeeded();
2510
- this._setStartedStateWithEmission("publishing-ipns");
2511
- this._clientsManager.updateKuboRpcState("publishing-ipns", kuboRpc.url);
2512
- await this._purgeDisapprovedCommentsOlderThan();
2513
- const commentUpdateRows = await this._updateCommentsThatNeedToBeUpdated();
2514
- this._requireCommunityUpdateIfModQueueChanged();
2515
- await this.updateCommunityIpnsIfNeeded(commentUpdateRows);
2516
- await this._cleanUpIpfsRepoRarely();
2517
- }
2518
- catch (e) {
2519
- //@ts-expect-error
2520
- e.details = { ...e.details, communityAddress: this.address };
2521
- const errorTyped = e;
2522
- this._setStartedStateWithEmission("failed");
2523
- this._clientsManager.updateKuboRpcState("stopped", kuboRpc.url);
2524
- log.error(`Failed to sync community`, this.address, `due to error,`, errorTyped, "Error.message", errorTyped.message, "Error keys", Object.keys(errorTyped));
2525
- throw e;
2526
- }
2527
- }
2528
- async _assertDomainResolvesCorrectly(newAddressAsDomain) {
2529
- if (isStringDomain(newAddressAsDomain)) {
2530
- const resolvedIpnsFromNewDomain = await this._clientsManager.resolveCommunityNameIfNeeded({
2531
- communityName: newAddressAsDomain,
2532
- // Admin domain edits don't need second-fresh data.
2533
- cache: { maxAge: 600 }
2534
- });
2535
- if (resolvedIpnsFromNewDomain !== this.signer.address)
2536
- throw new PKCError("ERR_DOMAIN_COMMUNITY_ADDRESS_TXT_RECORD_POINT_TO_DIFFERENT_ADDRESS", {
2537
- currentCommunityAddress: this.address,
2538
- newAddressAsDomain,
2539
- resolvedIpnsFromNewDomain,
2540
- signerAddress: this.signer.address,
2541
- started: this.started
2542
- });
2543
- }
2544
- }
2545
- async _initSignerProps(newSignerProps) {
2546
- this.signer = new SignerWithPublicKeyAddress(newSignerProps);
2547
- if (!this.signer?.ipfsKey?.byteLength || this.signer?.ipfsKey?.byteLength <= 0)
2548
- this.signer.ipfsKey = new Uint8Array(await getIpfsKeyFromPrivateKey(this.signer.privateKey));
2549
- if (!this.signer.ipnsKeyName)
2550
- this.signer.ipnsKeyName = this.signer.address;
2551
- if (!this.signer.publicKey)
2552
- this.signer.publicKey = await getPublicKeyFromPrivateKey(this.signer.privateKey);
2553
- this.encryption = {
2554
- type: "ed25519-aes-gcm",
2555
- publicKey: this.signer.publicKey
2556
- };
2557
- }
2558
- async _publishLoop(syncIntervalMs) {
2559
- const log = Logger("pkc-js:local-community:_publishLoop");
2560
- // we need to continue the loop if there's at least one pending edit
2561
- const shouldStopPublishLoop = () => {
2562
- return this.state !== "started" || (this._stopHasBeenCalled && this._pendingEditProps.length === 0);
2563
- };
2564
- const waitUntilNextSync = async () => {
2565
- const doneWithLoopTime = Date.now();
2566
- await new Promise((resolve) => {
2567
- const checkInterval = setInterval(() => {
2568
- const syncIntervalMsPassedSinceDoneWithLoop = Date.now() - doneWithLoopTime >= syncIntervalMs;
2569
- this._calculateLatestUpdateTrigger(); // will update this._communityUpdateTrigger
2570
- if (this._communityUpdateTrigger || shouldStopPublishLoop() || syncIntervalMsPassedSinceDoneWithLoop) {
2571
- clearInterval(checkInterval);
2572
- resolve(1);
2573
- }
2574
- }, 100);
2575
- });
2576
- };
2577
- while (!shouldStopPublishLoop()) {
2578
- try {
2579
- await this.syncIpnsWithDb();
2580
- }
2581
- catch (e) {
2582
- this.emit("error", e);
2583
- }
2584
- finally {
2585
- await waitUntilNextSync();
2586
- }
2587
- }
2588
- log("Stopping the publishing loop of community", this.address);
2589
- }
2590
- async _initBeforeStarting() {
2591
- this.protocolVersion = env.PROTOCOL_VERSION;
2592
- if (!this.signer?.address)
2593
- throw new PKCError("ERR_COMMUNITY_SIGNER_NOT_DEFINED");
2594
- if (!this._challengeAnswerPromises)
2595
- this._challengeAnswerPromises = new LRUCache({
2596
- max: 1000,
2597
- ttl: 600000
2598
- });
2599
- if (!this._challengeAnswerResolveReject)
2600
- this._challengeAnswerResolveReject = new LRUCache({
2601
- max: 1000,
2602
- ttl: 600000
2603
- });
2604
- if (!this._ongoingChallengeExchanges)
2605
- this._ongoingChallengeExchanges = new LRUCache({
2606
- max: 1000,
2607
- ttl: 600000
2608
- });
2609
- if (!this._duplicatePublicationAttempts)
2610
- this._duplicatePublicationAttempts = new LRUCache({
2611
- max: 1000,
2612
- ttl: 600000
2613
- });
2614
- await this._dbHandler.initDbIfNeeded();
2615
- }
2616
- async _parseRolesToEdit(newRawRoles) {
2617
- for (const [roleAddress, roleValue] of Object.entries(newRawRoles)) {
2618
- if (roleValue === undefined || roleValue === null)
2619
- continue; // skip removals
2620
- // Use this._clientsManager (not this._pkc) so nameResolver state changes emit on the community's clients
2621
- if (isStringDomain(roleAddress)) {
2622
- let resolved;
2623
- try {
2624
- ({ resolvedAuthorName: resolved } = await this._clientsManager.resolveAuthorNameIfNeeded({
2625
- authorName: roleAddress,
2626
- abortSignal: AbortSignal.timeout(this._pkc._timeouts["resolve-author-name"]),
2627
- // Role edits must apply to current state — bypass cache.
2628
- cache: { maxAge: 0 }
2629
- }));
2630
- }
2631
- catch {
2632
- resolved = null;
2633
- }
2634
- if (!resolved)
2635
- throw new PKCError("ERR_ROLE_ADDRESS_NAME_COULD_NOT_BE_RESOLVED", { roleAddress });
2636
- }
2637
- }
2638
- return remeda.omitBy(newRawRoles, (val, key) => val === undefined || val === null);
2639
- }
2640
- async _parseChallengesToEdit(newChallengeSettings) {
2641
- return {
2642
- challenges: await Promise.all(newChallengeSettings.map(async (cs) => (await getCommunityChallengeFromCommunityChallengeSettings({ communityChallengeSettings: cs, pkc: this._pkc }))
2643
- .communityChallenge)),
2644
- _usingDefaultChallenge: LocalCommunity._isDefaultChallengeStructure(newChallengeSettings)
2645
- };
2646
- }
2647
- async _validateNewAddressBeforeEditing(newAddress, log) {
2648
- if (doesDomainAddressHaveCapitalLetter(newAddress))
2649
- throw new PKCError("ERR_COMMUNITY_NAME_HAS_CAPITAL_LETTER", { communityAddress: newAddress });
2650
- // Check if any existing community (other than this one) already has an equivalent address
2651
- // This handles both exact matches and .eth/.bso alias equivalence
2652
- const existingEquivalent = this._pkc.communities.find((existing) => areEquivalentCommunityAddresses(existing, newAddress) && !areEquivalentCommunityAddresses(existing, this.address));
2653
- if (existingEquivalent)
2654
- throw new PKCError("ERR_COMMUNITY_OWNER_ATTEMPTED_EDIT_NEW_ADDRESS_THAT_ALREADY_EXISTS", {
2655
- currentCommunityAddress: this.address,
2656
- newCommunityAddress: newAddress,
2657
- currentSubs: this._pkc.communities
2658
- });
2659
- this._assertDomainResolvesCorrectly(newAddress).catch((err) => {
2660
- log.error(err);
2661
- this.emit("error", err);
2662
- });
2663
- }
2664
- async _editPropsOnStartedCommunity(parsedEditOptions) {
2665
- // 'this' is the started community with state="started"
2666
- // this._pkc._startedCommunities[this.address] === this
2667
- const log = Logger("pkc-js:local-community:start:editPropsOnStartedCommunity");
2668
- const oldAddress = remeda.clone(this.address);
2669
- if (typeof parsedEditOptions.address === "string" && this.address !== parsedEditOptions.address) {
2670
- await this._validateNewAddressBeforeEditing(parsedEditOptions.address, log);
2671
- log(`Attempting to edit community.address from ${oldAddress} to ${parsedEditOptions.address}. We will stop community first`);
2672
- await this.stop();
2673
- await this._dbHandler.changeDbFilename(oldAddress, parsedEditOptions.address);
2674
- this.setAddress(parsedEditOptions.address);
2675
- await this._dbHandler.initDbIfNeeded();
2676
- await this.start();
2677
- await this._movePostUpdatesFolderToNewAddress(oldAddress, parsedEditOptions.address);
2678
- }
2679
- const uniqueEditId = sha256(deterministicStringify(parsedEditOptions));
2680
- this._pendingEditProps.push({ ...parsedEditOptions, editId: uniqueEditId });
2681
- if (this.updateCid)
2682
- await this.initInternalCommunityAfterFirstUpdateNoMerge({
2683
- ...this.toJSONInternalAfterFirstUpdate(),
2684
- ...parsedEditOptions,
2685
- _internalStateUpdateId: uniqueEditId
2686
- });
2687
- else
2688
- await this.initInternalCommunityBeforeFirstUpdateNoMerge({
2689
- ...this.toJSONInternalBeforeFirstUpdate(),
2690
- ...parsedEditOptions,
2691
- _internalStateUpdateId: uniqueEditId
2692
- });
2693
- this._communityUpdateTrigger = true;
2694
- log(`Community (${this.address}) props (${remeda.keys.strict(parsedEditOptions)}) has been edited. Will be including edited props in next update: `, remeda.pick(this, remeda.keys.strict(parsedEditOptions)));
2695
- this.emit("update", this);
2696
- if (this.address !== oldAddress) {
2697
- trackStartedCommunity(this._pkc, this);
2698
- syncCommunityRegistryEntry(processStartedCommunities, this);
2699
- }
2700
- return this;
2701
- }
2702
- async _editPropsOnNotStartedCommunity(parsedEditOptions) {
2703
- // sceneario 3, the community is not running anywhere, we need to edit the db and update this instance
2704
- const log = Logger("pkc-js:local-community:edit:editPropsOnNotStartedCommunity");
2705
- const oldAddress = remeda.clone(this.address);
2706
- await this.initDbHandlerIfNeeded();
2707
- await this._dbHandler.initDbIfNeeded();
2708
- if (typeof parsedEditOptions.address === "string" && this.address !== parsedEditOptions.address) {
2709
- await this._validateNewAddressBeforeEditing(parsedEditOptions.address, log);
2710
- log(`Attempting to edit community.address from ${oldAddress} to ${parsedEditOptions.address}`);
2711
- // in this sceneario we're editing a community that's not started anywhere
2712
- log("will rename the community", this.address, "db in edit() because the community is not being ran anywhere else");
2713
- await this._movePostUpdatesFolderToNewAddress(this.address, parsedEditOptions.address);
2714
- this._dbHandler.destoryConnection();
2715
- await this._dbHandler.changeDbFilename(this.address, parsedEditOptions.address);
2716
- await this._dbHandler.initDbIfNeeded();
2717
- this.setAddress(parsedEditOptions.address);
2718
- }
2719
- const mergedInternalState = await this._updateDbInternalState(parsedEditOptions);
2720
- if ("updatedAt" in mergedInternalState && mergedInternalState.updatedAt)
2721
- await this.initInternalCommunityAfterFirstUpdateNoMerge(mergedInternalState);
2722
- else
2723
- await this.initInternalCommunityBeforeFirstUpdateNoMerge(mergedInternalState);
2724
- await this._dbHandler.destoryConnection();
2725
- this.emit("update", this);
2726
- return this;
2727
- }
2728
- async edit(newCommunityOptions) {
2729
- // scenearios
2730
- // 1 - calling edit() on a community instance that's not running, but the it's started in pkc._startedCommunities (should edit the started community)
2731
- // 2 - calling edit() on a community that's started in another process (should throw)
2732
- // 3 - calling edit() on a community that's not started (should load db and edit it)
2733
- // 4 - calling edit() on the community that's started (should edit the started community)
2734
- const startedCommunity = ((findStartedCommunity(this._pkc, { publicKey: this.publicKey, name: this.name }) ||
2735
- findCommunityInRegistry(processStartedCommunities, { publicKey: this.publicKey, name: this.name })));
2736
- if (startedCommunity && this.state !== "started") {
2737
- // sceneario 1
2738
- const editRes = await startedCommunity.edit(newCommunityOptions);
2739
- this.setAddress(editRes.address); // need to force an update of the address for this instance
2740
- await this._updateInstancePropsWithStartedCommunityOrDb();
2741
- return this;
2742
- }
2743
- await this.initDbHandlerIfNeeded();
2744
- await this._updateStartedValue();
2745
- if (this.started && this.state !== "started") {
2746
- // sceneario 2
2747
- this._dbHandler.destoryConnection();
2748
- throw new PKCError("ERR_CAN_NOT_EDIT_A_LOCAL_COMMUNITY_THAT_IS_ALREADY_STARTED_IN_ANOTHER_PROCESS", {
2749
- address: this.address,
2750
- dataPath: this._pkc.dataPath
2751
- });
2752
- }
2753
- const parsedEditOptions = parseCommunityEditOptionsSchemaWithPKCErrorIfItFails(newCommunityOptions);
2754
- // Convert backward-compat address → name for wire format when address is a domain
2755
- const editWithDerivedName = typeof parsedEditOptions.address === "string" && isStringDomain(parsedEditOptions.address)
2756
- ? { ...parsedEditOptions, name: parsedEditOptions.address }
2757
- : parsedEditOptions;
2758
- const newInternalProps = {
2759
- ...(editWithDerivedName.roles ? { roles: await this._parseRolesToEdit(editWithDerivedName.roles) } : undefined),
2760
- ...(editWithDerivedName?.settings?.challenges
2761
- ? await this._parseChallengesToEdit(editWithDerivedName.settings.challenges)
2762
- : undefined)
2763
- };
2764
- const newProps = {
2765
- ...remeda.omit(editWithDerivedName, ["roles"]), // we omit here to make tsc shut up
2766
- ...newInternalProps
2767
- };
2768
- if (!this.started && !startedCommunity) {
2769
- // sceneario 3
2770
- return this._editPropsOnNotStartedCommunity(newProps);
2771
- }
2772
- if (findStartedCommunity(this._pkc, { publicKey: this.publicKey, name: this.name }) === this) {
2773
- // sceneario 4
2774
- return this._editPropsOnStartedCommunity(newProps);
2775
- }
2776
- throw new Error("Can't edit a community that's started in another process");
2777
- }
2778
- async start() {
2779
- const log = Logger("pkc-js:local-community:start");
2780
- if (this.state === "updating")
2781
- throw new PKCError("ERR_NEED_TO_STOP_UPDATING_COMMUNITY_BEFORE_STARTING", { address: this.address });
2782
- this._stopHasBeenCalled = false;
2783
- this._firstUpdateAfterStart = true;
2784
- if (!this._clientsManager.getDefaultKuboRpcClientOrHelia())
2785
- throw Error("You need to define an IPFS client in your pkc instance to be able to start a local community");
2786
- await this.initDbHandlerIfNeeded();
2787
- await this._updateStartedValue();
2788
- if (this.started ||
2789
- findStartedCommunity(this._pkc, { publicKey: this.publicKey, name: this.name }) ||
2790
- findCommunityInRegistry(processStartedCommunities, { publicKey: this.publicKey, name: this.name }))
2791
- throw new PKCError("ERR_COMMUNITY_ALREADY_STARTED", { address: this.address });
2792
- try {
2793
- await this._initBeforeStarting();
2794
- // update started value twice because it could be started prior lockCommunityStart
2795
- this._setState("started");
2796
- await this._updateStartedValue();
2797
- await this._dbHandler.lockCommunityStart(); // Will throw if community is locked already
2798
- trackStartedCommunity(this._pkc, this);
2799
- syncCommunityRegistryEntry(processStartedCommunities, this);
2800
- await this._updateStartedValue();
2801
- await this._dbHandler.initDbIfNeeded();
2802
- await this._dbHandler.createOrMigrateTablesIfNeeded();
2803
- await this._updateInstanceStateWithDbState(); // sync in-memory state after potential migration
2804
- await this._setChallengesToDefaultIfNotDefined(log);
2805
- // Import community keys onto ipfs node
2806
- await this._importCommunitySignerIntoIpfsIfNeeded();
2807
- await this._providePubsubTopicRoutingCidsIfNeeded(true);
2808
- this._communityUpdateTrigger = true;
2809
- this._setStartedStateWithEmission("publishing-ipns");
2810
- await this._repinCommentsIPFSIfNeeded();
2811
- await this._repinCommentUpdateIfNeeded();
2812
- await this._listenToIncomingRequests();
2813
- this.challenges = await Promise.all(this.settings.challenges.map(async (cs) => (await getCommunityChallengeFromCommunityChallengeSettings({ communityChallengeSettings: cs, pkc: this._pkc }))
2814
- .communityChallenge)); // make sure community.challenges is using latest props from settings.challenges
2815
- }
2816
- catch (e) {
2817
- await this.stop(); // Make sure to reset the community state
2818
- //@ts-expect-error
2819
- e.details = { ...e.details, communityAddress: this.address };
2820
- throw e;
2821
- }
2822
- this._publishLoopPromise = this._publishLoop(this._pkc.publishInterval).catch((err) => {
2823
- log.error(err);
2824
- this.emit("error", err);
2825
- });
2826
- }
2827
- async _initMirroringStartedOrUpdatingCommunity(startedCommunity) {
2828
- const updatingStateChangeListener = (newState) => {
2829
- this._setUpdatingStateWithEventEmissionIfNewState(newState);
2830
- };
2831
- const startedStateChangeListener = (newState) => {
2832
- this._setStartedStateWithEmission(newState);
2833
- updatingStateChangeListener(newState);
2834
- };
2835
- const updateListener = async (updatedCommunity) => {
2836
- const startedCommunity = updatedCommunity;
2837
- if (startedCommunity.updateCid)
2838
- await this.initInternalCommunityAfterFirstUpdateNoMerge(startedCommunity.toJSONInternalAfterFirstUpdate());
2839
- else
2840
- await this.initInternalCommunityBeforeFirstUpdateNoMerge(startedCommunity.toJSONInternalBeforeFirstUpdate());
2841
- this.started = startedCommunity.started;
2842
- this.emit("update", this);
2843
- };
2844
- const stateChangeListener = async (newState) => {
2845
- // pkc._startedCommunities[address].stop() has been called, we need to stop mirroring
2846
- // or pkc._updatingCommunities[address].stop(), we need to stop mirroring
2847
- if (newState === "stopped")
2848
- await this._cleanUpMirroredStartedOrUpdatingCommunity();
2849
- };
2850
- this._mirroredStartedOrUpdatingCommunity = {
2851
- community: startedCommunity,
2852
- updatingstatechange: updatingStateChangeListener,
2853
- update: updateListener,
2854
- statechange: stateChangeListener,
2855
- startedstatechange: startedStateChangeListener,
2856
- error: (err) => this.emit("error", err),
2857
- challengerequest: (challengeRequest) => this.emit("challengerequest", challengeRequest),
2858
- challengeverification: (challengeVerification) => this.emit("challengeverification", challengeVerification),
2859
- challengeanswer: (challengeAnswer) => this.emit("challengeanswer", challengeAnswer),
2860
- challenge: (challenge) => this.emit("challenge", challenge)
2861
- };
2862
- this._mirroredStartedOrUpdatingCommunity.community.on("update", this._mirroredStartedOrUpdatingCommunity.update);
2863
- this._mirroredStartedOrUpdatingCommunity.community.on("startedstatechange", this._mirroredStartedOrUpdatingCommunity.startedstatechange);
2864
- this._mirroredStartedOrUpdatingCommunity.community.on("updatingstatechange", this._mirroredStartedOrUpdatingCommunity.updatingstatechange);
2865
- this._mirroredStartedOrUpdatingCommunity.community.on("statechange", this._mirroredStartedOrUpdatingCommunity.statechange);
2866
- this._mirroredStartedOrUpdatingCommunity.community.on("error", this._mirroredStartedOrUpdatingCommunity.error);
2867
- this._mirroredStartedOrUpdatingCommunity.community.on("challengerequest", this._mirroredStartedOrUpdatingCommunity.challengerequest);
2868
- this._mirroredStartedOrUpdatingCommunity.community.on("challengeverification", this._mirroredStartedOrUpdatingCommunity.challengeverification);
2869
- this._mirroredStartedOrUpdatingCommunity.community.on("challengeanswer", this._mirroredStartedOrUpdatingCommunity.challengeanswer);
2870
- this._mirroredStartedOrUpdatingCommunity.community.on("challenge", this._mirroredStartedOrUpdatingCommunity.challenge);
2871
- const clientKeys = remeda.keys.strict(this.clients);
2872
- for (const clientType of clientKeys)
2873
- if (this.clients[clientType])
2874
- for (const clientUrl of Object.keys(this.clients[clientType]))
2875
- if (clientUrl in this._mirroredStartedOrUpdatingCommunity.community.clients[clientType])
2876
- this.clients[clientType][clientUrl].mirror(this._mirroredStartedOrUpdatingCommunity.community.clients[clientType][clientUrl]);
2877
- if (startedCommunity.updateCid)
2878
- await this.initInternalCommunityAfterFirstUpdateNoMerge(startedCommunity.toJSONInternalAfterFirstUpdate());
2879
- else
2880
- await this.initInternalCommunityBeforeFirstUpdateNoMerge(startedCommunity.toJSONInternalBeforeFirstUpdate());
2881
- this.emit("update", this);
2882
- }
2883
- async _cleanUpMirroredStartedOrUpdatingCommunity() {
2884
- if (!this._mirroredStartedOrUpdatingCommunity)
2885
- return;
2886
- this._mirroredStartedOrUpdatingCommunity.community.removeListener("update", this._mirroredStartedOrUpdatingCommunity.update);
2887
- this._mirroredStartedOrUpdatingCommunity.community.removeListener("updatingstatechange", this._mirroredStartedOrUpdatingCommunity.updatingstatechange);
2888
- this._mirroredStartedOrUpdatingCommunity.community.removeListener("startedstatechange", this._mirroredStartedOrUpdatingCommunity.startedstatechange);
2889
- this._mirroredStartedOrUpdatingCommunity.community.removeListener("statechange", this._mirroredStartedOrUpdatingCommunity.statechange);
2890
- this._mirroredStartedOrUpdatingCommunity.community.removeListener("error", this._mirroredStartedOrUpdatingCommunity.error);
2891
- this._mirroredStartedOrUpdatingCommunity.community.removeListener("challengerequest", this._mirroredStartedOrUpdatingCommunity.challengerequest);
2892
- this._mirroredStartedOrUpdatingCommunity.community.removeListener("challengeverification", this._mirroredStartedOrUpdatingCommunity.challengeverification);
2893
- this._mirroredStartedOrUpdatingCommunity.community.removeListener("challengeanswer", this._mirroredStartedOrUpdatingCommunity.challengeanswer);
2894
- this._mirroredStartedOrUpdatingCommunity.community.removeListener("challenge", this._mirroredStartedOrUpdatingCommunity.challenge);
2895
- const clientKeys = remeda.keys.strict(this.clients);
2896
- for (const clientType of clientKeys)
2897
- if (this.clients[clientType])
2898
- for (const clientUrl of Object.keys(this.clients[clientType]))
2899
- this.clients[clientType][clientUrl].unmirror();
2900
- this._mirroredStartedOrUpdatingCommunity = undefined;
2901
- }
2902
- async _updateOnce() {
2903
- const log = Logger("pkc-js:local-community:_updateOnce");
2904
- await this.initDbHandlerIfNeeded();
2905
- await this._updateStartedValue();
2906
- const startedCommunity = ((findStartedCommunity(this._pkc, { publicKey: this.publicKey, name: this.name }) ||
2907
- findCommunityInRegistry(processStartedCommunities, { publicKey: this.publicKey, name: this.name })));
2908
- if (this._mirroredStartedOrUpdatingCommunity)
2909
- return; // we're already mirroring a started or updating community
2910
- else if (startedCommunity) {
2911
- // let's mirror the started community in this process
2912
- await this._initMirroringStartedOrUpdatingCommunity(startedCommunity);
2913
- untrackUpdatingCommunity(this._pkc, this);
2914
- return;
2915
- }
2916
- else {
2917
- const updatingCommunity = findUpdatingCommunity(this._pkc, { publicKey: this.publicKey, name: this.name });
2918
- if (updatingCommunity instanceof LocalCommunity && updatingCommunity !== this) {
2919
- // different instance is updating, let's mirror it
2920
- await this._initMirroringStartedOrUpdatingCommunity(updatingCommunity);
2921
- return;
2922
- }
2923
- // this community is not started or updated anywhere, but maybe another process will call edit() on it
2924
- trackUpdatingCommunity(this._pkc, this);
2925
- const oldUpdateId = remeda.clone(this._internalStateUpdateId);
2926
- await this._updateInstancePropsWithStartedCommunityOrDb(); // will update this instance props with DB
2927
- if (this._internalStateUpdateId !== oldUpdateId) {
2928
- log(`Local Community (${this.address}) received a new update from db with updatedAt (${this.updatedAt}). Will emit an update event`);
2929
- this._changeStateEmitEventEmitStateChangeEvent({
2930
- event: { name: "update", args: [this] },
2931
- newUpdatingState: "succeeded"
2932
- });
2933
- }
2934
- }
2935
- }
2936
- async _updateLoop() {
2937
- const log = Logger("pkc-js:local-community:update:_updateLoop");
2938
- while (this.state === "updating" && !this._stopHasBeenCalled) {
2939
- try {
2940
- await this._updateOnce();
2941
- }
2942
- catch (e) {
2943
- log.error("Error in update loop", e);
2944
- this.emit("error", e);
2945
- }
2946
- finally {
2947
- await new Promise((resolve) => {
2948
- if (this._updateLoopAbortController?.signal.aborted)
2949
- return resolve();
2950
- const timer = setTimeout(resolve, this._pkc.updateInterval);
2951
- this._updateLoopAbortController?.signal.addEventListener("abort", () => {
2952
- clearTimeout(timer);
2953
- resolve();
2954
- }, { once: true });
2955
- });
2956
- }
2957
- }
2958
- }
2959
- async update() {
2960
- if (this.state === "started")
2961
- throw new PKCError("ERR_COMMUNITY_ALREADY_STARTED", { address: this.address });
2962
- if (this.state === "updating")
2963
- return;
2964
- this._stopHasBeenCalled = false;
2965
- this._setState("updating");
2966
- try {
2967
- await this._updateOnce();
2968
- }
2969
- catch (e) {
2970
- this.emit("error", e);
2971
- }
2972
- this._updateLoopAbortController = new AbortController();
2973
- this._updateLoopPromise = this._updateLoop();
2974
- }
2975
- async stop() {
2976
- const log = Logger("pkc-js:local-community:stop");
2977
- this._stopHasBeenCalled = true;
2978
- if (this._updateLoopAbortController) {
2979
- this._updateLoopAbortController.abort();
2980
- }
2981
- this.posts._stop();
2982
- if (this.state === "started") {
2983
- log("Stopping running community", this.address);
2984
- try {
2985
- await this._clientsManager.pubsubUnsubscribe(this.pubsubTopicWithfallback(), this.handleChallengeExchange);
2986
- }
2987
- catch (e) {
2988
- log.error("Failed to unsubscribe from challenge exchange pubsub when stopping community", e);
2989
- }
2990
- if (this._publishLoopPromise) {
2991
- try {
2992
- await this._publishLoopPromise;
2993
- }
2994
- catch (e) {
2995
- log.error(`Failed to stop community publish loop`, e);
2996
- }
2997
- this._publishLoopPromise = undefined;
2998
- }
2999
- try {
3000
- await this._unpinStaleCids();
3001
- }
3002
- catch (e) {
3003
- log.error("Failed to unpin stale cids and remove mfs paths before stopping", e);
3004
- }
3005
- try {
3006
- await this._updateDbInternalState(this.updateCid ? this.toJSONInternalAfterFirstUpdate() : this.toJSONInternalBeforeFirstUpdate());
3007
- }
3008
- catch (e) {
3009
- log.error("Failed to update db internal state before stopping", e);
3010
- }
3011
- try {
3012
- await this._dbHandler.unlockCommunityStart();
3013
- }
3014
- catch (e) {
3015
- log.error(`Failed to unlock start lock on community (${this.address})`, e);
3016
- }
3017
- const kuboRpcClient = this._clientsManager.getDefaultKuboRpcClient();
3018
- const pubsubClient = this._clientsManager.getDefaultKuboPubsubClient();
3019
- this._setStartedStateWithEmission("stopped");
3020
- untrackStartedCommunity(this._pkc, this);
3021
- processStartedCommunities.untrack(this);
3022
- this._duplicatePublicationAttempts?.clear();
3023
- await this._dbHandler.rollbackAllTransactions();
3024
- await this._dbHandler.unlockCommunityState();
3025
- await this._updateStartedValue();
3026
- this._clientsManager.updateKuboRpcState("stopped", kuboRpcClient.url);
3027
- this._clientsManager.updateKuboRpcPubsubState("stopped", pubsubClient.url);
3028
- if (this._dbHandler)
3029
- this._dbHandler.destoryConnection();
3030
- log(`Stopped the running of local community (${this.address})`);
3031
- this._setState("stopped");
3032
- }
3033
- else if (this.state === "updating") {
3034
- if (this._updateLoopPromise) {
3035
- await this._updateLoopPromise;
3036
- this._updateLoopPromise = undefined;
3037
- }
3038
- this._updateLoopAbortController = undefined;
3039
- if (this._dbHandler)
3040
- this._dbHandler.destoryConnection();
3041
- if (this._mirroredStartedOrUpdatingCommunity)
3042
- await this._cleanUpMirroredStartedOrUpdatingCommunity();
3043
- if (findUpdatingCommunity(this._pkc, { publicKey: this.publicKey, name: this.name }) === this)
3044
- untrackUpdatingCommunity(this._pkc, this);
3045
- this._setUpdatingStateWithEventEmissionIfNewState("stopped");
3046
- log(`Stopped the updating of local community (${this.address})`);
3047
- this._setState("stopped");
3048
- }
3049
- }
3050
- async delete() {
3051
- const log = Logger("pkc-js:local-community:delete");
3052
- log.trace(`Attempting to stop the community (${this.address}) before deleting, if needed`);
3053
- const startedCommunity = ((findStartedCommunity(this._pkc, { publicKey: this.publicKey, name: this.name }) ||
3054
- findCommunityInRegistry(processStartedCommunities, { publicKey: this.publicKey, name: this.name })));
3055
- if (startedCommunity && startedCommunity !== this) {
3056
- await startedCommunity.delete();
3057
- await this.stop();
3058
- return;
3059
- }
3060
- if (this.state === "updating" || this.state === "started")
3061
- await this.stop();
3062
- const kuboClient = this._clientsManager.getDefaultKuboRpcClient();
3063
- if (!kuboClient)
3064
- throw Error("Ipfs client is not defined");
3065
- if (typeof this.signer?.ipnsKeyName === "string")
3066
- // Key may not exist on ipfs node
3067
- try {
3068
- await kuboClient._client.key.rm(this.signer.ipnsKeyName);
3069
- }
3070
- catch (e) {
3071
- log.error("Failed to delete ipns key", this.signer.ipnsKeyName, e);
3072
- }
3073
- try {
3074
- await removeMfsFilesSafely({ kuboRpcClient: kuboClient, paths: ["/" + this.address], log });
3075
- }
3076
- catch (e) {
3077
- log.error("Failed to delete community mfs folder", "/" + this.address, e);
3078
- }
3079
- // sceneario 1: we call delete() on a community that is not started or updating
3080
- // scenario 2: we call delete() on a community that is updating
3081
- // scenario 3: we call delete() on a community that is started
3082
- // scenario 4: we call delete() on a community that is not started, but the same community is started in pkc._startedCommunities[address]
3083
- try {
3084
- await this._addOldPageCidsToCidsToUnpin(this.raw?.communityIpfs?.posts, undefined);
3085
- }
3086
- catch (e) {
3087
- log.error("Failed to add old page cids from community.posts to be unpinned", e);
3088
- }
3089
- if (this.ipnsPubsubTopicRoutingCid)
3090
- this._cidsToUnPin.add(this.ipnsPubsubTopicRoutingCid);
3091
- if (this.pubsubTopicRoutingCid)
3092
- this._cidsToUnPin.add(this.pubsubTopicRoutingCid);
3093
- try {
3094
- await this.initDbHandlerIfNeeded();
3095
- await this._dbHandler.initDbIfNeeded();
3096
- const cidsAndReplies = this._dbHandler.queryAllCommentCidsAndTheirReplies();
3097
- for (const comment of cidsAndReplies) {
3098
- this._cidsToUnPin.add(comment.cid);
3099
- for (const pageCid of comment.allPageCids) {
3100
- this._cidsToUnPin.add(pageCid);
3101
- }
3102
- }
3103
- }
3104
- catch (e) {
3105
- log.error("Failed to query all cids under this community to delete them", e);
3106
- }
3107
- if (this.updateCid)
3108
- this._cidsToUnPin.add(this.updateCid);
3109
- if (this.statsCid)
3110
- this._cidsToUnPin.add(this.statsCid);
3111
- try {
3112
- await this._unpinStaleCids();
3113
- }
3114
- catch (e) {
3115
- log.error("Failed to unpin stale cids before deleting", e);
3116
- }
3117
- try {
3118
- await this._updateDbInternalState(typeof this.updatedAt === "number" ? this.toJSONInternalAfterFirstUpdate() : this.toJSONInternalBeforeFirstUpdate());
3119
- }
3120
- catch (e) {
3121
- log.error("Failed to update db internal state before deleting", e);
3122
- }
3123
- finally {
3124
- this._dbHandler.destoryConnection();
3125
- }
3126
- await moveCommunityDbToDeletedDirectory(this.address, this._pkc);
3127
- log(`Deleted community (${this.address}) successfully`);
327
+ _calculateLocalMfsPathForCommentUpdate(...args) {
328
+ return calculateLocalMfsPathForCommentUpdate(this, ...args);
3128
329
  }
3129
330
  }
3130
- LocalCommunity._defaultChallengeQuestionText = "What is the answer to this community's challenge? (check community.settings.challenges to see the answer, or set your own challenge)";
3131
331
  //# sourceMappingURL=local-community.js.map