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