Skip to content
Snippets Groups Projects
Select Git revision
  • 31340bc224b5e4e0ed2e40cf55b73be4bc2e56b9
  • master default protected
  • beta/202506161038
  • stable/20250613.0
  • nightly/20250613.0
  • beta/202506101658
  • stable/20250610.0
  • nightly/20250610.0
  • beta/202506091027
  • beta/202506061543
  • nightly/20250605.0
  • beta/202506051039
  • beta/202506051002
  • beta/202506041611
  • beta/202506041335
  • beta/202505231812
  • stable/20250523.0
  • nightly/20250523.0
  • nightly/20250515.0
  • nightly/20250510.0
  • nightly/20250509.1
  • nightly/20250509.0
22 results

ParticipantsLayoutHorizontal.qml

Blame
  • Code owners
    Assign users and groups as approvers for specific file changes. Learn more.
    jamid.ts 40.10 KiB
    /*
     * Copyright (C) 2022-2025 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'
    
    import fs from 'fs'
    import {
      AccountDetails,
      AccountMessageStatus,
      ComposingStatus,
      ContactDetails,
      ConversationInfos,
      ConversationMemberEventCode,
      ConversationMessage,
      ConversationPreferences,
      DeviceRevocationState,
      Devices,
      LookupResult,
      Message,
      Reaction,
      RegisteredNameFoundState,
      VolatileDetails,
      WebSocketMessage,
      WebSocketMessageType,
    } from 'jami-web-common'
    import { DataTransferError } from 'jami-web-common/src/enums/datatransfer-error-code.js'
    import log from 'loglevel'
    import os from 'os'
    import path from 'path'
    import { filter, firstValueFrom, map, Subject } from 'rxjs'
    import { Service } from 'typedi'
    
    import { getAvatar } from '../utils/vCard.js'
    import { WebSocketServer } from '../websocket/websocket-server.js'
    import { ConversationMemberInfos } from './conversation-member-infos.js'
    import { ConversationRequestMetadata } from './conversation-request-metadata.js'
    import { JamiSignal } from './jami-signal.js'
    import {
      AccountDetailsChanged,
      AccountMessageStatusChanged,
      AccountProfileReceived,
      ClearCache,
      ComposingStatusChanged,
      ContactAdded,
      ContactRemoved,
      ConversationLoaded,
      ConversationMemberEvent,
      ConversationReady,
      ConversationRemoved,
      ConversationRequestReceived,
      DataTransferEvent,
      DeviceRevocationEnded,
      IncomingAccountMessage,
      INearbyPeerNotification,
      INewBuddyNotification,
      INewServerSubscriptionRequest,
      IServerError,
      ISubscriptionStateChanged,
      KnownDevicesChanged,
      MessageFound,
      NameRegistrationEnded,
      ProfileReceived,
      ReactionAdded,
      ReactionRemoved,
      RegisteredNameFound,
      RegistrationStateChanged,
      SwarmLoaded,
      SwarmMessageReceived,
      SwarmMessageUpdated,
      UserSearchEnded,
      VolatileDetailsChanged,
    } from './jami-signal-interfaces.js'
    import { JamiSwig, StringMap, stringMapToRecord, stringVectToArray, vectMapToRecordArray } from './jami-swig.js'
    import { DataTransferEventCode, MessageState, NameRegistrationEndedState, RegistrationState } from './state-enums.js'
    
    const require = createRequire(import.meta.url)
    
    @Service()
    export class Jamid {
      private jamiSwig: JamiSwig
      private usernamesToAccountIds = new Map<string, string>()
      readonly events
    
      reformatMessage(message: Message): Message {
        return {
          id: message.id,
          author: message.body.author,
          linearizedParent: message.linearizedParent,
          parents: message.body.parents,
          timestamp: message.body.timestamp ? Number(message.body.timestamp) : 0,
          duration: message.body.duration,
          displayName: message.body.displayName,
          sha3sum: message.body.sha3sum,
          tid: message.body.tid,
          totalSize: message.body.totalSize,
          fileId: message.body.fileId,
          type: message.body.type,
          'reply-to': message.body['reply-to'],
          action: message.body.action,
          editions: message.editions,
          reactions: message.reactions,
          status: message.status,
          body: message.body.body,
        }
      }
    
      constructor(private webSocketServer: WebSocketServer) {
        this.jamiSwig = require('../../jamid.node') as JamiSwig
    
        // Setup signal handlers
        const handlers: Record<string, unknown> = {}
    
        // Add default handler for all signals
        const createDefaultHandler = (signal: string) => {
          return (...args: unknown[]) => log.warn('Unhandled', signal, args)
        }
        for (const signal in JamiSignal) {
          handlers[signal] = createDefaultHandler(signal)
        }
    
        // Overwrite handlers for handled signals using RxJS Subjects, converting multiple arguments to objects
        const onClearCache = new Subject<ClearCache>()
        handlers.ClearCache = (accountId: string, conversationId: string) =>
          onClearCache.next({ accountId, conversationId })
    
        const onAccountsChanged = new Subject<void>()
        handlers.AccountsChanged = () => onAccountsChanged.next()
    
        const onAccountDetailsChanged = new Subject<AccountDetailsChanged>()
        handlers.AccountDetailsChanged = (accountId: string, details: AccountDetails) =>
          onAccountDetailsChanged.next({ accountId, details })
    
        const onAccountProfileReceived = new Subject<AccountProfileReceived>()
        handlers.AccountProfileReceived = (accountId: string, displayName: string, photo: string) =>
          onAccountProfileReceived.next({ accountId, displayName, photo })
    
        const onVolatileDetailsChanged = new Subject<VolatileDetailsChanged>()
        handlers.VolatileDetailsChanged = (accountId: string, details: VolatileDetails) =>
          onVolatileDetailsChanged.next({ accountId, details })
    
        const onRegistrationStateChanged = new Subject<RegistrationStateChanged>()
        handlers.RegistrationStateChanged = (accountId: string, state: RegistrationState, code: number, details: string) =>
          onRegistrationStateChanged.next({ accountId, state, code, details })
    
        const onNameRegistrationEnded = new Subject<NameRegistrationEnded>()
        handlers.NameRegistrationEnded = (accountId: string, state: NameRegistrationEndedState, username: string) =>
          onNameRegistrationEnded.next({ accountId, state, username })
    
        const onRegisteredNameFound = new Subject<RegisteredNameFound>()
        handlers.RegisteredNameFound = (
          accountId: string,
          query: string,
          state: RegisteredNameFoundState,
          address: string,
          name: string,
        ) => onRegisteredNameFound.next({ accountId, query, state, address, name })
    
        const onKnownDevicesChanged = new Subject<KnownDevicesChanged>()
        handlers.KnownDevicesChanged = (accountId: string, devices: Devices) =>
          onKnownDevicesChanged.next({ accountId, devices })
    
        const onIncomingAccountMessage = new Subject<IncomingAccountMessage>()
        handlers.IncomingAccountMessage = (accountId: string, from: string, payload: Record<string, string>) =>
          onIncomingAccountMessage.next({ accountId, from, payload })
    
        const onAccountMessageStatusChanged = new Subject<AccountMessageStatusChanged>()
        handlers.AccountMessageStatusChanged = (
          accountId: string,
          messageId: string,
          peer: string,
          state: MessageState,
          conversationId: string,
        ) => onAccountMessageStatusChanged.next({ accountId, messageId, peer, state, conversationId })
    
        const onContactAdded = new Subject<ContactAdded>()
        handlers.ContactAdded = (accountId: string, contactId: string, confirmed: boolean) =>
          onContactAdded.next({ accountId, contactId, confirmed })
    
        const onContactRemoved = new Subject<ContactRemoved>()
        handlers.ContactRemoved = (accountId: string, contactId: string, banned: boolean) =>
          onContactRemoved.next({ accountId, contactId, banned })
    
        const onConversationRequestReceived = new Subject<ConversationRequestReceived>()
        handlers.ConversationRequestReceived = (
          accountId: string,
          conversationId: string,
          metadata: ConversationRequestMetadata,
        ) => onConversationRequestReceived.next({ accountId, conversationId, metadata })
    
        const onConversationReady = new Subject<ConversationReady>()
        handlers.ConversationReady = (accountId: string, conversationId: string) =>
          onConversationReady.next({ accountId, conversationId })
    
        const onConversationRemoved = new Subject<ConversationRemoved>()
        handlers.ConversationRemoved = (accountId: string, conversationId: string) =>
          onConversationRemoved.next({ accountId, conversationId })
    
        const onConversationLoaded = new Subject<ConversationLoaded>()
        handlers.ConversationLoaded = (id: number, accountId: string, conversationId: string, messages: Message[]) =>
          onConversationLoaded.next({ id, accountId, conversationId, messages })
    
        const onProfileReceived = new Subject<ProfileReceived>()
        handlers.ProfileReceived = (accountId: string, from: string, path: string) =>
          onProfileReceived.next({ accountId, from, path })
    
        const onSwarmLoaded = new Subject<SwarmLoaded>()
        handlers.SwarmLoaded = (id: number, accountId: string, conversationId: string, messages: Message[]) => {
          messages = messages.map((message) => this.reformatMessage(message))
          onSwarmLoaded.next({ id, accountId, conversationId, messages })
        }
    
        const onConversationMemberEvent = new Subject<ConversationMemberEvent>()
        handlers.ConversationMemberEvent = (
          accountId: string,
          conversationId: string,
          memberUri: string,
          event: ConversationMemberEventCode,
        ) => {
          onConversationMemberEvent.next({ accountId, conversationId, memberUri, event })
        }
    
        const onDataTransferEvent = new Subject<DataTransferEvent>()
        handlers.DataTransferEvent = (
          accountId: string,
          conversationId: string,
          interactionId: string,
          fileId: string,
          eventCode: number,
        ) => onDataTransferEvent.next({ accountId, conversationId, interactionId, fileId, eventCode })
    
        const onSwarmMessageReceived = new Subject<SwarmMessageReceived>()
        handlers.SwarmMessageReceived = (accountId: string, conversationId: string, message: Message) => {
          message = this.reformatMessage(message)
          onSwarmMessageReceived.next({ accountId, conversationId, message })
        }
    
        const onSwarmMessageUpdated = new Subject<SwarmMessageUpdated>()
        handlers.SwarmMessageUpdated = (accountId: string, conversationId: string, message: Message) => {
          message = this.reformatMessage(message)
          onSwarmMessageUpdated.next({ accountId, conversationId, message })
        }
    
        const onMessageFound = new Subject<MessageFound>()
        handlers.MessagesFound = (id: number, accountId: string, conversationId: string, messages: Message[]) => {
          onMessageFound.next({
            id,
            accountId,
            conversationId,
            messages,
          })
        }
    
        const onReactionAdded = new Subject<ReactionAdded>()
        handlers.ReactionAdded = (accountId: string, conversationId: string, messageId: string, reaction: Reaction) =>
          onReactionAdded.next({ accountId, conversationId, messageId, reaction })
    
        const onReactionRemoved = new Subject<ReactionRemoved>()
        handlers.ReactionRemoved = (accountId: string, conversationId: string, messageId: string, reactionId: string) =>
          onReactionRemoved.next({ accountId, conversationId, messageId, reactionId })
    
        const onComposingStatusChanged = new Subject<ComposingStatusChanged>()
        handlers.ComposingStatusChanged = (accountId: string, conversationId: string, from: string, status: number) =>
          onComposingStatusChanged.next({ accountId, conversationId, from, status })
    
        const onUserSearchEnded = new Subject<UserSearchEnded>()
        handlers.UserSearchEnded = (
          accountId: string,
          state: RegisteredNameFoundState,
          query: string,
          results: Record<string, string>,
        ) => {
          onUserSearchEnded.next({ accountId, state, query, results })
        }
        const onDeviceRevocationEnded = new Subject<DeviceRevocationEnded>()
        handlers.DeviceRevocationEnded = (accountId: string, device: string, state: DeviceRevocationState) =>
          onDeviceRevocationEnded.next({ accountId, device, state })
    
        const onSubscriptionStateChanged = new Subject<ISubscriptionStateChanged>()
        handlers.SubscriptionStateChanged = (accountId: string, buddyUri: string, state: number) =>
          onSubscriptionStateChanged.next({ accountId, buddyUri, state })
    
        const onNearbyPeerNotification = new Subject<INearbyPeerNotification>()
        handlers.NearbyPeerNotification = (accountId: string, buddyUri: string, state: number, displayName: string) =>
          onNearbyPeerNotification.next({ accountId, buddyUri, state, displayName })
    
        const onNewBuddyNotification = new Subject<INewBuddyNotification>()
        handlers.NewBuddyNotification = (accountId: string, buddyUri: string, status: number, lineStatus: string) =>
          onNewBuddyNotification.next({ accountId, buddyUri, status, lineStatus })
    
        const onNewServerSubscriptionRequest = new Subject<INewServerSubscriptionRequest>()
        handlers.NewServerSubscriptionRequest = (remote: string) => onNewServerSubscriptionRequest.next({ remote })
    
        const onServerError = new Subject<IServerError>()
        handlers.ServerError = (accountId: string, error: string, msg: string) =>
          onServerError.next({ accountId, error, msg })
    
        // Expose all signals in an events object to allow other handlers to subscribe after jamiSwig.init()
        this.events = {
          onAccountsChanged: onAccountsChanged.asObservable(),
          onAccountDetailsChanged: onAccountDetailsChanged.asObservable(),
          onAccountProfileReceived: onAccountProfileReceived.asObservable(),
          onVolatileDetailsChanged: onVolatileDetailsChanged.asObservable(),
          onRegistrationStateChanged: onRegistrationStateChanged.asObservable(),
          onNameRegistrationEnded: onNameRegistrationEnded.asObservable(),
          onRegisteredNameFound: onRegisteredNameFound.asObservable(),
          onKnownDevicesChanged: onKnownDevicesChanged.asObservable(),
          onIncomingAccountMessage: onIncomingAccountMessage.asObservable(),
          onAccountMessageStatusChanged: onAccountMessageStatusChanged.asObservable(),
          onClearCache: onClearCache.asObservable(),
          onContactAdded: onContactAdded.asObservable(),
          onContactRemoved: onContactRemoved.asObservable(),
          onConversationRequestReceived: onConversationRequestReceived.asObservable(),
          onConversationReady: onConversationReady.asObservable(),
          onConversationRemoved: onConversationRemoved.asObservable(),
          onConversationLoaded: onConversationLoaded.asObservable(),
          onDataTransferEvent: onDataTransferEvent.asObservable(),
          onSwarmLoaded: onSwarmLoaded.asObservable(),
          onConversationMemberEvent: onConversationMemberEvent.asObservable(),
          onSwarmMessageReceived: onSwarmMessageReceived.asObservable(),
          onSwarmMessageUpdated: onSwarmMessageUpdated.asObservable(),
          onComposingStatusChanged: onComposingStatusChanged.asObservable(),
          onReactionAdded: onReactionAdded.asObservable(),
          onReactionRemoved: onReactionRemoved.asObservable(),
          onProfileReceived: onProfileReceived.asObservable(),
          onUserSearchEnded: onUserSearchEnded.asObservable(),
          onDeviceRevocationEnded: onDeviceRevocationEnded.asObservable(),
          onMessageFound: onMessageFound.asObservable(),
          onSubscriptionStateChanged: onSubscriptionStateChanged.asObservable(),
          onNearbyPeerNotification: onNearbyPeerNotification.asObservable(),
          onNewBuddyNotification: onNewBuddyNotification.asObservable(),
          onNewServerSubscriptionRequest: onNewServerSubscriptionRequest.asObservable(),
          onServerError: onServerError.asObservable(),
        }
    
        this.setupSignalHandlers()
    
        if (process.env.JAMI_WEB_MONITOR === 'true') {
          this.jamiSwig.monitor(true)
        }
    
        // RxJS Subjects are used as signal handlers for the following reasons:
        // 1. You cannot change event handlers after calling jamiSwig.init()
        // 2. You cannot specify multiple handlers for the same event
        // 3. You cannot specify a default handler
        this.jamiSwig.init(handlers)
      }
    
      stop(): void {
        this.jamiSwig.fini()
      }
    
      startConversation(accountId: string): string {
        return this.jamiSwig.startConversation(accountId)
      }
    
      getVolatileAccountDetails(accountId: string): VolatileDetails {
        return stringMapToRecord(this.jamiSwig.getVolatileAccountDetails(accountId)) as unknown as VolatileDetails
      }
    
      getAccountDetails(accountId: string): AccountDetails {
        return stringMapToRecord(this.jamiSwig.getAccountDetails(accountId)) as unknown as AccountDetails
      }
    
      updateProfile(accountId: string, displayName: string, avatarPath: string, fileType: string, flag: number): void {
        this.jamiSwig.updateProfile(accountId, displayName, avatarPath, fileType, flag)
      }
    
      setAccountDetails(accountId: string, accountDetails: AccountDetails): void {
        const accountDetailsStringMap: StringMap = new this.jamiSwig.StringMap()
        for (const [key, value] of Object.entries(accountDetails)) {
          accountDetailsStringMap.set(key, value)
        }
        this.jamiSwig.setAccountDetails(accountId, accountDetailsStringMap)
      }
    
      async addAccount(accountDetails: Partial<AccountDetails>): Promise<RegistrationStateChanged> {
        accountDetails['Account.type'] = 'RING'
    
        const accountDetailsStringMap: StringMap = new this.jamiSwig.StringMap()
        for (const [key, value] of Object.entries(accountDetails)) {
          accountDetailsStringMap.set(key, value.toString())
        }
        const accountId = this.jamiSwig.addAccount(accountDetailsStringMap)
        return firstValueFrom(
          this.events.onRegistrationStateChanged.pipe(
            filter((value) => value.accountId === accountId),
            filter(
              (value) => value.state === RegistrationState.Registered || value.state === RegistrationState.ErrorGeneric,
            ),
          ),
        )
      }
    
      removeAccount(accountId: string): void {
        this.jamiSwig.removeAccount(accountId)
      }
    
      getAccountIds(): string[] {
        return stringVectToArray(this.jamiSwig.getAccountList())
      }
    
      sendAccountTextMessage(accountId: string, contactId: string, message: string): void {
        const messageStringMap: StringMap = new this.jamiSwig.StringMap()
        messageStringMap.set('application/json', message)
        this.jamiSwig.sendAccountTextMessage(accountId, contactId, messageStringMap, 0)
      }
    
      async lookupUsername(username: string, nameserver: string, accountId?: string): Promise<LookupResult> {
        const hasRingNs = this.jamiSwig.lookupName(accountId || '', nameserver, username)
        if (!hasRingNs) {
          throw new Error('Jami does not have a nameserver')
        }
    
        const data = firstValueFrom(this.events.onRegisteredNameFound.pipe(filter((value) => value.query === username)))
        return data
      }
    
      async lookupAddress(address: string, nameserver: string, accountId?: string): Promise<LookupResult> {
        if (address === undefined) {
          throw new Error('Address is undefined')
        }
        const hasRingNs = this.jamiSwig.lookupAddress(accountId || '', nameserver, address)
    
        if (!hasRingNs) {
          throw new Error('Jami does not have a nameserver')
        }
    
        const data = firstValueFrom(this.events.onRegisteredNameFound.pipe(filter((value) => value.query === address)))
        return data
      }
    
      async registerUsername(accountId: string, username: string, password: string): Promise<NameRegistrationEndedState> {
        const hasRingNs = this.jamiSwig.registerName(accountId, username, 'password', password)
        if (!hasRingNs) {
          throw new Error('Jami does not have a nameserver')
        }
        return firstValueFrom(
          this.events.onNameRegistrationEnded.pipe(
            filter((value) => value.accountId === accountId),
            map((value) => value.state),
          ),
        )
      }
    
      async searchUser(accountId: string, username: string) {
        const hasRingNs = this.jamiSwig.searchUser(accountId, username)
        if (!hasRingNs) {
          throw new Error('Jams does not have a nameserver')
        }
        return firstValueFrom(this.events.onUserSearchEnded.pipe(filter((value) => value.accountId === accountId)))
      }
    
      clearCache(accountId: string, conversationId: string): void {
        this.jamiSwig.clearCache(accountId, conversationId)
      }
    
      getDevices(accountId: string): Devices {
        const devices = stringMapToRecord(this.jamiSwig.getKnownRingDevices(accountId))
        return devices
      }
    
      revokeDevice(accountId: string, deviceId: string, scheme: string = 'password', password: string = '') {
        const isOk = this.jamiSwig.revokeDevice(accountId, deviceId, scheme, password)
    
        if (!isOk) {
          log.error(`Error revoking device: accountId=${accountId}, deviceId=${deviceId}`)
          throw new Error('Error revoking device. Please check the accountId and deviceId.')
        }
    
        return firstValueFrom(
          this.events.onDeviceRevocationEnded.pipe(
            filter((value) => value.accountId === accountId),
            filter((value) => value.device === deviceId),
          ),
        )
      }
    
      addContact(accountId: string, contactId: string): void {
        this.jamiSwig.addContact(accountId, contactId)
      }
    
      sendTrustRequest(accountId: string, contactId: string): void {
        this.jamiSwig.sendTrustRequest(accountId, contactId, new this.jamiSwig.Blob())
      }
    
      removeContact(accountId: string, contactId: string): void {
        this.jamiSwig.removeContact(accountId, contactId, false)
      }
    
      blockContact(accountId: string, contactId: string): void {
        this.jamiSwig.removeContact(accountId, contactId, true)
      }
    
      getContacts(accountId: string): ContactDetails[] {
        return vectMapToRecordArray(this.jamiSwig.getContacts(accountId)) as unknown as ContactDetails[]
      }
    
      getContactDetails(accountId: string, contactId: string): ContactDetails {
        return stringMapToRecord(this.jamiSwig.getContactDetails(accountId, contactId)) as unknown as ContactDetails
      }
    
      getDefaultModeratorUris(accountId: string): string[] {
        return stringVectToArray(this.jamiSwig.getDefaultModerators(accountId))
      }
    
      addDefaultModerator(accountId: string, contactId: string): void {
        this.jamiSwig.setDefaultModerator(accountId, contactId, true)
      }
    
      removeDefaultModerator(accountId: string, contactId: string): void {
        this.jamiSwig.setDefaultModerator(accountId, contactId, false)
      }
    
      getConversationIds(accountId: string): string[] {
        return stringVectToArray(this.jamiSwig.getConversations(accountId))
      }
    
      getConversationInfos(accountId: string, conversationId: string): ConversationInfos {
        return stringMapToRecord(this.jamiSwig.conversationInfos(accountId, conversationId)) as unknown as ConversationInfos
      }
    
      searchConversation(
        accountId: string,
        conversationId: string,
        author: string = '',
        lastId: string = '',
        regexSearch: string = '',
        type: string = '',
        after: number = 0,
        before: number = 0,
        maxResult: number = 1,
        flag: number = 0,
      ) {
        const searchId = this.jamiSwig.searchConversation(
          accountId,
          conversationId,
          author,
          lastId,
          regexSearch,
          type,
          after,
          before,
          maxResult,
          flag,
        )
        console.log('searchId', searchId)
      }
    
      async getAllFiles(accountId: string, conversationId: string): Promise<Message[]> {
        const searchId = this.jamiSwig.searchConversation(
          accountId,
          conversationId,
          '',
          '',
          '',
          'application/data-transfer+json',
          0,
          0,
          0,
          0,
        )
    
        return firstValueFrom(
          this.events.onMessageFound.pipe(
            filter((value) => value.id === Number(searchId)),
            map((value) => value.messages),
          ),
        )
      }
    
      getConversationMembers(accountId: string, conversationId: string): ConversationMemberInfos[] {
        return vectMapToRecordArray(
          this.jamiSwig.getConversationMembers(accountId, conversationId),
        ) as unknown as ConversationMemberInfos[]
      }
    
      addConversationMember(accountId: string, conversationId: string, uri: string): void {
        this.jamiSwig.addConversationMember(accountId, conversationId, uri)
      }
    
      removeConversationMember(accountId: string, conversationId: string, uri: string): void {
        this.jamiSwig.removeConversationMember(accountId, conversationId, uri)
      }
    
      getConversationPreferences(accountId: string, conversationId: string): ConversationPreferences {
        return stringMapToRecord(
          this.jamiSwig.getConversationPreferences(accountId, conversationId),
        ) as unknown as ConversationPreferences
      }
    
      setConversationPreferences(accountId: string, conversationId: string, preferences: ConversationPreferences): void {
        const preferencesStringMap: StringMap = new this.jamiSwig.StringMap()
        for (const [key, value] of Object.entries(preferences)) {
          preferencesStringMap.set(key, value.toString())
        }
        this.jamiSwig.setConversationPreferences(accountId, conversationId, preferencesStringMap)
      }
    
      updateConversationInfos(accountId: string, conversationId: string, infos: ConversationInfos): void {
        const infosStringMap: StringMap = new this.jamiSwig.StringMap()
        for (const [key, value] of Object.entries(infos)) {
          infosStringMap.set(key, value.toString())
        }
        this.jamiSwig.updateConversationInfos(accountId, conversationId, infosStringMap)
      }
    
      async getConversationMessages(
        accountId: string,
        conversationId: string,
        fromMessage = '',
        n = 32,
      ): Promise<Message[]> {
        const requestId = this.jamiSwig.loadConversationMessages(accountId, conversationId, fromMessage, n)
        return firstValueFrom(
          this.events.onConversationLoaded.pipe(
            filter((value) => value.id === requestId),
            map((value) => value.messages),
          ),
        )
      }
    
      async getSwarmMessages(accountId: string, conversationId: string, fromMessage = '', n = 8): Promise<Message[]> {
        const requestId = this.jamiSwig.loadConversation(accountId, conversationId, fromMessage, n)
        return firstValueFrom(
          this.events.onSwarmLoaded.pipe(
            filter((value) => value.id === requestId),
            map((value) => value.messages),
          ),
        )
      }
    
      async loadSwarmUntil(
        accountId: string,
        conversationId: string,
        fromMessage: string,
        toMessage: string,
      ): Promise<Message[]> {
        const requestId = this.jamiSwig.loadSwarmUntil(accountId, conversationId, fromMessage, toMessage)
        return firstValueFrom(
          this.events.onSwarmLoaded.pipe(
            filter((value) => value.id === requestId),
            map((value) => value.messages),
          ),
        )
      }
    
      removeConversation(accountId: string, conversationId: string) {
        this.jamiSwig.removeConversation(accountId, conversationId)
      }
    
      getConversationRequests(accountId: string): ConversationRequestMetadata[] {
        return vectMapToRecordArray(
          this.jamiSwig.getConversationRequests(accountId),
        ) as unknown as ConversationRequestMetadata[]
      }
    
      acceptConversationRequest(accountId: string, conversationId: string): Promise<ConversationReady> {
        this.jamiSwig.acceptConversationRequest(accountId, conversationId)
        return firstValueFrom(
          this.events.onConversationReady.pipe(
            filter((value) => value.accountId === accountId),
            filter((value) => value.conversationId === conversationId),
          ),
        )
      }
    
      declineConversationRequest(accountId: string, conversationId: string): void {
        this.jamiSwig.declineConversationRequest(accountId, conversationId)
      }
    
      sendConversationMessage(
        accountId: string,
        conversationId: string,
        message: string,
        replyTo?: string,
        flag?: number,
      ): void {
        this.jamiSwig.sendMessage(accountId, conversationId, message, replyTo ?? '', flag ?? 0)
      }
    
      transferMessage(accountId: string, conversationId: string, message: string): void {
        this.jamiSwig.sendMessage(accountId, conversationId, message, '', 0)
      }
    
      deleteConversationMessage(accountId: string, conversationId: string, messageId: string): void {
        this.jamiSwig.sendMessage(accountId, conversationId, '', messageId, 1)
      }
    
      setIsComposing(accountId: string, conversationId: string, isWriting: boolean) {
        this.jamiSwig.setIsComposing(accountId, conversationId, isWriting)
      }
    
      setMessageDisplayed(accountId: string, conversationId: string, messageId: string, status: number): boolean {
        return this.jamiSwig.setMessageDisplayed(accountId, conversationId, messageId, status)
      }
    
      getCallIds(accountId: string): string[] {
        return stringVectToArray(this.jamiSwig.getCallList(accountId))
      }
    
      // TODO: Replace Record with interface
      getCallDetails(accountId: string, callId: string): Record<string, string> {
        return stringMapToRecord(this.jamiSwig.getCallDetails(accountId, callId))
      }
    
      getAccountIdFromUsername(username: string): string | undefined {
        return this.usernamesToAccountIds.get(username.toLowerCase())
      }
    
      // File transfer
      sendFile(accountId: string, conversationId: string, path: string, displayName: string, replyTo: string): void {
        this.jamiSwig.sendFile(accountId, conversationId, path, displayName, replyTo)
      }
    
      downloadFile(accountId: string, conversationId: string, interactionId: string, fileId: string, path: string): number {
        return this.jamiSwig.downloadFile(accountId, conversationId, interactionId, fileId, path)
      }
    
      cancelDataTransfer(accountId: string, conversationId: string, fileId: string): DataTransferError {
        return this.jamiSwig.cancelDataTransfer(accountId, conversationId, fileId)
      }
    
      getFileTransferInfo(
        accountId: string,
        conversationId: string,
        fileId: string,
        pathOut: string,
        totalOut: number,
        progressOut: number,
      ): DataTransferError {
        return this.jamiSwig.fileTransferInfo(accountId, conversationId, fileId, pathOut, totalOut, progressOut)
      }
    
      async waitForDataTransferEvent(
        accountId: string,
        conversationId: string,
        interactionId: string,
        fileId: string,
      ): Promise<DataTransferEvent> {
        return firstValueFrom(
          this.events.onDataTransferEvent.pipe(
            filter(
              (event) =>
                event.accountId === accountId &&
                event.conversationId === conversationId &&
                event.interactionId === interactionId &&
                event.fileId === fileId,
            ),
          ),
        )
      }
    
      publish(accountId: string, status: boolean, note: string): void {
        this.jamiSwig.publish(accountId, status, note)
      }
    
      answerServerRequest(uri: string, flag: boolean) {
        this.jamiSwig.answerServerRequest(uri, flag)
      }
    
      subscribeBuddy(accountId: string, uri: string, flag: boolean): void {
        this.jamiSwig.subscribeBuddy(accountId, uri, flag)
      }
    
      getSubscriptions(accountId: string) {
        return vectMapToRecordArray(this.jamiSwig.getSubscriptions(accountId))
      }
    
      setSubscriptions(accountId: string, uris: string[]): void {
        this.jamiSwig.setSubscriptions(accountId, uris)
      }
    
      private setupSignalHandlers(): void {
        this.events.onSubscriptionStateChanged.subscribe((signal) => {
          log.debug('Received SubscriptionStateChanged:', JSON.stringify(signal))
        })
    
        this.events.onNearbyPeerNotification.subscribe((signal) => {
          log.debug('Received NearbyPeerNotification:', JSON.stringify(signal))
        })
    
        this.events.onNewBuddyNotification.subscribe((signal) => {
          log.debug('Received NewBuddyNotification:', JSON.stringify(signal))
        })
    
        this.events.onNewServerSubscriptionRequest.subscribe((signal) => {
          log.debug('Received NewServerSubscriptionRequest:', JSON.stringify(signal))
        })
    
        this.events.onServerError.subscribe((signal) => {
          log.debug('Received ServerError:', JSON.stringify(signal))
        })
    
        this.events.onDataTransferEvent.subscribe((signal) => {
          log.debug('Received DataTransferEvent:', JSON.stringify(signal))
    
          if (signal.eventCode === DataTransferEventCode.created) {
            // It is now safe to delete the associated file
            // in fileId we have the path to the file that we provided
            // to the sendFile function
            const filePath = path.resolve(signal.fileId)
    
            if (filePath.includes('.local/share/jami/')) {
              // do not remove cache files
              // these are not tmp files
              return
            }
            if (fs.existsSync(filePath)) {
              fs.unlinkSync(filePath)
            }
          }
        })
    
        this.events.onDeviceRevocationEnded.subscribe((signal) => {
          log.debug('Received DeviceRevocationEnded:', JSON.stringify(signal))
        })
    
        this.events.onAccountsChanged.subscribe(() => {
          log.debug('Received AccountsChanged')
        })
    
        this.events.onAccountDetailsChanged.subscribe((signal) => {
          log.debug('Received AccountsDetailsChanged', JSON.stringify(signal))
          const data = {
            details: signal.details,
          }
          console.log('data', data)
          this.webSocketServer.send(signal.accountId, WebSocketMessageType.AccountDetails, data)
        })
    
        this.events.onAccountProfileReceived.subscribe((signal) => {
          log.debug('\nReceived AccountProfileReceived\n', JSON.stringify(signal))
        })
    
        this.events.onVolatileDetailsChanged.subscribe(({ accountId, details }) => {
          const username = details['Account.registeredName']
          log.debug(
            `Received VolatileDetailsChanged: {"accountId":"${accountId}",` +
              `"details":{"Account.registeredName":"${username}", ...}}`,
          )
    
          if (username) {
            // Keep map of usernames to account IDs
            this.usernamesToAccountIds.set(username.toLowerCase(), accountId)
          }
        })
    
        this.events.onRegistrationStateChanged.subscribe((signal) => {
          log.debug('Received RegistrationStateChanged:', JSON.stringify(signal))
        })
    
        this.events.onNameRegistrationEnded.subscribe((signal) => {
          log.debug('Received NameRegistrationEnded:', JSON.stringify(signal))
        })
    
        this.events.onRegisteredNameFound.subscribe((signal) => {
          log.debug('Received RegisteredNameFound:', JSON.stringify(signal))
        })
    
        this.events.onKnownDevicesChanged.subscribe((signal) => {
          log.debug(`Received KnownDevicesChanged: {"accountId":"${signal.accountId}", "devices": ...}`)
          this.webSocketServer.send(signal.accountId, WebSocketMessageType.KnownDevicesChanged, { devices: signal.devices })
        })
    
        this.events.onIncomingAccountMessage.subscribe(<T extends WebSocketMessageType>(signal: IncomingAccountMessage) => {
          log.debug('Received IncomingAccountMessage:', JSON.stringify(signal))
    
          const message: Partial<WebSocketMessage<T>> = JSON.parse(signal.payload['application/json'])
    
          if (typeof message !== 'object' || message === null) {
            log.warn('Account message is not an object')
            return
          }
    
          if (message.type === undefined || message.data === undefined) {
            log.warn('Account message is not a valid WebSocketMessage (missing type or data fields)')
            return
          }
    
          if (!Object.values(WebSocketMessageType).includes(message.type)) {
            log.warn(`Invalid WebSocket message type: ${message.type}`)
            return
          }
    
          this.webSocketServer.send(signal.accountId, message.type, message.data)
        })
    
        this.events.onAccountMessageStatusChanged.subscribe((signal) => {
          log.debug('Received AccountMessageStatusChanged:', JSON.stringify(signal))
          const data: AccountMessageStatus = {
            conversationId: signal.conversationId,
            peer: signal.peer,
            messageId: signal.messageId,
            status: Number(signal.state),
          }
          this.webSocketServer.send(signal.accountId, WebSocketMessageType.AccountMessageStatus, data)
        })
    
        this.events.onContactAdded.subscribe((signal) => {
          log.debug('Received ContactAdded:', JSON.stringify(signal))
        })
    
        this.events.onContactRemoved.subscribe((signal) => {
          log.debug('Received ContactRemoved:', JSON.stringify(signal))
        })
    
        this.events.onConversationRequestReceived.subscribe((signal) => {
          log.debug('Received ConversationRequestReceived:', JSON.stringify(signal))
          //this.webSocketServer.send(signal.accountId, WebSocketMessageType.ConversationRequest, data);
        })
    
        this.events.onConversationReady.subscribe((signal) => {
          log.debug('Received ConversationReady:', JSON.stringify(signal))
          this.webSocketServer.send(signal.accountId, WebSocketMessageType.ConversationReady, {
            conversationId: signal.conversationId,
          })
        })
    
        this.events.onConversationRemoved.subscribe((signal) => {
          log.debug('Received ConversationRemoved:', JSON.stringify(signal))
        })
    
        this.events.onConversationLoaded.subscribe(({ id, accountId, conversationId }) => {
          log.debug(
            `Received ConversationLoaded: {"id":"${id}","accountId":"${accountId}",` +
              `"conversationId":"${conversationId}"}`,
          )
        })
    
        this.events.onSwarmLoaded.subscribe(({ id, accountId, conversationId }) => {
          log.debug(
            `Received SwarmLoaded: {"id":"${id}","accountId":"${accountId}",` + `"conversationId":"${conversationId}"}`,
          )
        })
    
        this.events.onConversationMemberEvent.subscribe((signal) => {
          log.debug('Received onConversationMemberEvent:', JSON.stringify(signal))
          const data = {
            conversationId: signal.conversationId,
            member: signal.memberUri,
            event: signal.event,
          }
          this.webSocketServer.send(signal.accountId, WebSocketMessageType.ConversationMemberEvent, data)
        })
    
        this.events.onSwarmMessageReceived.subscribe((signal) => {
          log.debug('Received SwarmMessageReceived:', JSON.stringify(signal))
          const data: ConversationMessage = {
            conversationId: signal.conversationId,
            message: signal.message,
          }
          this.webSocketServer.send(signal.accountId, WebSocketMessageType.ConversationMessage, data)
        })
    
        this.events.onSwarmMessageUpdated.subscribe((signal) => {
          log.debug('Received SwarmMessageUpdated:', JSON.stringify(signal))
          const data: ConversationMessage = {
            conversationId: signal.conversationId,
            message: signal.message,
          }
          // if message was a file and it is deleted we need to delete it from the cache
          if (
            Array.isArray(data.message.editions) &&
            data.message.type === 'application/data-transfer+json' &&
            data.message.fileId === ''
          ) {
            if (data.message.editions.length === 0) {
              console.error('Editions array is empty')
            } else {
              const fileName = data.message.editions[data.message.editions.length - 1].fileId
              if (fileName === undefined) {
                console.error('FileId is undefined')
              } else {
                const filePath = path.join(
                  os.homedir(),
                  '.local',
                  'share',
                  'jami',
                  signal.accountId,
                  'conversation_data',
                  signal.conversationId,
                  fileName,
                )
                try {
                  fs.unlinkSync(filePath)
                } catch (error) {
                  console.error('Error deleting file:', error)
                }
              }
            }
          }
          this.webSocketServer.send(signal.accountId, WebSocketMessageType.MessageEdition, data)
        })
    
        this.events.onReactionAdded.subscribe((signal) => {
          log.debug('Received Reaction Added ' + JSON.stringify(signal))
          const data: ReactionAdded = {
            accountId: signal.accountId,
            conversationId: signal.conversationId,
            messageId: signal.messageId,
            reaction: signal.reaction,
          }
          this.webSocketServer.send(signal.accountId, WebSocketMessageType.ReactionAdded, data)
        })
    
        this.events.onReactionRemoved.subscribe((signal) => {
          log.debug('Received Reaction Removed  ' + JSON.stringify(signal))
          const data: ReactionRemoved = {
            accountId: signal.accountId,
            conversationId: signal.conversationId,
            messageId: signal.messageId,
            reactionId: signal.reactionId,
          }
          this.webSocketServer.send(signal.accountId, WebSocketMessageType.ReactionRemoved, data)
        })
    
        this.events.onProfileReceived.subscribe(async (signal) => {
          log.debug('Received ProfileReceived:', JSON.stringify(signal))
          const peer = signal.from
          const accountId = signal.accountId
          const folder = os.homedir() + '/.local/share/jami/' + accountId + '/cache/profile/'
          const fileName = btoa(peer)
          const displayNameFile = fileName + '.txt'
          const profilePictureFile = fileName + '.png'
          try {
            fs.unlinkSync(folder + profilePictureFile)
          } catch (e) {
            log.debug('Profile picture does not exist')
          }
          try {
            fs.unlinkSync(folder + displayNameFile)
          } catch (e) {
            log.debug('Display name does not exist')
          }
          await getAvatar(accountId, peer)
    
          this.webSocketServer.send(accountId, WebSocketMessageType.ProfileReceived, { peer })
        })
    
        this.events.onComposingStatusChanged.subscribe((signal) => {
          log.debug('Received ComposingStatusChanged:', JSON.stringify(signal))
          const data: ComposingStatus = {
            contactId: signal.from,
            conversationId: signal.conversationId,
            isWriting: signal.status === 1,
          }
    
          this.webSocketServer.send(signal.accountId, WebSocketMessageType.ComposingStatus, data)
        })
    
        this.events.onUserSearchEnded.subscribe((signal) => {
          log.debug('Received UserSearchEnded:', JSON.stringify(signal))
        })
      }
    }