blob: 48c20f23425fab191c21c69be907a45cf42700b6 [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';
20import { filter, firstValueFrom, Subject } from 'rxjs';
21import { Service } from 'typedi';
22
Misha Krieger-Raynauldaddd6fe2022-10-22 12:46:04 -040023import { JamiSignal } from './jami-signal.js';
24import {
25 NameRegistrationEnded,
26 RegisteredNameFound,
27 RegistrationStateChanged,
28 VolatileDetailsChanged,
29} from './jami-signal-interfaces.js';
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -040030import { JamiSwig, StringMap, stringMapToRecord, stringVectToArray } from './jami-swig.js';
31import { require } from './utils.js';
Issam E. Maghnif796a092022-10-09 20:25:26 +000032
33@Service()
34export class Jamid {
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -040035 private readonly jamiSwig: JamiSwig;
36 private readonly usernamesToAccountIds: Map<string, string>;
Issam E. Maghnif796a092022-10-09 20:25:26 +000037 private readonly events;
38
39 constructor() {
40 // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
simon7d4386c2022-10-26 17:47:59 -040041 this.jamiSwig = require('../../jamid.node') as JamiSwig; // TODO: we should put the path in the .env
Issam E. Maghnif796a092022-10-09 20:25:26 +000042
43 const handlers: Record<string, unknown> = {};
44 const handler = (sig: string) => {
45 return (...args: unknown[]) => log.warn('Unhandled', sig, args);
46 };
47 Object.keys(JamiSignal).forEach((sig) => (handlers[sig] = handler(sig)));
48
49 const onVolatileDetailsChanged = new Subject<VolatileDetailsChanged>();
50 handlers.VolatileDetailsChanged = (accountId: string, details: Record<string, string>) =>
51 onVolatileDetailsChanged.next({ accountId, details: new Map(Object.entries(details)) });
52
53 const onRegistrationStateChanged = new Subject<RegistrationStateChanged>();
54 handlers.RegistrationStateChanged = (accountId: string, state: string, code: number, details: string) =>
55 onRegistrationStateChanged.next({ accountId, state, code, details });
56
57 const onNameRegistrationEnded = new Subject<NameRegistrationEnded>();
58 handlers.NameRegistrationEnded = (accountId: string, state: number, username: string) =>
59 onNameRegistrationEnded.next({ accountId, state, username });
60
61 const onRegisteredNameFound = new Subject<RegisteredNameFound>();
62 handlers.RegisteredNameFound = (accountId: string, state: number, address: string, username: string) =>
63 onRegisteredNameFound.next({ accountId, state, address, username });
64
65 this.events = {
66 onVolatileDetailsChanged: onVolatileDetailsChanged.asObservable(),
67 onRegistrationStateChanged: onRegistrationStateChanged.asObservable(),
68 onNameRegistrationEnded: onNameRegistrationEnded.asObservable(),
69 onRegisteredNameFound: onRegisteredNameFound.asObservable(),
70 };
71
72 this.events.onVolatileDetailsChanged.subscribe(({ accountId, details }) => {
73 log.debug('[1] Received onVolatileDetailsChanged with', { accountId, details });
74 // Keep map of usernames to account IDs as Jamid cannot do this by itself (AFAIK)
75 const username = details.get('Account.registeredName');
76 if (username) {
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -040077 this.usernamesToAccountIds.set(username, accountId);
Issam E. Maghnif796a092022-10-09 20:25:26 +000078 }
79 });
80 this.events.onRegistrationStateChanged.subscribe((ctx) =>
81 log.debug('[1] Received onRegistrationStateChanged with', ctx)
82 );
83 this.events.onNameRegistrationEnded.subscribe((ctx) => log.debug('[1] Received onNameRegistrationEnded with', ctx));
84 this.events.onRegisteredNameFound.subscribe((ctx) => log.debug('[1] Received onRegisteredNameFound with', ctx));
85
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -040086 this.usernamesToAccountIds = new Map<string, string>();
Issam E. Maghnif796a092022-10-09 20:25:26 +000087
88 // 1. You cannot change event handlers after init
89 // 2. You cannot specify multiple handlers for the same event
90 // 3. You cannot specify a default handler
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -040091 // So we rely on Subject() instead of Observable()
Issam E. Maghnif796a092022-10-09 20:25:26 +000092 // Also, handlers receive multiple argument instead of tuple or object!
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -040093 this.jamiSwig.init(handlers);
Issam E. Maghnif796a092022-10-09 20:25:26 +000094 }
95
Misha Krieger-Raynauld62a0da92022-10-22 13:46:59 -040096 stop() {
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -040097 this.jamiSwig.fini();
Misha Krieger-Raynauld62a0da92022-10-22 13:46:59 -040098 }
99
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -0400100 getVolatileAccountDetails(accountId: string): VolatileDetails {
101 return stringMapToRecord(this.jamiSwig.getVolatileAccountDetails(accountId)) as unknown as VolatileDetails;
Issam E. Maghnif796a092022-10-09 20:25:26 +0000102 }
103
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -0400104 getAccountDetails(accountId: string): AccountDetails {
105 return stringMapToRecord(this.jamiSwig.getAccountDetails(accountId)) as unknown as AccountDetails;
106 }
Issam E. Maghnif796a092022-10-09 20:25:26 +0000107
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -0400108 setAccountDetails(accountId: string, accountDetails: AccountDetails) {
109 const accountDetailsStringMap: StringMap = new (this.jamiSwig as any).StringMap();
110 for (const [key, value] of Object.entries(accountDetails)) {
111 accountDetailsStringMap.set(key, value);
112 }
113 this.jamiSwig.setAccountDetails(accountId, accountDetailsStringMap);
114 }
Issam E. Maghnif796a092022-10-09 20:25:26 +0000115
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -0400116 async addAccount(details: Map<string, string | number | boolean>) {
117 // TODO: Add proper typing directly into JamiSwig
118 const detailsStringMap: StringMap = new (this.jamiSwig as any).StringMap();
119
120 detailsStringMap.set('Account.type', 'RING');
121 for (const [key, value] of details.entries()) {
122 detailsStringMap.set('Account.' + key, value.toString());
123 }
124
125 const accountId = this.jamiSwig.addAccount(detailsStringMap);
Issam E. Maghnif796a092022-10-09 20:25:26 +0000126 return firstValueFrom(
127 this.events.onRegistrationStateChanged.pipe(
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -0400128 filter(({ accountId: addedAccountId }) => addedAccountId === accountId),
Issam E. Maghnif796a092022-10-09 20:25:26 +0000129 // TODO: is it the only state?
130 filter(({ state }) => state === 'REGISTERED')
131 )
132 );
133 }
134
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -0400135 removeAccount(accountId: string) {
136 this.jamiSwig.removeAccount(accountId);
Issam E. Maghnif796a092022-10-09 20:25:26 +0000137 }
138
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -0400139 getAccountList(): string[] {
140 return stringVectToArray(this.jamiSwig.getAccountList());
Issam E. Maghnif796a092022-10-09 20:25:26 +0000141 }
142
143 async lookupUsername(username: string) {
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -0400144 const hasRingNs = this.jamiSwig.lookupName('', '', username);
Issam E. Maghnif796a092022-10-09 20:25:26 +0000145 if (!hasRingNs) {
146 log.error('Jami does not have NS');
147 throw new Error('Jami does not have NS');
148 }
149 return firstValueFrom(this.events.onRegisteredNameFound.pipe(filter((r) => r.username === username)));
150 }
151
Misha Krieger-Raynauldb6f1c322022-10-23 20:42:57 -0400152 async registerUsername(accountId: string, username: string, password: string) {
153 const hasRingNs = this.jamiSwig.registerName(accountId, password, username);
154 if (!hasRingNs) {
155 log.error('Jami does not have NS');
156 throw new Error('Jami does not have NS');
157 }
158 return firstValueFrom(
159 this.events.onNameRegistrationEnded.pipe(
160 filter(({ accountId: registeredAccountId }) => registeredAccountId === accountId)
161 )
162 );
163 }
164
165 getDevices(accountId: string): Record<string, string> {
166 return stringMapToRecord(this.jamiSwig.getKnownRingDevices(accountId));
167 }
168
169 getDefaultModerators(accountId: string): string[] {
170 return stringVectToArray(this.jamiSwig.getDefaultModerators(accountId));
171 }
172
173 // TODO: Ideally, we would fetch the username directly from Jami instead of
174 // keeping an internal map.
175 getAccountIdFromUsername(username: string): string | undefined {
176 return this.usernamesToAccountIds.get(username);
Issam E. Maghnif796a092022-10-09 20:25:26 +0000177 }
178}