blob: b2d81c42115fba859e3e12df0e8730e1ebc82b3b [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 */
18import log from 'loglevel';
19import { filter, firstValueFrom, Subject } from 'rxjs';
20import { Service } from 'typedi';
21
Misha Krieger-Raynauldaddd6fe2022-10-22 12:46:04 -040022import { itMap, require } from '../utils.js';
23import { JamiSignal } from './jami-signal.js';
24import {
25 NameRegistrationEnded,
26 RegisteredNameFound,
27 RegistrationStateChanged,
28 VolatileDetailsChanged,
29} from './jami-signal-interfaces.js';
30import { JamiSwig, StringMap, stringMapToMap, stringVectToArr } from './jami-swig.js';
Issam E. Maghnif796a092022-10-09 20:25:26 +000031
32@Service()
33export class Jamid {
34 private readonly jamid: JamiSwig;
35 private readonly mapUsernameToAccountId: Map<string, string>;
36 private readonly events;
37
38 constructor() {
39 // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
40 this.jamid = require('../jamid.node') as JamiSwig;
41
42 const handlers: Record<string, unknown> = {};
43 const handler = (sig: string) => {
44 return (...args: unknown[]) => log.warn('Unhandled', sig, args);
45 };
46 Object.keys(JamiSignal).forEach((sig) => (handlers[sig] = handler(sig)));
47
48 const onVolatileDetailsChanged = new Subject<VolatileDetailsChanged>();
49 handlers.VolatileDetailsChanged = (accountId: string, details: Record<string, string>) =>
50 onVolatileDetailsChanged.next({ accountId, details: new Map(Object.entries(details)) });
51
52 const onRegistrationStateChanged = new Subject<RegistrationStateChanged>();
53 handlers.RegistrationStateChanged = (accountId: string, state: string, code: number, details: string) =>
54 onRegistrationStateChanged.next({ accountId, state, code, details });
55
56 const onNameRegistrationEnded = new Subject<NameRegistrationEnded>();
57 handlers.NameRegistrationEnded = (accountId: string, state: number, username: string) =>
58 onNameRegistrationEnded.next({ accountId, state, username });
59
60 const onRegisteredNameFound = new Subject<RegisteredNameFound>();
61 handlers.RegisteredNameFound = (accountId: string, state: number, address: string, username: string) =>
62 onRegisteredNameFound.next({ accountId, state, address, username });
63
64 this.events = {
65 onVolatileDetailsChanged: onVolatileDetailsChanged.asObservable(),
66 onRegistrationStateChanged: onRegistrationStateChanged.asObservable(),
67 onNameRegistrationEnded: onNameRegistrationEnded.asObservable(),
68 onRegisteredNameFound: onRegisteredNameFound.asObservable(),
69 };
70
71 this.events.onVolatileDetailsChanged.subscribe(({ accountId, details }) => {
72 log.debug('[1] Received onVolatileDetailsChanged with', { accountId, details });
73 // Keep map of usernames to account IDs as Jamid cannot do this by itself (AFAIK)
74 const username = details.get('Account.registeredName');
75 if (username) {
76 this.mapUsernameToAccountId.set(username, accountId);
77 }
78 });
79 this.events.onRegistrationStateChanged.subscribe((ctx) =>
80 log.debug('[1] Received onRegistrationStateChanged with', ctx)
81 );
82 this.events.onNameRegistrationEnded.subscribe((ctx) => log.debug('[1] Received onNameRegistrationEnded with', ctx));
83 this.events.onRegisteredNameFound.subscribe((ctx) => log.debug('[1] Received onRegisteredNameFound with', ctx));
84
85 this.mapUsernameToAccountId = new Map<string, string>();
86
87 // 1. You cannot change event handlers after init
88 // 2. You cannot specify multiple handlers for the same event
89 // 3. You cannot specify a default handler
90 // So we rely on the Subject() instead of Observable()
91 // Also, handlers receive multiple argument instead of tuple or object!
92 this.jamid.init(handlers);
93 }
94
Misha Krieger-Raynauld62a0da92022-10-22 13:46:59 -040095 stop() {
96 this.jamid.fini();
97 }
98
Issam E. Maghnif796a092022-10-09 20:25:26 +000099 getAccountList() {
100 return stringVectToArr(this.jamid.getAccountList());
101 }
102
103 async createAccount(details: Map<string, string | number | boolean>) {
104 // TODO: add proper typing directly into JamiSwig
105 const stringMapDetails: StringMap = new (this.jamid as any).StringMap();
106
107 stringMapDetails.set('Account.type', 'RING');
108 itMap(details.entries(), ([k, v]) => stringMapDetails.set('Account.' + k, v.toString()));
109
110 const id = this.jamid.addAccount(stringMapDetails);
111 return firstValueFrom(
112 this.events.onRegistrationStateChanged.pipe(
113 filter(({ accountId }) => accountId === id),
114 // TODO: is it the only state?
115 filter(({ state }) => state === 'REGISTERED')
116 )
117 );
118 }
119
120 destroyAccount(id: string) {
121 this.jamid.removeAccount(id);
122 }
123
124 async registerUsername(id: string, username: string, password: string) {
125 const hasRingNs = this.jamid.registerName(id, password, username);
126 if (!hasRingNs) {
127 log.error('Jami does not have NS');
128 throw new Error('Jami does not have NS');
129 }
130 return firstValueFrom(this.events.onNameRegistrationEnded.pipe(filter(({ accountId }) => accountId === id)));
131 }
132
133 // TODO: Ideally, we would fetch the username directly from Jami instead of
134 // keeping an internal map.
135 usernameToAccountId(username: string) {
136 return this.mapUsernameToAccountId.get(username);
137 }
138
139 async lookupUsername(username: string) {
140 const hasRingNs = this.jamid.lookupName('', '', username);
141 if (!hasRingNs) {
142 log.error('Jami does not have NS');
143 throw new Error('Jami does not have NS');
144 }
145 return firstValueFrom(this.events.onRegisteredNameFound.pipe(filter((r) => r.username === username)));
146 }
147
148 getAccountDetails(id: string) {
149 return stringMapToMap(this.jamid.getAccountDetails(id));
150 }
151}