Create contacts API routes

Change-Id: I35a51776c794d23fed3661db9ace6d54842bf83a
diff --git a/server/src/app.ts b/server/src/app.ts
index 161711c..634256a 100644
--- a/server/src/app.ts
+++ b/server/src/app.ts
@@ -23,6 +23,7 @@
 
 import { accountRouter } from './routers/account-router.js';
 import { authRouter } from './routers/auth-router.js';
+import { contactsRouter } from './routers/contacts-router.js';
 import { nameserverRouter } from './routers/nameserver-router.js';
 
 @Service()
@@ -37,6 +38,7 @@
     // Setup routing
     app.use('/auth', authRouter);
     app.use('/account', accountRouter);
+    app.use('/contacts', contactsRouter);
     app.use('/ns', nameserverRouter);
 
     // Setup 404 error handling
diff --git a/server/src/jamid/jami-swig.ts b/server/src/jamid/jami-swig.ts
index b072328..1ada8e9 100644
--- a/server/src/jamid/jami-swig.ts
+++ b/server/src/jamid/jami-swig.ts
@@ -16,7 +16,7 @@
  * <https://www.gnu.org/licenses/>.
  */
 import { Constructable } from '../interfaces.js';
-import { itMap, itRange, itToArr, itToMap, itToRecord } from './utils.js';
+import { itMap, itRange, itToArr, itToRecord } from './utils.js';
 
 enum Bool {
   False = 'false',
@@ -47,11 +47,9 @@
 export type VectMap = SwigVec<StringMap>;
 // export type Blob = SwigVec<number>;
 
-// TODO: Consider always converting to Record rather than Map as conversion to interfaces is easier
 export const stringVectToArray = (sv: StringVect) => itToArr(swigVecToIt(sv));
 export const stringMapToRecord = (sm: StringMap) => itToRecord(swigMapToIt(sm));
-export const stringMapToMap = (sm: StringMap) => itToMap(swigMapToIt(sm));
-// export const vectMapToArrayMap = (vm: VectMap) => itToArr(itMap(swigVecToIt(vm), stringMapToMap));
+export const vectMapToRecordArray = (vm: VectMap) => itToArr(itMap(swigVecToIt(vm), stringMapToRecord));
 
 /**
  * Non-exhaustive list of properties for JamiSwig.
diff --git a/server/src/jamid/jamid.ts b/server/src/jamid/jamid.ts
index 8676875..27ff0e3 100644
--- a/server/src/jamid/jamid.ts
+++ b/server/src/jamid/jamid.ts
@@ -34,10 +34,11 @@
   RegistrationStateChanged,
   VolatileDetailsChanged,
 } from './jami-signal-interfaces.js';
-import { JamiSwig, StringMap, stringMapToRecord, stringVectToArray } from './jami-swig.js';
+import { JamiSwig, StringMap, stringMapToRecord, stringVectToArray, vectMapToRecordArray } from './jami-swig.js';
 import { require } from './utils.js';
 
 // TODO: Mechanism to map account IDs to a list of WebSockets
+// TODO: Convert Records to interfaces and replace them in common/ (e.g. Contact)
 
 @Service()
 export class Jamid {
@@ -230,6 +231,26 @@
     return stringMapToRecord(this.jamiSwig.getKnownRingDevices(accountId));
   }
 
+  addContact(accountId: string, contactId: string) {
+    this.jamiSwig.addContact(accountId, contactId);
+  }
+
+  removeContact(accountId: string, contactId: string) {
+    this.jamiSwig.removeContact(accountId, contactId, false);
+  }
+
+  blockContact(accountId: string, contactId: string) {
+    this.jamiSwig.removeContact(accountId, contactId, true);
+  }
+
+  getContacts(accountId: string): Record<string, string>[] {
+    return vectMapToRecordArray(this.jamiSwig.getContacts(accountId));
+  }
+
+  getContactDetails(accountId: string, contactId: string): Record<string, string> {
+    return stringMapToRecord(this.jamiSwig.getContactDetails(accountId, contactId));
+  }
+
   getDefaultModerators(accountId: string): string[] {
     return stringVectToArray(this.jamiSwig.getDefaultModerators(accountId));
   }
diff --git a/server/src/jamid/utils.ts b/server/src/jamid/utils.ts
index 0952fec..f692578 100644
--- a/server/src/jamid/utils.ts
+++ b/server/src/jamid/utils.ts
@@ -43,14 +43,6 @@
 
 export const itToArr = <T>(it: Iterable<T>) => Array.from(it);
 
-export const itToMap = <T, U>(it: Iterable<[T, U]>) => {
-  const m = new Map<T, U>();
-  for (const [k, v] of it) {
-    m.set(k, v);
-  }
-  return m;
-};
-
 export const itToRecord = <T>(it: Iterable<[string, T]>) => {
   const r: Record<string, T> = {};
   for (const [k, v] of it) {
diff --git a/server/src/routers/account-router.ts b/server/src/routers/account-router.ts
index 7344e85..18b1554 100644
--- a/server/src/routers/account-router.ts
+++ b/server/src/routers/account-router.ts
@@ -51,12 +51,12 @@
   });
 });
 
-accountRouter.post('/', (req, res) => {
+accountRouter.patch('/', (req, res) => {
   const accountId = res.locals.accountId;
   const currentAccountDetails = jamid.getAccountDetails(accountId);
   const newAccountDetails: AccountDetails = { ...currentAccountDetails, ...req.body };
   jamid.setAccountDetails(res.locals.accountId, newAccountDetails);
-  res.end();
+  res.sendStatus(HttpStatusCode.NoContent);
 });
 
 accountRouter.post('/send-account-message', (req: Request<ParamsDictionary, any, SendAccountTextMessageApi>, res) => {
diff --git a/server/src/routers/contacts-router.ts b/server/src/routers/contacts-router.ts
new file mode 100644
index 0000000..22f87da
--- /dev/null
+++ b/server/src/routers/contacts-router.ts
@@ -0,0 +1,49 @@
+/*
+ * 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 { Router } from 'express';
+import { HttpStatusCode } from 'jami-web-common';
+import { Container } from 'typedi';
+
+import { Jamid } from '../jamid/jamid.js';
+import { authenticateToken } from '../middleware/auth.js';
+
+const jamid = Container.get(Jamid);
+
+export const contactsRouter = Router();
+
+contactsRouter.use(authenticateToken);
+
+contactsRouter.get('/', (_req, res) => {
+  const contacts = jamid.getContacts(res.locals.accountId);
+  res.send(contacts);
+});
+
+contactsRouter.get('/:contactId', (req, res) => {
+  const contactDetails = jamid.getContactDetails(res.locals.accountId, req.params.contactId);
+  res.send(contactDetails);
+});
+
+contactsRouter.delete('/:contactId', (req, res) => {
+  jamid.removeContact(res.locals.accountId, req.params.contactId);
+  res.sendStatus(HttpStatusCode.NoContent);
+});
+
+contactsRouter.post('/:contactId/block', (req, res) => {
+  jamid.removeContact(res.locals.accountId, req.params.contactId);
+  res.sendStatus(HttpStatusCode.NoContent);
+});