add basic revert

This commit is contained in:
tsukino
2023-12-30 22:31:10 -08:00
parent 0f845b19e7
commit dda50d6b30
15 changed files with 221 additions and 61 deletions

2
package-lock.json generated
View File

@@ -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"

View File

@@ -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"

View File

@@ -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 };
};

View File

@@ -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<string, AnyJSON>;
#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<any, any, string, string>,
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;
}
}
}

View File

@@ -142,8 +142,6 @@ export class Chat extends Base {
}
get hex(): string {
if (this.#hex) return this.#hex;
this.#hex =
super.hex +
[

View File

@@ -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;

View File

@@ -88,8 +88,6 @@ export class Group extends Base {
}
get hex(): string {
if (this.#hex) return this.#hex;
this.#hex =
super.hex +
[

View File

@@ -76,8 +76,6 @@ export class Profile extends Base {
}
get hex(): string {
if (this.#hex) return this.#hex;
this.#hex =
super.hex +
[

View File

@@ -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('');

View File

@@ -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);
}
}

View File

@@ -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) {

View File

@@ -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');

View File

@@ -25,6 +25,7 @@ export class Observable<ObservableValue = any> {
return;
}
this.#state = state;
for (const sub of this.#subscriptions) {
if (typeof sub === 'function') {
sub(state);

View File

@@ -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,

View File

@@ -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<string, UserProfileData>();
$postmetas = new ObservableMap<string, PostMeta>();
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.$);