Finalize JamiSwig interface and implement account detail routes
Changes:
- Add missing methods to JamiSwig interface
- Create new stringMapToRecord and itToRecord functions
- Rename id to accountId everywhere for consistency
- Rename jamid to jamiSwig inside Jamid service for clarity
- Implement Jamid functionality:
- getVolatileAccountDetails
- getAccountDetails with AccountDetails interface
- setAccountDetails
- getDevices
- getDefaultModerators
- Reorder and rename methods in Jamid for consistency with JamiSwig
- Implement all routes for accountRouter (GET/POST /account)
- Add various TODO comments for future work
Change-Id: Id14ddde3bc8b4484d82ad84c57384567d92bd70f
diff --git a/server/src/jamid/jami-swig.ts b/server/src/jamid/jami-swig.ts
index 63fc307..6ce5a71 100644
--- a/server/src/jamid/jami-swig.ts
+++ b/server/src/jamid/jami-swig.ts
@@ -15,7 +15,7 @@
* License along with this program. If not, see
* <https://www.gnu.org/licenses/>.
*/
-import { itMap, itRange, itToArr, itToMap } from '../utils.js';
+import { itMap, itRange, itToArr, itToMap, itToRecord } from './utils.js';
enum Bool {
False = 'false',
@@ -33,26 +33,66 @@
set(k: T, v: U): void;
}
+// TODO: Review these conversion functions
const swigVecToIt = <T>(v: SwigVec<T>) => itMap(itRange(0, v.size()), (i) => v.get(i));
const swigMapToIt = <T, U>(m: SwigMap<T, U>) => itMap(swigVecToIt(m.keys()), (k): [T, U] => [k, m.get(k)]);
-// type IntVect = SwigVec<number>;
-// type UintVect = SwigVec<number>;
-// type FloatVect = SwigVec<number>;
+// export type IntVect = SwigVec<number>;
+// export type UintVect = SwigVec<number>;
+// export type FloatVect = SwigVec<number>;
export type StringVect = SwigVec<string>;
-// type IntegerMap = SwigMap<string, number>;
+// export type IntegerMap = SwigMap<string, number>;
export type StringMap = SwigMap<string, string>;
-// type VectMap = SwigVec<StringMap>;
-// type Blob = SwigVec<number>;
+export type VectMap = SwigVec<StringMap>;
+// export type Blob = SwigVec<number>;
-export const stringVectToArr = (sv: StringVect) => itToArr(swigVecToIt(sv));
+// 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));
-// const vectMapToJs = (vm: VectMap) => itToArr(itMap(swigVecToIt(vm), stringMapToMap));
+// export const vectMapToArrayMap = (vm: VectMap) => itToArr(itMap(swigVecToIt(vm), stringMapToMap));
export interface JamiSwig {
init(args: Record<string, unknown>): void;
fini(): void;
+ getAccountDetails(accountId: string): StringMap;
+ getVolatileAccountDetails(accountId: string): StringMap;
+ setAccountDetails(accountId: string, details: StringMap): void;
+ setAccountActive(accountId: string, active: Bool): void;
+
+ addAccount(details: StringMap): string;
+ removeAccount(accountId: string): void;
+
+ getAccountList(): StringVect;
+
+ lookupName(accountId: string, nameserver: string, username: string): boolean;
+ lookupAddress(accountId: string, nameserver: string, address: string): boolean;
+ registerName(accountId: string, password: string, username: string): boolean;
+
+ getKnownRingDevices(accountId: string): StringMap;
+
+ getAudioOutputDeviceList(): StringVect;
+
+ getVolume(device: string): number;
+ setVolume(device: string, value: number): void;
+
+ addContact(accountId: string, contactId: string): void;
+ removeContact(accountId: string, contactId: string, ban: boolean): void;
+ getContacts(accountId: string): VectMap;
+ getContactDetails(accountId: string, contactId: string): StringMap;
+
+ getDefaultModerators(accountId: string): StringVect;
+ setDefaultModerators(accountId: string, uri: string, state: boolean): void;
+
+ getConversations(accountId: string): StringVect;
+ conversationInfos(accountId: string, conversationId: string): StringMap;
+
+ getConversationMembers(accountId: string, conversationId: string): VectMap;
+
+ sendMessage(accountId: string, conversationId: string, message: string, replyTo: string): void;
+ loadConversationMessages(accountId: string, conversationId: string, fromMessage: string, n: number): number;
+
// IntVect(): IntVect;
// UintVect(): UintVect;
// FloatVect(): FloatVect;
@@ -61,17 +101,4 @@
// StringMap(): StringMap;
// VectMap(): VectMap;
// IntegerMap(): IntegerMap;
-
- addAccount(details: StringMap): string;
- removeAccount(id: string): void;
-
- getAccountList(): StringVect;
-
- registerName(id: string, password: string, username: string): boolean;
- lookupName(id: string, nameserver: string, username: string): boolean;
- lookupAddress(id: string, nameserver: string, address: string): boolean;
-
- getAccountDetails(id: string): StringMap;
- setAccountDetails(id: string, details: StringMap): void;
- setAccountActive(id: string, active: Bool): void;
}
diff --git a/server/src/jamid/jamid.ts b/server/src/jamid/jamid.ts
index b2d81c4..51394eb 100644
--- a/server/src/jamid/jamid.ts
+++ b/server/src/jamid/jamid.ts
@@ -15,11 +15,11 @@
* License along with this program. If not, see
* <https://www.gnu.org/licenses/>.
*/
+import { AccountDetails, VolatileDetails } from 'jami-web-common';
import log from 'loglevel';
import { filter, firstValueFrom, Subject } from 'rxjs';
import { Service } from 'typedi';
-import { itMap, require } from '../utils.js';
import { JamiSignal } from './jami-signal.js';
import {
NameRegistrationEnded,
@@ -27,17 +27,18 @@
RegistrationStateChanged,
VolatileDetailsChanged,
} from './jami-signal-interfaces.js';
-import { JamiSwig, StringMap, stringMapToMap, stringVectToArr } from './jami-swig.js';
+import { JamiSwig, StringMap, stringMapToRecord, stringVectToArray } from './jami-swig.js';
+import { require } from './utils.js';
@Service()
export class Jamid {
- private readonly jamid: JamiSwig;
- private readonly mapUsernameToAccountId: Map<string, string>;
+ private readonly jamiSwig: JamiSwig;
+ private readonly usernamesToAccountIds: Map<string, string>;
private readonly events;
constructor() {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
- this.jamid = require('../jamid.node') as JamiSwig;
+ this.jamiSwig = require('../../jamid.node') as JamiSwig;
const handlers: Record<string, unknown> = {};
const handler = (sig: string) => {
@@ -73,7 +74,7 @@
// Keep map of usernames to account IDs as Jamid cannot do this by itself (AFAIK)
const username = details.get('Account.registeredName');
if (username) {
- this.mapUsernameToAccountId.set(username, accountId);
+ this.usernamesToAccountIds.set(username, accountId);
}
});
this.events.onRegistrationStateChanged.subscribe((ctx) =>
@@ -82,62 +83,65 @@
this.events.onNameRegistrationEnded.subscribe((ctx) => log.debug('[1] Received onNameRegistrationEnded with', ctx));
this.events.onRegisteredNameFound.subscribe((ctx) => log.debug('[1] Received onRegisteredNameFound with', ctx));
- this.mapUsernameToAccountId = new Map<string, string>();
+ this.usernamesToAccountIds = new Map<string, string>();
// 1. You cannot change event handlers after init
// 2. You cannot specify multiple handlers for the same event
// 3. You cannot specify a default handler
- // So we rely on the Subject() instead of Observable()
+ // So we rely on Subject() instead of Observable()
// Also, handlers receive multiple argument instead of tuple or object!
- this.jamid.init(handlers);
+ this.jamiSwig.init(handlers);
}
stop() {
- this.jamid.fini();
+ this.jamiSwig.fini();
}
- getAccountList() {
- return stringVectToArr(this.jamid.getAccountList());
+ getVolatileAccountDetails(accountId: string): VolatileDetails {
+ return stringMapToRecord(this.jamiSwig.getVolatileAccountDetails(accountId)) as unknown as VolatileDetails;
}
- async createAccount(details: Map<string, string | number | boolean>) {
- // TODO: add proper typing directly into JamiSwig
- const stringMapDetails: StringMap = new (this.jamid as any).StringMap();
+ getAccountDetails(accountId: string): AccountDetails {
+ return stringMapToRecord(this.jamiSwig.getAccountDetails(accountId)) as unknown as AccountDetails;
+ }
- stringMapDetails.set('Account.type', 'RING');
- itMap(details.entries(), ([k, v]) => stringMapDetails.set('Account.' + k, v.toString()));
+ setAccountDetails(accountId: string, accountDetails: AccountDetails) {
+ const accountDetailsStringMap: StringMap = new (this.jamiSwig as any).StringMap();
+ for (const [key, value] of Object.entries(accountDetails)) {
+ accountDetailsStringMap.set(key, value);
+ }
+ this.jamiSwig.setAccountDetails(accountId, accountDetailsStringMap);
+ }
- const id = this.jamid.addAccount(stringMapDetails);
+ async addAccount(details: Map<string, string | number | boolean>) {
+ // TODO: Add proper typing directly into JamiSwig
+ const detailsStringMap: StringMap = new (this.jamiSwig as any).StringMap();
+
+ detailsStringMap.set('Account.type', 'RING');
+ for (const [key, value] of details.entries()) {
+ detailsStringMap.set('Account.' + key, value.toString());
+ }
+
+ const accountId = this.jamiSwig.addAccount(detailsStringMap);
return firstValueFrom(
this.events.onRegistrationStateChanged.pipe(
- filter(({ accountId }) => accountId === id),
+ filter(({ accountId: addedAccountId }) => addedAccountId === accountId),
// TODO: is it the only state?
filter(({ state }) => state === 'REGISTERED')
)
);
}
- destroyAccount(id: string) {
- this.jamid.removeAccount(id);
+ removeAccount(accountId: string) {
+ this.jamiSwig.removeAccount(accountId);
}
- async registerUsername(id: string, username: string, password: string) {
- const hasRingNs = this.jamid.registerName(id, password, username);
- if (!hasRingNs) {
- log.error('Jami does not have NS');
- throw new Error('Jami does not have NS');
- }
- return firstValueFrom(this.events.onNameRegistrationEnded.pipe(filter(({ accountId }) => accountId === id)));
- }
-
- // TODO: Ideally, we would fetch the username directly from Jami instead of
- // keeping an internal map.
- usernameToAccountId(username: string) {
- return this.mapUsernameToAccountId.get(username);
+ getAccountList(): string[] {
+ return stringVectToArray(this.jamiSwig.getAccountList());
}
async lookupUsername(username: string) {
- const hasRingNs = this.jamid.lookupName('', '', username);
+ const hasRingNs = this.jamiSwig.lookupName('', '', username);
if (!hasRingNs) {
log.error('Jami does not have NS');
throw new Error('Jami does not have NS');
@@ -145,7 +149,30 @@
return firstValueFrom(this.events.onRegisteredNameFound.pipe(filter((r) => r.username === username)));
}
- getAccountDetails(id: string) {
- return stringMapToMap(this.jamid.getAccountDetails(id));
+ async registerUsername(accountId: string, username: string, password: string) {
+ const hasRingNs = this.jamiSwig.registerName(accountId, password, username);
+ if (!hasRingNs) {
+ log.error('Jami does not have NS');
+ throw new Error('Jami does not have NS');
+ }
+ return firstValueFrom(
+ this.events.onNameRegistrationEnded.pipe(
+ filter(({ accountId: registeredAccountId }) => registeredAccountId === accountId)
+ )
+ );
+ }
+
+ getDevices(accountId: string): Record<string, string> {
+ return stringMapToRecord(this.jamiSwig.getKnownRingDevices(accountId));
+ }
+
+ getDefaultModerators(accountId: string): string[] {
+ return stringVectToArray(this.jamiSwig.getDefaultModerators(accountId));
+ }
+
+ // TODO: Ideally, we would fetch the username directly from Jami instead of
+ // keeping an internal map.
+ getAccountIdFromUsername(username: string): string | undefined {
+ return this.usernamesToAccountIds.get(username);
}
}
diff --git a/server/src/jamid/utils.ts b/server/src/jamid/utils.ts
new file mode 100644
index 0000000..0952fec
--- /dev/null
+++ b/server/src/jamid/utils.ts
@@ -0,0 +1,62 @@
+/*
+ * 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 { createRequire } from 'node:module';
+
+// TODO: Move these functions to jami-swig.ts
+
+export function* itRange(lo: number, hi: number) {
+ for (let i = lo; i < hi; ++i) {
+ yield i;
+ }
+}
+
+export function* itMap<T, U>(it: Iterable<T>, cb: (value: T, index: number) => U) {
+ let i = 0;
+ for (const item of it) {
+ yield cb(item, i++);
+ }
+}
+
+export function* itFilter<T>(it: Iterable<T>, cb: (value: T, index: number) => boolean) {
+ let i = 0;
+ for (const item of it) {
+ if (cb(item, i++)) {
+ yield item;
+ }
+ }
+}
+
+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) {
+ r[k] = v;
+ }
+ return r;
+};
+
+export const require = createRequire(import.meta.url);