blob: 34531cda3d033e833691fdce255b66d438665927 [file] [log] [blame]
/*
* 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 log from 'loglevel';
import { filter, firstValueFrom, Subject } from 'rxjs';
import { Service } from 'typedi';
import { itMap, itRange, itToArr, itToMap, require } from './utils.js';
enum Bool {
False = 'false',
True = 'true',
}
interface SwigVec<T> {
size(): number;
get(i: number): T; // TODO: | undefined;
}
interface SwigMap<T, U> {
keys(): SwigVec<T>;
get(k: T): U; // TODO: | undefined;
set(k: T, v: U): void;
}
const swigVecToIt = <T>(v: SwigVec<T>) => itMap(itRange(0, v.size()), (i) => v.get(i));
const swigMapToIt = <T, U>(m: SwigMap<T, U>) => itMap(swigVecToIt(m.keys()), (k): [T, U] => [k, m.get(k)]);
// type IntVect = SwigVec<number>;
// type UintVect = SwigVec<number>;
// type FloatVect = SwigVec<number>;
type StringVect = SwigVec<string>;
// type IntegerMap = SwigMap<string, number>;
type StringMap = SwigMap<string, string>;
// type VectMap = SwigVec<StringMap>;
// type Blob = SwigVec<number>;
const stringVectToArr = (sv: StringVect) => itToArr(swigVecToIt(sv));
const stringMapToMap = (sm: StringMap) => itToMap(swigMapToIt(sm));
// const vectMapToJs = (vm: VectMap) => itToArr(itMap(swigVecToIt(vm), stringMapToMap));
interface JamiSwig {
init(args: Record<string, unknown>): void;
// IntVect(): IntVect;
// UintVect(): UintVect;
// FloatVect(): FloatVect;
// StringVect(): StringVect;
// IntegerMap(): IntegerMap
// StringMap(): StringMap;
// VectMap(): VectMap;
// IntegerMap(): IntegerMap;
addAccount(details: StringMap): string;
removeAccount(id: string): void;
getAccountList(): StringVect;
registerName(id: string, password: string, username: string): boolean;
lookupName(id: string, nameserver: string, username: string): boolean;
lookupAddress(id: string, nameserver: string, address: string): boolean;
getAccountDetails(id: string): StringMap;
setAccountDetails(id: string, details: StringMap): void;
setAccountActive(id: string, active: Bool): void;
}
enum JamiSignal {
// using DRing::ConfigurationSignal;
AccountsChanged = 'AccountsChanged',
AccountDetailsChanged = 'AccountDetailsChanged',
RegistrationStateChanged = 'RegistrationStateChanged',
ContactAdded = 'ContactAdded',
ContactRemoved = 'ContactRemoved',
ExportOnRingEnded = 'ExportOnRingEnded',
NameRegistrationEnded = 'NameRegistrationEnded',
RegisteredNameFound = 'RegisteredNameFound',
VolatileDetailsChanged = 'VolatileDetailsChanged',
KnownDevicesChanged = 'KnownDevicesChanged',
IncomingAccountMessage = 'IncomingAccountMessage',
AccountMessageStatusChanged = 'AccountMessageStatusChanged',
// using DRing::CallSignal;
StateChange = 'StateChange',
IncomingMessage = 'IncomingMessage',
IncomingCall = 'IncomingCall',
IncomingCallWithMedia = 'IncomingCallWithMedia',
MediaChangeRequested = 'MediaChangeRequested',
// using DRing::ConversationSignal;
ConversationLoaded = 'ConversationLoaded',
MessagesFound = 'MessagesFound',
MessageReceived = 'MessageReceived',
ConversationProfileUpdated = 'ConversationProfileUpdated',
ConversationRequestReceived = 'ConversationRequestReceived',
ConversationRequestDeclined = 'ConversationRequestDeclined',
ConversationReady = 'ConversationReady',
ConversationRemoved = 'ConversationRemoved',
ConversationMemberEvent = 'ConversationMemberEvent',
OnConversationError = 'OnConversationError',
OnConferenceInfosUpdated = 'OnConferenceInfosUpdated',
}
interface VolatileDetailsChanged {
accountId: string;
details: Map<string, string>;
}
interface RegistrationStateChanged {
accountId: string;
state: string;
code: number;
details: string;
}
interface NameRegistrationEnded {
accountId: string;
state: number;
username: string;
}
interface RegisteredNameFound {
accountId: string;
state: number;
address: string;
username: string;
}
@Service()
export class Jamid {
private readonly jamid: JamiSwig;
private readonly mapUsernameToAccountId: Map<string, string>;
private readonly events;
constructor() {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
this.jamid = require('../jamid.node') as JamiSwig;
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 });
this.events = {
onVolatileDetailsChanged: onVolatileDetailsChanged.asObservable(),
onRegistrationStateChanged: onRegistrationStateChanged.asObservable(),
onNameRegistrationEnded: onNameRegistrationEnded.asObservable(),
onRegisteredNameFound: onRegisteredNameFound.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.mapUsernameToAccountId.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.mapUsernameToAccountId = 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 the Subject() instead of Observable()
// Also, handlers receive multiple argument instead of tuple or object!
this.jamid.init(handlers);
}
getAccountList() {
return stringVectToArr(this.jamid.getAccountList());
}
async createAccount(details: Map<string, string | number | boolean>) {
// TODO: add proper typing directly into JamiSwig
const stringMapDetails: StringMap = new (this.jamid as any).StringMap();
stringMapDetails.set('Account.type', 'RING');
itMap(details.entries(), ([k, v]) => stringMapDetails.set('Account.' + k, v.toString()));
const id = this.jamid.addAccount(stringMapDetails);
return firstValueFrom(
this.events.onRegistrationStateChanged.pipe(
filter(({ accountId }) => accountId === id),
// TODO: is it the only state?
filter(({ state }) => state === 'REGISTERED')
)
);
}
destroyAccount(id: string) {
this.jamid.removeAccount(id);
}
async registerUsername(id: string, username: string, password: string) {
const hasRingNs = this.jamid.registerName(id, 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 }) => accountId === id)));
}
// TODO: Ideally, we would fetch the username directly from Jami instead of
// keeping an internal map.
usernameToAccountId(username: string) {
return this.mapUsernameToAccountId.get(username);
}
async lookupUsername(username: string) {
const hasRingNs = this.jamid.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)));
}
getAccountDetails(id: string) {
return stringMapToMap(this.jamid.getAccountDetails(id));
}
}