blob: 8676875eda4d53fa47ee09ab5d6780bdae65ee72 [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-Raynauldb6f1c322022-10-23 20:42:57 -040018import { AccountDetails, 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-Raynauldb6f1c322022-10-23 20:42:57 -040037import { JamiSwig, StringMap, stringMapToRecord, stringVectToArray } from './jami-swig.js';
38import { 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
41
Issam E. Maghnif796a092022-10-09 20:25:26 +000042@Service()
43export class Jamid {
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -040044 private readonly jamiSwig: JamiSwig;
45 private readonly usernamesToAccountIds: Map<string, string>;
Issam E. Maghnif796a092022-10-09 20:25:26 +000046 private readonly events;
47
48 constructor() {
49 // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
simon7d4386c2022-10-26 17:47:59 -040050 this.jamiSwig = require('../../jamid.node') as JamiSwig; // TODO: we should put the path in the .env
Issam E. Maghnif796a092022-10-09 20:25:26 +000051
Misha Krieger-Raynauld68a9b562022-10-28 19:47:46 -040052 this.usernamesToAccountIds = new Map<string, string>();
53
54 // Setup signal handlers
Issam E. Maghnif796a092022-10-09 20:25:26 +000055 const handlers: Record<string, unknown> = {};
Misha Krieger-Raynauld68a9b562022-10-28 19:47:46 -040056
57 // Add default handler for all signals
58 const createDefaultHandler = (signal: string) => {
59 return (...args: unknown[]) => log.warn('Unhandled', signal, args);
Issam E. Maghnif796a092022-10-09 20:25:26 +000060 };
Misha Krieger-Raynauld68a9b562022-10-28 19:47:46 -040061 for (const signal in JamiSignal) {
62 handlers[signal] = createDefaultHandler(signal);
63 }
64
65 // Overwrite handlers for handled signals using RxJS Subjects, converting multiple arguments to objects
66 const onAccountsChanged = new Subject<void>();
67 handlers.AccountsChanged = () => onAccountsChanged.next();
68
69 const onAccountDetailsChanged = new Subject<AccountDetailsChanged>();
70 handlers.AccountDetailsChanged = (accountId: string, details: AccountDetails) =>
71 onAccountDetailsChanged.next({ accountId, details });
Issam E. Maghnif796a092022-10-09 20:25:26 +000072
73 const onVolatileDetailsChanged = new Subject<VolatileDetailsChanged>();
Misha Krieger-Raynauld68a9b562022-10-28 19:47:46 -040074 handlers.VolatileDetailsChanged = (accountId: string, details: VolatileDetails) =>
75 onVolatileDetailsChanged.next({ accountId, details });
Issam E. Maghnif796a092022-10-09 20:25:26 +000076
77 const onRegistrationStateChanged = new Subject<RegistrationStateChanged>();
78 handlers.RegistrationStateChanged = (accountId: string, state: string, code: number, details: string) =>
79 onRegistrationStateChanged.next({ accountId, state, code, details });
80
81 const onNameRegistrationEnded = new Subject<NameRegistrationEnded>();
82 handlers.NameRegistrationEnded = (accountId: string, state: number, username: string) =>
83 onNameRegistrationEnded.next({ accountId, state, username });
84
85 const onRegisteredNameFound = new Subject<RegisteredNameFound>();
86 handlers.RegisteredNameFound = (accountId: string, state: number, address: string, username: string) =>
87 onRegisteredNameFound.next({ accountId, state, address, username });
88
Misha Krieger-Raynauld68a9b562022-10-28 19:47:46 -040089 const onKnownDevicesChanged = new Subject<KnownDevicesChanged>();
90 handlers.KnownDevicesChanged = (accountId: string, devices: Record<string, string>) =>
91 onKnownDevicesChanged.next({ accountId, devices });
92
Charlieb62c6782022-10-30 15:14:56 -040093 const onIncomingAccountMessage = new Subject<IncomingAccountMessage>();
94 handlers.IncomingAccountMessage = (accountId: string, from: string, message: Record<string, string>) =>
95 onIncomingAccountMessage.next({ accountId, from, message });
96
Misha Krieger-Raynauld68a9b562022-10-28 19:47:46 -040097 const onConversationReady = new Subject<ConversationReady>();
98 handlers.ConversationReady = (accountId: string, conversationId: string) =>
99 onConversationReady.next({ accountId, conversationId });
100
101 const onConversationRemoved = new Subject<ConversationRemoved>();
102 handlers.ConversationRemoved = (accountId: string, conversationId: string) =>
103 onConversationRemoved.next({ accountId, conversationId });
104
105 const onConversationLoaded = new Subject<ConversationLoaded>();
106 handlers.ConversationLoaded = (
107 id: number,
108 accountId: string,
109 conversationId: string,
110 messages: Record<string, string>[]
111 ) => onConversationLoaded.next({ id, accountId, conversationId, messages });
112
113 const onMessageReceived = new Subject<MessageReceived>();
114 handlers.MessageReceived = (accountId: string, conversationId: string, message: Record<string, string>) =>
115 onMessageReceived.next({ accountId, conversationId, message });
116
117 // Expose all signals in an events object to allow other handlers to subscribe after jamiSwig.init()
Issam E. Maghnif796a092022-10-09 20:25:26 +0000118 this.events = {
Misha Krieger-Raynauld68a9b562022-10-28 19:47:46 -0400119 onAccountsChanged: onAccountsChanged.asObservable(),
120 onAccountDetailsChanged: onAccountDetailsChanged.asObservable(),
Issam E. Maghnif796a092022-10-09 20:25:26 +0000121 onVolatileDetailsChanged: onVolatileDetailsChanged.asObservable(),
122 onRegistrationStateChanged: onRegistrationStateChanged.asObservable(),
123 onNameRegistrationEnded: onNameRegistrationEnded.asObservable(),
124 onRegisteredNameFound: onRegisteredNameFound.asObservable(),
Misha Krieger-Raynauld68a9b562022-10-28 19:47:46 -0400125 onKnownDevicesChanged: onKnownDevicesChanged.asObservable(),
Charlieb62c6782022-10-30 15:14:56 -0400126 onIncomingAccountMessage: onIncomingAccountMessage.asObservable(),
Misha Krieger-Raynauld68a9b562022-10-28 19:47:46 -0400127 onConversationReady: onConversationReady.asObservable(),
128 onConversationRemoved: onConversationRemoved.asObservable(),
129 onConversationLoaded: onConversationLoaded.asObservable(),
130 onMessageReceived: onMessageReceived.asObservable(),
Issam E. Maghnif796a092022-10-09 20:25:26 +0000131 };
132
Misha Krieger-Raynauld68a9b562022-10-28 19:47:46 -0400133 this.setupSignalHandlers();
Issam E. Maghnif796a092022-10-09 20:25:26 +0000134
Misha Krieger-Raynauld68a9b562022-10-28 19:47:46 -0400135 // RxJS Subjects are used as signal handlers for the following reasons:
136 // 1. You cannot change event handlers after calling jamiSwig.init()
Issam E. Maghnif796a092022-10-09 20:25:26 +0000137 // 2. You cannot specify multiple handlers for the same event
138 // 3. You cannot specify a default handler
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -0400139 this.jamiSwig.init(handlers);
Issam E. Maghnif796a092022-10-09 20:25:26 +0000140 }
141
Misha Krieger-Raynauld62a0da92022-10-22 13:46:59 -0400142 stop() {
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -0400143 this.jamiSwig.fini();
Misha Krieger-Raynauld62a0da92022-10-22 13:46:59 -0400144 }
145
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -0400146 getVolatileAccountDetails(accountId: string): VolatileDetails {
147 return stringMapToRecord(this.jamiSwig.getVolatileAccountDetails(accountId)) as unknown as VolatileDetails;
Issam E. Maghnif796a092022-10-09 20:25:26 +0000148 }
149
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -0400150 getAccountDetails(accountId: string): AccountDetails {
151 return stringMapToRecord(this.jamiSwig.getAccountDetails(accountId)) as unknown as AccountDetails;
152 }
Issam E. Maghnif796a092022-10-09 20:25:26 +0000153
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -0400154 setAccountDetails(accountId: string, accountDetails: AccountDetails) {
simon43da57b2022-10-26 18:22:22 -0400155 const accountDetailsStringMap: StringMap = new this.jamiSwig.StringMap();
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -0400156 for (const [key, value] of Object.entries(accountDetails)) {
157 accountDetailsStringMap.set(key, value);
158 }
159 this.jamiSwig.setAccountDetails(accountId, accountDetailsStringMap);
160 }
Issam E. Maghnif796a092022-10-09 20:25:26 +0000161
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -0400162 async addAccount(details: Map<string, string | number | boolean>) {
simon43da57b2022-10-26 18:22:22 -0400163 const detailsStringMap: StringMap = new this.jamiSwig.StringMap();
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -0400164
165 detailsStringMap.set('Account.type', 'RING');
166 for (const [key, value] of details.entries()) {
167 detailsStringMap.set('Account.' + key, value.toString());
168 }
169
170 const accountId = this.jamiSwig.addAccount(detailsStringMap);
Issam E. Maghnif796a092022-10-09 20:25:26 +0000171 return firstValueFrom(
172 this.events.onRegistrationStateChanged.pipe(
Misha Krieger-Raynauld6f9c7ae2022-10-28 11:41:45 -0400173 filter((value) => value.accountId === accountId),
Issam E. Maghnif796a092022-10-09 20:25:26 +0000174 // TODO: is it the only state?
Misha Krieger-Raynauld6f9c7ae2022-10-28 11:41:45 -0400175 // TODO: Replace with string enum in common/
Issam E. Maghnif796a092022-10-09 20:25:26 +0000176 filter(({ state }) => state === 'REGISTERED')
177 )
178 );
179 }
180
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -0400181 removeAccount(accountId: string) {
182 this.jamiSwig.removeAccount(accountId);
Issam E. Maghnif796a092022-10-09 20:25:26 +0000183 }
184
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -0400185 getAccountList(): string[] {
186 return stringVectToArray(this.jamiSwig.getAccountList());
Issam E. Maghnif796a092022-10-09 20:25:26 +0000187 }
188
Charlieb62c6782022-10-30 15:14:56 -0400189 sendAccountTextMessage(accountId: string, contactId: string, type: string, message: string) {
190 const messageStringMap: StringMap = new this.jamiSwig.StringMap();
191 messageStringMap.set(type, message);
192 this.jamiSwig.sendAccountTextMessage(accountId, contactId, messageStringMap);
193 }
194
Misha Krieger-Raynauld6f9c7ae2022-10-28 11:41:45 -0400195 async lookupUsername(username: string, accountId?: string) {
196 const hasRingNs = this.jamiSwig.lookupName(accountId || '', '', username);
Issam E. Maghnif796a092022-10-09 20:25:26 +0000197 if (!hasRingNs) {
Issam E. Maghnif796a092022-10-09 20:25:26 +0000198 throw new Error('Jami does not have NS');
199 }
Misha Krieger-Raynauld6f9c7ae2022-10-28 11:41:45 -0400200 return firstValueFrom(
201 this.events.onRegisteredNameFound.pipe(
202 filter((value) => value.username === username),
203 map(({ accountId: _, ...response }) => response) // Remove accountId from response
204 )
205 );
206 }
207
208 async lookupAddress(address: string, accountId?: string) {
209 const hasRingNs = this.jamiSwig.lookupAddress(accountId || '', '', address);
210 if (!hasRingNs) {
211 throw new Error('Jami does not have NS');
212 }
213 return firstValueFrom(
214 this.events.onRegisteredNameFound.pipe(
215 filter((value) => value.address === address),
216 map(({ accountId: _, ...response }) => response) // Remove accountId from response
217 )
218 );
Issam E. Maghnif796a092022-10-09 20:25:26 +0000219 }
220
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -0400221 async registerUsername(accountId: string, username: string, password: string) {
222 const hasRingNs = this.jamiSwig.registerName(accountId, password, username);
223 if (!hasRingNs) {
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -0400224 throw new Error('Jami does not have NS');
225 }
Misha Krieger-Raynauld6f9c7ae2022-10-28 11:41:45 -0400226 return firstValueFrom(this.events.onNameRegistrationEnded.pipe(filter((value) => value.accountId === accountId)));
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -0400227 }
228
229 getDevices(accountId: string): Record<string, string> {
230 return stringMapToRecord(this.jamiSwig.getKnownRingDevices(accountId));
231 }
232
233 getDefaultModerators(accountId: string): string[] {
234 return stringVectToArray(this.jamiSwig.getDefaultModerators(accountId));
235 }
236
237 // TODO: Ideally, we would fetch the username directly from Jami instead of
238 // keeping an internal map.
239 getAccountIdFromUsername(username: string): string | undefined {
240 return this.usernamesToAccountIds.get(username);
Issam E. Maghnif796a092022-10-09 20:25:26 +0000241 }
Misha Krieger-Raynauld68a9b562022-10-28 19:47:46 -0400242
243 private setupSignalHandlers() {
244 this.events.onAccountsChanged.subscribe(() => {
245 log.debug('Received AccountsChanged');
246 });
247
248 this.events.onAccountDetailsChanged.subscribe((signal) => {
249 log.debug('Received AccountsDetailsChanged', JSON.stringify(signal));
250 });
251
252 this.events.onVolatileDetailsChanged.subscribe(({ accountId, details }) => {
253 log.debug(`Received VolatileDetailsChanged: {"accountId":"${accountId}", ...}`);
254 // Keep map of usernames to account IDs
255 const username = details['Account.registeredName'];
256 if (username) {
257 this.usernamesToAccountIds.set(username, accountId);
258 }
259 });
260
261 this.events.onRegistrationStateChanged.subscribe((signal) => {
262 log.debug('Received RegistrationStateChanged:', JSON.stringify(signal));
263 });
264
265 this.events.onNameRegistrationEnded.subscribe((signal) => {
266 log.debug('Received NameRegistrationEnded:', JSON.stringify(signal));
267 });
268
269 this.events.onRegisteredNameFound.subscribe((signal) => {
270 log.debug('Received RegisteredNameFound:', JSON.stringify(signal));
271 });
272
273 this.events.onKnownDevicesChanged.subscribe(({ accountId }) => {
274 log.debug(`Received KnownDevicesChanged: {"accountId":"${accountId}", ...}`);
275 });
276
277 this.events.onIncomingAccountMessage.subscribe((signal) => {
278 log.debug('Received IncomingAccountMessage:', JSON.stringify(signal));
279 });
280
281 this.events.onConversationReady.subscribe((signal) => {
282 log.debug('Received ConversationReady:', JSON.stringify(signal));
283 });
284
285 this.events.onConversationRemoved.subscribe((signal) => {
286 log.debug('Received ConversationRemoved:', JSON.stringify(signal));
287 });
288
289 this.events.onConversationLoaded.subscribe((signal) => {
290 log.debug('Received ConversationLoaded:', JSON.stringify(signal));
291 });
292
293 this.events.onMessageReceived.subscribe((signal) => {
294 log.debug('Received MessageReceived:', JSON.stringify(signal));
295 // TODO: Send message to client using WebSocket
296 });
297 }
Issam E. Maghnif796a092022-10-09 20:25:26 +0000298}