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.js b/JamiDaemon.ts
similarity index 78%
rename from JamiDaemon.js
rename to JamiDaemon.ts
index 44211d1..d40cc63 100755
--- a/JamiDaemon.js
+++ b/JamiDaemon.ts
@@ -22,21 +22,29 @@
 import { createRequire } from 'module';
 import path from 'path';
 
-import Account from './model/Account.js';
-import Conversation from './model/Conversation.js';
+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 {
-  constructor(onMessage) {
+  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.tempAccounts = {};
     this.dring = require(path.join(process.cwd(), 'jamid.node'));
     this.dring.init({
       AccountsChanged: () => {
         console.log('AccountsChanged');
-        const newAccounts = [];
+        const newAccounts: Account[] = [];
         JamiDaemon.vectToJs(this.dring.getAccountList()).forEach((accountId) => {
           for (const account of this.accounts) {
             if (account.getId() === accountId) {
@@ -47,14 +55,14 @@
           newAccounts.push(
             new Account(
               accountId,
-              JamiDaemon.mapToJs(this.dring.getAccountDetails(accountId)),
-              JamiDaemon.mapToJs(this.dring.getVolatileAccountDetails(accountId))
+              JamiDaemon.mapToJs(this.dring.getAccountDetails(accountId)) as AccountDetails,
+              JamiDaemon.mapToJs(this.dring.getVolatileAccountDetails(accountId)) as VolatileDetails
             )
           );
         });
         this.accounts = newAccounts;
       },
-      AccountDetailsChanged: (accountId, details) => {
+      AccountDetailsChanged: (accountId: string, details: AccountDetails) => {
         console.log(`AccountDetailsChanged ${accountId}`);
         const account = this.getAccount(accountId);
         if (!account) {
@@ -63,7 +71,7 @@
         }
         account.details = details;
       },
-      VolatileDetailsChanged: (accountId, details) => {
+      VolatileDetailsChanged: (accountId: string, details: VolatileDetails) => {
         console.log(`VolatileDetailsChanged ${accountId}`);
         const account = this.getAccount(accountId);
         if (!account) {
@@ -72,7 +80,7 @@
         }
         account.volatileDetails = details;
       },
-      IncomingAccountMessage: (accountId, from, message) => {
+      IncomingAccountMessage: (accountId: string, from: Account, message: Message) => {
         console.log(`Received message: ${accountId} ${from} ${message['text/plain']}`);
         /*
                 if (parser.validate(message["text/plain"]) === true) {
@@ -85,18 +93,19 @@
                     //io.emit('receivedMessage', message["text/plain"])
                 }*/
       },
-      RegistrationStateChanged: (accountId, state, code, detail) => {
+      RegistrationStateChanged: (accountId: string, state: RegistrationState, code: number, detail: string) => {
         console.log('RegistrationStateChanged: ' + accountId + ' ' + state + ' ' + code + ' ' + detail);
-        const account = this.getAccount(accountId);
+        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));
+            account.details = JamiDaemon.mapToJs(this.dring.getAccountDetails(accountId)) as AccountDetails;
             ctx.resolve(accountId);
             delete this.tempAccounts[accountId];
           } else if (state === 'ERROR_AUTH') {
@@ -106,7 +115,7 @@
           }
         }
       },
-      RegisteredNameFound: (accountId, state, address, name) => {
+      RegisteredNameFound: (accountId: string, state: number, address: string, name: string) => {
         console.log(`RegisteredNameFound: ${accountId} ${state} ${address} ${name}`);
         let lookups;
         if (accountId) {
@@ -133,7 +142,7 @@
           index -= 1;
         }
       },
-      NameRegistrationEnded: (accountId, state, name) => {
+      NameRegistrationEnded: (accountId: string, state: number, name: string) => {
         console.log(`NameRegistrationEnded: ${accountId} ${state} ${name}`);
         const account = this.getAccount(accountId);
         if (account) {
@@ -147,7 +156,7 @@
         }
       },
       // Conversations
-      ConversationReady: (accountId, conversationId) => {
+      ConversationReady: (accountId: string, conversationId: string) => {
         console.log(`conversationReady: ${accountId} ${conversationId}`);
         const account = this.getAccount(accountId);
         if (!account) {
@@ -162,7 +171,7 @@
           account.addConversation(conversation);
         }
       },
-      ConversationRemoved: (accountId, conversationId) => {
+      ConversationRemoved: (accountId: string, conversationId: string) => {
         console.log(`conversationRemoved: ${accountId} ${conversationId}`);
         const account = this.getAccount(accountId);
         if (!account) {
@@ -171,7 +180,7 @@
         }
         account.removeConversation(conversationId);
       },
-      ConversationLoaded: (id, accountId, conversationId, messages) => {
+      ConversationLoaded: (id: number, accountId: string, conversationId: string, messages: Message[]) => {
         console.log(`conversationLoaded: ${accountId} ${conversationId}`);
         const account = this.getAccount(accountId);
         if (!account) {
@@ -187,7 +196,7 @@
           }
         }
       },
-      MessageReceived: (accountId, conversationId, message) => {
+      MessageReceived: (accountId: string, conversationId: string, message: Message) => {
         console.log(`messageReceived: ${accountId} ${conversationId}`);
         console.log(message);
         const account = this.getAccount(accountId);
@@ -201,7 +210,7 @@
           if (onMessage) onMessage(account, conversation, message);
         }
       },
-      ConversationRequestReceived: (accountId, conversationId) => {
+      ConversationRequestReceived: (accountId: string, conversationId: string, message: Message) => {
         console.log(`conversationRequestReceived: ${accountId} ${conversationId}`);
         const account = this.getAccount(accountId);
         if (!account) {
@@ -209,7 +218,7 @@
           return;
         }
       },
-      ConversationMemberEvent: (accountId, conversationId) => {
+      ConversationMemberEvent: (accountId: string, conversationId: string, memberUri: string, event: number) => {
         console.log(`conversationMemberEvent: ${accountId} ${conversationId}`);
         const account = this.getAccount(accountId);
         if (!account) {
@@ -217,7 +226,7 @@
           return;
         }
       },
-      OnConversationError: (accountId, conversationId) => {
+      OnConversationError: (accountId: string, conversationId: string, code: number, what: string) => {
         console.log(`onConversationError: ${accountId} ${conversationId}`);
         const account = this.getAccount(accountId);
         if (!account) {
@@ -226,22 +235,22 @@
         }
       },
       // Calls
-      CallStateChanged: (callId, state, code) => {
-        console.log(`CallStateChanged: ${callId} ${state} ${code}`);
+      StateChange: (callId: string, state: string, code: number) => {
+        console.log(`CallStateChange: ${callId} ${state} ${code}`);
       },
-      IncomingCall: (accountId, callId, peerUri) => {
+      IncomingCall: (accountId: string, callId: string, peerUri: string) => {
         console.log(`IncomingCall: ${accountId} ${callId} ${peerUri}`);
       },
-      ConferenceCreated: (confId) => {
+      ConferenceCreated: (confId: string) => {
         console.log(`ConferenceCreated: ${confId}`);
       },
-      ConferenceChanged: (confId) => {
+      ConferenceChanged: (accountId: string, confId: string, state: string) => {
         console.log(`ConferenceChanged: ${confId}`);
       },
-      ConferenceRemoved: (confId) => {
+      ConferenceRemoved: (confId: string) => {
         console.log(`ConferenceRemoved: ${confId}`);
       },
-      OnConferenceInfosUpdated: (confId) => {
+      OnConferenceInfosUpdated: (confId: string) => {
         console.log(`onConferenceInfosUpdated: ${confId}`);
       },
     });
@@ -249,11 +258,11 @@
     JamiDaemon.vectToJs(this.dring.getAccountList()).forEach((accountId) => {
       const account = new Account(
         accountId,
-        JamiDaemon.mapToJs(this.dring.getAccountDetails(accountId)),
-        JamiDaemon.mapToJs(this.dring.getVolatileAccountDetails(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));
+      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));
@@ -264,41 +273,40 @@
             if (!member.uri) return;
             console.log(`lookupAddress ${accountId} ${member.uri}`, member);
             member.contact.setRegisteredName(
-              new Promise((resolve, 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;
-                }
-              )
+              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.setInfos(JamiDaemon.mapToJs(this.dring.conversationInfos(accountId, conversationId)));
+        conversation.infos = JamiDaemon.mapToJs(this.dring.conversationInfos(accountId, conversationId));
         account.addConversation(conversation);
       });
-      account.setDevices();
 
       this.accounts.push(account);
     });
   }
 
-  addAccount(accountConfig) {
+  addAccount(accountConfig: AccountConfig) {
     const params = this.accountDetailsToNative(accountConfig);
     params.set('Account.type', 'RING');
-    return new Promise((resolve, reject) => {
+    return new Promise<string>((resolve, reject) => {
       const accountId = this.dring.addAccount(params);
       this.tempAccounts[accountId] = { resolve, reject };
     });
   }
 
-  getDevices(accountId) {
+  getDevices(accountId: string) {
     return JamiDaemon.mapToJs(this.dring.getKnownRingDevices(accountId));
   }
 
-  getAccount(accountId) {
+  getAccount(accountId: string) {
     for (let i = 0; i < this.accounts.length; i++) {
       const account = this.accounts[i];
       if (account.getId() === accountId) return account;
@@ -308,7 +316,7 @@
   getAccountList() {
     return this.accounts;
   }
-  registerName(accountId, password, name) {
+  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);
@@ -320,28 +328,28 @@
     });
   }
 
-  getConversation(accountId, conversationId) {
+  getConversation(accountId: string, conversationId: string) {
     const account = this.getAccount(accountId);
     if (account) return account.getConversation(conversationId);
     return null;
   }
-  getAccountDetails(accountId) {
+  getAccountDetails(accountId: string) {
     return JamiDaemon.mapToJs(this.dring.getAccountDetails(accountId));
   }
-  setAccountDetails(accountId, details) {
+  setAccountDetails(accountId: string, details: AccountDetails) {
     this.dring.setAccountDetails(accountId, this.mapToNative(details));
   }
   getAudioOutputDeviceList() {
     return JamiDaemon.vectToJs(this.dring.getAudioOutputDeviceList());
   }
-  getVolume(deviceName) {
+  getVolume(deviceName: string): number {
     return this.dring.getVolume(deviceName);
   }
-  setVolume(deviceName, volume) {
+  setVolume(deviceName: string, volume: number) {
     return this.dring.setVolume(deviceName, volume);
   }
 
-  lookupName(accountId, name) {
+  lookupName(accountId: string, name: string) {
     const p = new Promise((resolve, reject) => {
       if (accountId) {
         const account = this.getAccount(accountId);
@@ -358,7 +366,7 @@
     return p;
   }
 
-  lookupAddress(accountId, address) {
+  lookupAddress(accountId: string, address: string) {
     console.log(`lookupAddress ${accountId} ${address}`);
     const p = new Promise((resolve, reject) => {
       if (accountId) {
@@ -380,7 +388,7 @@
     this.dring.fini();
   }
 
-  addContact(accountId, contactId) {
+  addContact(accountId: string, contactId: string) {
     this.dring.addContact(accountId, contactId);
     const details = JamiDaemon.mapToJs(this.dring.getContactDetails(accountId, contactId));
     if (details.conversationId) {
@@ -398,21 +406,21 @@
     return details;
   }
 
-  removeContact(accountId, contactId) {
+  removeContact(accountId: string, contactId: string) {
     //bool ban false
     this.dring.removeContact(accountId, contactId, false);
   }
 
-  blockContact(accountId, contactId) {
+  blockContact(accountId: string, contactId: string) {
     //bool ban true
     this.dring.removeContact(accountId, contactId, true);
   }
 
-  getContactDetails(accountId, contactId) {
+  getContactDetails(accountId: string, contactId: string) {
     return JamiDaemon.mapToJs(this.dring.getContactDetails(accountId, contactId));
   }
 
-  getDefaultModerators(accountId) {
+  getDefaultModerators(accountId: string) {
     const account = this.getAccount(accountId);
     if (!account) {
       console.log(`Unknown account ${accountId}`);
@@ -423,19 +431,19 @@
     );
   }
 
-  addDefaultModerator(accountId, uri) {
+  addDefaultModerator(accountId: string, uri: string) {
     this.dring.setDefaultModerator(accountId, uri, true);
   }
 
-  removeDefaultModerator(accountId, uri) {
+  removeDefaultModerator(accountId: string, uri: string) {
     this.dring.setDefaultModerator(accountId, uri, false);
   }
 
-  sendMessage(accountId, conversationId, message) {
+  sendMessage(accountId: string, conversationId: string, message: string) {
     this.dring.sendMessage(accountId, conversationId, message, '');
   }
 
-  loadMessages(accountId, conversationId, fromMessage) {
+  loadMessages(accountId: string, conversationId: string, fromMessage?: string) {
     const account = this.getAccount(accountId);
     if (!account) throw new Error('Unknown account');
     const conversation = account.getConversation(conversationId);
@@ -448,11 +456,11 @@
     });
   }
 
-  boolToStr(bool) {
+  boolToStr(bool: boolean) {
     return bool ? 'true' : 'false';
   }
 
-  accountDetailsToNative(account) {
+  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);
@@ -490,25 +498,25 @@
     if (account.upnpEnabled !== undefined) params.set('Account.upnpEnabled', this.boolToStr(account.upnpEnabled));
     return params;
   }
-  static vectToJs(vect) {
+  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) {
-    const outputObj = {};
+  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) {
+  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) {
+  mapToNative(map: any) {
     const ret = new this.dring.StringMap();
     for (const [key, value] of Object.entries(map)) ret.set(key, value);
     return ret;
diff --git a/app.ts b/app.ts
index c8a5331..42e81a8 100644
--- a/app.ts
+++ b/app.ts
@@ -10,13 +10,14 @@
 import { promises as fs } from 'fs';
 import http from 'http';
 import passport from 'passport';
-import { Strategy as LocalStrategy } from 'passport-local';
+import { IVerifyOptions, Strategy as LocalStrategy } from 'passport-local';
 import path from 'path';
 import { Server, Socket } from 'socket.io';
 import { ExtendedError } from 'socket.io/dist/namespace';
 
-import JamiDaemon from './JamiDaemon.js';
+import JamiDaemon from './JamiDaemon';
 import Account from './model/Account';
+import { Session } from './model/util';
 //import { createRequire } from 'module';
 //const require = createRequire(import.meta.url);
 //const redis = require('redis-url').connect()
@@ -31,7 +32,15 @@
 //const sessionStore = new RedisStore({ client: redis })
 const sessionStore = new session.MemoryStore();
 
+interface User {
+  id: string;
+  config: UserConfig;
+  username: string;
+  accountFilter?: (account: any) => boolean;
+}
+
 interface UserConfig {
+  accountId?: string;
   accounts: string;
   password?: string;
   username?: string;
@@ -40,7 +49,7 @@
 
 interface AppConfig {
   users: Record<string, UserConfig>;
-  authMethods: any[];
+  authMethods: unknown[];
 }
 
 const loadConfig = async (filePath: string): Promise<AppConfig> => {
@@ -78,8 +87,14 @@
 web socket call
 
 */
-const tempAccounts: Record<string, any> = {};
-const connectedUsers: Record<string, any> = {};
+const tempAccounts: Record<
+  string,
+  {
+    newUser: Express.User;
+    done: (error: any, user?: any, options?: IVerifyOptions) => void;
+  }
+> = {};
+const connectedUsers: Record<string, UserConfig> = {};
 
 const createServer = async (appConfig: AppConfig) => {
   const app = express();
@@ -160,7 +175,7 @@
     return 'admin' in appConfig.users;
   };
 
-  const accountFilter = (filter: string | any[]) => {
+  const accountFilter = (filter: string | string[]) => {
     if (typeof filter === 'string') {
       if (filter === '*') return undefined;
       else return (account: Account) => account.getId() === filter;
@@ -181,6 +196,7 @@
   };
 
   passport.serializeUser((user: any, done) => {
+    user = user as User;
     connectedUsers[user.id] = user.config;
     console.log('=============================SerializeUser called ' + user.id);
     console.log(user);
@@ -237,7 +253,8 @@
     res.status(401).end();
   };
   const securedRedirect = (req: Request, res: Response, next: NextFunction) => {
-    if (req.user && (req.user as any)?.accountId) {
+    const user = req.user as UserConfig | undefined;
+    if (user?.accountId) {
       return next();
     }
     (req.session as any).returnTo = req.originalUrl;
@@ -264,12 +281,12 @@
     res.json({ loggedin: true });
   });
   app.post('/auth/local', passport.authenticate('local'), (req, res) => {
-    res.json({ loggedin: true, user: (req.user as any)?.id });
+    res.json({ loggedin: true, user: (req.user as User | undefined)?.id });
   });
 
   const getState = (req: Request) => {
     if (req.user) {
-      const user = (req.user || {}) as UserConfig;
+      const user = req.user as UserConfig;
       return { loggedin: true, username: user.username, type: user.type };
     } else if (isSetupComplete()) {
       return {};
@@ -322,7 +339,7 @@
   });
   io.on('connect', (socket) => {
     console.log(`new connection ${socket.id}`);
-    const session = (socket.request as any).session;
+    const session: Session = (socket.request as any).session;
     console.log(`saving sid ${socket.id} in session ${session.id}`);
     session.socketId = socket.id;
     session.save();
@@ -333,15 +350,21 @@
       if (session.conversation) {
         console.log(`disconnect from old conversation ${session.conversation.conversationId}`);
         const conversation = jami.getConversation(session.conversation.accountId, session.conversation.conversationId);
-        delete conversation.listeners[socket.id];
+        if (conversation) {
+          delete conversation.listeners[socket.id];
+        }
       }
       session.conversation = { accountId: data.accountId, conversationId: data.conversationId };
       const conversation = jami.getConversation(data.accountId, data.conversationId);
-      if (!conversation.listeners) conversation.listeners = {};
-      conversation.listeners[socket.id] = {
-        socket,
-        session,
-      };
+      if (conversation) {
+        if (!conversation.listeners) {
+          conversation.listeners = {};
+        }
+        conversation.listeners[socket.id] = {
+          socket,
+          session,
+        };
+      }
       session.save();
     });
   });
diff --git a/client/tsconfig.json b/client/tsconfig.json
index 9e31f8f..9af3397 100644
--- a/client/tsconfig.json
+++ b/client/tsconfig.json
@@ -5,5 +5,6 @@
     "jsx": "react-jsx",
     /* Specify an output folder for all emitted files. */
     "outDir": "./dist/"
-  }
+  },
+  "include": ["src/"]
 }
diff --git a/model/Account.js b/model/Account.js
deleted file mode 100644
index aa1e957..0000000
--- a/model/Account.js
+++ /dev/null
@@ -1,140 +0,0 @@
-import Contact from './Contact.js';
-
-class Account {
-  constructor(id, details, volatileDetails) {
-    this.id = id;
-    this.details = details;
-    this.volatileDetails = volatileDetails;
-    this.contactCache = {};
-    this.contacts = {};
-    this.conversations = {};
-    this.lookups = [];
-    this.devices = {};
-  }
-
-  static from(object) {
-    const account = new Account(object.id, object.details, object.volatileDetails);
-    if (object.defaultModerators) account.defaultModerators = object.defaultModerators.map((m) => Contact.from(m));
-    return account;
-  }
-
-  update(data) {
-    this.details = data.details;
-    this.volatileDetails = data.volatileDetails;
-  }
-
-  async getObject() {
-    const hasModerators = this.defaultModerators && this.defaultModerators.length;
-    return {
-      id: this.id,
-      details: this.details,
-      defaultModerators: hasModerators
-        ? await Promise.all(this.defaultModerators.map(async (c) => await c.getObject()))
-        : undefined,
-      volatileDetails: this.volatileDetails,
-    };
-  }
-
-  getId() {
-    return this.id;
-  }
-
-  getType() {
-    return this.details['Account.type'];
-  }
-
-  getUri() {
-    return this.details['Account.username'];
-  }
-
-  getRegisteredName() {
-    return this.volatileDetails['Account.registeredName'];
-  }
-
-  isRendezVous() {
-    return this.details['Account.rendezVous'] === Account.BOOL_TRUE;
-  }
-
-  isPublicIn() {
-    return this.details['DHT.PublicInCalls'] === Account.BOOL_TRUE;
-  }
-
-  setDetail(detail, value) {
-    this.details[detail] = value;
-  }
-
-  updateDetails(details) {
-    return Object.assign(this.details, details);
-  }
-
-  getDetails() {
-    return this.details;
-  }
-
-  getSummary() {
-    return this.getObject();
-  }
-
-  getDisplayName() {
-    return this.details['Account.displayName'] || this.getDisplayUri();
-  }
-
-  getDisplayUri() {
-    return this.getRegisteredName() || this.getUri();
-  }
-
-  getDisplayNameNoFallback() {
-    return this.details['Account.displayName'] || this.getRegisteredName();
-  }
-
-  getConversationIds() {
-    return Object.keys(this.conversations);
-  }
-  getConversations() {
-    return this.conversations;
-  }
-
-  getConversation(conversationId) {
-    return this.conversations[conversationId];
-  }
-
-  addConversation(conversation) {
-    this.conversations[conversation.getId()] = conversation;
-  }
-
-  removeConversation(conversationId) {
-    delete this.conversations[conversationId];
-  }
-
-  getContactFromCache(uri) {
-    let contact = this.contactCache[uri];
-    if (!contact) {
-      contact = new Contact(uri);
-      this.contactCache[uri] = contact;
-    }
-    return contact;
-  }
-
-  getContacts() {
-    return this.contacts;
-  }
-
-  getDefaultModerators() {
-    return this.defaultModerators;
-  }
-
-  setDevices(devices) {
-    this.devices = { ...devices };
-  }
-  getDevices() {
-    return this.devices;
-  }
-}
-
-Account.TYPE_JAMI = 'RING';
-Account.TYPE_SIP = 'SIP';
-
-Account.BOOL_TRUE = 'true';
-Account.BOOL_FALSE = 'false';
-
-export default Account;
diff --git a/model/Account.ts b/model/Account.ts
new file mode 100644
index 0000000..ce529c0
--- /dev/null
+++ b/model/Account.ts
@@ -0,0 +1,216 @@
+import AccountDetails, { VolatileDetails } from './AccountDetails';
+import Contact from './Contact';
+import Conversation from './Conversation';
+import { Lookup, PromiseExecutor } from './util';
+
+type Devices = Record<string, string>;
+
+export type RegistrationState =
+  | 'UNREGISTERED'
+  | 'TRYING'
+  | 'REGISTERED'
+  | 'ERROR_GENERIC'
+  | 'ERROR_AUTH'
+  | 'ERROR_NETWORK'
+  | 'ERROR_HOST'
+  | 'ERROR_SERVICE_UNAVAILABLE'
+  | 'ERROR_NEED_MIGRATION'
+  | 'INITIALIZING';
+
+interface AccountRegisteringName extends PromiseExecutor<number> {
+  name: string;
+}
+
+class Account {
+  private readonly id: string;
+  private _details: AccountDetails;
+  private _volatileDetails: VolatileDetails;
+  private contactCache: Record<string, Contact> = {};
+  private _contacts: Contact[] = [];
+  private conversations: Record<string, Conversation> = {};
+  private defaultModerators: Contact[] = [];
+  private _lookups: Lookup[] = [];
+  private devices: Devices = {};
+  private _registrationState: RegistrationState | undefined = undefined;
+  private _registeringName: AccountRegisteringName | undefined = undefined;
+
+  static TYPE_JAMI: string;
+  static TYPE_SIP: string;
+  static BOOL_TRUE: string;
+  static BOOL_FALSE: string;
+
+  constructor(id: string, details: AccountDetails, volatileDetails: VolatileDetails) {
+    this.id = id;
+    this._details = details;
+    this._volatileDetails = volatileDetails;
+  }
+
+  static from(object: Account) {
+    const account = new Account(object.id, object._details, object._volatileDetails);
+    if (object.defaultModerators) account.defaultModerators = object.defaultModerators.map((m) => Contact.from(m));
+    return account;
+  }
+
+  update(data: Account) {
+    this._details = data._details;
+    this._volatileDetails = data._volatileDetails;
+  }
+
+  async getObject() {
+    const hasModerators = this.defaultModerators && this.defaultModerators.length;
+    return {
+      id: this.id,
+      details: this._details,
+      defaultModerators: hasModerators
+        ? await Promise.all(this.defaultModerators.map(async (c) => await c.getObject()))
+        : undefined,
+      volatileDetails: this._volatileDetails,
+    };
+  }
+
+  getId() {
+    return this.id;
+  }
+
+  getType() {
+    return this._details['Account.type'];
+  }
+
+  getUri() {
+    return this._details['Account.username'];
+  }
+
+  getRegisteredName() {
+    return this._volatileDetails['Account.registeredName'];
+  }
+
+  isRendezVous() {
+    return this._details['Account.rendezVous'] === Account.BOOL_TRUE;
+  }
+
+  isPublicIn() {
+    return this._details['DHT.PublicInCalls'] === Account.BOOL_TRUE;
+  }
+
+  setDetail(detail: keyof AccountDetails, value: string) {
+    this._details[detail] = value;
+  }
+
+  updateDetails(details: Partial<AccountDetails>) {
+    return Object.assign(this._details, details);
+  }
+
+  getDetails() {
+    return this._details;
+  }
+
+  getSummary() {
+    return this.getObject();
+  }
+
+  getDisplayName() {
+    return this._details['Account.displayName'] || this.getDisplayUri();
+  }
+
+  getDisplayUri() {
+    return this.getRegisteredName() || this.getUri();
+  }
+
+  getDisplayNameNoFallback() {
+    return this._details['Account.displayName'] || this.getRegisteredName();
+  }
+
+  getConversationIds() {
+    return Object.keys(this.conversations);
+  }
+
+  getConversations() {
+    return this.conversations;
+  }
+
+  getConversation(conversationId: string) {
+    return this.conversations[conversationId];
+  }
+
+  addConversation(conversation: Conversation) {
+    const conversationId = conversation.getId();
+    if (conversationId != null) {
+      this.conversations[conversationId] = conversation;
+    } else {
+      throw new Error('Conversation ID cannot be undefined');
+    }
+  }
+
+  removeConversation(conversationId: string) {
+    delete this.conversations[conversationId];
+  }
+
+  getContactFromCache(uri: string) {
+    let contact = this.contactCache[uri];
+    if (!contact) {
+      contact = new Contact(uri);
+      this.contactCache[uri] = contact;
+    }
+    return contact;
+  }
+
+  getContacts() {
+    return this._contacts;
+  }
+
+  set contacts(contacts: Contact[]) {
+    this._contacts = contacts;
+  }
+
+  getDefaultModerators() {
+    return this.defaultModerators;
+  }
+
+  set details(value: AccountDetails) {
+    this._details = value;
+  }
+
+  set volatileDetails(value: VolatileDetails) {
+    this._volatileDetails = value;
+  }
+
+  get lookups(): Lookup[] {
+    return this._lookups;
+  }
+
+  set lookups(lookups: Lookup[]) {
+    this._lookups = lookups;
+  }
+
+  setDevices(devices: Devices) {
+    this.devices = { ...devices };
+  }
+
+  getDevices() {
+    return this.devices;
+  }
+
+  get registrationState(): RegistrationState | undefined {
+    return this._registrationState;
+  }
+
+  set registrationState(registrationState: RegistrationState | undefined) {
+    this._registrationState = registrationState;
+  }
+
+  get registeringName(): AccountRegisteringName | undefined {
+    return this._registeringName;
+  }
+
+  set registeringName(registeringName: AccountRegisteringName | undefined) {
+    this._registeringName = registeringName;
+  }
+}
+
+Account.TYPE_JAMI = 'RING';
+Account.TYPE_SIP = 'SIP';
+
+Account.BOOL_TRUE = 'true';
+Account.BOOL_FALSE = 'false';
+
+export default Account;
diff --git a/model/AccountDetails.ts b/model/AccountDetails.ts
new file mode 100644
index 0000000..e9514aa
--- /dev/null
+++ b/model/AccountDetails.ts
@@ -0,0 +1,169 @@
+/**
+ * Account parameters
+ *
+ * See `jami-daemon/src/account_schema.h`
+ */
+export default interface AccountDetails {
+  // Common account parameters
+  'Account.type': string;
+  'Account.alias': string;
+  'Account.displayName': string;
+  'Account.mailbox': string;
+  'Account.enable': string;
+  'Account.autoAnswer': string;
+  'Account.sendReadReceipt': string;
+  'Account.rendezVous': string;
+  'Account.registrationExpire': string;
+  'Account.dtmfType': string;
+  'Account.ringtonePath': string;
+  'Account.ringtoneEnabled': string;
+  'Account.videoEnabled': string;
+  'Account.keepAliveEnabled': string;
+  'Account.presenceEnabled': string;
+  'Account.presencePublishSupported': string;
+  'Account.presenceSubscribeSupported': string;
+  'Account.presenceStatus': string;
+  'Account.presenceNote': string;
+
+  'Account.hostname': string;
+  'Account.username': string;
+  'Account.routeset': string;
+  'Account.allowIPAutoRewrite': string;
+  'Account.password': string;
+  'Account.realm': string;
+  'Account.useragent': string;
+  'Account.hasCustomUserAgent': string;
+  'Account.audioPortMin': string;
+  'Account.audioPortMax': string;
+  'Account.videoPortMin': string;
+  'Account.videoPortMax': string;
+
+  'Account.bindAddress': string;
+  'Account.localInterface': string;
+  'Account.publishedSameAsLocal': string;
+  'Account.localPort': string;
+  'Account.publishedPort': string;
+  'Account.publishedAddress': string;
+  'Account.upnpEnabled': string;
+  'Account.defaultModerators': string;
+  'Account.localModeratorsEnabled': string;
+  'Account.allModeratorEnabled': string;
+
+  // SIP specific parameters
+  'STUN.server': string;
+  'STUN.enable': string;
+  'TURN.server': string;
+  'TURN.enable': string;
+  'TURN.username': string;
+  'TURN.password': string;
+  'TURN.realm': string;
+
+  // SRTP specific parameters
+  'SRTP.enable': string;
+  'SRTP.keyExchange': string;
+  'SRTP.rtpFallback': string;
+
+  'TLS.listenerPort': string;
+  'TLS.enable': string;
+  'TLS.certificateListFile': string;
+  'TLS.certificateFile': string;
+  'TLS.privateKeyFile': string;
+  'TLS.password': string;
+  'TLS.method': string;
+  'TLS.ciphers': string;
+  'TLS.serverName': string;
+  'TLS.verifyServer': string;
+  'TLS.verifyClient': string;
+  'TLS.requireClientCertificate': string;
+  'TLS.negotiationTimeoutSec': string;
+
+  // DHT specific parameters
+  'DHT.port': string;
+  'DHT.PublicInCalls': string;
+
+  // Volatile parameters
+  'Account.registrationStatus': string;
+  'Account.registrationCode': string;
+  'Account.registrationDescription': string;
+  'Transport.statusCode': string;
+  'Transport.statusDescription': string;
+}
+
+/**
+ * Volatile properties
+ *
+ * See `jami-daemon/src/jami/account_const.h`
+ */
+export interface VolatileDetails {
+  'Account.active': string;
+  'Account.deviceAnnounced': string;
+  'Account.registeredName': string;
+}
+
+/**
+ * See `ConfProperties` in `jami-daemon/src/jami/account_const.h
+ */
+export interface AccountConfig {
+  id?: string;
+  type?: string;
+  alias?: string;
+  displayName?: string;
+  enable?: boolean;
+  mailbox?: string;
+  dtmfType?: string;
+  autoAnswer?: boolean;
+  sendReadReceipt?: string;
+  rendezVous?: boolean;
+  activeCallLimit?: string;
+  hostname?: string;
+  username?: string;
+  bindAddress?: string;
+  routeset?: string;
+  password?: string;
+  realm?: string;
+  localInterface?: string;
+  publishedSameAsLocal?: boolean;
+  localPort?: string;
+  publishedPort?: string;
+  publishedAddress?: string;
+  useragent?: string;
+  upnpEnabled?: boolean;
+  hasCustomUserAgent?: string;
+  allowCertFromHistory?: string;
+  allowCertFromContact?: string;
+  allowCertFromTrusted?: string;
+  archivePassword?: string;
+  archiveHasPassword?: string;
+  archivePath?: string;
+  archivePIN?: string;
+  deviceID?: string;
+  deviceName?: string;
+  proxyEnabled?: boolean;
+  proxyServer?: string;
+  proxyPushToken?: string;
+  keepAliveEnabled?: boolean;
+  peerDiscovery?: string;
+  accountDiscovery?: string;
+  accountPublish?: string;
+  managerUri?: string;
+  managerUsername?: string;
+  bootstrapListUrl?: string;
+  dhtProxyListUrl?: string;
+  defaultModerators?: string;
+  localModeratorsEnabled?: boolean;
+  allModeratorsEnabled?: boolean;
+  allowIPAutoRewrite?: string;
+
+  // Audio
+  audioPortMax?: string;
+  audioPortMin?: string;
+
+  // Video
+  videoEnabled?: boolean;
+  videoPortMax?: boolean;
+  videoPortMin?: string;
+
+  // Ringtone
+  ringtonePath?: string;
+  ringtoneEnabled?: boolean;
+}
diff --git a/model/Contact.js b/model/Contact.ts
similarity index 73%
rename from model/Contact.js
rename to model/Contact.ts
index cc8e386..01d4a02 100644
--- a/model/Contact.js
+++ b/model/Contact.ts
@@ -1,11 +1,13 @@
 class Contact {
-  constructor(uri) {
+  private readonly uri: string;
+  private displayName: string | undefined = undefined;
+  private registeredName: string | undefined = undefined;
+
+  constructor(uri: string) {
     this.uri = uri;
-    this.displayName = undefined;
-    this.registeredName = undefined;
   }
 
-  static from(object) {
+  static from(object: Contact) {
     const contact = new Contact(object.uri);
     if (object.registeredName) contact.setRegisteredName(object.registeredName);
     return contact;
@@ -19,7 +21,7 @@
     return this.registeredName;
   }
 
-  setRegisteredName(name) {
+  setRegisteredName(name: string | undefined) {
     this.registeredName = name;
   }
 
diff --git a/model/Conversation.js b/model/Conversation.js
deleted file mode 100644
index 174ba03..0000000
--- a/model/Conversation.js
+++ /dev/null
@@ -1,104 +0,0 @@
-import Contact from './Contact.js';
-
-class Conversation {
-  constructor(id, accountId, members) {
-    this.id = id;
-    this.accountId = accountId;
-    this.members = members || [];
-    this.messages = [];
-    this.infos = {};
-  }
-
-  static from(accountId, object) {
-    const conversation = new Conversation(
-      object.id,
-      accountId,
-      object.members.map((member) => {
-        member.contact = Contact.from(member.contact);
-        return member;
-      })
-    );
-    conversation.messages = object.messages;
-    return conversation;
-  }
-  static fromSingleContact(accountId, contact) {
-    return new Conversation(undefined, accountId, [{ contact }]);
-  }
-
-  getId() {
-    return this.id;
-  }
-
-  getAccountId() {
-    return this.accountId;
-  }
-
-  getDisplayName() {
-    if (this.members.length !== 0) {
-      return this.members[0].contact.getDisplayName();
-    }
-    return this.getDisplayUri();
-  }
-
-  getDisplayNameNoFallback() {
-    if (this.members.length !== 0) {
-      return this.members[0].contact.getDisplayNameNoFallback();
-    }
-  }
-
-  async getObject(params) {
-    const members = params.memberFilter ? this.members.filter(params.memberFilter) : this.members;
-    return {
-      id: this.id,
-      messages: this.messages,
-      members: await Promise.all(
-        members.map(async (member) => {
-          const copiedMember = { role: member.role }; //Object.assign({}, member);
-          copiedMember.contact = await member.contact.getObject();
-          return copiedMember;
-        })
-      ),
-    };
-  }
-
-  getSummary() {
-    return this.getObject();
-  }
-
-  getDisplayUri() {
-    return this.getId() || this.getFirstMember().contact.getUri();
-  }
-
-  getFirstMember() {
-    return this.members[0];
-  }
-
-  getMembers() {
-    return this.members;
-  }
-
-  addMessage(message) {
-    if (this.messages.length === 0) this.messages.push(message);
-    else if (message.id === this.messages[this.messages.length - 1].linearizedParent) {
-      this.messages.push(message);
-    } else if (message.linearizedParent === this.messages[0].id) {
-      this.messages.unshift(message);
-    } else {
-      console.log("Can't insert message " + message.id);
-    }
-  }
-
-  addLoadedMessages(messages) {
-    messages.forEach((message) => this.addMessage(message));
-  }
-
-  getMessages() {
-    return this.messages;
-  }
-
-  setInfos(infos) {
-    this.infos = infos;
-  }
-}
-
-export default Conversation;
diff --git a/model/Conversation.ts b/model/Conversation.ts
new file mode 100644
index 0000000..3a75567
--- /dev/null
+++ b/model/Conversation.ts
@@ -0,0 +1,151 @@
+import { Socket } from 'socket.io';
+
+import Contact from './Contact';
+import { PromiseExecutor, Session } from './util';
+
+export interface ConversationMember {
+  contact: Contact;
+  role?: 'admin' | 'member' | 'invited' | 'banned' | 'left';
+}
+
+type ConversationInfos = Record<string, unknown>;
+
+export type Message = Record<string, string>;
+type ConversationRequest = PromiseExecutor<Message[]>;
+
+type ConversationListeners = Record<
+  string,
+  {
+    socket: Socket;
+    session: Session;
+  }
+>;
+
+class Conversation {
+  private readonly id: string | undefined;
+  private readonly accountId: string;
+  private readonly members: ConversationMember[];
+  private messages: Message[] = [];
+  private _infos: ConversationInfos = {};
+  private _requests: Record<string, ConversationRequest> = {};
+  private _listeners: ConversationListeners = {};
+
+  constructor(id: string | undefined, accountId: string, members?: ConversationMember[]) {
+    this.id = id;
+    this.accountId = accountId;
+    this.members = members || [];
+  }
+
+  static from(accountId: string, object: Conversation) {
+    const conversation = new Conversation(
+      object.id,
+      accountId,
+      object.members.map((member) => {
+        member.contact = Contact.from(member.contact);
+        return member;
+      })
+    );
+    conversation.messages = object.messages;
+    return conversation;
+  }
+  static fromSingleContact(accountId: string, contact: Contact) {
+    return new Conversation(undefined, accountId, [{ contact }]);
+  }
+
+  getId() {
+    return this.id;
+  }
+
+  getAccountId() {
+    return this.accountId;
+  }
+
+  getDisplayName() {
+    if (this.members.length !== 0) {
+      return this.members[0].contact.getDisplayName();
+    }
+    return this.getDisplayUri();
+  }
+
+  getDisplayNameNoFallback() {
+    if (this.members.length !== 0) {
+      return this.members[0].contact.getDisplayNameNoFallback();
+    }
+  }
+
+  async getObject(params?: {
+    memberFilter: (value: ConversationMember, index: number, array: ConversationMember[]) => boolean;
+  }) {
+    const members = params?.memberFilter ? this.members.filter(params.memberFilter) : this.members;
+    return {
+      id: this.id,
+      messages: this.messages,
+      members: await Promise.all(
+        members.map(async (member) => {
+          //Object.assign({}, member);
+          return {
+            role: member.role,
+            contact: await member.contact.getObject(),
+          };
+        })
+      ),
+    };
+  }
+
+  getSummary() {
+    return this.getObject();
+  }
+
+  getDisplayUri() {
+    return this.getId() || this.getFirstMember().contact.getUri();
+  }
+
+  getFirstMember() {
+    return this.members[0];
+  }
+
+  getMembers() {
+    return this.members;
+  }
+
+  addMessage(message: Message) {
+    if (this.messages.length === 0) this.messages.push(message);
+    else if (message.id === this.messages[this.messages.length - 1].linearizedParent) {
+      this.messages.push(message);
+    } else if (message.linearizedParent === this.messages[0].id) {
+      this.messages.unshift(message);
+    } else {
+      console.log("Can't insert message " + message.id);
+    }
+  }
+
+  addLoadedMessages(messages: Message[]) {
+    messages.forEach((message) => this.addMessage(message));
+  }
+
+  getMessages() {
+    return this.messages;
+  }
+
+  set infos(infos: ConversationInfos) {
+    this._infos = infos;
+  }
+
+  get requests(): Record<string, ConversationRequest> {
+    return this._requests;
+  }
+
+  set requests(value: Record<string, ConversationRequest>) {
+    this._requests = value;
+  }
+
+  get listeners(): ConversationListeners {
+    return this._listeners;
+  }
+
+  set listeners(listeners: ConversationListeners) {
+    this._listeners = listeners;
+  }
+}
+
+export default Conversation;
diff --git a/model/util.ts b/model/util.ts
new file mode 100644
index 0000000..630b358
--- /dev/null
+++ b/model/util.ts
@@ -0,0 +1,22 @@
+import { Session as ISession } from 'express-session';
+
+export interface PromiseExecutor<T> {
+  resolve: (value: T) => void;
+  reject: (reason?: any) => void;
+}
+
+export interface LookupResolveValue {
+  address: string;
+  name: string;
+  state: number;
+}
+
+export interface Lookup extends PromiseExecutor<LookupResolveValue> {
+  name?: string;
+  address?: string;
+}
+
+export interface Session extends ISession {
+  socketId: string;
+  conversation: any;
+}