diff --git a/package-lock.json b/package-lock.json index ab8388e..29e89ed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19225,6 +19225,7 @@ "buffer": "^6.0.3", "charwise": "^3.0.1", "crypto-browserify": "^3.12.0", + "eventemitter2": "^6.4.9", "level": "^8.0.0", "process": "^0.11.10", "stream-browserify": "^3.0.0" @@ -19456,6 +19457,7 @@ "buffer": "^6.0.3", "charwise": "^3.0.1", "crypto-browserify": "^3.12.0", + "eventemitter2": "^6.4.9", "level": "^8.0.0", "process": "^0.11.10", "stream-browserify": "^3.0.0" diff --git a/packages/db/package.json b/packages/db/package.json index afed37b..f9b8bbf 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -36,6 +36,7 @@ "charwise": "^3.0.1", "crypto-browserify": "^3.12.0", "browser-level": "^1.0.1", + "eventemitter2": "^6.4.9", "level": "^8.0.0", "process": "^0.11.10", "stream-browserify": "^3.0.0" diff --git a/packages/db/src/base.ts b/packages/db/src/base.ts index 1d64f29..c2182db 100644 --- a/packages/db/src/base.ts +++ b/packages/db/src/base.ts @@ -14,9 +14,9 @@ export type UserProfileData = { }; export type PostMeta = { - moderated: { [key in ModerationSubtype]?: boolean }; + moderated: { [key in ModerationSubtype]?: string }; moderations: { [subtype: string]: number }; - threaded: { [key in PostSubtype]?: boolean }; + threaded: { [key in PostSubtype]?: string }; threads: { [subtype: string]: number }; }; diff --git a/packages/db/src/level.ts b/packages/db/src/level.ts index 77b0762..8a21201 100644 --- a/packages/db/src/level.ts +++ b/packages/db/src/level.ts @@ -4,22 +4,28 @@ import { BaseDBAdapter, PostMeta, UserProfileData } from './base'; import { Any, AnyJSON, - MessageType, - PostSubtype, - Post, - Moderation, Connection, - Profile, - Message, - ModerationSubtype, ConnectionSubtype, + Message, + MessageType, + Moderation, + ModerationSubtype, + Post, + PostSubtype, + Profile, ProfileSubtype, + Reference, + Revert, } from '@message'; import { Mutex } from 'async-mutex'; +import { ConstructorOptions, EventEmitter2 } from 'eventemitter2'; const charwise = require('charwise'); -export default class LevelDBAdapter implements BaseDBAdapter { +export default class LevelDBAdapter + extends EventEmitter2 + implements BaseDBAdapter +{ #db: Level; #mutex: Mutex; @@ -33,8 +39,9 @@ export default class LevelDBAdapter implements BaseDBAdapter { param: { path?: string; prefix?: string; - } = {}, + } & ConstructorOptions = {}, ) { + super(param); const { path = process.cwd() + '/build', prefix = '' } = param; this.#db = new Level(`${path}/${prefix}/db`, { @@ -177,11 +184,115 @@ export default class LevelDBAdapter implements BaseDBAdapter { break; } + + case MessageType.Revert: { + const rvt = message as Revert; + const msg = await this.getMessage(Reference.from(rvt.reference).hash); + if (msg) { + await this.revertMessage(msg); + } + break; + } } return message; } + async revertMessage(message: Any) { + const exist = await this.getMessage(message.hash); + + if (!exist) { + return null; + } + + const time = + charwise.encode(message.createdAt.getTime()) + + '-' + + message.creator + + '-' + + message.type + + '-' + + message.subtype; + + await this.#db.del(message.hash); + + await this.#indices.global + .sublevel(MessageType[message.type], { valueEncoding: 'json' }) + .del(time); + + await this.#indices.user + .sublevel(message.creator, { valueEncoding: 'json' }) + .sublevel(MessageType[message.type], { valueEncoding: 'json' }) + .del(time); + + await this.#indices.user + .sublevel(message.creator, { valueEncoding: 'json' }) + .sublevel('all', { valueEncoding: 'json' }) + .del(time); + + await this.#indices.user.sublevel('list').del(message.creator); + + switch (message.type) { + case MessageType.Post: { + const msg = message as Post; + + if (msg.reference) { + const hash = msg.reference.split('/')[1]; + + if (hash) { + await this.#indices.thread + .sublevel(msg.reference.split('/')[1], { valueEncoding: 'json' }) + .sublevel(MessageType[msg.type], { valueEncoding: 'json' }) + .del(time); + } + } + + break; + } + + case MessageType.Moderation: { + const msg = message as Moderation; + + if (msg.reference) { + await this.#indices.thread + .sublevel(msg.reference.split('/')[1], { valueEncoding: 'json' }) + .sublevel(MessageType[msg.type], { valueEncoding: 'json' }) + .del(time); + } + + break; + } + + case MessageType.Connection: { + const msg = message as Connection; + + if (msg.value) { + await this.#indices.user + .sublevel(msg.value, { valueEncoding: 'json' }) + .sublevel(MessageType[msg.type], { valueEncoding: 'json' }) + .del(time); + } + + break; + } + + case MessageType.Profile: { + const msg = message as Profile; + + if (msg.value) { + await this.#indices.user + .sublevel(msg.creator, { valueEncoding: 'json' }) + .sublevel(MessageType[msg.type], { valueEncoding: 'json' }) + .del(time); + } + + break; + } + } + + this.emit('db:message:revert', message.json); + } + async #query( db: AbstractSublevel, predicate: (msg: Any) => boolean = () => true, @@ -576,7 +687,10 @@ function flattenByCreatorSubtype(items: Any[]): { [subtype: string]: number } { return items.reduce((sum: { [k: string]: number }, item) => { sum[item.subtype] = sum[item.subtype] || 0; - if (!exists[item.creator + '/' + item.subtype]) { + if ( + item.subtype === PostSubtype.Comment || + !exists[item.creator + '/' + item.subtype] + ) { sum[item.subtype]++; exists[item.creator + '/' + item.subtype] = true; } @@ -588,15 +702,15 @@ function flattenByCreatorSubtype(items: Any[]): { [subtype: string]: number } { function reduceByCreatorSubtype( msgs: Any[], own?: string | null, -): { [subtype: string]: boolean } { +): { [subtype: string]: string } { const obj: { - [key: string]: boolean; + [key: string]: string; } = {}; if (own) { for (const msg of msgs) { if (msg.creator === own) { - obj[msg.subtype] = true; + obj[msg.subtype] = msg.messageId; } } } diff --git a/packages/message/src/models/chat.ts b/packages/message/src/models/chat.ts index 58a05fa..389c89d 100644 --- a/packages/message/src/models/chat.ts +++ b/packages/message/src/models/chat.ts @@ -142,8 +142,6 @@ export class Chat extends Base { } get hex(): string { - if (this.#hex) return this.#hex; - this.#hex = super.hex + [ diff --git a/packages/message/src/models/connection.ts b/packages/message/src/models/connection.ts index b78aff0..60e44b5 100644 --- a/packages/message/src/models/connection.ts +++ b/packages/message/src/models/connection.ts @@ -65,8 +65,6 @@ export class Connection extends Base { } get hex(): string { - if (this.#hex) return this.#hex; - this.#hex = super.hex + [encodeString(this.value || '', 0xfff)].join(''); return this.#hex; diff --git a/packages/message/src/models/group.ts b/packages/message/src/models/group.ts index c84a66a..dfbe90c 100644 --- a/packages/message/src/models/group.ts +++ b/packages/message/src/models/group.ts @@ -88,8 +88,6 @@ export class Group extends Base { } get hex(): string { - if (this.#hex) return this.#hex; - this.#hex = super.hex + [ diff --git a/packages/message/src/models/profile.ts b/packages/message/src/models/profile.ts index 5a95d01..b4ca384 100644 --- a/packages/message/src/models/profile.ts +++ b/packages/message/src/models/profile.ts @@ -76,8 +76,6 @@ export class Profile extends Base { } get hex(): string { - if (this.#hex) return this.#hex; - this.#hex = super.hex + [ diff --git a/packages/message/src/models/revert.ts b/packages/message/src/models/revert.ts index 2582e46..d55c32b 100644 --- a/packages/message/src/models/revert.ts +++ b/packages/message/src/models/revert.ts @@ -62,8 +62,6 @@ export class Revert extends Base { } get hex(): string { - if (this.#hex) return this.#hex; - this.#hex = super.hex + [encodeString(this.reference || '', 0xfff)].join(''); diff --git a/packages/protocol/src/services/browser.ts b/packages/protocol/src/services/browser.ts index 24e40a6..b8b3376 100644 --- a/packages/protocol/src/services/browser.ts +++ b/packages/protocol/src/services/browser.ts @@ -1,10 +1,10 @@ -import { EventEmitter2, ConstructorOptions } from 'eventemitter2'; +import { ConstructorOptions, EventEmitter2 } from 'eventemitter2'; import { Any, Message, ProofType } from '@message'; import { P2P, - ProtocolType, - ProtocolResponseParam, ProtocolRequestParam, + ProtocolResponseParam, + ProtocolType, } from './p2p/browser.ts'; import { DB } from './db'; import { PubsubTopics } from '../utils/types'; @@ -48,6 +48,10 @@ export class Autism extends EventEmitter2 { name: this.#name, }); + this.db.db.on('db:message:revert', (json) => { + this.emit('pubsub:message:revert', json); + }); + this.#sync = options?.sync || 5 * 60 * 1000; // default: 5m; this.#syncTimeout = null; } @@ -70,6 +74,7 @@ export class Autism extends EventEmitter2 { const message = Message.fromHex(Buffer.from(value.data).toString('hex')); if (!message) return; + if (!message.proof) return; if (message.proof.type === ProofType.ECDSA) { @@ -272,7 +277,7 @@ export class Autism extends EventEmitter2 { if (!verified) return; - return this.p2p.publish(PubsubTopics.Global, message.buffer); + this.p2p.publish(PubsubTopics.Global, message.buffer); } } diff --git a/packages/protocol/src/services/index.ts b/packages/protocol/src/services/index.ts index 056b9f2..63a6300 100644 --- a/packages/protocol/src/services/index.ts +++ b/packages/protocol/src/services/index.ts @@ -48,6 +48,10 @@ export class Autism extends EventEmitter2 { name: this.#name, }); + this.db.db.on('db:message:revert', (json) => { + this.emit('pubsub:message:revert', json); + }); + this.#sync = options?.sync || 5 * 60 * 1000; // default: 5m; this.#syncTimeout = null; } @@ -70,6 +74,7 @@ export class Autism extends EventEmitter2 { const message = Message.fromHex(Buffer.from(value.data).toString('hex')); if (!message) return; + if (!message.proof) return; if (message.proof.type === ProofType.ECDSA) { diff --git a/packages/protocol/src/services/p2p/browser.ts b/packages/protocol/src/services/p2p/browser.ts index efe7cb7..662ab75 100644 --- a/packages/protocol/src/services/p2p/browser.ts +++ b/packages/protocol/src/services/p2p/browser.ts @@ -130,6 +130,7 @@ export class P2P extends EventEmitter2 { }); const { name = 'node', bootstrap } = this; + // @ts-ignore const { createLibp2p } = await import('libp2p'); const { circuitRelayTransport } = await import('libp2p/circuit-relay'); const { identifyService } = await import('libp2p/identify'); @@ -137,6 +138,7 @@ export class P2P extends EventEmitter2 { const { all } = await import('@libp2p/websockets/filters'); const { noise } = await import('@chainsafe/libp2p-noise'); const { kadDHT } = await import('@libp2p/kad-dht'); + // @ts-ignore const { gossipsub } = await import('@chainsafe/libp2p-gossipsub'); const { yamux } = await import('@chainsafe/libp2p-yamux'); const { mplex } = await import('@libp2p/mplex'); diff --git a/packages/web/lib/state.ts b/packages/web/lib/state.ts index acdd475..3565e3e 100644 --- a/packages/web/lib/state.ts +++ b/packages/web/lib/state.ts @@ -25,6 +25,7 @@ export class Observable { return; } this.#state = state; + for (const sub of this.#subscriptions) { if (typeof sub === 'function') { sub(state); diff --git a/packages/web/src/components/Post/index.ts b/packages/web/src/components/Post/index.ts index e9c114d..a691aae 100644 --- a/packages/web/src/components/Post/index.ts +++ b/packages/web/src/components/Post/index.ts @@ -23,6 +23,8 @@ import { Post as AustismPost, PostSubtype, ProofType, + Revert, + RevertSubtype, } from '@message'; import $signer from '../../state/signer.ts'; import { useEffect } from '../../../lib/state.ts'; @@ -79,7 +81,7 @@ export default class Post extends CustomElement { return false; }; - toggleRepost = (evt: PointerEvent) => { + toggleRepost = async (evt: PointerEvent) => { evt.stopPropagation(); const repost = $node.getRepostRef(this.state.hash); const hash = repost?.hash || this.state.hash; @@ -89,23 +91,40 @@ export default class Post extends CustomElement { if (!$signer.$ecdsa.$ || !$signer.$ecdsa.$.publicKey) return; - const post = new AustismPost({ - type: MessageType.Post, - subtype: PostSubtype.Repost, - reference: p.$.messageId, - creator: $signer.$ecdsa.$.publicKey, - createdAt: new Date(), - }); + const postmeta = await $node.node.db.db.getPostMeta( + p.$.messageId, + $signer.$ecdsa.$.publicKey, + ); - post.commit({ + let msg; + + if (!!postmeta.threaded[PostSubtype.Repost]) { + msg = new Revert({ + type: MessageType.Revert, + subtype: RevertSubtype.Default, + reference: postmeta.threaded[PostSubtype.Repost], + creator: $signer.$ecdsa.$.publicKey, + createdAt: new Date(), + }); + } else { + msg = new AustismPost({ + type: MessageType.Post, + subtype: PostSubtype.Repost, + reference: p.$.messageId, + creator: $signer.$ecdsa.$.publicKey, + createdAt: new Date(), + }); + } + + msg.commit({ type: ProofType.ECDSA, - value: $signer.$ecdsa.$.sign(post.hash), + value: $signer.$ecdsa.$.sign(msg.hash), }); - $node.node.publish(post); + $node.node.publish(msg); }; - toggleLike = (evt: PointerEvent) => { + toggleLike = async (evt: PointerEvent) => { evt.stopPropagation(); const repost = $node.getRepostRef(this.state.hash); const hash = repost?.hash || this.state.hash; @@ -115,20 +134,37 @@ export default class Post extends CustomElement { if (!$signer.$ecdsa.$ || !$signer.$ecdsa.$.publicKey) return; - const mod = new Moderation({ - type: MessageType.Moderation, - subtype: ModerationSubtype.Like, - reference: p.$.messageId, - creator: $signer.$ecdsa.$.publicKey, - createdAt: new Date(), - }); + const postmeta = await $node.node.db.db.getPostMeta( + p.$.messageId, + $signer.$ecdsa.$.publicKey, + ); - mod.commit({ + let msg; + + if (!!postmeta.moderated[ModerationSubtype.Like]) { + msg = new Revert({ + type: MessageType.Revert, + subtype: RevertSubtype.Default, + reference: postmeta.moderated[ModerationSubtype.Like], + creator: $signer.$ecdsa.$.publicKey, + createdAt: new Date(), + }); + } else { + msg = new Moderation({ + type: MessageType.Moderation, + subtype: ModerationSubtype.Like, + reference: p.$.messageId, + creator: $signer.$ecdsa.$.publicKey, + createdAt: new Date(), + }); + } + + msg.commit({ type: ProofType.ECDSA, - value: $signer.$ecdsa.$.sign(mod.hash), + value: $signer.$ecdsa.$.sign(msg.hash), }); - $node.node.publish(mod); + $node.node.publish(msg); }; render() { @@ -194,14 +230,14 @@ export default class Post extends CustomElement { this.btn({ title: 'Reply', className: 'comment-btn', - active: postmeta?.threaded[PostSubtype.Comment], + active: !!postmeta?.threaded[PostSubtype.Comment], count: postmeta?.threads[PostSubtype.Comment], onclick: this.comment, src: CommentIcon, }), this.btn({ className: 'repost-btn', - active: postmeta?.threaded[PostSubtype.Repost], + active: !!postmeta?.threaded[PostSubtype.Repost], count: postmeta?.threads[PostSubtype.Repost], onclick: this.toggleRepost, src: RepostIcon, @@ -209,7 +245,7 @@ export default class Post extends CustomElement { }), this.btn({ className: 'like-btn', - active: postmeta?.moderated[ModerationSubtype.Like], + active: !!postmeta?.moderated[ModerationSubtype.Like], count: postmeta?.moderations[ModerationSubtype.Like], onclick: this.toggleLike, src: LikeIcon, diff --git a/packages/web/src/state/node.ts b/packages/web/src/state/node.ts index 2f3abb2..7015bd5 100644 --- a/packages/web/src/state/node.ts +++ b/packages/web/src/state/node.ts @@ -1,7 +1,7 @@ import { Observable, ObservableMap } from '../../lib/state.ts'; import { Autism } from '@protocol/browser'; import { - Any, + AnyJSON, MessageType, Moderation, Post, @@ -21,18 +21,18 @@ export class NodeStore { $users = new ObservableMap(); $postmetas = new ObservableMap(); - onPubsub = (msg: Any) => { + onPubsub = async (msg: AnyJSON, isRevert = false) => { switch (msg.type) { case MessageType.Post: { const post = msg as Post; if (post.subtype === PostSubtype.Default) { - this.#updatePosts(msg); + this.#updatePosts(isRevert ? undefined : post); this.getPost(post.hash!); this.getReplies(post.messageId); this.getPostMeta(post.messageId); } if ([PostSubtype.Repost].includes(post.subtype)) { - this.#updatePosts(msg); + this.#updatePosts(isRevert ? undefined : post); this.getPost(Reference.from(post.reference!).hash); this.getReplies(post.reference!); this.getPostMeta(post.reference); @@ -47,14 +47,18 @@ export class NodeStore { case MessageType.Moderation: { const mod = msg as Moderation; this.getPostMeta(mod.reference); + return; } + default: + console.log('unknown pubsub message', msg); + return; } }; constructor() { const node = new Autism({ bootstrap: [ - '/ip4/192.168.86.24/tcp/58937/ws/p2p/12D3KooWCU7HfBs3Md9e7DyNqnCubWH5w7dZLLdQSwjHaQXDSiGD', + '/ip4/192.168.86.24/tcp/54884/ws/p2p/12D3KooWBLCTz8qFy5tHT6HCbBF5wEeHvd4qa99PeZR72AkugDrP', ], }); @@ -65,7 +69,8 @@ export class NodeStore { console.log('peer connected', peer); }); - node.on('pubsub:message:success', this.onPubsub); + node.on('pubsub:message:success', (msg) => this.onPubsub(msg, false)); + node.on('pubsub:message:revert', (msg) => this.onPubsub(msg, true)); node.on('sync:new_message', this.onPubsub); @@ -82,7 +87,6 @@ export class NodeStore { #updatePosts = async (msg?: Post) => { const posts = await this.node.db.db.getPosts(); - console.log(`updating ${posts.length} posts...`, msg); if (msg) { this.$globalPosts.$ = [msg.hash].concat(this.$globalPosts.$);