blob: a868afe5c90a82c0a7c3611d44385decd2ab5e1b [file] [log] [blame]
Issam E. Maghnif796a092022-10-09 20:25:26 +00001/*
2 * Copyright (C) 2022 Savoir-faire Linux Inc.
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU Affero General Public License as
6 * published by the Free Software Foundation; either version 3 of the
7 * License, or (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU Affero General Public License for more details.
13 *
14 * You should have received a copy of the GNU Affero General Public
15 * License along with this program. If not, see
16 * <https://www.gnu.org/licenses/>.
17 */
Misha Krieger-Raynauldb933fbb2022-11-15 15:11:09 -050018import { createRequire } from 'node:module';
19
Issam E. Maghni0432cb72022-11-12 06:09:26 +000020import {
21 AccountDetails,
Misha Krieger-Raynauldcfa44302022-11-30 18:36:36 -050022 ContactDetails,
23 ConversationInfos,
Issam E. Maghni0432cb72022-11-12 06:09:26 +000024 ConversationMessage,
Misha Krieger-Raynauldcfa44302022-11-30 18:36:36 -050025 Devices,
26 LookupResult,
Issam E. Maghni0432cb72022-11-12 06:09:26 +000027 Message,
Misha Krieger-Raynauld99e5a702022-11-30 22:36:52 -050028 RegisteredNameFoundState,
Issam E. Maghni0432cb72022-11-12 06:09:26 +000029 VolatileDetails,
30 WebSocketMessage,
31 WebSocketMessageType,
32} from 'jami-web-common';
Issam E. Maghnif796a092022-10-09 20:25:26 +000033import log from 'loglevel';
Misha Krieger-Raynauld6f9c7ae2022-10-28 11:41:45 -040034import { filter, firstValueFrom, map, Subject } from 'rxjs';
Issam E. Maghnif796a092022-10-09 20:25:26 +000035import { Service } from 'typedi';
36
Misha Krieger-Raynauldb933fbb2022-11-15 15:11:09 -050037import { WebSocketServer } from '../websocket/websocket-server.js';
Misha Krieger-Raynauldcfa44302022-11-30 18:36:36 -050038import { ConversationMemberInfos } from './conversation-member-infos.js';
39import { ConversationRequestMetadata } from './conversation-request-metadata.js';
Misha Krieger-Raynauldaddd6fe2022-10-22 12:46:04 -040040import { JamiSignal } from './jami-signal.js';
41import {
Misha Krieger-Raynauld68a9b562022-10-28 19:47:46 -040042 AccountDetailsChanged,
Misha Krieger-Raynauldcb11bba2022-11-11 18:08:33 -050043 AccountMessageStatusChanged,
44 ContactAdded,
45 ContactRemoved,
Misha Krieger-Raynauld68a9b562022-10-28 19:47:46 -040046 ConversationLoaded,
Michelle Sepkap Simedd82cbf2022-11-17 23:31:49 -050047 ConversationMemberEvent,
Misha Krieger-Raynauld68a9b562022-10-28 19:47:46 -040048 ConversationReady,
49 ConversationRemoved,
Michelle Sepkap Simedd82cbf2022-11-17 23:31:49 -050050 ConversationRequestReceived,
Charlieb62c6782022-10-30 15:14:56 -040051 IncomingAccountMessage,
Misha Krieger-Raynauld68a9b562022-10-28 19:47:46 -040052 KnownDevicesChanged,
53 MessageReceived,
Misha Krieger-Raynauldaddd6fe2022-10-22 12:46:04 -040054 NameRegistrationEnded,
55 RegisteredNameFound,
56 RegistrationStateChanged,
57 VolatileDetailsChanged,
58} from './jami-signal-interfaces.js';
Misha Krieger-Raynauldbfed1732022-11-01 20:49:35 -040059import { JamiSwig, StringMap, stringMapToRecord, stringVectToArray, vectMapToRecordArray } from './jami-swig.js';
Misha Krieger-Raynauld99e5a702022-11-30 22:36:52 -050060import {
61 ConversationMemberEventType,
62 MessageState,
63 NameRegistrationEndedState,
64 RegistrationState,
65} from './state-enums.js';
Issam E. Maghnif796a092022-10-09 20:25:26 +000066
Misha Krieger-Raynauldb933fbb2022-11-15 15:11:09 -050067const require = createRequire(import.meta.url);
68
Issam E. Maghnif796a092022-10-09 20:25:26 +000069@Service()
70export class Jamid {
Misha Krieger-Raynauldb933fbb2022-11-15 15:11:09 -050071 private jamiSwig: JamiSwig;
72 private usernamesToAccountIds = new Map<string, string>();
Issam E. Maghnif796a092022-10-09 20:25:26 +000073 private readonly events;
74
Misha Krieger-Raynauldb933fbb2022-11-15 15:11:09 -050075 constructor(private webSocketServer: WebSocketServer) {
76 this.jamiSwig = require('../../jamid.node') as JamiSwig;
Misha Krieger-Raynauld68a9b562022-10-28 19:47:46 -040077
78 // Setup signal handlers
Issam E. Maghnif796a092022-10-09 20:25:26 +000079 const handlers: Record<string, unknown> = {};
Misha Krieger-Raynauld68a9b562022-10-28 19:47:46 -040080
81 // Add default handler for all signals
82 const createDefaultHandler = (signal: string) => {
83 return (...args: unknown[]) => log.warn('Unhandled', signal, args);
Issam E. Maghnif796a092022-10-09 20:25:26 +000084 };
Misha Krieger-Raynauld68a9b562022-10-28 19:47:46 -040085 for (const signal in JamiSignal) {
86 handlers[signal] = createDefaultHandler(signal);
87 }
88
89 // Overwrite handlers for handled signals using RxJS Subjects, converting multiple arguments to objects
90 const onAccountsChanged = new Subject<void>();
91 handlers.AccountsChanged = () => onAccountsChanged.next();
92
93 const onAccountDetailsChanged = new Subject<AccountDetailsChanged>();
94 handlers.AccountDetailsChanged = (accountId: string, details: AccountDetails) =>
95 onAccountDetailsChanged.next({ accountId, details });
Issam E. Maghnif796a092022-10-09 20:25:26 +000096
97 const onVolatileDetailsChanged = new Subject<VolatileDetailsChanged>();
Misha Krieger-Raynauld68a9b562022-10-28 19:47:46 -040098 handlers.VolatileDetailsChanged = (accountId: string, details: VolatileDetails) =>
99 onVolatileDetailsChanged.next({ accountId, details });
Issam E. Maghnif796a092022-10-09 20:25:26 +0000100
101 const onRegistrationStateChanged = new Subject<RegistrationStateChanged>();
Misha Krieger-Raynauld99e5a702022-11-30 22:36:52 -0500102 handlers.RegistrationStateChanged = (accountId: string, state: RegistrationState, code: number, details: string) =>
Issam E. Maghnif796a092022-10-09 20:25:26 +0000103 onRegistrationStateChanged.next({ accountId, state, code, details });
104
105 const onNameRegistrationEnded = new Subject<NameRegistrationEnded>();
Misha Krieger-Raynauld99e5a702022-11-30 22:36:52 -0500106 handlers.NameRegistrationEnded = (accountId: string, state: NameRegistrationEndedState, username: string) =>
Issam E. Maghnif796a092022-10-09 20:25:26 +0000107 onNameRegistrationEnded.next({ accountId, state, username });
108
109 const onRegisteredNameFound = new Subject<RegisteredNameFound>();
Misha Krieger-Raynauld99e5a702022-11-30 22:36:52 -0500110 handlers.RegisteredNameFound = (
111 accountId: string,
112 state: RegisteredNameFoundState,
113 address: string,
114 username: string
115 ) => onRegisteredNameFound.next({ accountId, state, address, username });
Issam E. Maghnif796a092022-10-09 20:25:26 +0000116
Misha Krieger-Raynauld68a9b562022-10-28 19:47:46 -0400117 const onKnownDevicesChanged = new Subject<KnownDevicesChanged>();
Misha Krieger-Raynauldcfa44302022-11-30 18:36:36 -0500118 handlers.KnownDevicesChanged = (accountId: string, devices: Devices) =>
Misha Krieger-Raynauld68a9b562022-10-28 19:47:46 -0400119 onKnownDevicesChanged.next({ accountId, devices });
120
Charlieb62c6782022-10-30 15:14:56 -0400121 const onIncomingAccountMessage = new Subject<IncomingAccountMessage>();
Misha Krieger-Raynauldcb11bba2022-11-11 18:08:33 -0500122 handlers.IncomingAccountMessage = (accountId: string, from: string, payload: Record<string, string>) =>
123 onIncomingAccountMessage.next({ accountId, from, payload });
124
125 const onAccountMessageStatusChanged = new Subject<AccountMessageStatusChanged>();
Misha Krieger-Raynauld99e5a702022-11-30 22:36:52 -0500126 handlers.AccountMessageStatusChanged = (accountId: string, messageId: string, peer: string, state: MessageState) =>
Misha Krieger-Raynauldcb11bba2022-11-11 18:08:33 -0500127 onAccountMessageStatusChanged.next({ accountId, messageId, peer, state });
128
129 const onContactAdded = new Subject<ContactAdded>();
130 handlers.ContactAdded = (accountId: string, contactId: string, confirmed: boolean) =>
131 onContactAdded.next({ accountId, contactId, confirmed });
132
133 const onContactRemoved = new Subject<ContactRemoved>();
134 handlers.ContactRemoved = (accountId: string, contactId: string, banned: boolean) =>
135 onContactRemoved.next({ accountId, contactId, banned });
Charlieb62c6782022-10-30 15:14:56 -0400136
Michelle Sepkap Simedd82cbf2022-11-17 23:31:49 -0500137 const onConversationRequestReceived = new Subject<ConversationRequestReceived>();
Misha Krieger-Raynauldcfa44302022-11-30 18:36:36 -0500138 handlers.ConversationRequestReceived = (
139 accountId: string,
140 conversationId: string,
141 metadata: ConversationRequestMetadata
142 ) => onConversationRequestReceived.next({ accountId, conversationId, metadata });
Michelle Sepkap Simedd82cbf2022-11-17 23:31:49 -0500143
Misha Krieger-Raynauld68a9b562022-10-28 19:47:46 -0400144 const onConversationReady = new Subject<ConversationReady>();
145 handlers.ConversationReady = (accountId: string, conversationId: string) =>
146 onConversationReady.next({ accountId, conversationId });
147
148 const onConversationRemoved = new Subject<ConversationRemoved>();
149 handlers.ConversationRemoved = (accountId: string, conversationId: string) =>
150 onConversationRemoved.next({ accountId, conversationId });
151
152 const onConversationLoaded = new Subject<ConversationLoaded>();
Misha Krieger-Raynauld8a381da2022-11-03 17:37:51 -0400153 handlers.ConversationLoaded = (id: number, accountId: string, conversationId: string, messages: Message[]) =>
154 onConversationLoaded.next({ id, accountId, conversationId, messages });
Misha Krieger-Raynauld68a9b562022-10-28 19:47:46 -0400155
Michelle Sepkap Simedd82cbf2022-11-17 23:31:49 -0500156 const onConversationMemberEvent = new Subject<ConversationMemberEvent>();
157 handlers.ConversationMemberEvent = (
158 accountId: string,
159 conversationId: string,
160 memberUri: string,
Misha Krieger-Raynauld99e5a702022-11-30 22:36:52 -0500161 event: ConversationMemberEventType
Michelle Sepkap Simedd82cbf2022-11-17 23:31:49 -0500162 ) => {
163 onConversationMemberEvent.next({ accountId, conversationId, memberUri, event });
164 };
165
Misha Krieger-Raynauld68a9b562022-10-28 19:47:46 -0400166 const onMessageReceived = new Subject<MessageReceived>();
Misha Krieger-Raynauld8a381da2022-11-03 17:37:51 -0400167 handlers.MessageReceived = (accountId: string, conversationId: string, message: Message) =>
Misha Krieger-Raynauld68a9b562022-10-28 19:47:46 -0400168 onMessageReceived.next({ accountId, conversationId, message });
169
170 // Expose all signals in an events object to allow other handlers to subscribe after jamiSwig.init()
Issam E. Maghnif796a092022-10-09 20:25:26 +0000171 this.events = {
Misha Krieger-Raynauld68a9b562022-10-28 19:47:46 -0400172 onAccountsChanged: onAccountsChanged.asObservable(),
173 onAccountDetailsChanged: onAccountDetailsChanged.asObservable(),
Issam E. Maghnif796a092022-10-09 20:25:26 +0000174 onVolatileDetailsChanged: onVolatileDetailsChanged.asObservable(),
175 onRegistrationStateChanged: onRegistrationStateChanged.asObservable(),
176 onNameRegistrationEnded: onNameRegistrationEnded.asObservable(),
177 onRegisteredNameFound: onRegisteredNameFound.asObservable(),
Misha Krieger-Raynauld68a9b562022-10-28 19:47:46 -0400178 onKnownDevicesChanged: onKnownDevicesChanged.asObservable(),
Charlieb62c6782022-10-30 15:14:56 -0400179 onIncomingAccountMessage: onIncomingAccountMessage.asObservable(),
Misha Krieger-Raynauldcb11bba2022-11-11 18:08:33 -0500180 onAccountMessageStatusChanged: onAccountMessageStatusChanged.asObservable(),
181 onContactAdded: onContactAdded.asObservable(),
182 onContactRemoved: onContactRemoved.asObservable(),
Michelle Sepkap Simedd82cbf2022-11-17 23:31:49 -0500183 onConversationRequestReceived: onConversationRequestReceived.asObservable(),
Misha Krieger-Raynauld68a9b562022-10-28 19:47:46 -0400184 onConversationReady: onConversationReady.asObservable(),
185 onConversationRemoved: onConversationRemoved.asObservable(),
186 onConversationLoaded: onConversationLoaded.asObservable(),
Michelle Sepkap Simedd82cbf2022-11-17 23:31:49 -0500187 onConversationMemberEvent: onConversationMemberEvent.asObservable(),
Misha Krieger-Raynauld68a9b562022-10-28 19:47:46 -0400188 onMessageReceived: onMessageReceived.asObservable(),
Issam E. Maghnif796a092022-10-09 20:25:26 +0000189 };
190
Misha Krieger-Raynauld68a9b562022-10-28 19:47:46 -0400191 this.setupSignalHandlers();
Issam E. Maghnif796a092022-10-09 20:25:26 +0000192
idillon559ce282022-12-22 15:18:32 -0500193 if (process.env.JAMI_WEB_MONITOR === 'true') {
194 this.jamiSwig.monitor(true);
195 }
196
Misha Krieger-Raynauld68a9b562022-10-28 19:47:46 -0400197 // RxJS Subjects are used as signal handlers for the following reasons:
198 // 1. You cannot change event handlers after calling jamiSwig.init()
Issam E. Maghnif796a092022-10-09 20:25:26 +0000199 // 2. You cannot specify multiple handlers for the same event
200 // 3. You cannot specify a default handler
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -0400201 this.jamiSwig.init(handlers);
Issam E. Maghnif796a092022-10-09 20:25:26 +0000202 }
203
Misha Krieger-Raynauld8a381da2022-11-03 17:37:51 -0400204 stop(): void {
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -0400205 this.jamiSwig.fini();
Misha Krieger-Raynauld62a0da92022-10-22 13:46:59 -0400206 }
207
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -0400208 getVolatileAccountDetails(accountId: string): VolatileDetails {
209 return stringMapToRecord(this.jamiSwig.getVolatileAccountDetails(accountId)) as unknown as VolatileDetails;
Issam E. Maghnif796a092022-10-09 20:25:26 +0000210 }
211
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -0400212 getAccountDetails(accountId: string): AccountDetails {
213 return stringMapToRecord(this.jamiSwig.getAccountDetails(accountId)) as unknown as AccountDetails;
214 }
Issam E. Maghnif796a092022-10-09 20:25:26 +0000215
Misha Krieger-Raynauld8a381da2022-11-03 17:37:51 -0400216 setAccountDetails(accountId: string, accountDetails: AccountDetails): void {
simon43da57b2022-10-26 18:22:22 -0400217 const accountDetailsStringMap: StringMap = new this.jamiSwig.StringMap();
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -0400218 for (const [key, value] of Object.entries(accountDetails)) {
219 accountDetailsStringMap.set(key, value);
220 }
221 this.jamiSwig.setAccountDetails(accountId, accountDetailsStringMap);
222 }
Issam E. Maghnif796a092022-10-09 20:25:26 +0000223
Issam E. Maghnib05ad992022-11-20 23:50:48 +0000224 async addAccount(accountDetails: Partial<AccountDetails>): Promise<RegistrationStateChanged> {
225 accountDetails['Account.type'] = 'RING';
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -0400226
Misha Krieger-Raynauld99e5a702022-11-30 22:36:52 -0500227 const accountDetailsStringMap: StringMap = new this.jamiSwig.StringMap();
Issam E. Maghnib05ad992022-11-20 23:50:48 +0000228 for (const [key, value] of Object.entries(accountDetails)) {
Misha Krieger-Raynauld99e5a702022-11-30 22:36:52 -0500229 accountDetailsStringMap.set(key, value.toString());
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -0400230 }
231
Misha Krieger-Raynauld99e5a702022-11-30 22:36:52 -0500232 const accountId = this.jamiSwig.addAccount(accountDetailsStringMap);
Issam E. Maghnif796a092022-10-09 20:25:26 +0000233 return firstValueFrom(
234 this.events.onRegistrationStateChanged.pipe(
Misha Krieger-Raynauld6f9c7ae2022-10-28 11:41:45 -0400235 filter((value) => value.accountId === accountId),
Misha Krieger-Raynauld99e5a702022-11-30 22:36:52 -0500236 filter(
237 (value) => value.state === RegistrationState.Registered || value.state === RegistrationState.ErrorGeneric
238 )
Issam E. Maghnif796a092022-10-09 20:25:26 +0000239 )
240 );
241 }
242
Misha Krieger-Raynauld8a381da2022-11-03 17:37:51 -0400243 removeAccount(accountId: string): void {
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -0400244 this.jamiSwig.removeAccount(accountId);
Issam E. Maghnif796a092022-10-09 20:25:26 +0000245 }
246
Misha Krieger-Raynauld8a381da2022-11-03 17:37:51 -0400247 getAccountIds(): string[] {
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -0400248 return stringVectToArray(this.jamiSwig.getAccountList());
Issam E. Maghnif796a092022-10-09 20:25:26 +0000249 }
250
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -0500251 sendAccountTextMessage(accountId: string, contactId: string, message: string): void {
Charlieb62c6782022-10-30 15:14:56 -0400252 const messageStringMap: StringMap = new this.jamiSwig.StringMap();
Charlie2bc0d672022-11-04 11:53:44 -0400253 messageStringMap.set('application/json', message);
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -0500254 this.jamiSwig.sendAccountTextMessage(accountId, contactId, messageStringMap);
Charlieb62c6782022-10-30 15:14:56 -0400255 }
256
Misha Krieger-Raynauldcfa44302022-11-30 18:36:36 -0500257 async lookupUsername(username: string, accountId?: string): Promise<LookupResult> {
Misha Krieger-Raynauld6f9c7ae2022-10-28 11:41:45 -0400258 const hasRingNs = this.jamiSwig.lookupName(accountId || '', '', username);
Issam E. Maghnif796a092022-10-09 20:25:26 +0000259 if (!hasRingNs) {
Misha Krieger-Raynauld99e5a702022-11-30 22:36:52 -0500260 throw new Error('Jami does not have a nameserver');
Issam E. Maghnif796a092022-10-09 20:25:26 +0000261 }
Misha Krieger-Raynauld6f9c7ae2022-10-28 11:41:45 -0400262 return firstValueFrom(
263 this.events.onRegisteredNameFound.pipe(
264 filter((value) => value.username === username),
265 map(({ accountId: _, ...response }) => response) // Remove accountId from response
266 )
267 );
268 }
269
Misha Krieger-Raynauldcfa44302022-11-30 18:36:36 -0500270 async lookupAddress(address: string, accountId?: string): Promise<LookupResult> {
Misha Krieger-Raynauld6f9c7ae2022-10-28 11:41:45 -0400271 const hasRingNs = this.jamiSwig.lookupAddress(accountId || '', '', address);
272 if (!hasRingNs) {
Misha Krieger-Raynauld99e5a702022-11-30 22:36:52 -0500273 throw new Error('Jami does not have a nameserver');
Misha Krieger-Raynauld6f9c7ae2022-10-28 11:41:45 -0400274 }
275 return firstValueFrom(
276 this.events.onRegisteredNameFound.pipe(
277 filter((value) => value.address === address),
278 map(({ accountId: _, ...response }) => response) // Remove accountId from response
279 )
280 );
Issam E. Maghnif796a092022-10-09 20:25:26 +0000281 }
282
Misha Krieger-Raynauld99e5a702022-11-30 22:36:52 -0500283 async registerUsername(accountId: string, username: string, password: string): Promise<NameRegistrationEndedState> {
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -0400284 const hasRingNs = this.jamiSwig.registerName(accountId, password, username);
285 if (!hasRingNs) {
Misha Krieger-Raynauld99e5a702022-11-30 22:36:52 -0500286 throw new Error('Jami does not have a nameserver');
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -0400287 }
Misha Krieger-Raynauld8a381da2022-11-03 17:37:51 -0400288 return firstValueFrom(
289 this.events.onNameRegistrationEnded.pipe(
290 filter((value) => value.accountId === accountId),
291 map((value) => value.state)
292 )
293 );
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -0400294 }
295
Misha Krieger-Raynauldcfa44302022-11-30 18:36:36 -0500296 getDevices(accountId: string): Devices {
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -0400297 return stringMapToRecord(this.jamiSwig.getKnownRingDevices(accountId));
298 }
299
Misha Krieger-Raynauld8a381da2022-11-03 17:37:51 -0400300 addContact(accountId: string, contactId: string): void {
Misha Krieger-Raynauldbfed1732022-11-01 20:49:35 -0400301 this.jamiSwig.addContact(accountId, contactId);
302 }
303
Michelle Sepkap Simedd82cbf2022-11-17 23:31:49 -0500304 sendTrustRequest(accountId: string, contactId: string): void {
305 this.jamiSwig.sendTrustRequest(accountId, contactId, new this.jamiSwig.Blob());
306 }
307
Misha Krieger-Raynauld8a381da2022-11-03 17:37:51 -0400308 removeContact(accountId: string, contactId: string): void {
Misha Krieger-Raynauldbfed1732022-11-01 20:49:35 -0400309 this.jamiSwig.removeContact(accountId, contactId, false);
310 }
311
Misha Krieger-Raynauld8a381da2022-11-03 17:37:51 -0400312 blockContact(accountId: string, contactId: string): void {
Misha Krieger-Raynauldbfed1732022-11-01 20:49:35 -0400313 this.jamiSwig.removeContact(accountId, contactId, true);
314 }
315
Misha Krieger-Raynauldcfa44302022-11-30 18:36:36 -0500316 getContacts(accountId: string): ContactDetails[] {
317 return vectMapToRecordArray(this.jamiSwig.getContacts(accountId)) as unknown as ContactDetails[];
Misha Krieger-Raynauldbfed1732022-11-01 20:49:35 -0400318 }
319
Misha Krieger-Raynauldcfa44302022-11-30 18:36:36 -0500320 getContactDetails(accountId: string, contactId: string): ContactDetails {
321 return stringMapToRecord(this.jamiSwig.getContactDetails(accountId, contactId)) as unknown as ContactDetails;
Misha Krieger-Raynauldbfed1732022-11-01 20:49:35 -0400322 }
323
Misha Krieger-Raynauld153a1482022-11-05 12:00:41 -0400324 getDefaultModeratorUris(accountId: string): string[] {
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -0400325 return stringVectToArray(this.jamiSwig.getDefaultModerators(accountId));
326 }
327
Misha Krieger-Raynauld153a1482022-11-05 12:00:41 -0400328 addDefaultModerator(accountId: string, contactId: string): void {
329 this.jamiSwig.setDefaultModerator(accountId, contactId, true);
330 }
331
332 removeDefaultModerator(accountId: string, contactId: string): void {
333 this.jamiSwig.setDefaultModerator(accountId, contactId, false);
334 }
335
Misha Krieger-Raynauld8a381da2022-11-03 17:37:51 -0400336 getConversationIds(accountId: string): string[] {
337 return stringVectToArray(this.jamiSwig.getConversations(accountId));
338 }
339
Misha Krieger-Raynauldcfa44302022-11-30 18:36:36 -0500340 getConversationInfos(accountId: string, conversationId: string): ConversationInfos {
341 return stringMapToRecord(
342 this.jamiSwig.conversationInfos(accountId, conversationId)
343 ) as unknown as ConversationInfos;
Misha Krieger-Raynauld8a381da2022-11-03 17:37:51 -0400344 }
345
Misha Krieger-Raynauldcfa44302022-11-30 18:36:36 -0500346 getConversationMembers(accountId: string, conversationId: string): ConversationMemberInfos[] {
347 return vectMapToRecordArray(
348 this.jamiSwig.getConversationMembers(accountId, conversationId)
349 ) as unknown as ConversationMemberInfos[];
Misha Krieger-Raynauld8a381da2022-11-03 17:37:51 -0400350 }
351
idillon07d31cc2022-12-06 22:40:14 -0500352 async getConversationMessages(
353 accountId: string,
354 conversationId: string,
355 fromMessage = '',
356 n = 32
357 ): Promise<Message[]> {
358 const requestId = this.jamiSwig.loadConversationMessages(accountId, conversationId, fromMessage, n);
Misha Krieger-Raynauld8a381da2022-11-03 17:37:51 -0400359 return firstValueFrom(
360 this.events.onConversationLoaded.pipe(
361 filter((value) => value.id === requestId),
362 map((value) => value.messages)
363 )
364 );
365 }
366
idillon07d31cc2022-12-06 22:40:14 -0500367 removeConversation(accountId: string, conversationId: string) {
368 this.jamiSwig.removeConversation(accountId, conversationId);
369 }
370
Issam E. Maghni2cb239b2022-11-18 22:39:32 +0000371 sendConversationMessage(
372 accountId: string,
373 conversationId: string,
374 message: string,
375 replyTo?: string,
376 flag?: number
377 ): void {
378 this.jamiSwig.sendMessage(accountId, conversationId, message, replyTo ?? '', flag ?? 0);
Misha Krieger-Raynauld8a381da2022-11-03 17:37:51 -0400379 }
380
Misha Krieger-Raynauld7f959332022-11-04 15:12:53 -0400381 getCallIds(accountId: string): string[] {
382 return stringVectToArray(this.jamiSwig.getCallList(accountId));
383 }
384
385 // TODO: Replace Record with interface
386 getCallDetails(accountId: string, callId: string): Record<string, string> {
387 return stringMapToRecord(this.jamiSwig.getCallDetails(accountId, callId));
388 }
389
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -0400390 getAccountIdFromUsername(username: string): string | undefined {
391 return this.usernamesToAccountIds.get(username);
Issam E. Maghnif796a092022-10-09 20:25:26 +0000392 }
Misha Krieger-Raynauld68a9b562022-10-28 19:47:46 -0400393
Misha Krieger-Raynauld8a381da2022-11-03 17:37:51 -0400394 private setupSignalHandlers(): void {
Misha Krieger-Raynauld68a9b562022-10-28 19:47:46 -0400395 this.events.onAccountsChanged.subscribe(() => {
396 log.debug('Received AccountsChanged');
397 });
398
399 this.events.onAccountDetailsChanged.subscribe((signal) => {
400 log.debug('Received AccountsDetailsChanged', JSON.stringify(signal));
401 });
402
403 this.events.onVolatileDetailsChanged.subscribe(({ accountId, details }) => {
Misha Krieger-Raynauld68a9b562022-10-28 19:47:46 -0400404 const username = details['Account.registeredName'];
Misha Krieger-Raynauld8a381da2022-11-03 17:37:51 -0400405 log.debug(
406 `Received VolatileDetailsChanged: {"accountId":"${accountId}",` +
407 `"details":{"Account.registeredName":"${username}", ...}}`
408 );
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -0500409
Misha Krieger-Raynauld68a9b562022-10-28 19:47:46 -0400410 if (username) {
Michelle Sepkap Simedd82cbf2022-11-17 23:31:49 -0500411 // Keep map of usernames to account IDs
Misha Krieger-Raynauld68a9b562022-10-28 19:47:46 -0400412 this.usernamesToAccountIds.set(username, accountId);
413 }
414 });
415
416 this.events.onRegistrationStateChanged.subscribe((signal) => {
417 log.debug('Received RegistrationStateChanged:', JSON.stringify(signal));
418 });
419
420 this.events.onNameRegistrationEnded.subscribe((signal) => {
421 log.debug('Received NameRegistrationEnded:', JSON.stringify(signal));
422 });
423
424 this.events.onRegisteredNameFound.subscribe((signal) => {
425 log.debug('Received RegisteredNameFound:', JSON.stringify(signal));
426 });
427
428 this.events.onKnownDevicesChanged.subscribe(({ accountId }) => {
429 log.debug(`Received KnownDevicesChanged: {"accountId":"${accountId}", ...}`);
430 });
431
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -0500432 this.events.onIncomingAccountMessage.subscribe(<T extends WebSocketMessageType>(signal: IncomingAccountMessage) => {
Misha Krieger-Raynauld68a9b562022-10-28 19:47:46 -0400433 log.debug('Received IncomingAccountMessage:', JSON.stringify(signal));
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -0500434
Misha Krieger-Raynauld99e5a702022-11-30 22:36:52 -0500435 const message: Partial<WebSocketMessage<T>> = JSON.parse(signal.payload['application/json']);
Charlie2bc0d672022-11-04 11:53:44 -0400436
Misha Krieger-Raynauld99e5a702022-11-30 22:36:52 -0500437 if (typeof message !== 'object' || message === null) {
438 log.warn('Account message is not an object');
Misha Krieger-Raynauldb933fbb2022-11-15 15:11:09 -0500439 return;
440 }
441
442 if (message.type === undefined || message.data === undefined) {
443 log.warn('Account message is not a valid WebSocketMessage (missing type or data fields)');
Charlie2bc0d672022-11-04 11:53:44 -0400444 return;
445 }
446
447 if (!Object.values(WebSocketMessageType).includes(message.type)) {
Misha Krieger-Raynauldb933fbb2022-11-15 15:11:09 -0500448 log.warn(`Invalid WebSocket message type: ${message.type}`);
Charlie2bc0d672022-11-04 11:53:44 -0400449 return;
450 }
451
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -0500452 this.webSocketServer.send(signal.accountId, message.type, message.data);
Misha Krieger-Raynauld68a9b562022-10-28 19:47:46 -0400453 });
454
Misha Krieger-Raynauldcb11bba2022-11-11 18:08:33 -0500455 this.events.onAccountMessageStatusChanged.subscribe((signal) => {
456 log.debug('Received AccountMessageStatusChanged:', JSON.stringify(signal));
457 });
458
459 this.events.onContactAdded.subscribe((signal) => {
460 log.debug('Received ContactAdded:', JSON.stringify(signal));
461 });
462
463 this.events.onContactRemoved.subscribe((signal) => {
464 log.debug('Received ContactRemoved:', JSON.stringify(signal));
465 });
466
Michelle Sepkap Simedd82cbf2022-11-17 23:31:49 -0500467 this.events.onConversationRequestReceived.subscribe((signal) => {
468 log.debug('Received ConversationRequestReceived:', JSON.stringify(signal));
469
470 // TODO: Prompt user to accept conversation request on client
471 // Currently, we auto-accept all incoming conversation requests. In future, we
472 // need to ask the user if they accept the conversation request or not. Part of
473 // it can be done by sending a WebSocket event.
474 // See other implementations e.g. block contact / decline request / accept request.
475 this.jamiSwig.acceptConversationRequest(signal.accountId, signal.conversationId);
476 });
477
Misha Krieger-Raynauld68a9b562022-10-28 19:47:46 -0400478 this.events.onConversationReady.subscribe((signal) => {
479 log.debug('Received ConversationReady:', JSON.stringify(signal));
480 });
481
482 this.events.onConversationRemoved.subscribe((signal) => {
483 log.debug('Received ConversationRemoved:', JSON.stringify(signal));
484 });
485
Misha Krieger-Raynauld8a381da2022-11-03 17:37:51 -0400486 this.events.onConversationLoaded.subscribe(({ id, accountId, conversationId }) => {
487 log.debug(
488 `Received ConversationLoaded: {"id":"${id}","accountId":"${accountId}",` +
idillon9e542ca2022-12-15 17:54:07 -0500489 `"conversationId":"${conversationId}"}`
Misha Krieger-Raynauld8a381da2022-11-03 17:37:51 -0400490 );
Misha Krieger-Raynauld68a9b562022-10-28 19:47:46 -0400491 });
492
Michelle Sepkap Simedd82cbf2022-11-17 23:31:49 -0500493 this.events.onConversationMemberEvent.subscribe((signal) => {
494 log.debug('Received onConversationMemberEvent:', JSON.stringify(signal));
495 });
496
Misha Krieger-Raynauld68a9b562022-10-28 19:47:46 -0400497 this.events.onMessageReceived.subscribe((signal) => {
498 log.debug('Received MessageReceived:', JSON.stringify(signal));
Michelle Sepkap Simedd82cbf2022-11-17 23:31:49 -0500499
Issam E. Maghni0432cb72022-11-12 06:09:26 +0000500 const data: ConversationMessage = {
501 conversationId: signal.conversationId,
502 message: signal.message,
503 };
Misha Krieger-Raynauldb933fbb2022-11-15 15:11:09 -0500504 this.webSocketServer.send(signal.accountId, WebSocketMessageType.ConversationMessage, data);
Misha Krieger-Raynauld68a9b562022-10-28 19:47:46 -0400505 });
506 }
Issam E. Maghnif796a092022-10-09 20:25:26 +0000507}