Split up jamid.ts into multiple files

Change-Id: I792eceb81c033b8f17183de2686af1227982e779
diff --git a/server/src/jamid/jami-signal-interfaces.ts b/server/src/jamid/jami-signal-interfaces.ts
new file mode 100644
index 0000000..5c62482
--- /dev/null
+++ b/server/src/jamid/jami-signal-interfaces.ts
@@ -0,0 +1,41 @@
+/*
+ * 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/>.
+ */
+export interface VolatileDetailsChanged {
+  accountId: string;
+  details: Map<string, string>;
+}
+
+export interface RegistrationStateChanged {
+  accountId: string;
+  state: string;
+  code: number;
+  details: string;
+}
+
+export interface NameRegistrationEnded {
+  accountId: string;
+  state: number;
+  username: string;
+}
+
+export interface RegisteredNameFound {
+  accountId: string;
+  state: number;
+  address: string;
+  username: string;
+}
diff --git a/server/src/jamid/jami-signal.ts b/server/src/jamid/jami-signal.ts
new file mode 100644
index 0000000..f76127b
--- /dev/null
+++ b/server/src/jamid/jami-signal.ts
@@ -0,0 +1,52 @@
+/*
+ * 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/>.
+ */
+export 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',
+}
diff --git a/server/src/jamid/jami-swig.ts b/server/src/jamid/jami-swig.ts
new file mode 100644
index 0000000..3f48fbc
--- /dev/null
+++ b/server/src/jamid/jami-swig.ts
@@ -0,0 +1,76 @@
+/*
+ * 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 { itMap, itRange, itToArr, itToMap } 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>;
+export type StringVect = SwigVec<string>;
+// type IntegerMap = SwigMap<string, number>;
+export type StringMap = SwigMap<string, string>;
+// type VectMap = SwigVec<StringMap>;
+// type Blob = SwigVec<number>;
+
+export const stringVectToArr = (sv: StringVect) => itToArr(swigVecToIt(sv));
+export const stringMapToMap = (sm: StringMap) => itToMap(swigMapToIt(sm));
+// const vectMapToJs = (vm: VectMap) => itToArr(itMap(swigVecToIt(vm), stringMapToMap));
+
+export 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;
+}
diff --git a/server/src/jamid/jamid.ts b/server/src/jamid/jamid.ts
new file mode 100644
index 0000000..5583f5c
--- /dev/null
+++ b/server/src/jamid/jamid.ts
@@ -0,0 +1,147 @@
+/*
+ * 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, require } from '../utils.js';
+import { JamiSignal } from './jami-signal.js';
+import {
+  NameRegistrationEnded,
+  RegisteredNameFound,
+  RegistrationStateChanged,
+  VolatileDetailsChanged,
+} from './jami-signal-interfaces.js';
+import { JamiSwig, StringMap, stringMapToMap, stringVectToArr } from './jami-swig.js';
+
+@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));
+  }
+}