Create new interfaces for objects transmitted using the REST API
Changes:
- Create new IContact, IAccount, and IConversation interfaces in common/
- These interfaces represent the serialized versions of the models which are transferred
- The client models are classes which implement these interfaces
- Create new LookupResult interface for nameserver lookup results
- Create new IConversationMember interface for conversation members
- The client interface ConversationMember extends this interface to have a Contact field rather than IContact
- Create new ConversationInfos interface for conversation infos
- Create new ContactDetails interface for contact details (used by contacts routes)
- Move request and response body interfaces into common/
- Merge AccountConfig into AccountDetails interface
- Create interfaces for server-only objects:
- ConversationMemberInfos
- ConversationRequestMetadata
- Ensure interfaces in jami-signal-interfaces.ts do not contain fields with JamiSwig types
- Rename models/ filenames to camelCase as they are not components
- Rewrite client models to have proper TypeScript accessors and remove unused getters
- Rewrite how client models are initialized from the serialized interface using .fromInterface static methods
- Make client models implement the interfaces in common/ for consistency
- Remove unneeded _next parameter for Express.js route handlers
- Use Partial<T> for all Express.js request body types on server
- Type all Axios response body types with interfaces
GitLab: #92
Change-Id: I4b2c75ac632ec5d9bf12a874a5ba04467c76fa6d
diff --git a/client/src/models/Account.ts b/client/src/models/Account.ts
deleted file mode 100644
index 7471e5e..0000000
--- a/client/src/models/Account.ts
+++ /dev/null
@@ -1,151 +0,0 @@
-/*
- * 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 { AccountDetails, VolatileDetails } from 'jami-web-common';
-
-import { Contact } from './Contact.js';
-import { Conversation } from './Conversation.js';
-
-export class Account {
- private readonly id: string;
- private _details: AccountDetails;
- private _volatileDetails: VolatileDetails;
- private _contacts: Contact[];
- private readonly conversations: Record<string, Conversation>;
- private defaultModerators: Contact[];
- private devices: Record<string, string>;
-
- static readonly TYPE_JAMI: string = 'RING';
- static readonly TYPE_SIP: string = 'SIP';
-
- constructor(id: string, details: AccountDetails, volatileDetails: VolatileDetails) {
- this.id = id;
- this._details = details || {};
- this._volatileDetails = volatileDetails || {};
- this._contacts = [];
- this.conversations = {};
- this.defaultModerators = [];
- this.devices = {};
- }
-
- static from(object: any) {
- const account = new Account(object.id, object.details, object.volatileDetails);
- if (object.defaultModerators) account.defaultModerators = object.defaultModerators.map((m: any) => Contact.from(m));
- return account;
- }
-
- 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'] === 'true';
- }
-
- isPublicIn() {
- return this._details['DHT.PublicInCalls'] === '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;
- }
-
- 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];
- }
-
- 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;
- }
-
- setDevices(devices: Record<string, string>) {
- this.devices = { ...devices };
- }
-
- getDevices() {
- return this.devices;
- }
-}
diff --git a/client/src/models/Contact.ts b/client/src/models/Contact.ts
deleted file mode 100644
index bd526ae..0000000
--- a/client/src/models/Contact.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * 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 class Contact {
- private readonly uri: string;
- private readonly displayName: string | undefined;
- private registeredName: string | undefined;
-
- constructor(uri: string) {
- this.uri = uri;
- this.displayName = undefined;
- this.registeredName = undefined;
- }
-
- static from(object: any) {
- const contact = new Contact(object.uri);
- if (object.registeredName) contact.setRegisteredName(object.registeredName);
- return contact;
- }
-
- getUri() {
- return this.uri;
- }
-
- getRegisteredName() {
- return this.registeredName;
- }
-
- setRegisteredName(name: string | undefined) {
- this.registeredName = name;
- }
-
- getDisplayName() {
- return this.getDisplayNameNoFallback() || this.getUri();
- }
-
- getDisplayNameNoFallback() {
- return this.displayName || this.getRegisteredName();
- }
-}
diff --git a/client/src/models/Conversation.ts b/client/src/models/Conversation.ts
deleted file mode 100644
index fa72af9..0000000
--- a/client/src/models/Conversation.ts
+++ /dev/null
@@ -1,121 +0,0 @@
-/*
- * 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 { Message } from 'jami-web-common';
-
-import { Contact } from './Contact';
-
-export interface ConversationMember {
- contact: Contact;
- role?: 'admin' | 'member' | 'invited' | 'banned' | 'left';
-}
-
-type ConversationInfos = Record<string, unknown>;
-
-export class Conversation {
- private readonly id: string | undefined;
- private readonly accountId: string;
- private readonly members: ConversationMember[];
- private messages: Message[];
- private _infos: ConversationInfos;
-
- constructor(id: string | undefined, accountId: string, members?: ConversationMember[]) {
- this.id = id;
- this.accountId = accountId;
- this.members = members || [];
-
- this.messages = [];
- this._infos = {};
- }
-
- static from(accountId: string, object: any) {
- const conversation = new Conversation(
- object.id,
- accountId,
- object.members.map((member: any) => {
- member.contact = Contact.from(member.contact);
- return member;
- })
- );
- conversation.messages = object.messages;
- conversation.infos = object.infos;
- 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();
- }
- }
-
- 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;
- }
-
- get infos() {
- return this._infos;
- }
-
- set infos(infos: ConversationInfos) {
- this._infos = infos;
- }
-}
diff --git a/client/src/models/account.ts b/client/src/models/account.ts
new file mode 100644
index 0000000..60ac1d8
--- /dev/null
+++ b/client/src/models/account.ts
@@ -0,0 +1,97 @@
+/*
+ * 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 { AccountDetails, Devices, IAccount, VolatileDetails } from 'jami-web-common';
+
+import { Contact } from './contact';
+import { Conversation } from './conversation';
+
+export type AccountType = 'RING' | 'SIP';
+
+export class Account implements IAccount {
+ readonly id: string;
+ details: AccountDetails;
+ volatileDetails: VolatileDetails;
+ defaultModerators: Contact[] = [];
+ devices: Devices = {};
+ contacts: Contact[] = [];
+
+ private _conversations: Record<string, Conversation> = {};
+
+ constructor(id: string, details: AccountDetails, volatileDetails: VolatileDetails) {
+ this.id = id;
+ this.details = details;
+ this.volatileDetails = volatileDetails;
+ }
+
+ static fromInterface(accountInterface: IAccount) {
+ const account = new Account(accountInterface.id, accountInterface.details, accountInterface.volatileDetails);
+ account.defaultModerators = accountInterface.defaultModerators.map(Contact.fromInterface);
+ return account;
+ }
+
+ getType(): AccountType {
+ return this.details['Account.type'] as AccountType;
+ }
+
+ getUri() {
+ return this.details['Account.username'];
+ }
+
+ getRegisteredName() {
+ return this.volatileDetails['Account.registeredName'];
+ }
+
+ isRendezVous() {
+ return this.details['Account.rendezVous'] === 'true';
+ }
+
+ isPublicIn() {
+ return this.details['DHT.PublicInCalls'] === 'true';
+ }
+
+ updateDetails(details: Partial<AccountDetails>) {
+ this.details = { ...this.details, ...details };
+ }
+
+ getDisplayUri() {
+ return this.getRegisteredName() ?? this.getUri();
+ }
+
+ getDisplayName() {
+ return this.details['Account.displayName'] ?? this.getDisplayUri();
+ }
+
+ getDisplayNameNoFallback() {
+ return this.details['Account.displayName'] ?? this.getRegisteredName();
+ }
+
+ get conversations() {
+ return this._conversations;
+ }
+
+ addConversation(conversation: Conversation) {
+ if (conversation.id === undefined) {
+ throw new Error('Conversation ID cannot be undefined');
+ }
+ this._conversations[conversation.id] = conversation;
+ }
+
+ removeConversation(conversationId: string) {
+ delete this.conversations[conversationId];
+ }
+}
diff --git a/client/src/models/contact.ts b/client/src/models/contact.ts
new file mode 100644
index 0000000..e696db0
--- /dev/null
+++ b/client/src/models/contact.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/>.
+ */
+import { IContact } from 'jami-web-common';
+
+export class Contact implements IContact {
+ readonly uri: string;
+ registeredName?: string;
+ displayName?: string;
+
+ constructor(uri: string, registeredName?: string) {
+ this.uri = uri;
+ this.registeredName = registeredName;
+ }
+
+ static fromInterface(contactInterface: IContact) {
+ return new Contact(contactInterface.uri, contactInterface.registeredName);
+ }
+
+ getDisplayName() {
+ return this.getDisplayNameNoFallback() ?? this.uri;
+ }
+
+ getDisplayNameNoFallback() {
+ return this.displayName ?? this.registeredName;
+ }
+}
diff --git a/client/src/models/conversation.ts b/client/src/models/conversation.ts
new file mode 100644
index 0000000..484f166
--- /dev/null
+++ b/client/src/models/conversation.ts
@@ -0,0 +1,94 @@
+/*
+ * 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 { ConversationInfos, IConversation, IConversationMember, Message } from 'jami-web-common';
+
+import { Contact } from './contact';
+
+export interface ConversationMember extends IConversationMember {
+ contact: Contact;
+}
+
+export class Conversation implements IConversation {
+ readonly id: string;
+ members: ConversationMember[];
+ messages: Message[] = [];
+ infos: ConversationInfos = {};
+
+ constructor(id: string, members?: ConversationMember[]) {
+ this.id = id;
+ this.members = members ?? [];
+ }
+
+ static fromInterface(conversationInterface: IConversation) {
+ const conversation = new Conversation(
+ conversationInterface.id,
+ conversationInterface.members.map((member) => {
+ const contact = Contact.fromInterface(member.contact);
+ return { contact } as ConversationMember;
+ })
+ );
+
+ conversation.messages = conversationInterface.messages;
+ conversation.infos = conversationInterface.infos;
+
+ return conversation;
+ }
+
+ static fromSingleContact(contact: Contact) {
+ return new Conversation('', [{ contact } as ConversationMember]);
+ }
+
+ getDisplayUri() {
+ return this.id ?? this.getFirstMember().contact.uri;
+ }
+
+ getDisplayName() {
+ if (this.members.length !== 0) {
+ return this.getFirstMember().contact.registeredName;
+ }
+ return this.getDisplayUri();
+ }
+
+ getDisplayNameNoFallback() {
+ if (this.members.length !== 0) {
+ return this.getFirstMember().contact.registeredName;
+ }
+ }
+
+ getFirstMember() {
+ return this.members[0];
+ }
+
+ 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('Could not insert message', message.id);
+ }
+ }
+
+ addMessages(messages: Message[]) {
+ for (const message of messages) {
+ this.addMessage(message);
+ }
+ }
+}