Create conversations API routes
Changes:
- Create new conversationRouter with routes for conversations
- Add conversation-related methods to Jamid
- Use Message interface over Record<string, string>
- Add return type annotations for functions in Jamid
- Simplify returned value for account creation/registration
GitLab: #95
Change-Id: Ib0af8b60a92d08ddf4843f874c811e4ead870174
diff --git a/server/src/routers/account-router.ts b/server/src/routers/account-router.ts
index 18b1554..59bad05 100644
--- a/server/src/routers/account-router.ts
+++ b/server/src/routers/account-router.ts
@@ -59,12 +59,16 @@
res.sendStatus(HttpStatusCode.NoContent);
});
-accountRouter.post('/send-account-message', (req: Request<ParamsDictionary, any, SendAccountTextMessageApi>, res) => {
- const { from, to, type, data } = req.body;
- if (!from || !to || !type || !data) {
- res.status(HttpStatusCode.BadRequest).send('Missing arguments in request');
- return;
+accountRouter.post(
+ '/send-account-message',
+ (req: Request<ParamsDictionary, string, SendAccountTextMessageApi>, res) => {
+ const { from, to, type, data } = req.body;
+ if (!from || !to || !type || !data) {
+ res.status(HttpStatusCode.BadRequest).send('Missing arguments in request');
+ return;
+ }
+
+ jamid.sendAccountTextMessage(from, to, type, data);
+ res.end();
}
- jamid.sendAccountTextMessage(from, to, type, data);
- res.end();
-});
+);
diff --git a/server/src/routers/auth-router.ts b/server/src/routers/auth-router.ts
index d0fc6f0..086f640 100644
--- a/server/src/routers/auth-router.ts
+++ b/server/src/routers/auth-router.ts
@@ -40,7 +40,7 @@
authRouter.post(
'/new-account',
- asyncHandler(async (req: Request<ParamsDictionary, any, Credentials>, res, _next) => {
+ asyncHandler(async (req: Request<ParamsDictionary, string, Credentials>, res, _next) => {
const { username, password } = req.body;
if (!username || !password) {
res.status(HttpStatusCode.BadRequest).send('Missing username or password');
@@ -55,10 +55,10 @@
// TODO: find a way to store the password directly in Jami
// Maybe by using the "password" field? But as I tested, it's not
// returned when getting user infos.
- const { accountId } = await jamid.addAccount(new Map());
+ const accountId = await jamid.addAccount(new Map());
// TODO: understand why the password arg in this call must be empty
- const { state } = await jamid.registerUsername(accountId, username, '');
+ const state = await jamid.registerUsername(accountId, username, '');
if (state !== 0) {
jamid.removeAccount(accountId);
if (state === 2) {
@@ -80,7 +80,7 @@
authRouter.post(
'/login',
- asyncHandler(async (req: Request<ParamsDictionary, any, Credentials>, res, _next) => {
+ asyncHandler(async (req: Request<ParamsDictionary, { accessToken: string } | string, Credentials>, res, _next) => {
const { username, password } = req.body;
if (!username || !password) {
res.status(HttpStatusCode.BadRequest).send('Missing username or password');
@@ -117,6 +117,6 @@
.setAudience('urn:example:audience')
.setExpirationTime('2h')
.sign(vault.privateKey);
- res.json({ accessToken: jwt });
+ res.send({ accessToken: jwt });
})
);
diff --git a/server/src/routers/conversation-router.ts b/server/src/routers/conversation-router.ts
new file mode 100644
index 0000000..164ea3e
--- /dev/null
+++ b/server/src/routers/conversation-router.ts
@@ -0,0 +1,167 @@
+/*
+ * 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 { Request, Router } from 'express';
+import asyncHandler from 'express-async-handler';
+import { ParamsDictionary } from 'express-serve-static-core';
+import { HttpStatusCode } from 'jami-web-common';
+import { Container } from 'typedi';
+
+import { Jamid } from '../jamid/jamid.js';
+import { authenticateToken } from '../middleware/auth.js';
+
+interface ConversationMembers {
+ members: string[];
+}
+
+interface ConversationMessage {
+ message: string;
+}
+
+const jamid = Container.get(Jamid);
+
+// TODO: Create interface for return type in common/ when Records and interfaces are refactored
+async function createConversationResponseObject(accountId: string, accountUri: string, conversationId: string) {
+ const infos = jamid.getConversationInfos(accountId, conversationId);
+ const members = jamid.getConversationMembers(accountId, conversationId);
+
+ const namedMembers = [];
+ for (const member of members) {
+ // Exclude current user from returned conversation members
+ if (member.uri === accountUri) {
+ continue;
+ }
+
+ // Add usernames for conversation members
+ // TODO: Add caching in jamid to avoid too many address -> username lookups?
+ const { username } = await jamid.lookupAddress(member.uri, accountId);
+ namedMembers.push({
+ role: member.role,
+ contact: {
+ uri: member.uri,
+ registeredName: username,
+ },
+ });
+ }
+
+ // TODO: Check if messages actually need to be added to response
+ // (does the client really need it for all endpoints, or just the /conversations/conversationId/messages endpoint?)
+ const messages = await jamid.getConversationMessages(accountId, conversationId);
+
+ return {
+ id: conversationId,
+ messages: messages,
+ members: namedMembers,
+ infos: infos,
+ };
+}
+
+export const conversationRouter = Router();
+
+conversationRouter.use(authenticateToken);
+
+conversationRouter.get(
+ '/',
+ asyncHandler(async (_req, res) => {
+ const accountId = res.locals.accountId;
+
+ // Retrieve the URI of the current account (Account.username actually stores the URI rather than the username)
+ const accountUri = jamid.getAccountDetails(accountId)['Account.username'];
+
+ const conversationIds = jamid.getConversationIds(accountId);
+
+ const conversations = [];
+ for (const conversationId of conversationIds) {
+ const conversation = await createConversationResponseObject(accountId, accountUri, conversationId);
+ conversations.push(conversation);
+ }
+
+ res.send(conversations);
+ })
+);
+
+conversationRouter.post('/', (req: Request<ParamsDictionary, Record<string, string>, ConversationMembers>, res) => {
+ const accountId = res.locals.accountId;
+
+ const { members } = req.body;
+ if (members === undefined || members.length !== 1) {
+ res.sendStatus(HttpStatusCode.BadRequest);
+ return;
+ }
+
+ const contactId = members[0];
+ jamid.addContact(accountId, contactId);
+
+ const contactDetails = jamid.getContactDetails(accountId, contactId);
+ res.send(contactDetails);
+});
+
+// TODO: Check if we actually need this endpoint to return messages.
+// Verify by checking what is truly needed in the client when migrating, to clean up the API.
+// At the moment, /conversations does a lot of work returning all the conversations with the same
+// level of detail as this, and /conversations/messages returns just the messages. Check whether or not
+// this is what we want, and if so, if we can be more economical with client requests.
+conversationRouter.get(
+ '/:conversationId',
+ asyncHandler(async (req, res) => {
+ const accountId = res.locals.accountId;
+ const conversationId = req.params.conversationId;
+
+ // Retrieve the URI of the current account (Account.username actually stores the URI rather than the username)
+ const accountUri = jamid.getAccountDetails(accountId)['Account.username'];
+
+ const conversationIds = jamid.getConversationIds(accountId);
+ if (!conversationIds.includes(conversationId)) {
+ res.sendStatus(HttpStatusCode.NotFound);
+ return;
+ }
+
+ const conversation = await createConversationResponseObject(accountId, accountUri, conversationId);
+ res.send(conversation);
+ })
+);
+
+conversationRouter.get(
+ '/:conversationId/messages',
+ asyncHandler(async (req, res) => {
+ const accountId = res.locals.accountId;
+ const conversationId = req.params.conversationId;
+
+ const conversationIds = jamid.getConversationIds(accountId);
+ if (!conversationIds.includes(conversationId)) {
+ res.sendStatus(HttpStatusCode.NotFound);
+ return;
+ }
+
+ const messages = await jamid.getConversationMessages(accountId, conversationId);
+ res.send(messages);
+ })
+);
+
+conversationRouter.post(
+ '/:conversationId/messages',
+ (req: Request<ParamsDictionary, any, ConversationMessage>, res) => {
+ const { message } = req.body;
+ if (message === undefined) {
+ res.sendStatus(HttpStatusCode.BadRequest);
+ return;
+ }
+
+ jamid.sendConversationMessage(res.locals.accountId, req.params.conversationId, message);
+ res.end();
+ }
+);