mirror of
https://github.com/logos-messaging/OpChan.git
synced 2026-01-02 21:03:09 +00:00
feat: implement waku
This commit is contained in:
parent
95401cdb5b
commit
56c9c8d889
@ -8,7 +8,7 @@ import { Textarea } from '@/components/ui/textarea';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { ArrowLeft, ArrowUp, ArrowDown, Clock, MessageCircle, Send } from 'lucide-react';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import { Comment } from '@/types/forum';
|
||||
import { Comment } from '@/types';
|
||||
|
||||
const PostDetail = () => {
|
||||
const { postId } = useParams<{ postId: string }>();
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
|
||||
import React, { createContext, useContext, useState, useEffect } from 'react';
|
||||
import { useToast } from '@/components/ui/use-toast';
|
||||
import { User } from '@/types/forum';
|
||||
import { User } from '@/types';
|
||||
|
||||
interface AuthContextType {
|
||||
currentUser: User | null;
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import React, { createContext, useContext, useState, useEffect } from 'react';
|
||||
import { useToast } from '@/components/ui/use-toast';
|
||||
import { Cell, Post, Comment } from '@/types/forum';
|
||||
import { Cell, Post, Comment } from '@/types';
|
||||
import { mockCells, mockPosts, mockComments } from '@/data/mockData';
|
||||
import { useAuth } from './AuthContext';
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
|
||||
import { Cell, Post, Comment } from "../types/forum";
|
||||
import { Cell, Post, Comment } from "../types";
|
||||
|
||||
export const mockCells: Cell[] = [
|
||||
{
|
||||
|
||||
@ -1,35 +1,39 @@
|
||||
import { createDecoder, createEncoder } from '@waku/sdk';
|
||||
import { MessageType } from './types';
|
||||
import { OpchanMessage, CellMessage, PostMessage, CommentMessage, VoteMessage } from './types';
|
||||
import { CellMessage, PostMessage, CommentMessage, VoteMessage } from './types';
|
||||
import { CONTENT_TOPICS } from './constants';
|
||||
import { OpchanMessage } from '@/types';
|
||||
|
||||
export const encoders = {
|
||||
[MessageType.CELL]: createEncoder({
|
||||
contentTopic: CONTENT_TOPICS['cell'],
|
||||
}),
|
||||
[MessageType.POST]: createEncoder({
|
||||
contentTopic: CONTENT_TOPICS['post'],
|
||||
}),
|
||||
[MessageType.COMMENT]: createEncoder({
|
||||
contentTopic: CONTENT_TOPICS['comment'],
|
||||
}),
|
||||
[MessageType.VOTE]: createEncoder({
|
||||
contentTopic: CONTENT_TOPICS['vote'],
|
||||
}),
|
||||
}
|
||||
|
||||
export const decoders = {
|
||||
[MessageType.CELL]: createDecoder(CONTENT_TOPICS['cell']),
|
||||
[MessageType.POST]: createDecoder(CONTENT_TOPICS['post']),
|
||||
[MessageType.COMMENT]: createDecoder(CONTENT_TOPICS['comment']),
|
||||
[MessageType.VOTE]: createDecoder(CONTENT_TOPICS['vote']),
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode a message object into a Uint8Array for transmission
|
||||
*/
|
||||
export function encodeMessage(message: OpchanMessage): Uint8Array {
|
||||
// Convert the message to a JSON string
|
||||
const messageJson = JSON.stringify(message);
|
||||
|
||||
// Convert the string to a Uint8Array
|
||||
return new TextEncoder().encode(messageJson);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a message from a Uint8Array based on its type
|
||||
*/
|
||||
export function decodeMessage(payload: Uint8Array, type?: MessageType): OpchanMessage {
|
||||
// Convert the Uint8Array to a string
|
||||
const messageJson = new TextDecoder().decode(payload);
|
||||
|
||||
// Parse the JSON string to an object
|
||||
const message = JSON.parse(messageJson) as OpchanMessage;
|
||||
|
||||
// Validate the message type if specified
|
||||
if (type && message.type !== type) {
|
||||
throw new Error(`Expected message of type ${type}, but got ${message.type}`);
|
||||
}
|
||||
|
||||
// Return the decoded message
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type-specific decoders
|
||||
@ -48,4 +52,19 @@ export function decodeCommentMessage(payload: Uint8Array): CommentMessage {
|
||||
|
||||
export function decodeVoteMessage(payload: Uint8Array): VoteMessage {
|
||||
return decodeMessage(payload, MessageType.VOTE) as VoteMessage;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a message from a Uint8Array based on its type
|
||||
*/
|
||||
function decodeMessage(payload: Uint8Array, type?: MessageType): OpchanMessage {
|
||||
const messageJson = new TextDecoder().decode(payload);
|
||||
const message = JSON.parse(messageJson) as OpchanMessage;
|
||||
|
||||
if (type && message.type !== type) {
|
||||
throw new Error(`Expected message of type ${type}, but got ${message.type}`);
|
||||
}
|
||||
|
||||
// Return the decoded message
|
||||
return message;
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
import { NetworkConfig, ShardInfo } from "@waku/sdk";
|
||||
import { MessageType } from "./types";
|
||||
import type { QueryRequestParams } from '@waku/sdk'
|
||||
|
||||
/**
|
||||
* Content topics for different message types
|
||||
@ -11,20 +11,15 @@ export const CONTENT_TOPICS: Record<MessageType, string> = {
|
||||
[MessageType.VOTE]: '/opchan/1/vote/proto'
|
||||
};
|
||||
|
||||
export const NETWORK_CONFIG: NetworkConfig = {
|
||||
contentTopics: Object.values(CONTENT_TOPICS),
|
||||
shards: [1],
|
||||
clusterId: 42
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap nodes for the Waku network
|
||||
* These are public Waku nodes that our node will connect to on startup
|
||||
*/
|
||||
export const BOOTSTRAP_NODES = [
|
||||
'/dns4/node-01.ac-cn-hongkong-c.wakuv2.test.statusim.net/tcp/443/wss/p2p/16Uiu2HAkykgaECHswi3YKJ5dMLbq2kPVCo89fcyTd2Hz8tHPeV4y',
|
||||
'/dns4/node-01.do-ams3.wakuv2.test.statusim.net/tcp/443/wss/p2p/16Uiu2HAmPLe7Mzm8TsYUubgCAW1aJoeFScxrLj8ppHFivPo97bUZ',
|
||||
'/dns4/node-01.gc-us-central1-a.wakuv2.test.statusim.net/tcp/443/wss/p2p/16Uiu2HAmJb2e28qLXxT5kZxVUUoJt72EMzNGXB47Rxx5hw3q4YjS'
|
||||
];
|
||||
|
||||
// Default store query options
|
||||
// export const DEFAULT_STORE_QUERY_OPTIONS: QueryRequestParams = {
|
||||
// contentTopics: [CONTENT_TOPICS[MessageType.CELL], CONTENT_TOPICS[MessageType.POST], CONTENT_TOPICS[MessageType.COMMENT], CONTENT_TOPICS[MessageType.VOTE]],
|
||||
// includeData: true,
|
||||
// paginationForward: false,
|
||||
// pubsubTopic: ""
|
||||
// };
|
||||
];
|
||||
68
src/lib/waku/lightpush_filter.ts
Normal file
68
src/lib/waku/lightpush_filter.ts
Normal file
@ -0,0 +1,68 @@
|
||||
import { LightNode } from "@waku/sdk";
|
||||
import { decodeCellMessage, decodeCommentMessage, decodePostMessage, decoders, decodeVoteMessage, encodeMessage, encoders } from "./codec";
|
||||
import { CellMessage, CommentMessage, MessageType, PostMessage, VoteMessage } from "./types";
|
||||
import { CONTENT_TOPICS } from "./constants";
|
||||
import { OpchanMessage } from "@/types";
|
||||
|
||||
export class EphemeralProtocolsManager {
|
||||
private node: LightNode;
|
||||
|
||||
constructor(node: LightNode) {
|
||||
this.node = node;
|
||||
}
|
||||
|
||||
public async sendMessage(message: OpchanMessage) {
|
||||
const encodedMessage = encodeMessage(message);
|
||||
await this.node.lightPush.send(encoders[message.type], {
|
||||
payload: encodedMessage
|
||||
});
|
||||
}
|
||||
|
||||
public async subscribeToMessages(types: MessageType[]) {
|
||||
const result: (CellMessage | PostMessage | CommentMessage | VoteMessage)[] = [];
|
||||
|
||||
const subscription = await this.node.filter.subscribe(Object.values(decoders), async (message) => {
|
||||
const {contentTopic, payload} = message;
|
||||
const toDecode = [
|
||||
types.includes(MessageType.CELL) ? decodeCellMessage(payload) : null,
|
||||
types.includes(MessageType.POST) ? decodePostMessage(payload) : null,
|
||||
types.includes(MessageType.COMMENT) ? decodeCommentMessage(payload) : null,
|
||||
types.includes(MessageType.VOTE) ? decodeVoteMessage(payload) : null
|
||||
]
|
||||
const decodedMessage = await Promise.race(toDecode);
|
||||
|
||||
let parsedMessage: OpchanMessage | null = null;
|
||||
switch(contentTopic) {
|
||||
case CONTENT_TOPICS['cell']:
|
||||
parsedMessage = decodedMessage as CellMessage;
|
||||
break;
|
||||
case CONTENT_TOPICS['post']:
|
||||
parsedMessage = decodedMessage as PostMessage;
|
||||
break;
|
||||
case CONTENT_TOPICS['comment']:
|
||||
parsedMessage = decodedMessage as CommentMessage;
|
||||
break;
|
||||
case CONTENT_TOPICS['vote']:
|
||||
parsedMessage = decodedMessage as VoteMessage;
|
||||
break;
|
||||
default:
|
||||
console.error(`Unknown content topic: ${contentTopic}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (parsedMessage) {
|
||||
result.push(parsedMessage);
|
||||
}
|
||||
});
|
||||
|
||||
if (subscription.error) {
|
||||
throw new Error(subscription.error);
|
||||
}
|
||||
|
||||
if (subscription.results.successes.length === 0) {
|
||||
throw new Error("No successes");
|
||||
}
|
||||
|
||||
return {result, subscription};
|
||||
}
|
||||
}
|
||||
@ -1,142 +0,0 @@
|
||||
import { MessageType } from './constants';
|
||||
import { DecodedMessage } from '@waku/sdk';
|
||||
import { Cell, Post, Comment } from '@/types/forum';
|
||||
|
||||
// Base structure for all messages
|
||||
export interface WakuMessageBase {
|
||||
messageType: MessageType;
|
||||
timestamp: number;
|
||||
sender: string; // Bitcoin address of sender
|
||||
signature?: string; // Signature to verify sender
|
||||
}
|
||||
|
||||
// Message structures for different content types
|
||||
export interface CellMessage extends WakuMessageBase {
|
||||
messageType: MessageType.CELL;
|
||||
cellId: string;
|
||||
name: string;
|
||||
description: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
export interface PostMessage extends WakuMessageBase {
|
||||
messageType: MessageType.POST;
|
||||
postId: string;
|
||||
cellId: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface CommentMessage extends WakuMessageBase {
|
||||
messageType: MessageType.COMMENT;
|
||||
commentId: string;
|
||||
postId: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface VoteMessage extends WakuMessageBase {
|
||||
messageType: MessageType.VOTE;
|
||||
targetId: string; // postId or commentId
|
||||
isUpvote: boolean;
|
||||
}
|
||||
|
||||
// Type for all possible messages
|
||||
export type WakuMessage =
|
||||
| CellMessage
|
||||
| PostMessage
|
||||
| CommentMessage
|
||||
| VoteMessage;
|
||||
|
||||
// Utility functions for converting between message types and application models
|
||||
export function cellToMessage(cell: Cell, sender: string): CellMessage {
|
||||
return {
|
||||
messageType: MessageType.CELL,
|
||||
timestamp: Date.now(),
|
||||
sender,
|
||||
cellId: cell.id,
|
||||
name: cell.name,
|
||||
description: cell.description,
|
||||
icon: cell.icon
|
||||
};
|
||||
}
|
||||
|
||||
export function messageToCell(message: CellMessage): Cell {
|
||||
return {
|
||||
id: message.cellId,
|
||||
name: message.name,
|
||||
description: message.description,
|
||||
icon: message.icon
|
||||
};
|
||||
}
|
||||
|
||||
export function postToMessage(post: Post, sender: string): PostMessage {
|
||||
return {
|
||||
messageType: MessageType.POST,
|
||||
timestamp: Date.now(),
|
||||
sender,
|
||||
postId: post.id,
|
||||
cellId: post.cellId,
|
||||
content: post.content
|
||||
};
|
||||
}
|
||||
|
||||
export function messageToPost(message: PostMessage): Post {
|
||||
return {
|
||||
id: message.postId,
|
||||
cellId: message.cellId,
|
||||
authorAddress: message.sender,
|
||||
content: message.content,
|
||||
timestamp: message.timestamp,
|
||||
upvotes: [],
|
||||
downvotes: []
|
||||
};
|
||||
}
|
||||
|
||||
export function commentToMessage(comment: Comment, sender: string): CommentMessage {
|
||||
return {
|
||||
messageType: MessageType.COMMENT,
|
||||
timestamp: Date.now(),
|
||||
sender,
|
||||
commentId: comment.id,
|
||||
postId: comment.postId,
|
||||
content: comment.content
|
||||
};
|
||||
}
|
||||
|
||||
export function messageToComment(message: CommentMessage): Comment {
|
||||
return {
|
||||
id: message.commentId,
|
||||
postId: message.postId,
|
||||
authorAddress: message.sender,
|
||||
content: message.content,
|
||||
timestamp: message.timestamp,
|
||||
upvotes: [],
|
||||
downvotes: []
|
||||
};
|
||||
}
|
||||
|
||||
// Parse message from decoded waku message
|
||||
export function parseMessage(decodedMessage: DecodedMessage): WakuMessage | null {
|
||||
try {
|
||||
if (!decodedMessage.payload) return null;
|
||||
|
||||
const messageString = new TextDecoder().decode(decodedMessage.payload);
|
||||
const message = JSON.parse(messageString) as WakuMessage;
|
||||
|
||||
// Validate message has required fields
|
||||
if (!message.messageType || !message.timestamp || !message.sender) {
|
||||
console.error('Invalid message format:', message);
|
||||
return null;
|
||||
}
|
||||
|
||||
return message;
|
||||
} catch (error) {
|
||||
console.error('Error parsing message:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Serialize message to payload bytes
|
||||
export function serializeMessage(message: WakuMessage): Uint8Array {
|
||||
const messageString = JSON.stringify(message);
|
||||
return new TextEncoder().encode(messageString);
|
||||
}
|
||||
100
src/lib/waku/messages_parser.ts
Normal file
100
src/lib/waku/messages_parser.ts
Normal file
@ -0,0 +1,100 @@
|
||||
import { IDecodedMessage } from '@waku/sdk';
|
||||
import { Cell, Post, Comment } from '@/types';
|
||||
import { CellMessage, CommentMessage, MessageType, PostMessage } from './types';
|
||||
import { OpchanMessage } from '@/types';
|
||||
// Utility functions for converting between message types and application models
|
||||
export function cellToMessage(cell: Cell, sender: string): CellMessage {
|
||||
return {
|
||||
type: MessageType.CELL,
|
||||
timestamp: Date.now(),
|
||||
author: sender,
|
||||
id: cell.id,
|
||||
name: cell.name,
|
||||
description: cell.description,
|
||||
icon: cell.icon
|
||||
};
|
||||
}
|
||||
|
||||
export function messageToCell(message: CellMessage): Cell {
|
||||
return {
|
||||
id: message.id,
|
||||
name: message.name,
|
||||
description: message.description,
|
||||
icon: message.icon
|
||||
};
|
||||
}
|
||||
|
||||
export function postToMessage(post: Post, sender: string): PostMessage {
|
||||
return {
|
||||
type: MessageType.POST,
|
||||
timestamp: Date.now(),
|
||||
author: sender,
|
||||
id: post.id,
|
||||
title: post.title,
|
||||
cellId: post.cellId,
|
||||
content: post.content
|
||||
};
|
||||
}
|
||||
|
||||
export function messageToPost(message: PostMessage): Post {
|
||||
return {
|
||||
id: message.id,
|
||||
cellId: message.cellId,
|
||||
authorAddress: message.author,
|
||||
content: message.content,
|
||||
timestamp: message.timestamp,
|
||||
title: message.title,
|
||||
upvotes: [],
|
||||
downvotes: []
|
||||
};
|
||||
}
|
||||
|
||||
export function commentToMessage(comment: Comment, sender: string): CommentMessage {
|
||||
return {
|
||||
type: MessageType.COMMENT,
|
||||
timestamp: Date.now(),
|
||||
author: sender,
|
||||
id: comment.id,
|
||||
postId: comment.postId,
|
||||
content: comment.content
|
||||
};
|
||||
}
|
||||
|
||||
export function messageToComment(message: CommentMessage): Comment {
|
||||
return {
|
||||
id: message.id,
|
||||
postId: message.postId,
|
||||
authorAddress: message.author,
|
||||
content: message.content,
|
||||
timestamp: message.timestamp,
|
||||
upvotes: [],
|
||||
downvotes: []
|
||||
};
|
||||
}
|
||||
|
||||
// Parse message from decoded waku message
|
||||
export function parseMessage(decodedMessage: IDecodedMessage): OpchanMessage | null {
|
||||
try {
|
||||
if (!decodedMessage.payload) return null;
|
||||
|
||||
const messageString = new TextDecoder().decode(decodedMessage.payload);
|
||||
const message = JSON.parse(messageString) as OpchanMessage;
|
||||
|
||||
// Validate message has required fields
|
||||
if (!message.type || !message.timestamp || !message.author) {
|
||||
console.error('Invalid message format:', message);
|
||||
return null;
|
||||
}
|
||||
|
||||
return message;
|
||||
} catch (error) {
|
||||
console.error('Error parsing message:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Serialize message to payload bytes
|
||||
export function serializeMessage(message: OpchanMessage): Uint8Array {
|
||||
const messageString = JSON.stringify(message);
|
||||
return new TextEncoder().encode(messageString);
|
||||
}
|
||||
50
src/lib/waku/store.ts
Normal file
50
src/lib/waku/store.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import { IDecodedMessage, LightNode } from "@waku/sdk";
|
||||
import { decoders, decodeCellMessage, decodePostMessage, decodeCommentMessage, decodeVoteMessage } from "./codec";
|
||||
import { CONTENT_TOPICS } from "./constants";
|
||||
import { CellMessage, PostMessage, CommentMessage, VoteMessage } from "./types";
|
||||
|
||||
class StoreManager {
|
||||
private node: LightNode;
|
||||
|
||||
constructor(node: LightNode) {
|
||||
this.node = node;
|
||||
}
|
||||
|
||||
public async queryStore() {
|
||||
const result: (CellMessage | PostMessage | CommentMessage | VoteMessage)[] = [];
|
||||
|
||||
await this.node.store.queryWithOrderedCallback(
|
||||
Object.values(decoders),
|
||||
(message: IDecodedMessage) => {
|
||||
const {contentTopic, payload} = message;
|
||||
let parsedMessage: (CellMessage | PostMessage | CommentMessage | VoteMessage) | null = null;
|
||||
|
||||
switch(contentTopic) {
|
||||
case CONTENT_TOPICS['cell']:
|
||||
parsedMessage = decodeCellMessage(payload) as CellMessage;
|
||||
break;
|
||||
case CONTENT_TOPICS['post']:
|
||||
parsedMessage = decodePostMessage(payload) as PostMessage;
|
||||
break;
|
||||
case CONTENT_TOPICS['comment']:
|
||||
parsedMessage = decodeCommentMessage(payload) as CommentMessage;
|
||||
break;
|
||||
case CONTENT_TOPICS['vote']:
|
||||
parsedMessage = decodeVoteMessage(payload) as VoteMessage;
|
||||
break;
|
||||
default:
|
||||
console.error(`Unknown content topic: ${contentTopic}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (parsedMessage) {
|
||||
result.push(parsedMessage);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
export default StoreManager;
|
||||
@ -25,6 +25,7 @@ export interface CellMessage extends BaseMessage {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -45,7 +46,6 @@ export interface CommentMessage extends BaseMessage {
|
||||
type: MessageType.COMMENT;
|
||||
id: string;
|
||||
postId: string;
|
||||
parentId?: string; // Optional for nested comments
|
||||
content: string;
|
||||
}
|
||||
|
||||
@ -56,24 +56,7 @@ export interface VoteMessage extends BaseMessage {
|
||||
type: MessageType.VOTE;
|
||||
id: string;
|
||||
targetId: string; // ID of the post or comment being voted on
|
||||
value: number; // 1 for upvote, -1 for downvote
|
||||
}
|
||||
|
||||
/**
|
||||
* Union type of all possible message types
|
||||
*/
|
||||
export type OpchanMessage = CellMessage | PostMessage | CommentMessage | VoteMessage;
|
||||
|
||||
/**
|
||||
* Listener function type for Waku service events
|
||||
*/
|
||||
export type MessageListener<T extends OpchanMessage> = (message: T) => void;
|
||||
|
||||
/**
|
||||
* Subscription object returned when registering listeners
|
||||
*/
|
||||
export interface Subscription {
|
||||
unsubscribe: () => void;
|
||||
value: 1 | -1;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -1,3 +1,6 @@
|
||||
import { CellMessage, CommentMessage, PostMessage, VoteMessage } from "@/lib/waku/types";
|
||||
|
||||
export type OpchanMessage = CellMessage | PostMessage | CommentMessage | VoteMessage;
|
||||
|
||||
export interface User {
|
||||
address: string;
|
||||
@ -17,10 +20,11 @@ export interface Post {
|
||||
id: string;
|
||||
cellId: string;
|
||||
authorAddress: string;
|
||||
title: string;
|
||||
content: string;
|
||||
timestamp: number;
|
||||
upvotes: string[];
|
||||
downvotes: string[];
|
||||
upvotes: VoteMessage[];
|
||||
downvotes: VoteMessage[];
|
||||
}
|
||||
|
||||
export interface Comment {
|
||||
@ -29,6 +33,6 @@ export interface Comment {
|
||||
authorAddress: string;
|
||||
content: string;
|
||||
timestamp: number;
|
||||
upvotes: string[];
|
||||
downvotes: string[];
|
||||
upvotes: VoteMessage[];
|
||||
downvotes: VoteMessage[];
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user