You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			211 lines
		
	
	
		
			6.9 KiB
		
	
	
	
		
			TypeScript
		
	
			
		
		
	
	
			211 lines
		
	
	
		
			6.9 KiB
		
	
	
	
		
			TypeScript
		
	
import {
 | 
						|
  getAllOpenGroupV2Conversations,
 | 
						|
  getAllV2OpenGroupRooms,
 | 
						|
  OpenGroupV2Room,
 | 
						|
  removeV2OpenGroupRoom,
 | 
						|
  saveV2OpenGroupRoom,
 | 
						|
} from '../../data/opengroups';
 | 
						|
import { ConversationModel, ConversationTypeEnum } from '../../models/conversation';
 | 
						|
import { ConversationController } from '../../session/conversations';
 | 
						|
import { allowOnlyOneAtATime } from '../../session/utils/Promise';
 | 
						|
import { getOpenGroupV2ConversationId } from '../utils/OpenGroupUtils';
 | 
						|
import { OpenGroupRequestCommonType } from './ApiUtil';
 | 
						|
import { openGroupV2GetRoomInfo } from './OpenGroupAPIV2';
 | 
						|
import { OpenGroupServerPoller } from './OpenGroupServerPoller';
 | 
						|
 | 
						|
import _ from 'lodash';
 | 
						|
import { deleteAuthToken } from './ApiAuth';
 | 
						|
 | 
						|
