2022-02-23 14:49:00 +00:00
|
|
|
import debug from 'debug'
|
2022-05-25 12:52:48 +00:00
|
|
|
import { waku_message, WakuMessage } from 'js-waku'
|
2022-02-23 14:03:14 +00:00
|
|
|
|
2022-02-23 14:49:00 +00:00
|
|
|
import { Chat } from './chat'
|
|
|
|
import { ApplicationMetadataMessage_Type } from './proto/status/v1/application_metadata_message'
|
|
|
|
import { getLatestUserNickname } from './utils'
|
|
|
|
import { ApplicationMetadataMessage } from './wire/application_metadata_message'
|
2022-02-24 21:58:50 +00:00
|
|
|
import { ChatMessage } from './wire/chat_message'
|
|
|
|
|
|
|
|
import type { Identity } from './identity'
|
|
|
|
import type { Content } from './wire/chat_message'
|
2022-05-25 12:52:48 +00:00
|
|
|
import type { Waku } from 'js-waku'
|
2022-02-23 14:03:14 +00:00
|
|
|
|
2022-02-23 14:49:00 +00:00
|
|
|
const dbg = debug('communities:messenger')
|
2022-02-23 14:03:14 +00:00
|
|
|
|
2022-05-29 21:06:18 +00:00
|
|
|
// TODO: pass waku client
|
2022-02-23 14:03:14 +00:00
|
|
|
export class Messenger {
|
2022-02-23 14:49:00 +00:00
|
|
|
waku: Waku
|
|
|
|
chatsById: Map<string, Chat>
|
2022-02-23 14:03:14 +00:00
|
|
|
observers: {
|
|
|
|
[chatId: string]: Set<
|
|
|
|
(
|
|
|
|
message: ApplicationMetadataMessage,
|
|
|
|
timestamp: Date,
|
|
|
|
chatId: string
|
|
|
|
) => void
|
2022-02-23 14:49:00 +00:00
|
|
|
>
|
|
|
|
}
|
|
|
|
identity: Identity | undefined
|
2022-02-23 14:03:14 +00:00
|
|
|
|
|
|
|
private constructor(identity: Identity | undefined, waku: Waku) {
|
2022-02-23 14:49:00 +00:00
|
|
|
this.identity = identity
|
|
|
|
this.waku = waku
|
|
|
|
this.chatsById = new Map()
|
|
|
|
this.observers = {}
|
2022-02-23 14:03:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
public static async create(
|
|
|
|
identity: Identity | undefined,
|
2022-05-25 12:52:48 +00:00
|
|
|
// wakuOptions?: waku.CreateOptions
|
|
|
|
// TODO: pass waku as client
|
2022-06-01 05:39:47 +00:00
|
|
|
wakuOptions?: waku.CreateOptions
|
2022-02-23 14:03:14 +00:00
|
|
|
): Promise<Messenger> {
|
2022-06-01 05:39:47 +00:00
|
|
|
const _wakuOptions = Object.assign(
|
|
|
|
{ bootstrap: { default: true } },
|
|
|
|
wakuOptions
|
|
|
|
)
|
|
|
|
const waku = await Waku.create(_wakuOptions)
|
2022-02-23 14:49:00 +00:00
|
|
|
return new Messenger(identity, waku)
|
2022-02-23 14:03:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Joins a public chat using its id.
|
|
|
|
*
|
|
|
|
* For community chats, prefer [[joinChat]].
|
|
|
|
*
|
|
|
|
* Use `addListener` to get messages received on this chat.
|
|
|
|
*/
|
|
|
|
public async joinChatById(chatId: string): Promise<void> {
|
2022-02-23 14:49:00 +00:00
|
|
|
const chat = await Chat.create(chatId)
|
2022-02-23 14:03:14 +00:00
|
|
|
|
2022-02-23 14:49:00 +00:00
|
|
|
await this.joinChat(chat)
|
2022-02-23 14:03:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Joins several of public chats.
|
|
|
|
*
|
|
|
|
* Use `addListener` to get messages received on these chats.
|
|
|
|
*/
|
|
|
|
public async joinChats(chats: Iterable<Chat>): Promise<void> {
|
|
|
|
await Promise.all(
|
2022-02-23 14:49:00 +00:00
|
|
|
Array.from(chats).map(chat => {
|
|
|
|
return this.joinChat(chat)
|
2022-02-23 14:03:14 +00:00
|
|
|
})
|
2022-02-23 14:49:00 +00:00
|
|
|
)
|
2022-02-23 14:03:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Joins a public chat.
|
|
|
|
*
|
|
|
|
* Use `addListener` to get messages received on this chat.
|
|
|
|
*/
|
|
|
|
public async joinChat(chat: Chat): Promise<void> {
|
|
|
|
if (this.chatsById.has(chat.id))
|
2022-02-23 14:49:00 +00:00
|
|
|
throw `Failed to join chat, it is already joined: ${chat.id}`
|
2022-02-23 14:03:14 +00:00
|
|
|
|
|
|
|
this.waku.addDecryptionKey(chat.symKey, {
|
2022-05-29 21:53:38 +00:00
|
|
|
method: waku_message.DecryptionMethod.Symmetric,
|
2022-02-23 14:03:14 +00:00
|
|
|
contentTopics: [chat.contentTopic],
|
2022-02-23 14:49:00 +00:00
|
|
|
})
|
2022-02-23 14:03:14 +00:00
|
|
|
|
|
|
|
this.waku.relay.addObserver(
|
|
|
|
(wakuMessage: WakuMessage) => {
|
2022-02-23 14:49:00 +00:00
|
|
|
if (!wakuMessage.payload || !wakuMessage.timestamp) return
|
2022-02-23 14:03:14 +00:00
|
|
|
|
2022-02-23 14:49:00 +00:00
|
|
|
const message = ApplicationMetadataMessage.decode(wakuMessage.payload)
|
2022-02-23 14:03:14 +00:00
|
|
|
|
|
|
|
switch (message.type) {
|
|
|
|
case ApplicationMetadataMessage_Type.TYPE_CHAT_MESSAGE:
|
2022-02-23 14:49:00 +00:00
|
|
|
this._handleNewChatMessage(chat, message, wakuMessage.timestamp)
|
|
|
|
break
|
2022-02-23 14:03:14 +00:00
|
|
|
default:
|
2022-02-23 14:49:00 +00:00
|
|
|
dbg('Received unsupported message type', message.type)
|
2022-02-23 14:03:14 +00:00
|
|
|
}
|
|
|
|
},
|
|
|
|
[chat.contentTopic]
|
2022-02-23 14:49:00 +00:00
|
|
|
)
|
2022-02-23 14:03:14 +00:00
|
|
|
|
2022-02-23 14:49:00 +00:00
|
|
|
this.chatsById.set(chat.id, chat)
|
2022-02-23 14:03:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Sends a message on the given chat Id.
|
|
|
|
*/
|
|
|
|
public async sendMessage(
|
|
|
|
chatId: string,
|
|
|
|
content: Content,
|
|
|
|
responseTo?: string
|
|
|
|
): Promise<void> {
|
|
|
|
if (this.identity) {
|
2022-02-23 14:49:00 +00:00
|
|
|
const chat = this.chatsById.get(chatId)
|
|
|
|
if (!chat) throw `Failed to send message, chat not joined: ${chatId}`
|
2022-02-23 14:03:14 +00:00
|
|
|
|
2022-02-23 14:49:00 +00:00
|
|
|
const chatMessage = chat.createMessage(content, responseTo)
|
2022-02-23 14:03:14 +00:00
|
|
|
|
|
|
|
const appMetadataMessage = ApplicationMetadataMessage.create(
|
|
|
|
chatMessage.encode(),
|
|
|
|
ApplicationMetadataMessage_Type.TYPE_CHAT_MESSAGE,
|
|
|
|
this.identity
|
2022-02-23 14:49:00 +00:00
|
|
|
)
|
2022-02-23 14:03:14 +00:00
|
|
|
|
|
|
|
const wakuMessage = await WakuMessage.fromBytes(
|
|
|
|
appMetadataMessage.encode(),
|
|
|
|
chat.contentTopic,
|
|
|
|
{ symKey: chat.symKey, sigPrivKey: this.identity.privateKey }
|
2022-02-23 14:49:00 +00:00
|
|
|
)
|
2022-02-23 14:03:14 +00:00
|
|
|
|
2022-02-23 14:49:00 +00:00
|
|
|
await this.waku.relay.send(wakuMessage)
|
2022-02-23 14:03:14 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Add an observer of new messages received on the given chat id.
|
|
|
|
*
|
|
|
|
* @throws string If the chat has not been joined first using [joinChat].
|
|
|
|
*/
|
|
|
|
public addObserver(
|
|
|
|
observer: (
|
|
|
|
message: ApplicationMetadataMessage,
|
|
|
|
timestamp: Date,
|
|
|
|
chatId: string
|
|
|
|
) => void,
|
|
|
|
chatId: string | string[]
|
|
|
|
): void {
|
2022-02-23 14:49:00 +00:00
|
|
|
let chats = []
|
2022-02-23 14:03:14 +00:00
|
|
|
|
2022-02-23 14:49:00 +00:00
|
|
|
if (typeof chatId === 'string') {
|
|
|
|
chats.push(chatId)
|
2022-02-23 14:03:14 +00:00
|
|
|
} else {
|
2022-02-23 14:49:00 +00:00
|
|
|
chats = [...chatId]
|
2022-02-23 14:03:14 +00:00
|
|
|
}
|
|
|
|
|
2022-02-23 14:49:00 +00:00
|
|
|
chats.forEach(id => {
|
2022-02-23 14:03:14 +00:00
|
|
|
if (!this.chatsById.has(id))
|
2022-02-23 14:49:00 +00:00
|
|
|
throw 'Cannot add observer on a chat that is not joined.'
|
2022-02-23 14:03:14 +00:00
|
|
|
if (!this.observers[id]) {
|
2022-02-23 14:49:00 +00:00
|
|
|
this.observers[id] = new Set()
|
2022-02-23 14:03:14 +00:00
|
|
|
}
|
|
|
|
|
2022-02-23 14:49:00 +00:00
|
|
|
this.observers[id].add(observer)
|
|
|
|
})
|
2022-02-23 14:03:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Delete an observer of new messages received on the given chat id.
|
|
|
|
*
|
|
|
|
* @throws string If the chat has not been joined first using [joinChat].
|
|
|
|
*/
|
|
|
|
|
|
|
|
deleteObserver(
|
|
|
|
observer: (message: ApplicationMetadataMessage) => void,
|
|
|
|
chatId: string
|
|
|
|
): void {
|
|
|
|
if (this.observers[chatId]) {
|
2022-02-23 14:49:00 +00:00
|
|
|
this.observers[chatId].delete(observer)
|
2022-02-23 14:03:14 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Stops the messenger.
|
|
|
|
*/
|
|
|
|
public async stop(): Promise<void> {
|
2022-02-23 14:49:00 +00:00
|
|
|
await this.waku.stop()
|
2022-02-23 14:03:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Retrieve previous messages from a Waku Store node for the given chat Id.
|
|
|
|
*
|
|
|
|
* Note: note sure what is the preferred interface: callback or returning all messages
|
|
|
|
* Callback is more flexible and allow processing messages as they are retrieved instead of waiting for the
|
|
|
|
* full retrieval via paging to be done.
|
|
|
|
*/
|
|
|
|
public async retrievePreviousMessages(
|
|
|
|
chatId: string,
|
|
|
|
startTime: Date,
|
|
|
|
endTime: Date,
|
|
|
|
callback?: (messages: ApplicationMetadataMessage[]) => void
|
|
|
|
): Promise<number> {
|
2022-02-23 14:49:00 +00:00
|
|
|
const chat = this.chatsById.get(chatId)
|
2022-02-23 14:03:14 +00:00
|
|
|
if (!chat)
|
2022-02-23 14:49:00 +00:00
|
|
|
throw `Failed to retrieve messages, chat is not joined: ${chatId}`
|
2022-02-23 14:03:14 +00:00
|
|
|
|
|
|
|
const _callback = (wakuMessages: WakuMessage[]): void => {
|
|
|
|
const isDefined = (
|
|
|
|
msg: ApplicationMetadataMessage | undefined
|
|
|
|
): msg is ApplicationMetadataMessage => {
|
2022-02-23 14:49:00 +00:00
|
|
|
return !!msg
|
|
|
|
}
|
2022-02-23 14:03:14 +00:00
|
|
|
|
|
|
|
const messages = wakuMessages.map((wakuMessage: WakuMessage) => {
|
2022-02-23 14:49:00 +00:00
|
|
|
if (!wakuMessage.payload || !wakuMessage.timestamp) return
|
2022-02-23 14:03:14 +00:00
|
|
|
|
2022-02-23 14:49:00 +00:00
|
|
|
const message = ApplicationMetadataMessage.decode(wakuMessage.payload)
|
2022-02-23 14:03:14 +00:00
|
|
|
|
|
|
|
switch (message.type) {
|
|
|
|
case ApplicationMetadataMessage_Type.TYPE_CHAT_MESSAGE:
|
2022-02-23 14:49:00 +00:00
|
|
|
this._handleNewChatMessage(chat, message, wakuMessage.timestamp)
|
|
|
|
return message
|
2022-02-23 14:03:14 +00:00
|
|
|
default:
|
2022-02-23 14:49:00 +00:00
|
|
|
dbg('Retrieved unsupported message type', message.type)
|
|
|
|
return
|
2022-02-23 14:03:14 +00:00
|
|
|
}
|
2022-02-23 14:49:00 +00:00
|
|
|
})
|
2022-02-23 14:03:14 +00:00
|
|
|
if (callback) {
|
2022-02-23 14:49:00 +00:00
|
|
|
callback(messages.filter(isDefined))
|
2022-02-23 14:03:14 +00:00
|
|
|
}
|
2022-02-23 14:49:00 +00:00
|
|
|
}
|
2022-02-23 14:03:14 +00:00
|
|
|
const allMessages = await this.waku.store.queryHistory(
|
|
|
|
[chat.contentTopic],
|
|
|
|
{
|
|
|
|
timeFilter: { startTime, endTime },
|
|
|
|
callback: _callback,
|
|
|
|
}
|
2022-02-23 14:49:00 +00:00
|
|
|
)
|
|
|
|
return allMessages.length
|
2022-02-23 14:03:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
private _handleNewChatMessage(
|
|
|
|
chat: Chat,
|
|
|
|
message: ApplicationMetadataMessage,
|
|
|
|
timestamp: Date
|
|
|
|
): void {
|
2022-02-23 14:49:00 +00:00
|
|
|
if (!message.payload || !message.type || !message.signature) return
|
2022-02-23 14:03:14 +00:00
|
|
|
|
2022-02-23 14:49:00 +00:00
|
|
|
const chatMessage = ChatMessage.decode(message.payload)
|
|
|
|
chat.handleNewMessage(chatMessage)
|
2022-02-23 14:03:14 +00:00
|
|
|
|
|
|
|
if (this.observers[chat.id]) {
|
2022-02-23 14:49:00 +00:00
|
|
|
this.observers[chat.id].forEach(observer => {
|
|
|
|
observer(message, timestamp, chat.id)
|
|
|
|
})
|
2022-02-23 14:03:14 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async checkIfUserInWakuNetwork(publicKey: Uint8Array): Promise<boolean> {
|
|
|
|
const { clock, nickname } = await getLatestUserNickname(
|
|
|
|
publicKey,
|
|
|
|
this.waku
|
2022-02-23 14:49:00 +00:00
|
|
|
)
|
|
|
|
return clock > 0 && nickname !== ''
|
2022-02-23 14:03:14 +00:00
|
|
|
}
|
|
|
|
}
|