blob: 34531cda3d033e833691fdce255b66d438665927 [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
22import { itMap, itRange, itToArr, itToMap, require } from './utils.js';
23
24enum Bool {
25 False = 'false',
26 True = 'true',
27}
28
29interface SwigVec<T> {
30 size(): number;
31 get(i: number): T; // TODO: | undefined;
32}
33
34interface SwigMap<T, U> {
35 keys(): SwigVec<T>;
36 get(k: T): U; // TODO: | undefined;
37 set(k: T, v: U): void;
38}
39
40const swigVecToIt = <T>(v: SwigVec<T>) => itMap(itRange(0, v.size()), (i) => v.get(i));
41const swigMapToIt = <T, U>(m: SwigMap<T, U>) => itMap(swigVecToIt(m.keys()), (k): [T, U] => [k, m.get(k)]);
42
43// type IntVect = SwigVec<number>;
44// type UintVect = SwigVec<number>;
45// type FloatVect = SwigVec<number>;
46type StringVect = SwigVec<string>;
47// type IntegerMap = SwigMap<string, number>;
48type StringMap = SwigMap<string, string>;
49// type VectMap = SwigVec<StringMap>;
50// type Blob = SwigVec<number>;
51
52const stringVectToArr = (sv: StringVect) => itToArr(swigVecToIt(sv));
53const stringMapToMap = (sm: StringMap) => itToMap(swigMapToIt(sm));
54// const vectMapToJs = (vm: VectMap) => itToArr(itMap(swigVecToIt(vm), stringMapToMap));
55
56interface JamiSwig {
57 init(args: Record<string, unknown>): void;
58
59 // IntVect(): IntVect;
60 // UintVect(): UintVect;
61 // FloatVect(): FloatVect;
62 // StringVect(): StringVect;
63 // IntegerMap(): IntegerMap
64 // StringMap(): StringMap;
65 // VectMap(): VectMap;
66 // IntegerMap(): IntegerMap;
67
68 addAccount(details: StringMap): string;
69 removeAccount(id: string): void;
70
71 getAccountList(): StringVect;
72
73 registerName(id: string, password: string, username: string): boolean;
74 lookupName(id: string, nameserver: string, username: string): boolean;
75 lookupAddress(id: string, nameserver: string, address: string): boolean;
76
77 getAccountDetails(id: string): StringMap;
78 setAccountDetails(id: string, details: StringMap): void;
79 setAccountActive(id: string, active: Bool): void;
80}
81
82enum JamiSignal {
83 // using DRing::ConfigurationSignal;
84 AccountsChanged = 'AccountsChanged',
85 AccountDetailsChanged = 'AccountDetailsChanged',
86 RegistrationStateChanged = 'RegistrationStateChanged',
87 ContactAdded = 'ContactAdded',
88 ContactRemoved = 'ContactRemoved',
89 ExportOnRingEnded = 'ExportOnRingEnded',
90 NameRegistrationEnded = 'NameRegistrationEnded',
91 RegisteredNameFound = 'RegisteredNameFound',
92 VolatileDetailsChanged = 'VolatileDetailsChanged',
93 KnownDevicesChanged = 'KnownDevicesChanged',
94 IncomingAccountMessage = 'IncomingAccountMessage',
95 AccountMessageStatusChanged = 'AccountMessageStatusChanged',
96
97 // using DRing::CallSignal;
98 StateChange = 'StateChange',
99 IncomingMessage = 'IncomingMessage',
100 IncomingCall = 'IncomingCall',
101 IncomingCallWithMedia = 'IncomingCallWithMedia',
102 MediaChangeRequested = 'MediaChangeRequested',
103
104 // using DRing::ConversationSignal;
105 ConversationLoaded = 'ConversationLoaded',
106 MessagesFound = 'MessagesFound',
107 MessageReceived = 'MessageReceived',
108 ConversationProfileUpdated = 'ConversationProfileUpdated',
109 ConversationRequestReceived = 'ConversationRequestReceived',
110 ConversationRequestDeclined = 'ConversationRequestDeclined',
111 ConversationReady = 'ConversationReady',
112 ConversationRemoved = 'ConversationRemoved',
113 ConversationMemberEvent = 'ConversationMemberEvent',
114 OnConversationError = 'OnConversationError',
115 OnConferenceInfosUpdated = 'OnConferenceInfosUpdated',
116}
117
118interface VolatileDetailsChanged {
119 accountId: string;
120 details: Map<string, string>;
121}
122
123interface RegistrationStateChanged {
124 accountId: string;
125 state: string;
126 code: number;
127 details: string;
128}
129
130interface NameRegistrationEnded {
131 accountId: string;
132 state: number;
133 username: string;
134}
135
136interface RegisteredNameFound {
137 accountId: string;
138 state: number;
139 address: string;
140 username: string;
141}
142
143@Service()
144export class Jamid {
145 private readonly jamid: JamiSwig;
146 private readonly mapUsernameToAccountId: Map<string, string>;
147 private readonly events;
148
149 constructor() {
150 // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
151 this.jamid = require('../jamid.node') as JamiSwig;
152
153 const handlers: Record<string, unknown> = {};
154 const handler = (sig: string) => {
155 return (...args: unknown[]) => log.warn('Unhandled', sig, args);
156 };
157 Object.keys(JamiSignal).forEach((sig) => (handlers[sig] = handler(sig)));
158
159 const onVolatileDetailsChanged = new Subject<VolatileDetailsChanged>();
160 handlers.VolatileDetailsChanged = (accountId: string, details: Record<string, string>) =>
161 onVolatileDetailsChanged.next({ accountId, details: new Map(Object.entries(details)) });
162
163 const onRegistrationStateChanged = new Subject<RegistrationStateChanged>();
164 handlers.RegistrationStateChanged = (accountId: string, state: string, code: number, details: string) =>
165 onRegistrationStateChanged.next({ accountId, state, code, details });
166
167 const onNameRegistrationEnded = new Subject<NameRegistrationEnded>();
168 handlers.NameRegistrationEnded = (accountId: string, state: number, username: string) =>
169 onNameRegistrationEnded.next({ accountId, state, username });
170
171 const onRegisteredNameFound = new Subject<RegisteredNameFound>();
172 handlers.RegisteredNameFound = (accountId: string, state: number, address: string, username: string) =>
173 onRegisteredNameFound.next({ accountId, state, address, username });
174
175 this.events = {
176 onVolatileDetailsChanged: onVolatileDetailsChanged.asObservable(),
177 onRegistrationStateChanged: onRegistrationStateChanged.asObservable(),
178 onNameRegistrationEnded: onNameRegistrationEnded.asObservable(),
179 onRegisteredNameFound: onRegisteredNameFound.asObservable(),
180 };
181
182 this.events.onVolatileDetailsChanged.subscribe(({ accountId, details }) => {
183 log.debug('[1] Received onVolatileDetailsChanged with', { accountId, details });
184 // Keep map of usernames to account IDs as Jamid cannot do this by itself (AFAIK)
185 const username = details.get('Account.registeredName');
186 if (username) {
187 this.mapUsernameToAccountId.set(username, accountId);
188 }
189 });
190 this.events.onRegistrationStateChanged.subscribe((ctx) =>
191 log.debug('[1] Received onRegistrationStateChanged with', ctx)
192 );
193 this.events.onNameRegistrationEnded.subscribe((ctx) => log.debug('[1] Received onNameRegistrationEnded with', ctx));
194 this.events.onRegisteredNameFound.subscribe((ctx) => log.debug('[1] Received onRegisteredNameFound with', ctx));
195
196 this.mapUsernameToAccountId = new Map<string, string>();
197
198 // 1. You cannot change event handlers after init
199 // 2. You cannot specify multiple handlers for the same event
200 // 3. You cannot specify a default handler
201 // So we rely on the Subject() instead of Observable()
202 // Also, handlers receive multiple argument instead of tuple or object!
203 this.jamid.init(handlers);
204 }
205
206 getAccountList() {
207 return stringVectToArr(this.jamid.getAccountList());
208 }
209
210 async createAccount(details: Map<string, string | number | boolean>) {
211 // TODO: add proper typing directly into JamiSwig
212 const stringMapDetails: StringMap = new (this.jamid as any).StringMap();
213
214 stringMapDetails.set('Account.type', 'RING');
215 itMap(details.entries(), ([k, v]) => stringMapDetails.set('Account.' + k, v.toString()));
216
217 const id = this.jamid.addAccount(stringMapDetails);
218 return firstValueFrom(
219 this.events.onRegistrationStateChanged.pipe(
220 filter(({ accountId }) => accountId === id),
221 // TODO: is it the only state?
222 filter(({ state }) => state === 'REGISTERED')
223 )
224 );
225 }
226
227 destroyAccount(id: string) {
228 this.jamid.removeAccount(id);
229 }
230
231 async registerUsername(id: string, username: string, password: string) {
232 const hasRingNs = this.jamid.registerName(id, password, username);
233 if (!hasRingNs) {
234 log.error('Jami does not have NS');
235 throw new Error('Jami does not have NS');
236 }
237 return firstValueFrom(this.events.onNameRegistrationEnded.pipe(filter(({ accountId }) => accountId === id)));
238 }
239
240 // TODO: Ideally, we would fetch the username directly from Jami instead of
241 // keeping an internal map.
242 usernameToAccountId(username: string) {
243 return this.mapUsernameToAccountId.get(username);
244 }
245
246 async lookupUsername(username: string) {
247 const hasRingNs = this.jamid.lookupName('', '', username);
248 if (!hasRingNs) {
249 log.error('Jami does not have NS');
250 throw new Error('Jami does not have NS');
251 }
252 return firstValueFrom(this.events.onRegisteredNameFound.pipe(filter((r) => r.username === username)));
253 }
254
255 getAccountDetails(id: string) {
256 return stringMapToMap(this.jamid.getAccountDetails(id));
257 }
258}