Convert js files in `model/` to Typescript

Convert `JamiDaemon.js` to Typescript
Add `model/util.ts` containing some utility types.

Gitlab: #30
Change-Id: Ia5d120330011e89a0be28732965ae9814ab68d19
diff --git a/JamiDaemon.ts b/JamiDaemon.ts
new file mode 100755
index 0000000..d40cc63
--- /dev/null
+++ b/JamiDaemon.ts
@@ -0,0 +1,526 @@
+/*
+ *  Copyright (c) 2017-2021 Savoir-faire Linux Inc.
+ *
+ *  Author: Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ *  Author: Asad Salman <me@asad.co>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU 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 General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+'use strict';
+
+import { createRequire } from 'module';
+import path from 'path';
+
+import Account, { RegistrationState } from './model/Account';
+import AccountDetails, { AccountConfig, VolatileDetails } from './model/AccountDetails';
+import Contact from './model/Contact';
+import Conversation, { Message } from './model/Conversation';
+import { Lookup, LookupResolveValue, PromiseExecutor } from './model/util';
+
+const require = createRequire(import.meta.url);
+
+class JamiDaemon {
+  private accounts: Account[];
+  private readonly lookups: Lookup[];
+  private readonly tempAccounts: Record<string, PromiseExecutor<string>>;
+  private dring: Record<string, any>;
+
+  constructor(onMessage: (account: Account, conversation: Conversation, message: Message) => void) {
+    this.accounts = [];
+    this.lookups = [];
+    this.tempAccounts = {};
+    this.dring = require(path.join(process.cwd(), 'jamid.node'));
+    this.dring.init({
+      AccountsChanged: () => {
+        console.log('AccountsChanged');
+        const newAccounts: Account[] = [];
+        JamiDaemon.vectToJs(this.dring.getAccountList()).forEach((accountId) => {
+          for (const account of this.accounts) {
+            if (account.getId() === accountId) {
+              newAccounts.push(account);
+              return;
+            }
+          }
+          newAccounts.push(
+            new Account(
+              accountId,
+              JamiDaemon.mapToJs(this.dring.getAccountDetails(accountId)) as AccountDetails,
+              JamiDaemon.mapToJs(this.dring.getVolatileAccountDetails(accountId)) as VolatileDetails
+            )
+          );
+        });
+        this.accounts = newAccounts;
+      },
+      AccountDetailsChanged: (accountId: string, details: AccountDetails) => {
+        console.log(`AccountDetailsChanged ${accountId}`);
+        const account = this.getAccount(accountId);
+        if (!account) {
+          console.log(`Unknown account ${accountId}`);
+          return;
+        }
+        account.details = details;
+      },
+      VolatileDetailsChanged: (accountId: string, details: VolatileDetails) => {
+        console.log(`VolatileDetailsChanged ${accountId}`);
+        const account = this.getAccount(accountId);
+        if (!account) {
+          console.log(`Unknown account ${accountId}`);
+          return;
+        }
+        account.volatileDetails = details;
+      },
+      IncomingAccountMessage: (accountId: string, from: Account, message: Message) => {
+        console.log(`Received message: ${accountId} ${from} ${message['text/plain']}`);
+        /*
+                if (parser.validate(message["text/plain"]) === true) {
+                    console.log(message["text/plain"])
+                } else {
+
+                    user = connectedUsers[accountId]
+                    console.log(user.socketId)
+                    io.to(user.socketId).emit('receivedMessage', message["text/plain"])
+                    //io.emit('receivedMessage', message["text/plain"])
+                }*/
+      },
+      RegistrationStateChanged: (accountId: string, state: RegistrationState, code: number, detail: string) => {
+        console.log('RegistrationStateChanged: ' + accountId + ' ' + state + ' ' + code + ' ' + detail);
+        const account: Account | undefined = this.getAccount(accountId);
+        if (account) {
+          account.registrationState = state;
+        } else {
+          console.log(`Unknown account ${accountId}`);
+          return;
+        }
+        const ctx = this.tempAccounts[accountId];
+        if (ctx) {
+          if (state === 'REGISTERED') {
+            account.details = JamiDaemon.mapToJs(this.dring.getAccountDetails(accountId)) as AccountDetails;
+            ctx.resolve(accountId);
+            delete this.tempAccounts[accountId];
+          } else if (state === 'ERROR_AUTH') {
+            this.dring.removeAccount(accountId);
+            ctx.reject(state);
+            delete this.tempAccounts[accountId];
+          }
+        }
+      },
+      RegisteredNameFound: (accountId: string, state: number, address: string, name: string) => {
+        console.log(`RegisteredNameFound: ${accountId} ${state} ${address} ${name}`);
+        let lookups;
+        if (accountId) {
+          const account = this.getAccount(accountId);
+          if (!account) {
+            console.log(`Unknown account ${accountId}`);
+            return;
+          }
+          if (state == 0) {
+            const contact = account.getContactFromCache(address);
+            if (!contact.isRegisteredNameResolved()) contact.setRegisteredName(name);
+          }
+          lookups = account.lookups;
+        } else {
+          lookups = this.lookups;
+        }
+        let index = lookups.length - 1;
+        while (index >= 0) {
+          const lookup = lookups[index];
+          if ((lookup.address && lookup.address === address) || (lookup.name && lookup.name === name)) {
+            lookup.resolve({ address, name, state });
+            lookups.splice(index, 1);
+          }
+          index -= 1;
+        }
+      },
+      NameRegistrationEnded: (accountId: string, state: number, name: string) => {
+        console.log(`NameRegistrationEnded: ${accountId} ${state} ${name}`);
+        const account = this.getAccount(accountId);
+        if (account) {
+          if (state === 0) account.volatileDetails['Account.registeredName'] = name;
+          if (account.registeringName) {
+            account.registeringName.resolve(state);
+            delete account.registeringName;
+          }
+        } else {
+          console.log(`Unknown account ${accountId}`);
+        }
+      },
+      // Conversations
+      ConversationReady: (accountId: string, conversationId: string) => {
+        console.log(`conversationReady: ${accountId} ${conversationId}`);
+        const account = this.getAccount(accountId);
+        if (!account) {
+          console.log(`Unknown account ${accountId}`);
+          return;
+        }
+        let conversation = account.getConversation(conversationId);
+        if (!conversation) {
+          const members = JamiDaemon.vectMapToJs(this.dring.getConversationMembers(accountId, conversationId));
+          members.forEach((member) => (member.contact = account.getContactFromCache(member.uri)));
+          conversation = new Conversation(conversationId, accountId, members);
+          account.addConversation(conversation);
+        }
+      },
+      ConversationRemoved: (accountId: string, conversationId: string) => {
+        console.log(`conversationRemoved: ${accountId} ${conversationId}`);
+        const account = this.getAccount(accountId);
+        if (!account) {
+          console.log(`Unknown account ${accountId}`);
+          return;
+        }
+        account.removeConversation(conversationId);
+      },
+      ConversationLoaded: (id: number, accountId: string, conversationId: string, messages: Message[]) => {
+        console.log(`conversationLoaded: ${accountId} ${conversationId}`);
+        const account = this.getAccount(accountId);
+        if (!account) {
+          console.log(`Unknown account ${accountId}`);
+          return;
+        }
+        const conversation = account.getConversation(conversationId);
+        if (conversation) {
+          //conversation.addLoadedMessages(messages)
+          const request = conversation.requests[id];
+          if (request) {
+            request.resolve(messages);
+          }
+        }
+      },
+      MessageReceived: (accountId: string, conversationId: string, message: Message) => {
+        console.log(`messageReceived: ${accountId} ${conversationId}`);
+        console.log(message);
+        const account = this.getAccount(accountId);
+        if (!account) {
+          console.log(`Unknown account ${accountId}`);
+          return;
+        }
+        const conversation = account.getConversation(conversationId);
+        if (conversation) {
+          conversation.addMessage(message);
+          if (onMessage) onMessage(account, conversation, message);
+        }
+      },
+      ConversationRequestReceived: (accountId: string, conversationId: string, message: Message) => {
+        console.log(`conversationRequestReceived: ${accountId} ${conversationId}`);
+        const account = this.getAccount(accountId);
+        if (!account) {
+          console.log(`Unknown account ${accountId}`);
+          return;
+        }
+      },
+      ConversationMemberEvent: (accountId: string, conversationId: string, memberUri: string, event: number) => {
+        console.log(`conversationMemberEvent: ${accountId} ${conversationId}`);
+        const account = this.getAccount(accountId);
+        if (!account) {
+          console.log(`Unknown account ${accountId}`);
+          return;
+        }
+      },
+      OnConversationError: (accountId: string, conversationId: string, code: number, what: string) => {
+        console.log(`onConversationError: ${accountId} ${conversationId}`);
+        const account = this.getAccount(accountId);
+        if (!account) {
+          console.log(`Unknown account ${accountId}`);
+          return;
+        }
+      },
+      // Calls
+      StateChange: (callId: string, state: string, code: number) => {
+        console.log(`CallStateChange: ${callId} ${state} ${code}`);
+      },
+      IncomingCall: (accountId: string, callId: string, peerUri: string) => {
+        console.log(`IncomingCall: ${accountId} ${callId} ${peerUri}`);
+      },
+      ConferenceCreated: (confId: string) => {
+        console.log(`ConferenceCreated: ${confId}`);
+      },
+      ConferenceChanged: (accountId: string, confId: string, state: string) => {
+        console.log(`ConferenceChanged: ${confId}`);
+      },
+      ConferenceRemoved: (confId: string) => {
+        console.log(`ConferenceRemoved: ${confId}`);
+      },
+      OnConferenceInfosUpdated: (confId: string) => {
+        console.log(`onConferenceInfosUpdated: ${confId}`);
+      },
+    });
+
+    JamiDaemon.vectToJs(this.dring.getAccountList()).forEach((accountId) => {
+      const account = new Account(
+        accountId,
+        JamiDaemon.mapToJs(this.dring.getAccountDetails(accountId)) as AccountDetails,
+        JamiDaemon.mapToJs(this.dring.getVolatileAccountDetails(accountId)) as VolatileDetails
+      );
+
+      account.contacts = JamiDaemon.vectMapToJs(this.dring.getContacts(accountId)) as Contact[];
+
+      JamiDaemon.vectToJs(this.dring.getConversations(accountId)).forEach((conversationId) => {
+        const members = JamiDaemon.vectMapToJs(this.dring.getConversationMembers(accountId, conversationId));
+        console.log('\n\nXMEMBERS: ', members);
+        members.forEach((member) => {
+          member.contact = account.getContactFromCache(member.uri);
+          if (!member.contact.isRegisteredNameResolved()) {
+            if (!member.uri) return;
+            console.log(`lookupAddress ${accountId} ${member.uri}`, member);
+            member.contact.setRegisteredName(
+              new Promise((resolve: (value: LookupResolveValue) => void, reject) =>
+                account.lookups.push({ address: member.uri, resolve, reject })
+              ).then((result) => {
+                if (result.state == 0) return result.name;
+                else if (result.state == 1) return undefined;
+                else return null;
+              })
+            );
+            this.dring.lookupAddress(accountId, '', member.uri);
+          }
+        });
+        const conversation = new Conversation(conversationId, accountId, members);
+        conversation.infos = JamiDaemon.mapToJs(this.dring.conversationInfos(accountId, conversationId));
+        account.addConversation(conversation);
+      });
+
+      this.accounts.push(account);
+    });
+  }
+
+  addAccount(accountConfig: AccountConfig) {
+    const params = this.accountDetailsToNative(accountConfig);
+    params.set('Account.type', 'RING');
+    return new Promise<string>((resolve, reject) => {
+      const accountId = this.dring.addAccount(params);
+      this.tempAccounts[accountId] = { resolve, reject };
+    });
+  }
+
+  getDevices(accountId: string) {
+    return JamiDaemon.mapToJs(this.dring.getKnownRingDevices(accountId));
+  }
+
+  getAccount(accountId: string) {
+    for (let i = 0; i < this.accounts.length; i++) {
+      const account = this.accounts[i];
+      if (account.getId() === accountId) return account;
+    }
+    return undefined;
+  }
+  getAccountList() {
+    return this.accounts;
+  }
+  registerName(accountId: string, password: string, name: string) {
+    return new Promise((resolve, reject) => {
+      if (!name) return reject(new Error('Invalid name'));
+      const account = this.getAccount(accountId);
+      if (!account) return reject(new Error("Can't find account"));
+      if (account.registeringName) return reject(new Error('Username already being registered'));
+      if (this.dring.registerName(accountId, password, name)) {
+        account.registeringName = { name, resolve, reject };
+      }
+    });
+  }
+
+  getConversation(accountId: string, conversationId: string) {
+    const account = this.getAccount(accountId);
+    if (account) return account.getConversation(conversationId);
+    return null;
+  }
+  getAccountDetails(accountId: string) {
+    return JamiDaemon.mapToJs(this.dring.getAccountDetails(accountId));
+  }
+  setAccountDetails(accountId: string, details: AccountDetails) {
+    this.dring.setAccountDetails(accountId, this.mapToNative(details));
+  }
+  getAudioOutputDeviceList() {
+    return JamiDaemon.vectToJs(this.dring.getAudioOutputDeviceList());
+  }
+  getVolume(deviceName: string): number {
+    return this.dring.getVolume(deviceName);
+  }
+  setVolume(deviceName: string, volume: number) {
+    return this.dring.setVolume(deviceName, volume);
+  }
+
+  lookupName(accountId: string, name: string) {
+    const p = new Promise((resolve, reject) => {
+      if (accountId) {
+        const account = this.getAccount(accountId);
+        if (!account) {
+          reject(new Error("Can't find account"));
+        } else {
+          account.lookups.push({ name, resolve, reject });
+        }
+      } else {
+        this.lookups.push({ name, resolve, reject });
+      }
+    });
+    this.dring.lookupName(accountId || '', '', name);
+    return p;
+  }
+
+  lookupAddress(accountId: string, address: string) {
+    console.log(`lookupAddress ${accountId} ${address}`);
+    const p = new Promise((resolve, reject) => {
+      if (accountId) {
+        const account = this.getAccount(accountId);
+        if (!account) {
+          reject(new Error("Can't find account"));
+        } else {
+          account.lookups.push({ address, resolve, reject });
+        }
+      } else {
+        this.lookups.push({ address, resolve, reject });
+      }
+    });
+    this.dring.lookupAddress(accountId || '', '', address);
+    return p;
+  }
+
+  stop() {
+    this.dring.fini();
+  }
+
+  addContact(accountId: string, contactId: string) {
+    this.dring.addContact(accountId, contactId);
+    const details = JamiDaemon.mapToJs(this.dring.getContactDetails(accountId, contactId));
+    if (details.conversationId) {
+      const account = this.getAccount(accountId);
+      if (account) {
+        let conversation = account.getConversation(details.conversationId);
+        if (!conversation) {
+          const members = JamiDaemon.vectMapToJs(this.dring.getConversationMembers(accountId, details.conversationId));
+          members.forEach((member) => (member.contact = account.getContactFromCache(member.uri)));
+          conversation = new Conversation(details.conversationId, accountId, members);
+          account.addConversation(conversation);
+        }
+      }
+    }
+    return details;
+  }
+
+  removeContact(accountId: string, contactId: string) {
+    //bool ban false
+    this.dring.removeContact(accountId, contactId, false);
+  }
+
+  blockContact(accountId: string, contactId: string) {
+    //bool ban true
+    this.dring.removeContact(accountId, contactId, true);
+  }
+
+  getContactDetails(accountId: string, contactId: string) {
+    return JamiDaemon.mapToJs(this.dring.getContactDetails(accountId, contactId));
+  }
+
+  getDefaultModerators(accountId: string) {
+    const account = this.getAccount(accountId);
+    if (!account) {
+      console.log(`Unknown account ${accountId}`);
+      return {};
+    }
+    return JamiDaemon.vectToJs(this.dring.getDefaultModerators(accountId)).map((contactId) =>
+      account.getContactFromCache(contactId)
+    );
+  }
+
+  addDefaultModerator(accountId: string, uri: string) {
+    this.dring.setDefaultModerator(accountId, uri, true);
+  }
+
+  removeDefaultModerator(accountId: string, uri: string) {
+    this.dring.setDefaultModerator(accountId, uri, false);
+  }
+
+  sendMessage(accountId: string, conversationId: string, message: string) {
+    this.dring.sendMessage(accountId, conversationId, message, '');
+  }
+
+  loadMessages(accountId: string, conversationId: string, fromMessage?: string) {
+    const account = this.getAccount(accountId);
+    if (!account) throw new Error('Unknown account');
+    const conversation = account.getConversation(conversationId);
+    if (!conversation) throw new Error(`Unknown conversation ${conversationId}`);
+
+    return new Promise((resolve, reject) => {
+      if (!conversation.requests) conversation.requests = {};
+      const requestId = this.dring.loadConversationMessages(accountId, conversationId, fromMessage || '', 32);
+      conversation.requests[requestId] = { resolve, reject };
+    });
+  }
+
+  boolToStr(bool: boolean) {
+    return bool ? 'true' : 'false';
+  }
+
+  accountDetailsToNative(account: AccountConfig) {
+    const params = new this.dring.StringMap();
+    if (account.managerUri) params.set('Account.managerUri', account.managerUri);
+    if (account.managerUsername) params.set('Account.managerUsername', account.managerUsername);
+    if (account.archivePassword) {
+      params.set('Account.archivePassword', account.archivePassword);
+    } /* else {
+            console.log("archivePassword required")
+            return
+        }*/
+    if (account.alias) params.set('Account.alias', account.alias);
+    if (account.displayName) params.set('Account.displayName', account.displayName);
+    if (account.enable !== undefined) params.set('Account.enable', this.boolToStr(account.enable));
+    if (account.autoAnswer !== undefined) params.set('Account.autoAnswer', this.boolToStr(account.autoAnswer));
+    if (account.autoAnswer !== undefined) params.set('Account.autoAnswer', this.boolToStr(account.autoAnswer));
+    if (account.ringtonePath) params.set('Account.ringtonePath', account.ringtonePath);
+    if (account.ringtoneEnabled !== undefined)
+      params.set('Account.ringtoneEnabled', this.boolToStr(account.ringtoneEnabled));
+    if (account.videoEnabled !== undefined) params.set('Account.videoEnabled', this.boolToStr(account.videoEnabled));
+    if (account.useragent) {
+      params.set('Account.useragent', account.useragent);
+      params.set('Account.hasCustomUserAgent', 'TRUE');
+    } else {
+      params.set('Account.hasCustomUserAgent', 'FALSE');
+    }
+    if (account.audioPortMin) params.set('Account.audioPortMin', account.audioPortMin);
+    if (account.audioPortMax) params.set('Account.audioPortMax', account.audioPortMax);
+    if (account.videoPortMin) params.set('Account.videoPortMin', account.videoPortMin);
+    if (account.videoPortMax) params.set('Account.videoPortMax', account.videoPortMax);
+    if (account.localInterface) params.set('Account.localInterface', account.localInterface);
+    if (account.publishedSameAsLocal !== undefined)
+      params.set('Account.publishedSameAsLocal', this.boolToStr(account.publishedSameAsLocal));
+    if (account.localPort) params.set('Account.localPort', account.localPort);
+    if (account.publishedPort) params.set('Account.publishedPort', account.publishedPort);
+    if (account.rendezVous !== undefined) params.set('Account.rendezVous', this.boolToStr(account.rendezVous));
+    if (account.upnpEnabled !== undefined) params.set('Account.upnpEnabled', this.boolToStr(account.upnpEnabled));
+    return params;
+  }
+  static vectToJs(vect: any) {
+    const len = vect.size();
+    const outputArr = new Array(len);
+    for (let i = 0; i < len; i++) outputArr[i] = vect.get(i);
+    return outputArr;
+  }
+  static mapToJs(m: any): Record<string, any> {
+    const outputObj: Record<string, any> = {};
+    JamiDaemon.vectToJs(m.keys()).forEach((k) => (outputObj[k] = m.get(k)));
+    return outputObj;
+  }
+  static vectMapToJs(vectMap: any) {
+    const len = vectMap.size();
+    const outputArr = new Array(len);
+    for (let i = 0; i < len; i++) outputArr[i] = JamiDaemon.mapToJs(vectMap.get(i));
+    return outputArr;
+  }
+
+  mapToNative(map: any) {
+    const ret = new this.dring.StringMap();
+    for (const [key, value] of Object.entries(map)) ret.set(key, value);
+    return ret;
+  }
+}
+
+export default JamiDaemon;