| /* |
| * Copyright (C) 2022 Savoir-faire Linux Inc. |
| * |
| * This program is free software; you can redistribute it and/or modify |
| * it under the terms of the GNU Affero General Public License as |
| * published by the Free Software Foundation; either version 3 of the |
| * License, or (at your option) any later version. |
| * |
| * This program is distributed in the hope that it will be useful, |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| * GNU Affero General Public License for more details. |
| * |
| * You should have received a copy of the GNU Affero General Public |
| * License along with this program. If not, see |
| * <https://www.gnu.org/licenses/>. |
| */ |
| import { AccountDetails, VolatileDetails } from 'jami-web-common'; |
| import log from 'loglevel'; |
| import { filter, firstValueFrom, Subject } from 'rxjs'; |
| import { Service } from 'typedi'; |
| |
| import { JamiSignal } from './jami-signal.js'; |
| import { |
| IncomingAccountMessage, |
| NameRegistrationEnded, |
| RegisteredNameFound, |
| RegistrationStateChanged, |
| VolatileDetailsChanged, |
| } from './jami-signal-interfaces.js'; |
| import { JamiSwig, StringMap, stringMapToRecord, stringVectToArray } from './jami-swig.js'; |
| import { require } from './utils.js'; |
| |
| @Service() |
| export class Jamid { |
| private readonly jamiSwig: JamiSwig; |
| private readonly usernamesToAccountIds: Map<string, string>; |
| private readonly events; |
| |
| constructor() { |
| // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment |
| this.jamiSwig = require('../../jamid.node') as JamiSwig; // TODO: we should put the path in the .env |
| |
| const handlers: Record<string, unknown> = {}; |
| const handler = (sig: string) => { |
| return (...args: unknown[]) => log.warn('Unhandled', sig, args); |
| }; |
| Object.keys(JamiSignal).forEach((sig) => (handlers[sig] = handler(sig))); |
| |
| const onVolatileDetailsChanged = new Subject<VolatileDetailsChanged>(); |
| handlers.VolatileDetailsChanged = (accountId: string, details: Record<string, string>) => |
| onVolatileDetailsChanged.next({ accountId, details: new Map(Object.entries(details)) }); |
| |
| const onRegistrationStateChanged = new Subject<RegistrationStateChanged>(); |
| handlers.RegistrationStateChanged = (accountId: string, state: string, code: number, details: string) => |
| onRegistrationStateChanged.next({ accountId, state, code, details }); |
| |
| const onNameRegistrationEnded = new Subject<NameRegistrationEnded>(); |
| handlers.NameRegistrationEnded = (accountId: string, state: number, username: string) => |
| onNameRegistrationEnded.next({ accountId, state, username }); |
| |
| const onRegisteredNameFound = new Subject<RegisteredNameFound>(); |
| handlers.RegisteredNameFound = (accountId: string, state: number, address: string, username: string) => |
| onRegisteredNameFound.next({ accountId, state, address, username }); |
| |
| const onIncomingAccountMessage = new Subject<IncomingAccountMessage>(); |
| handlers.IncomingAccountMessage = (accountId: string, from: string, message: Record<string, string>) => |
| onIncomingAccountMessage.next({ accountId, from, message }); |
| |
| this.events = { |
| onVolatileDetailsChanged: onVolatileDetailsChanged.asObservable(), |
| onRegistrationStateChanged: onRegistrationStateChanged.asObservable(), |
| onNameRegistrationEnded: onNameRegistrationEnded.asObservable(), |
| onRegisteredNameFound: onRegisteredNameFound.asObservable(), |
| onIncomingAccountMessage: onIncomingAccountMessage.asObservable(), |
| }; |
| |
| this.events.onVolatileDetailsChanged.subscribe(({ accountId, details }) => { |
| log.debug('[1] Received onVolatileDetailsChanged with', { accountId, details }); |
| // Keep map of usernames to account IDs as Jamid cannot do this by itself (AFAIK) |
| const username = details.get('Account.registeredName'); |
| if (username) { |
| this.usernamesToAccountIds.set(username, accountId); |
| } |
| }); |
| this.events.onRegistrationStateChanged.subscribe((ctx) => |
| log.debug('[1] Received onRegistrationStateChanged with', ctx) |
| ); |
| this.events.onNameRegistrationEnded.subscribe((ctx) => log.debug('[1] Received onNameRegistrationEnded with', ctx)); |
| this.events.onRegisteredNameFound.subscribe((ctx) => log.debug('[1] Received onRegisteredNameFound with', ctx)); |
| this.events.onIncomingAccountMessage.subscribe((ctx) => |
| log.debug('[1] Received onIncomingAccountMessage with', ctx) |
| ); |
| |
| this.usernamesToAccountIds = new Map<string, string>(); |
| |
| // 1. You cannot change event handlers after init |
| // 2. You cannot specify multiple handlers for the same event |
| // 3. You cannot specify a default handler |
| // So we rely on Subject() instead of Observable() |
| // Also, handlers receive multiple argument instead of tuple or object! |
| this.jamiSwig.init(handlers); |
| } |
| |
| stop() { |
| this.jamiSwig.fini(); |
| } |
| |
| getVolatileAccountDetails(accountId: string): VolatileDetails { |
| return stringMapToRecord(this.jamiSwig.getVolatileAccountDetails(accountId)) as unknown as VolatileDetails; |
| } |
| |
| getAccountDetails(accountId: string): AccountDetails { |
| return stringMapToRecord(this.jamiSwig.getAccountDetails(accountId)) as unknown as AccountDetails; |
| } |
| |
| setAccountDetails(accountId: string, accountDetails: AccountDetails) { |
| const accountDetailsStringMap: StringMap = new this.jamiSwig.StringMap(); |
| for (const [key, value] of Object.entries(accountDetails)) { |
| accountDetailsStringMap.set(key, value); |
| } |
| this.jamiSwig.setAccountDetails(accountId, accountDetailsStringMap); |
| } |
| |
| async addAccount(details: Map<string, string | number | boolean>) { |
| const detailsStringMap: StringMap = new this.jamiSwig.StringMap(); |
| |
| detailsStringMap.set('Account.type', 'RING'); |
| for (const [key, value] of details.entries()) { |
| detailsStringMap.set('Account.' + key, value.toString()); |
| } |
| |
| const accountId = this.jamiSwig.addAccount(detailsStringMap); |
| return firstValueFrom( |
| this.events.onRegistrationStateChanged.pipe( |
| filter(({ accountId: addedAccountId }) => addedAccountId === accountId), |
| // TODO: is it the only state? |
| filter(({ state }) => state === 'REGISTERED') |
| ) |
| ); |
| } |
| |
| removeAccount(accountId: string) { |
| this.jamiSwig.removeAccount(accountId); |
| } |
| |
| getAccountList(): string[] { |
| return stringVectToArray(this.jamiSwig.getAccountList()); |
| } |
| |
| sendAccountTextMessage(accountId: string, contactId: string, type: string, message: string) { |
| const messageStringMap: StringMap = new this.jamiSwig.StringMap(); |
| messageStringMap.set(type, message); |
| this.jamiSwig.sendAccountTextMessage(accountId, contactId, messageStringMap); |
| } |
| |
| async lookupUsername(username: string) { |
| const hasRingNs = this.jamiSwig.lookupName('', '', username); |
| if (!hasRingNs) { |
| log.error('Jami does not have NS'); |
| throw new Error('Jami does not have NS'); |
| } |
| return firstValueFrom(this.events.onRegisteredNameFound.pipe(filter((r) => r.username === username))); |
| } |
| |
| async registerUsername(accountId: string, username: string, password: string) { |
| const hasRingNs = this.jamiSwig.registerName(accountId, password, username); |
| if (!hasRingNs) { |
| log.error('Jami does not have NS'); |
| throw new Error('Jami does not have NS'); |
| } |
| return firstValueFrom( |
| this.events.onNameRegistrationEnded.pipe( |
| filter(({ accountId: registeredAccountId }) => registeredAccountId === accountId) |
| ) |
| ); |
| } |
| |
| getDevices(accountId: string): Record<string, string> { |
| return stringMapToRecord(this.jamiSwig.getKnownRingDevices(accountId)); |
| } |
| |
| getDefaultModerators(accountId: string): string[] { |
| return stringVectToArray(this.jamiSwig.getDefaultModerators(accountId)); |
| } |
| |
| // TODO: Ideally, we would fetch the username directly from Jami instead of |
| // keeping an internal map. |
| getAccountIdFromUsername(username: string): string | undefined { |
| return this.usernamesToAccountIds.get(username); |
| } |
| } |