blob: 858f4d551b0722ffafdd66073769cf66967255a3 [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-Raynauld8a381da2022-11-03 17:37:51 -040018import { AccountDetails, Message, VolatileDetails } from 'jami-web-common';
Issam E. Maghnif796a092022-10-09 20:25:26 +000019import log from 'loglevel';
Misha Krieger-Raynauld6f9c7ae2022-10-28 11:41:45 -040020import { filter, firstValueFrom, map, Subject } from 'rxjs';
Issam E. Maghnif796a092022-10-09 20:25:26 +000021import { Service } from 'typedi';
22
Misha Krieger-Raynauldaddd6fe2022-10-22 12:46:04 -040023import { JamiSignal } from './jami-signal.js';
24import {
Misha Krieger-Raynauld68a9b562022-10-28 19:47:46 -040025 AccountDetailsChanged,
26 ConversationLoaded,
27 ConversationReady,
28 ConversationRemoved,
Charlieb62c6782022-10-30 15:14:56 -040029 IncomingAccountMessage,
Misha Krieger-Raynauld68a9b562022-10-28 19:47:46 -040030 KnownDevicesChanged,
31 MessageReceived,
Misha Krieger-Raynauldaddd6fe2022-10-22 12:46:04 -040032 NameRegistrationEnded,
33 RegisteredNameFound,
34 RegistrationStateChanged,
35 VolatileDetailsChanged,
36} from './jami-signal-interfaces.js';
Misha Krieger-Raynauldbfed1732022-11-01 20:49:35 -040037import { JamiSwig, StringMap, stringMapToRecord, stringVectToArray, vectMapToRecordArray } from './jami-swig.js';
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -040038import { require } from './utils.js';
Issam E. Maghnif796a092022-10-09 20:25:26 +000039
Misha Krieger-Raynauld68a9b562022-10-28 19:47:46 -040040// TODO: Mechanism to map account IDs to a list of WebSockets
Misha Krieger-Raynauldbfed1732022-11-01 20:49:35 -040041// TODO: Convert Records to interfaces and replace them in common/ (e.g. Contact)
Misha Krieger-Raynauld68a9b562022-10-28 19:47:46 -040042
Issam E. Maghnif796a092022-10-09 20:25:26 +000043@Service()
44export class Jamid {
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -040045 private readonly jamiSwig: JamiSwig;
46 private readonly usernamesToAccountIds: Map<string, string>;
Issam E. Maghnif796a092022-10-09 20:25:26 +000047 private readonly events;
48
49 constructor() {
50 // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
simon7d4386c2022-10-26 17:47:59 -040051 this.jamiSwig = require('../../jamid.node') as JamiSwig; // TODO: we should put the path in the .env
Issam E. Maghnif796a092022-10-09 20:25:26 +000052
Misha Krieger-Raynauld68a9b562022-10-28 19:47:46 -040053 this.usernamesToAccountIds = new Map<string, string>();
54
55 // Setup signal handlers
Issam E. Maghnif796a092022-10-09 20:25:26 +000056 const handlers: Record<string, unknown> = {};
Misha Krieger-Raynauld68a9b562022-10-28 19:47:46 -040057
58 // Add default handler for all signals
59 const createDefaultHandler = (signal: string) => {
60 return (...args: unknown[]) => log.warn('Unhandled', signal, args);
Issam E. Maghnif796a092022-10-09 20:25:26 +000061 };
Misha Krieger-Raynauld68a9b562022-10-28 19:47:46 -040062 for (const signal in JamiSignal) {
63 handlers[signal] = createDefaultHandler(signal);
64 }
65
66 // Overwrite handlers for handled signals using RxJS Subjects, converting multiple arguments to objects
67 const onAccountsChanged = new Subject<void>();
68 handlers.AccountsChanged = () => onAccountsChanged.next();
69
70 const onAccountDetailsChanged = new Subject<AccountDetailsChanged>();
71 handlers.AccountDetailsChanged = (accountId: string, details: AccountDetails) =>
72 onAccountDetailsChanged.next({ accountId, details });
Issam E. Maghnif796a092022-10-09 20:25:26 +000073
74 const onVolatileDetailsChanged = new Subject<VolatileDetailsChanged>();
Misha Krieger-Raynauld68a9b562022-10-28 19:47:46 -040075 handlers.VolatileDetailsChanged = (accountId: string, details: VolatileDetails) =>
76 onVolatileDetailsChanged.next({ accountId, details });
Issam E. Maghnif796a092022-10-09 20:25:26 +000077
78 const onRegistrationStateChanged = new Subject<RegistrationStateChanged>();
79 handlers.RegistrationStateChanged = (accountId: string, state: string, code: number, details: string) =>
80 onRegistrationStateChanged.next({ accountId, state, code, details });
81
82 const onNameRegistrationEnded = new Subject<NameRegistrationEnded>();
83 handlers.NameRegistrationEnded = (accountId: string, state: number, username: string) =>
84 onNameRegistrationEnded.next({ accountId, state, username });
85
86 const onRegisteredNameFound = new Subject<RegisteredNameFound>();
87 handlers.RegisteredNameFound = (accountId: string, state: number, address: string, username: string) =>
88 onRegisteredNameFound.next({ accountId, state, address, username });
89
Misha Krieger-Raynauld68a9b562022-10-28 19:47:46 -040090 const onKnownDevicesChanged = new Subject<KnownDevicesChanged>();
91 handlers.KnownDevicesChanged = (accountId: string, devices: Record<string, string>) =>
92 onKnownDevicesChanged.next({ accountId, devices });
93
Charlieb62c6782022-10-30 15:14:56 -040094 const onIncomingAccountMessage = new Subject<IncomingAccountMessage>();
95 handlers.IncomingAccountMessage = (accountId: string, from: string, message: Record<string, string>) =>
96 onIncomingAccountMessage.next({ accountId, from, message });
97
Misha Krieger-Raynauld68a9b562022-10-28 19:47:46 -040098 const onConversationReady = new Subject<ConversationReady>();
99 handlers.ConversationReady = (accountId: string, conversationId: string) =>
100 onConversationReady.next({ accountId, conversationId });
101
102 const onConversationRemoved = new Subject<ConversationRemoved>();
103 handlers.ConversationRemoved = (accountId: string, conversationId: string) =>
104 onConversationRemoved.next({ accountId, conversationId });
105
106 const onConversationLoaded = new Subject<ConversationLoaded>();
Misha Krieger-Raynauld8a381da2022-11-03 17:37:51 -0400107 handlers.ConversationLoaded = (id: number, accountId: string, conversationId: string, messages: Message[]) =>
108 onConversationLoaded.next({ id, accountId, conversationId, messages });
Misha Krieger-Raynauld68a9b562022-10-28 19:47:46 -0400109
110 const onMessageReceived = new Subject<MessageReceived>();
Misha Krieger-Raynauld8a381da2022-11-03 17:37:51 -0400111 handlers.MessageReceived = (accountId: string, conversationId: string, message: Message) =>
Misha Krieger-Raynauld68a9b562022-10-28 19:47:46 -0400112 onMessageReceived.next({ accountId, conversationId, message });
113
114 // Expose all signals in an events object to allow other handlers to subscribe after jamiSwig.init()
Issam E. Maghnif796a092022-10-09 20:25:26 +0000115 this.events = {
Misha Krieger-Raynauld68a9b562022-10-28 19:47:46 -0400116 onAccountsChanged: onAccountsChanged.asObservable(),
117 onAccountDetailsChanged: onAccountDetailsChanged.asObservable(),
Issam E. Maghnif796a092022-10-09 20:25:26 +0000118 onVolatileDetailsChanged: onVolatileDetailsChanged.asObservable(),
119 onRegistrationStateChanged: onRegistrationStateChanged.asObservable(),
120 onNameRegistrationEnded: onNameRegistrationEnded.asObservable(),
121 onRegisteredNameFound: onRegisteredNameFound.asObservable(),
Misha Krieger-Raynauld68a9b562022-10-28 19:47:46 -0400122 onKnownDevicesChanged: onKnownDevicesChanged.asObservable(),
Charlieb62c6782022-10-30 15:14:56 -0400123 onIncomingAccountMessage: onIncomingAccountMessage.asObservable(),
Misha Krieger-Raynauld68a9b562022-10-28 19:47:46 -0400124 onConversationReady: onConversationReady.asObservable(),
125 onConversationRemoved: onConversationRemoved.asObservable(),
126 onConversationLoaded: onConversationLoaded.asObservable(),
127 onMessageReceived: onMessageReceived.asObservable(),
Issam E. Maghnif796a092022-10-09 20:25:26 +0000128 };
129
Misha Krieger-Raynauld68a9b562022-10-28 19:47:46 -0400130 this.setupSignalHandlers();
Issam E. Maghnif796a092022-10-09 20:25:26 +0000131
Misha Krieger-Raynauld68a9b562022-10-28 19:47:46 -0400132 // RxJS Subjects are used as signal handlers for the following reasons:
133 // 1. You cannot change event handlers after calling jamiSwig.init()
Issam E. Maghnif796a092022-10-09 20:25:26 +0000134 // 2. You cannot specify multiple handlers for the same event
135 // 3. You cannot specify a default handler
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -0400136 this.jamiSwig.init(handlers);
Charlie6ddaefe2022-11-01 18:36:29 -0400137
138 // TODO: Bind websocket callbacks for webrtc action on Incoming account message
Issam E. Maghnif796a092022-10-09 20:25:26 +0000139 }
140
Misha Krieger-Raynauld8a381da2022-11-03 17:37:51 -0400141 stop(): void {
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -0400142 this.jamiSwig.fini();
Misha Krieger-Raynauld62a0da92022-10-22 13:46:59 -0400143 }
144
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -0400145 getVolatileAccountDetails(accountId: string): VolatileDetails {
146 return stringMapToRecord(this.jamiSwig.getVolatileAccountDetails(accountId)) as unknown as VolatileDetails;
Issam E. Maghnif796a092022-10-09 20:25:26 +0000147 }
148
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -0400149 getAccountDetails(accountId: string): AccountDetails {
150 return stringMapToRecord(this.jamiSwig.getAccountDetails(accountId)) as unknown as AccountDetails;
151 }
Issam E. Maghnif796a092022-10-09 20:25:26 +0000152
Misha Krieger-Raynauld8a381da2022-11-03 17:37:51 -0400153 setAccountDetails(accountId: string, accountDetails: AccountDetails): void {
simon43da57b2022-10-26 18:22:22 -0400154 const accountDetailsStringMap: StringMap = new this.jamiSwig.StringMap();
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -0400155 for (const [key, value] of Object.entries(accountDetails)) {
156 accountDetailsStringMap.set(key, value);
157 }
158 this.jamiSwig.setAccountDetails(accountId, accountDetailsStringMap);
159 }
Issam E. Maghnif796a092022-10-09 20:25:26 +0000160
Misha Krieger-Raynauld8a381da2022-11-03 17:37:51 -0400161 async addAccount(details: Map<string, string | number | boolean>): Promise<string> {
simon43da57b2022-10-26 18:22:22 -0400162 const detailsStringMap: StringMap = new this.jamiSwig.StringMap();
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -0400163
164 detailsStringMap.set('Account.type', 'RING');
165 for (const [key, value] of details.entries()) {
166 detailsStringMap.set('Account.' + key, value.toString());
167 }
168
169 const accountId = this.jamiSwig.addAccount(detailsStringMap);
Issam E. Maghnif796a092022-10-09 20:25:26 +0000170 return firstValueFrom(
171 this.events.onRegistrationStateChanged.pipe(
Misha Krieger-Raynauld6f9c7ae2022-10-28 11:41:45 -0400172 filter((value) => value.accountId === accountId),
Issam E. Maghnif796a092022-10-09 20:25:26 +0000173 // TODO: is it the only state?
Misha Krieger-Raynauld6f9c7ae2022-10-28 11:41:45 -0400174 // TODO: Replace with string enum in common/
Misha Krieger-Raynauld8a381da2022-11-03 17:37:51 -0400175 filter((value) => value.state === 'REGISTERED'),
176 map((value) => value.accountId)
Issam E. Maghnif796a092022-10-09 20:25:26 +0000177 )
178 );
179 }
180
Misha Krieger-Raynauld8a381da2022-11-03 17:37:51 -0400181 removeAccount(accountId: string): void {
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -0400182 this.jamiSwig.removeAccount(accountId);
Issam E. Maghnif796a092022-10-09 20:25:26 +0000183 }
184
Misha Krieger-Raynauld8a381da2022-11-03 17:37:51 -0400185 getAccountIds(): string[] {
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -0400186 return stringVectToArray(this.jamiSwig.getAccountList());
Issam E. Maghnif796a092022-10-09 20:25:26 +0000187 }
188
Misha Krieger-Raynauld8a381da2022-11-03 17:37:51 -0400189 sendAccountTextMessage(accountId: string, contactId: string, type: string, message: string): void {
Charlieb62c6782022-10-30 15:14:56 -0400190 const messageStringMap: StringMap = new this.jamiSwig.StringMap();
Charlie6ddaefe2022-11-01 18:36:29 -0400191 messageStringMap.set(type, JSON.stringify(message));
Charlieb62c6782022-10-30 15:14:56 -0400192 this.jamiSwig.sendAccountTextMessage(accountId, contactId, messageStringMap);
193 }
194
Misha Krieger-Raynauld8a381da2022-11-03 17:37:51 -0400195 // TODO: Add interface for returned type
Misha Krieger-Raynauld6f9c7ae2022-10-28 11:41:45 -0400196 async lookupUsername(username: string, accountId?: string) {
197 const hasRingNs = this.jamiSwig.lookupName(accountId || '', '', username);
Issam E. Maghnif796a092022-10-09 20:25:26 +0000198 if (!hasRingNs) {
Issam E. Maghnif796a092022-10-09 20:25:26 +0000199 throw new Error('Jami does not have NS');
200 }
Misha Krieger-Raynauld6f9c7ae2022-10-28 11:41:45 -0400201 return firstValueFrom(
202 this.events.onRegisteredNameFound.pipe(
203 filter((value) => value.username === username),
204 map(({ accountId: _, ...response }) => response) // Remove accountId from response
205 )
206 );
207 }
208
Misha Krieger-Raynauld8a381da2022-11-03 17:37:51 -0400209 // TODO: Add interface for returned type
Misha Krieger-Raynauld6f9c7ae2022-10-28 11:41:45 -0400210 async lookupAddress(address: string, accountId?: string) {
211 const hasRingNs = this.jamiSwig.lookupAddress(accountId || '', '', address);
212 if (!hasRingNs) {
213 throw new Error('Jami does not have NS');
214 }
215 return firstValueFrom(
216 this.events.onRegisteredNameFound.pipe(
217 filter((value) => value.address === address),
218 map(({ accountId: _, ...response }) => response) // Remove accountId from response
219 )
220 );
Issam E. Maghnif796a092022-10-09 20:25:26 +0000221 }
222
Misha Krieger-Raynauld8a381da2022-11-03 17:37:51 -0400223 // TODO: Create enum for state and return that rather than a number
224 async registerUsername(accountId: string, username: string, password: string): Promise<number> {
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -0400225 const hasRingNs = this.jamiSwig.registerName(accountId, password, username);
226 if (!hasRingNs) {
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -0400227 throw new Error('Jami does not have NS');
228 }
Misha Krieger-Raynauld8a381da2022-11-03 17:37:51 -0400229 return firstValueFrom(
230 this.events.onNameRegistrationEnded.pipe(
231 filter((value) => value.accountId === accountId),
232 map((value) => value.state)
233 )
234 );
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -0400235 }
236
237 getDevices(accountId: string): Record<string, string> {
238 return stringMapToRecord(this.jamiSwig.getKnownRingDevices(accountId));
239 }
240
Misha Krieger-Raynauld8a381da2022-11-03 17:37:51 -0400241 addContact(accountId: string, contactId: string): void {
Misha Krieger-Raynauldbfed1732022-11-01 20:49:35 -0400242 this.jamiSwig.addContact(accountId, contactId);
243 }
244
Misha Krieger-Raynauld8a381da2022-11-03 17:37:51 -0400245 removeContact(accountId: string, contactId: string): void {
Misha Krieger-Raynauldbfed1732022-11-01 20:49:35 -0400246 this.jamiSwig.removeContact(accountId, contactId, false);
247 }
248
Misha Krieger-Raynauld8a381da2022-11-03 17:37:51 -0400249 blockContact(accountId: string, contactId: string): void {
Misha Krieger-Raynauldbfed1732022-11-01 20:49:35 -0400250 this.jamiSwig.removeContact(accountId, contactId, true);
251 }
252
Misha Krieger-Raynauld8a381da2022-11-03 17:37:51 -0400253 // TODO: Replace Record with interface
Misha Krieger-Raynauldbfed1732022-11-01 20:49:35 -0400254 getContacts(accountId: string): Record<string, string>[] {
255 return vectMapToRecordArray(this.jamiSwig.getContacts(accountId));
256 }
257
Misha Krieger-Raynauld8a381da2022-11-03 17:37:51 -0400258 // TODO: Replace Record with interface
Misha Krieger-Raynauldbfed1732022-11-01 20:49:35 -0400259 getContactDetails(accountId: string, contactId: string): Record<string, string> {
260 return stringMapToRecord(this.jamiSwig.getContactDetails(accountId, contactId));
261 }
262
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -0400263 getDefaultModerators(accountId: string): string[] {
264 return stringVectToArray(this.jamiSwig.getDefaultModerators(accountId));
265 }
266
Misha Krieger-Raynauld8a381da2022-11-03 17:37:51 -0400267 getConversationIds(accountId: string): string[] {
268 return stringVectToArray(this.jamiSwig.getConversations(accountId));
269 }
270
271 // TODO: Replace Record with interface
272 getConversationInfos(accountId: string, conversationId: string): Record<string, string> {
273 return stringMapToRecord(this.jamiSwig.conversationInfos(accountId, conversationId));
274 }
275
276 // TODO: Replace Record with interface
277 getConversationMembers(accountId: string, conversationId: string): Record<string, string>[] {
278 return vectMapToRecordArray(this.jamiSwig.getConversationMembers(accountId, conversationId));
279 }
280
281 async getConversationMessages(accountId: string, conversationId: string, fromMessage?: string): Promise<Message[]> {
282 const requestId = this.jamiSwig.loadConversationMessages(accountId, conversationId, fromMessage || '', 32);
283 return firstValueFrom(
284 this.events.onConversationLoaded.pipe(
285 filter((value) => value.id === requestId),
286 map((value) => value.messages)
287 )
288 );
289 }
290
291 sendConversationMessage(accountId: string, conversationId: string, message: string, replyTo?: string): void {
292 this.jamiSwig.sendMessage(accountId, conversationId, message, replyTo || '');
293 }
294
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -0400295 getAccountIdFromUsername(username: string): string | undefined {
296 return this.usernamesToAccountIds.get(username);
Issam E. Maghnif796a092022-10-09 20:25:26 +0000297 }
Misha Krieger-Raynauld68a9b562022-10-28 19:47:46 -0400298
Misha Krieger-Raynauld8a381da2022-11-03 17:37:51 -0400299 private setupSignalHandlers(): void {
Misha Krieger-Raynauld68a9b562022-10-28 19:47:46 -0400300 this.events.onAccountsChanged.subscribe(() => {
301 log.debug('Received AccountsChanged');
302 });
303
304 this.events.onAccountDetailsChanged.subscribe((signal) => {
305 log.debug('Received AccountsDetailsChanged', JSON.stringify(signal));
306 });
307
308 this.events.onVolatileDetailsChanged.subscribe(({ accountId, details }) => {
Misha Krieger-Raynauld68a9b562022-10-28 19:47:46 -0400309 const username = details['Account.registeredName'];
Misha Krieger-Raynauld8a381da2022-11-03 17:37:51 -0400310 log.debug(
311 `Received VolatileDetailsChanged: {"accountId":"${accountId}",` +
312 `"details":{"Account.registeredName":"${username}", ...}}`
313 );
314 // Keep map of usernames to account IDs
Misha Krieger-Raynauld68a9b562022-10-28 19:47:46 -0400315 if (username) {
316 this.usernamesToAccountIds.set(username, accountId);
317 }
318 });
319
320 this.events.onRegistrationStateChanged.subscribe((signal) => {
321 log.debug('Received RegistrationStateChanged:', JSON.stringify(signal));
322 });
323
324 this.events.onNameRegistrationEnded.subscribe((signal) => {
325 log.debug('Received NameRegistrationEnded:', JSON.stringify(signal));
326 });
327
328 this.events.onRegisteredNameFound.subscribe((signal) => {
329 log.debug('Received RegisteredNameFound:', JSON.stringify(signal));
330 });
331
332 this.events.onKnownDevicesChanged.subscribe(({ accountId }) => {
333 log.debug(`Received KnownDevicesChanged: {"accountId":"${accountId}", ...}`);
334 });
335
336 this.events.onIncomingAccountMessage.subscribe((signal) => {
337 log.debug('Received IncomingAccountMessage:', JSON.stringify(signal));
338 });
339
340 this.events.onConversationReady.subscribe((signal) => {
341 log.debug('Received ConversationReady:', JSON.stringify(signal));
342 });
343
344 this.events.onConversationRemoved.subscribe((signal) => {
345 log.debug('Received ConversationRemoved:', JSON.stringify(signal));
346 });
347
Misha Krieger-Raynauld8a381da2022-11-03 17:37:51 -0400348 this.events.onConversationLoaded.subscribe(({ id, accountId, conversationId }) => {
349 log.debug(
350 `Received ConversationLoaded: {"id":"${id}","accountId":"${accountId}",` +
351 `"conversationId":"${conversationId}","messages":[...]}`
352 );
Misha Krieger-Raynauld68a9b562022-10-28 19:47:46 -0400353 });
354
355 this.events.onMessageReceived.subscribe((signal) => {
356 log.debug('Received MessageReceived:', JSON.stringify(signal));
Charlie6ddaefe2022-11-01 18:36:29 -0400357 // TODO: Send message to client using WS service
Misha Krieger-Raynauld68a9b562022-10-28 19:47:46 -0400358 });
359 }
Issam E. Maghnif796a092022-10-09 20:25:26 +0000360}