export class OpenGroupManagerV2 {
 | 
						|
  public static readonly useV2OpenGroups = false;
 | 
						|
 | 
						|
  private static instance: OpenGroupManagerV2;
 | 
						|
 | 
						|
  /**
 | 
						|
   * The map of opengroup pollers, by serverUrl.
 | 
						|
   * A single poller polls for every room on the specified serverUrl
 | 
						|
   */
 | 
						|
  private readonly pollers: Map<string, OpenGroupServerPoller> = new Map();
 | 
						|
  private isPolling = false;
 | 
						|
 | 
						|
  private constructor() {
 | 
						|
    this.startPollingBouncy = this.startPollingBouncy.bind(this);
 | 
						|
    this.attemptConnectionV2 = this.attemptConnectionV2.bind(this);
 | 
						|
  }
 | 
						|
 | 
						|
  public static getInstance() {
 | 
						|
    if (!OpenGroupManagerV2.instance) {
 | 
						|
      OpenGroupManagerV2.instance = new OpenGroupManagerV2();
 | 
						|
    }
 | 
						|
    return OpenGroupManagerV2.instance;
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * When we get our configuration from the network, we might get a few times the same open group on two different messages.
 | 
						|
   * If we don't do anything, we will join them multiple times.
 | 
						|
   * Even if the convo exists only once, the lokiPublicChat API will have several instances polling for the same open group.
 | 
						|
   * Which will cause a lot of duplicate messages as they will be merged on a single conversation.
 | 
						|
   *
 | 
						|
   * To avoid this issue, we allow only a single join of a specific opengroup at a time.
 | 
						|
   */
 | 
						|
  public async attemptConnectionV2OneAtATime(
 | 
						|
    serverUrl: string,
 | 
						|
    roomId: string,
 | 
						|
    publicKey: string
 | 
						|
  ): Promise<ConversationModel> {
 | 
						|
    const oneAtaTimeStr = `oneAtaTimeOpenGroupV2Join:${serverUrl}${roomId}`;
 | 
						|
    return allowOnlyOneAtATime(oneAtaTimeStr, async () => {
 | 
						|
      return this.attemptConnectionV2(serverUrl, roomId, publicKey);
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  public async startPolling() {
 | 
						|
    await allowOnlyOneAtATime('V2ManagerStartPolling', this.startPollingBouncy);
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * This is not designed to be restarted for now. If you stop polling
 | 
						|
   */
 | 
						|
  public stopPolling() {
 | 
						|
    if (!this.isPolling) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    // the stop call calls the abortController, which will effectively cancel the request right away,
 | 
						|
    // or drop the result from it.
 | 
						|
    this.pollers.forEach(poller => {
 | 
						|
      poller.stop();
 | 
						|
    });
 | 
						|
    this.pollers.clear();
 | 
						|
 | 
						|
    this.isPolling = false;
 | 
						|
  }
 | 
						|
 | 
						|
  public addRoomToPolledRooms(roomInfos: OpenGroupRequestCommonType) {
 | 
						|
    const poller = this.pollers.get(roomInfos.serverUrl);
 | 
						|
    if (!poller) {
 | 
						|
      this.pollers.set(roomInfos.serverUrl, new OpenGroupServerPoller([roomInfos]));
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    // this won't do a thing if the room is already polled for
 | 
						|
    poller.addRoomToPoll(roomInfos);
 | 
						|
  }
 | 
						|
 | 
						|
  public removeRoomFromPolledRooms(roomInfos: OpenGroupRequestCommonType) {
 | 
						|
    const poller = this.pollers.get(roomInfos.serverUrl);
 | 
						|
    if (!poller) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    // this won't do a thing if the room is already polled for
 | 
						|
    poller.removeRoomFromPoll(roomInfos);
 | 
						|
    if (poller.getPolledRoomsCount() === 0) {
 | 
						|
      this.pollers.delete(roomInfos.serverUrl);
 | 
						|
      // this poller is not needed anymore, kill it
 | 
						|
      poller.stop();
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * This function is private because we want to make sure it only runs once at a time.
 | 
						|
   */
 | 
						|
  private async startPollingBouncy() {
 | 
						|
    if (this.isPolling) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    const allConvos = await getAllOpenGroupV2Conversations();
 | 
						|
    let allRoomInfos = await getAllV2OpenGroupRooms();
 | 
						|
 | 
						|
    // this is time for some cleanup!
 | 
						|
    // We consider the conversations are our source-of-truth,
 | 
						|
    // so if there is a roomInfo without an associated convo, we remove it
 | 
						|
    if (allRoomInfos) {
 | 
						|
      await Promise.all(
 | 
						|
        [...allRoomInfos.values()].map(async infos => {
 | 
						|
          try {
 | 
						|
            const roomConvoId = getOpenGroupV2ConversationId(infos.serverUrl, infos.roomId);
 | 
						|
            if (!allConvos.get(roomConvoId)) {
 | 
						|
              // leave the group on the remote server
 | 
						|
              // this request doesn't throw
 | 
						|
              await deleteAuthToken(_.pick(infos, 'serverUrl', 'roomId'));
 | 
						|
              // remove the roomInfos locally for this open group room
 | 
						|
              await removeV2OpenGroupRoom(roomConvoId);
 | 
						|
              // no need to remove it from the ConversationController, the convo is already not there
 | 
						|
            }
 | 
						|
          } catch (e) {
 | 
						|
            window.log.warn('cleanup roomInfos error', e);
 | 
						|
          }
 | 
						|
        })
 | 
						|
      );
 | 
						|
    }
 | 
						|
    // refresh our roomInfos list
 | 
						|
    allRoomInfos = await getAllV2OpenGroupRooms();
 | 
						|
    if (allRoomInfos) {
 | 
						|
      allRoomInfos.forEach(infos => {
 | 
						|
        this.addRoomToPolledRooms(infos);
 | 
						|
      });
 | 
						|
    }
 | 
						|
 | 
						|
    this.isPolling = true;
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   *
 | 
						|
   * @param serverUrl with protocol, hostname and port included
 | 
						|
   */
 | 
						|
  private async attemptConnectionV2(
 | 
						|
    serverUrl: string,
 | 
						|
    roomId: string,
 | 
						|
    serverPublicKey: string
 | 
						|
  ): Promise<ConversationModel | undefined> {
 | 
						|
    const conversationId = getOpenGroupV2ConversationId(serverUrl, roomId);
 | 
						|
 | 
						|
    if (ConversationController.getInstance().get(conversationId)) {
 | 
						|
      // Url incorrect or server not compatible
 | 
						|
      throw new Error(window.i18n('publicChatExists'));
 | 
						|
    }
 | 
						|
 | 
						|
    // here, the convo does not exist. Make sure the db is clean too
 | 
						|
    await removeV2OpenGroupRoom(conversationId);
 | 
						|
 | 
						|
    const room: OpenGroupV2Room = {
 | 
						|
      serverUrl,
 | 
						|
      roomId,
 | 
						|
      conversationId,
 | 
						|
      serverPublicKey,
 | 
						|
    };
 | 
						|
 | 
						|
    try {
 | 
						|
      // save the pubkey to the db right now, the request for room Info
 | 
						|
      // will need it and access it from the db
 | 
						|
      await saveV2OpenGroupRoom(room);
 | 
						|
      const roomInfos = await openGroupV2GetRoomInfo({ roomId, serverUrl });
 | 
						|
      if (!roomInfos) {
 | 
						|
        throw new Error('Invalid open group roomInfo result');
 | 
						|
      }
 | 
						|
      const conversation = await ConversationController.getInstance().getOrCreateAndWait(
 | 
						|
        conversationId,
 | 
						|
        ConversationTypeEnum.GROUP
 | 
						|
      );
 | 
						|
      room.imageID = roomInfos.imageId || undefined;
 | 
						|
      room.roomName = roomInfos.name || undefined;
 | 
						|
      await saveV2OpenGroupRoom(room);
 | 
						|
 | 
						|
      // mark active so it's not in the contacts list but in the conversation list
 | 
						|
      conversation.set({
 | 
						|
        active_at: Date.now(),
 | 
						|
        name: room.roomName,
 | 
						|
        avatarPath: room.roomName,
 | 
						|
      });
 | 
						|
      await conversation.commit();
 | 
						|
 | 
						|
      // start polling this room
 | 
						|
      this.addRoomToPolledRooms(room);
 | 
						|
 | 
						|
      return conversation;
 | 
						|
    } catch (e) {
 | 
						|
      window.log.warn('Failed to join open group v2', e);
 | 
						|
      await removeV2OpenGroupRoom(conversationId);
 | 
						|
      throw new Error(window.i18n('connectToServerFail'));
 | 
						|
    }
 | 
						|
  }
 | 
						|
}
 |