blob: ab1963f5d907b797001727ddf0351549295bd5d4 [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 {
Charlieb62c6782022-10-30 15:14:56 -040025 IncomingAccountMessage,
Misha Krieger-Raynauldaddd6fe2022-10-22 12:46:04 -040026 NameRegistrationEnded,
27 RegisteredNameFound,
28 RegistrationStateChanged,
29 VolatileDetailsChanged,
30} from './jami-signal-interfaces.js';
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -040031import { JamiSwig, StringMap, stringMapToRecord, stringVectToArray } from './jami-swig.js';
32import { require } from './utils.js';
Issam E. Maghnif796a092022-10-09 20:25:26 +000033
34@Service()
35export class Jamid {
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -040036 private readonly jamiSwig: JamiSwig;
37 private readonly usernamesToAccountIds: Map<string, string>;
Issam E. Maghnif796a092022-10-09 20:25:26 +000038 private readonly events;
39
40 constructor() {
41 // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
simon7d4386c2022-10-26 17:47:59 -040042 this.jamiSwig = require('../../jamid.node') as JamiSwig; // TODO: we should put the path in the .env
Issam E. Maghnif796a092022-10-09 20:25:26 +000043
44 const handlers: Record<string, unknown> = {};
45 const handler = (sig: string) => {
46 return (...args: unknown[]) => log.warn('Unhandled', sig, args);
47 };
48 Object.keys(JamiSignal).forEach((sig) => (handlers[sig] = handler(sig)));
49
50 const onVolatileDetailsChanged = new Subject<VolatileDetailsChanged>();
51 handlers.VolatileDetailsChanged = (accountId: string, details: Record<string, string>) =>
52 onVolatileDetailsChanged.next({ accountId, details: new Map(Object.entries(details)) });
53
54 const onRegistrationStateChanged = new Subject<RegistrationStateChanged>();
55 handlers.RegistrationStateChanged = (accountId: string, state: string, code: number, details: string) =>
56 onRegistrationStateChanged.next({ accountId, state, code, details });
57
58 const onNameRegistrationEnded = new Subject<NameRegistrationEnded>();
59 handlers.NameRegistrationEnded = (accountId: string, state: number, username: string) =>
60 onNameRegistrationEnded.next({ accountId, state, username });
61
62 const onRegisteredNameFound = new Subject<RegisteredNameFound>();
63 handlers.RegisteredNameFound = (accountId: string, state: number, address: string, username: string) =>
64 onRegisteredNameFound.next({ accountId, state, address, username });
65
Charlieb62c6782022-10-30 15:14:56 -040066 const onIncomingAccountMessage = new Subject<IncomingAccountMessage>();
67 handlers.IncomingAccountMessage = (accountId: string, from: string, message: Record<string, string>) =>
68 onIncomingAccountMessage.next({ accountId, from, message });
69
Issam E. Maghnif796a092022-10-09 20:25:26 +000070 this.events = {
71 onVolatileDetailsChanged: onVolatileDetailsChanged.asObservable(),
72 onRegistrationStateChanged: onRegistrationStateChanged.asObservable(),
73 onNameRegistrationEnded: onNameRegistrationEnded.asObservable(),
74 onRegisteredNameFound: onRegisteredNameFound.asObservable(),
Charlieb62c6782022-10-30 15:14:56 -040075 onIncomingAccountMessage: onIncomingAccountMessage.asObservable(),
Issam E. Maghnif796a092022-10-09 20:25:26 +000076 };
77
78 this.events.onVolatileDetailsChanged.subscribe(({ accountId, details }) => {
79 log.debug('[1] Received onVolatileDetailsChanged with', { accountId, details });
80 // Keep map of usernames to account IDs as Jamid cannot do this by itself (AFAIK)
81 const username = details.get('Account.registeredName');
82 if (username) {
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -040083 this.usernamesToAccountIds.set(username, accountId);
Issam E. Maghnif796a092022-10-09 20:25:26 +000084 }
85 });
86 this.events.onRegistrationStateChanged.subscribe((ctx) =>
87 log.debug('[1] Received onRegistrationStateChanged with', ctx)
88 );
89 this.events.onNameRegistrationEnded.subscribe((ctx) => log.debug('[1] Received onNameRegistrationEnded with', ctx));
90 this.events.onRegisteredNameFound.subscribe((ctx) => log.debug('[1] Received onRegisteredNameFound with', ctx));
Charlieb62c6782022-10-30 15:14:56 -040091 this.events.onIncomingAccountMessage.subscribe((ctx) =>
92 log.debug('[1] Received onIncomingAccountMessage with', ctx)
93 );
Issam E. Maghnif796a092022-10-09 20:25:26 +000094
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -040095 this.usernamesToAccountIds = new Map<string, string>();
Issam E. Maghnif796a092022-10-09 20:25:26 +000096
97 // 1. You cannot change event handlers after init
98 // 2. You cannot specify multiple handlers for the same event
99 // 3. You cannot specify a default handler
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -0400100 // So we rely on Subject() instead of Observable()
Issam E. Maghnif796a092022-10-09 20:25:26 +0000101 // Also, handlers receive multiple argument instead of tuple or object!
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -0400102 this.jamiSwig.init(handlers);
Issam E. Maghnif796a092022-10-09 20:25:26 +0000103 }
104
Misha Krieger-Raynauld62a0da92022-10-22 13:46:59 -0400105 stop() {
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -0400106 this.jamiSwig.fini();
Misha Krieger-Raynauld62a0da92022-10-22 13:46:59 -0400107 }
108
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -0400109 getVolatileAccountDetails(accountId: string): VolatileDetails {
110 return stringMapToRecord(this.jamiSwig.getVolatileAccountDetails(accountId)) as unknown as VolatileDetails;
Issam E. Maghnif796a092022-10-09 20:25:26 +0000111 }
112
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -0400113 getAccountDetails(accountId: string): AccountDetails {
114 return stringMapToRecord(this.jamiSwig.getAccountDetails(accountId)) as unknown as AccountDetails;
115 }
Issam E. Maghnif796a092022-10-09 20:25:26 +0000116
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -0400117 setAccountDetails(accountId: string, accountDetails: AccountDetails) {
simon43da57b2022-10-26 18:22:22 -0400118 const accountDetailsStringMap: StringMap = new this.jamiSwig.StringMap();
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -0400119 for (const [key, value] of Object.entries(accountDetails)) {
120 accountDetailsStringMap.set(key, value);
121 }
122 this.jamiSwig.setAccountDetails(accountId, accountDetailsStringMap);
123 }
Issam E. Maghnif796a092022-10-09 20:25:26 +0000124
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -0400125 async addAccount(details: Map<string, string | number | boolean>) {
simon43da57b2022-10-26 18:22:22 -0400126 const detailsStringMap: StringMap = new this.jamiSwig.StringMap();
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -0400127
128 detailsStringMap.set('Account.type', 'RING');
129 for (const [key, value] of details.entries()) {
130 detailsStringMap.set('Account.' + key, value.toString());
131 }
132
133 const accountId = this.jamiSwig.addAccount(detailsStringMap);
Issam E. Maghnif796a092022-10-09 20:25:26 +0000134 return firstValueFrom(
135 this.events.onRegistrationStateChanged.pipe(
Misha Krieger-Raynauld6f9c7ae2022-10-28 11:41:45 -0400136 filter((value) => value.accountId === accountId),
Issam E. Maghnif796a092022-10-09 20:25:26 +0000137 // TODO: is it the only state?
Misha Krieger-Raynauld6f9c7ae2022-10-28 11:41:45 -0400138 // TODO: Replace with string enum in common/
Issam E. Maghnif796a092022-10-09 20:25:26 +0000139 filter(({ state }) => state === 'REGISTERED')
140 )
141 );
142 }
143
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -0400144 removeAccount(accountId: string) {
145 this.jamiSwig.removeAccount(accountId);
Issam E. Maghnif796a092022-10-09 20:25:26 +0000146 }
147
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -0400148 getAccountList(): string[] {
149 return stringVectToArray(this.jamiSwig.getAccountList());
Issam E. Maghnif796a092022-10-09 20:25:26 +0000150 }
151
Charlieb62c6782022-10-30 15:14:56 -0400152 sendAccountTextMessage(accountId: string, contactId: string, type: string, message: string) {
153 const messageStringMap: StringMap = new this.jamiSwig.StringMap();
154 messageStringMap.set(type, message);
155 this.jamiSwig.sendAccountTextMessage(accountId, contactId, messageStringMap);
156 }
157
Misha Krieger-Raynauld6f9c7ae2022-10-28 11:41:45 -0400158 async lookupUsername(username: string, accountId?: string) {
159 const hasRingNs = this.jamiSwig.lookupName(accountId || '', '', username);
Issam E. Maghnif796a092022-10-09 20:25:26 +0000160 if (!hasRingNs) {
Issam E. Maghnif796a092022-10-09 20:25:26 +0000161 throw new Error('Jami does not have NS');
162 }
Misha Krieger-Raynauld6f9c7ae2022-10-28 11:41:45 -0400163 return firstValueFrom(
164 this.events.onRegisteredNameFound.pipe(
165 filter((value) => value.username === username),
166 map(({ accountId: _, ...response }) => response) // Remove accountId from response
167 )
168 );
169 }
170
171 async lookupAddress(address: string, accountId?: string) {
172 const hasRingNs = this.jamiSwig.lookupAddress(accountId || '', '', address);
173 if (!hasRingNs) {
174 throw new Error('Jami does not have NS');
175 }
176 return firstValueFrom(
177 this.events.onRegisteredNameFound.pipe(
178 filter((value) => value.address === address),
179 map(({ accountId: _, ...response }) => response) // Remove accountId from response
180 )
181 );
Issam E. Maghnif796a092022-10-09 20:25:26 +0000182 }
183
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -0400184 async registerUsername(accountId: string, username: string, password: string) {
185 const hasRingNs = this.jamiSwig.registerName(accountId, password, username);
186 if (!hasRingNs) {
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -0400187 throw new Error('Jami does not have NS');
188 }
Misha Krieger-Raynauld6f9c7ae2022-10-28 11:41:45 -0400189 return firstValueFrom(this.events.onNameRegistrationEnded.pipe(filter((value) => value.accountId === accountId)));
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -0400190 }
191
192 getDevices(accountId: string): Record<string, string> {
193 return stringMapToRecord(this.jamiSwig.getKnownRingDevices(accountId));
194 }
195
196 getDefaultModerators(accountId: string): string[] {
197 return stringVectToArray(this.jamiSwig.getDefaultModerators(accountId));
198 }
199
200 // TODO: Ideally, we would fetch the username directly from Jami instead of
201 // keeping an internal map.
202 getAccountIdFromUsername(username: string): string | undefined {
203 return this.usernamesToAccountIds.get(username);
Issam E. Maghnif796a092022-10-09 20:25:26 +0000204 }
205}