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.
		
		
		
		
		
			
		
			
				
	
	
		
			347 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			TypeScript
		
	
			
		
		
	
	
			347 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			TypeScript
		
	
| import { ConversationModel, ConversationTypeEnum } from '../../models/conversation';
 | |
| import { ConversationController } from '../../session/conversations';
 | |
| import { PromiseUtils } from '../../session/utils';
 | |
| import { allowOnlyOneAtATime } from '../../session/utils/Promise';
 | |
| import { forceSyncConfigurationNowIfNeeded } from '../../session/utils/syncUtils';
 | |
| import { arrayBufferFromFile } from '../../types/Attachment';
 | |
| import { openGroupPrefix, prefixify } from '../utils/OpenGroupUtils';
 | |
| 
 | |
| interface OpenGroupParams {
 | |
|   server: string;
 | |
|   channel: number;
 | |
|   conversationId: string;
 | |
| }
 | |
| 
 | |
| export async function updateOpenGroupV1(convo: any, groupName: string, avatar: any) {
 | |
|   const API = await convo.getPublicSendData();
 | |
| 
 | |
|   if (avatar) {
 | |
|     // I hate duplicating this...
 | |
|     const readFile = async (attachment: any) =>
 | |
|       new Promise((resolve, reject) => {
 | |
|         const fileReader = new FileReader();
 | |
|         fileReader.onload = (e: any) => {
 | |
|           const data = e.target.result;
 | |
|           resolve({
 | |
|             ...attachment,
 | |
|             data,
 | |
|             size: data.byteLength,
 | |
|           });
 | |
|         };
 | |
|         fileReader.onerror = reject;
 | |
|         fileReader.onabort = reject;
 | |
|         fileReader.readAsArrayBuffer(attachment.file);
 | |
|       });
 | |
|     const avatarAttachment: any = await readFile({ file: avatar });
 | |
| 
 | |
|     // We want a square for iOS
 | |
|     const withBlob = await window.Signal.Util.AttachmentUtil.autoScale(
 | |
|       {
 | |
|         contentType: avatar.type,
 | |
|         file: new Blob([avatarAttachment.data], {
 | |
|           type: avatar.contentType,
 | |
|         }),
 | |
|       },
 | |
|       {
 | |
|         maxSide: 640,
 | |
|         maxSize: 1000 * 1024,
 | |
|       }
 | |
|     );
 | |
|     const dataResized = await arrayBufferFromFile(withBlob.file);
 | |
|     // const tempUrl = window.URL.createObjectURL(avatar);
 | |
| 
 | |
|     // Get file onto public chat server
 | |
|     const fileObj = await API.serverAPI.putAttachment(dataResized);
 | |
|     if (fileObj === null) {
 | |
|       // problem
 | |
|       window.log.warn('File upload failed');
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     // lets not allow ANY URLs, lets force it to be local to public chat server
 | |
|     const url = new URL(fileObj.url);
 | |
| 
 | |
|     // write it to the channel
 | |
|     await API.setChannelAvatar(url.pathname);
 | |
|   }
 | |
| 
 | |
|   if (await API.setChannelName(groupName)) {
 | |
|     // queue update from server
 | |
|     // and let that set the conversation
 | |
|     API.pollForChannelOnce();
 | |
|     // or we could just directly call
 | |
|     // convo.setGroupName(groupName);
 | |
|     // but gut is saying let the server be the definitive storage of the state
 | |
|     // and trickle down from there
 | |
|   }
 | |
| }
 | |
| 
 | |
| export class OpenGroup {
 | |
|   private static readonly serverRegex = new RegExp(
 | |
|     '^((https?:\\/\\/){0,1})([\\w-]{2,}\\.){1,2}[\\w-]{2,}$'
 | |
|   );
 | |
|   private static readonly groupIdRegex = new RegExp(
 | |
|     `^${openGroupPrefix}:[0-9]*@([\\w-]{2,}.){1,2}[\\w-]{2,}$`
 | |
|   );
 | |
| 
 | |
|   public readonly server: string;
 | |
|   public readonly channel: number;
 | |
|   public readonly groupId?: string;
 | |
|   public readonly conversationId: string;
 | |
| 
 | |
|   /**
 | |
|    * An OpenGroup object.
 | |
|    * If `params.server` is not valid, this will throw an `Error`.
 | |
|    *
 | |
|    * @param params.server The server URL. `https` will be prepended if `http` or `https` is not explicitly set
 | |
|    * @param params.channel The server channel
 | |
|    * @param params.groupId The string corresponding to the server. Eg. `${openGroupPrefix}1@chat.getsession.org`
 | |
|    * @param params.conversationId The conversation ID for the backbone model
 | |
|    */
 | |
|   constructor(params: OpenGroupParams) {
 | |
|     this.server = prefixify(params.server.toLowerCase());
 | |
| 
 | |
|     // Validate server format
 | |
|     const isValid = OpenGroup.serverRegex.test(this.server);
 | |
|     if (!isValid) {
 | |
|       throw Error('an invalid server or groupId was provided');
 | |
|     }
 | |
| 
 | |
|     this.channel = params.channel;
 | |
|     this.conversationId = params.conversationId;
 | |
|     this.groupId = OpenGroup.getGroupId(this.server, this.channel);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Validate the URL of an open group server
 | |
|    *
 | |
|    * @param serverUrl The server URL to validate
 | |
|    */
 | |
|   public static validate(serverUrl: string): boolean {
 | |
|     return this.serverRegex.test(serverUrl);
 | |
|   }
 | |
| 
 | |
|   public static getAllAlreadyJoinedOpenGroupsUrl(): Array<string> {
 | |
|     const convos = ConversationController.getInstance().getConversations();
 | |
|     return convos
 | |
|       .filter(c => !!c.get('active_at') && c.isPublic() && !c.get('left'))
 | |
|       .map(c => c.id.substring((c.id as string).lastIndexOf('@') + 1)) as Array<string>;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Try to make a new instance of `OpenGroup`.
 | |
|    * This does NOT respect `ConversationController` and does not guarentee the conversation's existence.
 | |
|    *
 | |
|    * @param groupId The string corresponding to the server. Eg. `${openGroupPrefix}1@chat.getsession.org`
 | |
|    * @param conversationId The conversation ID for the backbone model
 | |
|    * @returns `OpenGroup` if valid otherwise returns `undefined`.
 | |
|    */
 | |
|   public static from(
 | |
|     groupId: string,
 | |
|     conversationId: string,
 | |
|     hasSSL: boolean = true
 | |
|   ): OpenGroup | undefined {
 | |
|     const server = this.getServer(groupId, hasSSL);
 | |
|     const channel = this.getChannel(groupId);
 | |
| 
 | |
|     // Was groupId successfully utilized?
 | |
|     if (!server || !channel) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     const openGroupParams = {
 | |
|       server,
 | |
|       channel,
 | |
|       groupId,
 | |
|       conversationId,
 | |
|     } as OpenGroupParams;
 | |
| 
 | |
|     const isValid = OpenGroup.serverRegex.test(server);
 | |
|     if (!isValid) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     return new OpenGroup(openGroupParams);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Join an open group
 | |
|    *
 | |
|    * @param server The server URL
 | |
|    * @param onLoading Callback function to be called once server begins connecting
 | |
|    * @returns `OpenGroup` if connection success or if already connected
 | |
|    */
 | |
|   public static async join(server: string, fromSyncMessage: boolean = false): Promise<void> {
 | |
|     const prefixedServer = prefixify(server);
 | |
|     if (!OpenGroup.validate(server)) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     // Make this not hard coded
 | |
|     const channel = 1;
 | |
|     let conversation;
 | |
|     let conversationId;
 | |
| 
 | |
|     // Return OpenGroup if we're already connected
 | |
|     conversation = OpenGroup.getConversation(prefixedServer);
 | |
| 
 | |
|     if (conversation) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     // Try to connect to server
 | |
|     try {
 | |
|       conversation = await PromiseUtils.timeout(
 | |
|         OpenGroup.attemptConnectionOneAtATime(prefixedServer, channel),
 | |
|         20000
 | |
|       );
 | |
| 
 | |
|       if (!conversation) {
 | |
|         throw new Error(window.i18n('connectToServerFail'));
 | |
|       }
 | |
|       conversationId = (conversation as any)?.cid;
 | |
| 
 | |
|       // here we managed to connect to the group.
 | |
|       // if this is not a Sync Message, we should trigger one
 | |
|       if (!fromSyncMessage) {
 | |
|         await forceSyncConfigurationNowIfNeeded();
 | |
|       }
 | |
|     } catch (e) {
 | |
|       throw new Error(e);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Get the conversation model of a server from its URL
 | |
|    *
 | |
|    * @param server The server URL
 | |
|    * @returns BackBone conversation model corresponding to the server if it exists, otherwise `undefined`
 | |
|    */
 | |
|   public static getConversation(server: string): ConversationModel | undefined {
 | |
|     if (!OpenGroup.validate(server)) {
 | |
|       return;
 | |
|     }
 | |
|     const rawServerURL = server.replace(/^https?:\/\//i, '').replace(/[/\\]+$/i, '');
 | |
|     const channelId = 1;
 | |
|     const conversationId = `${openGroupPrefix}${channelId}@${rawServerURL}`;
 | |
| 
 | |
|     // Quickly peak to make sure we don't already have it
 | |
|     return ConversationController.getInstance().get(conversationId);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Check if the server exists.
 | |
|    * This does not compare against your conversations with the server.
 | |
|    *
 | |
|    * @param server The server URL
 | |
|    */
 | |
|   public static async serverExists(server: string): Promise<boolean> {
 | |
|     if (!OpenGroup.validate(server)) {
 | |
|       return false;
 | |
|     }
 | |
| 
 | |
|     const prefixedServer = prefixify(server);
 | |
|     return Boolean(await window.lokiPublicChatAPI.findOrCreateServer(prefixedServer));
 | |
|   }
 | |
| 
 | |
|   private static getServer(groupId: string, hasSSL: boolean): string | undefined {
 | |
|     const isValid = this.groupIdRegex.test(groupId);
 | |
|     const strippedServer = isValid ? groupId.split('@')[1] : undefined;
 | |
| 
 | |
|     // We don't know for sure if the server is https or http when taken from the groupId. Preifx accordingly.
 | |
|     return strippedServer ? prefixify(strippedServer.toLowerCase(), hasSSL) : undefined;
 | |
|   }
 | |
| 
 | |
|   private static getChannel(groupId: string): number | undefined {
 | |
|     const isValid = this.groupIdRegex.test(groupId);
 | |
|     const channelMatch = groupId.match(/^.*\:([0-9]*)\@.*$/);
 | |
| 
 | |
|     return channelMatch && isValid ? Number(channelMatch[1]) : undefined;
 | |
|   }
 | |
| 
 | |
|   private static getGroupId(server: string, channel: number): string {
 | |
|     // Server is already validated in constructor; no need to re-check
 | |
| 
 | |
|     // Strip server prefix
 | |
|     const prefixRegex = new RegExp('https?:\\/\\/');
 | |
|     const strippedServer = server.replace(prefixRegex, '');
 | |
| 
 | |
|     return `${openGroupPrefix}${channel}@${strippedServer}`;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * 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.
 | |
|    */
 | |
|   private static async attemptConnectionOneAtATime(
 | |
|     serverUrl: string,
 | |
|     channelId: number = 1
 | |
|   ): Promise<ConversationModel> {
 | |
|     if (!serverUrl) {
 | |
|       throw new Error('Cannot join open group with empty URL');
 | |
|     }
 | |
|     const oneAtaTimeStr = `oneAtaTimeOpenGroupJoin:${serverUrl}${channelId}`;
 | |
|     return allowOnlyOneAtATime(oneAtaTimeStr, async () => {
 | |
|       return OpenGroup.attemptConnection(serverUrl, channelId);
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   // Attempts a connection to an open group server
 | |
|   private static async attemptConnection(
 | |
|     serverUrl: string,
 | |
|     channelId: number
 | |
|   ): Promise<ConversationModel> {
 | |
|     let completeServerURL = serverUrl.toLowerCase();
 | |
|     const valid = OpenGroup.validate(completeServerURL);
 | |
|     if (!valid) {
 | |
|       return new Promise((_resolve, reject) => {
 | |
|         reject(window.i18n('connectToServerFail'));
 | |
|       });
 | |
|     }
 | |
| 
 | |
|     // Add http or https prefix to server
 | |
|     completeServerURL = prefixify(completeServerURL);
 | |
| 
 | |
|     const rawServerURL = serverUrl.replace(/^https?:\/\//i, '').replace(/[/\\]+$/i, '');
 | |
| 
 | |
|     const conversationId = `${openGroupPrefix}${channelId}@${rawServerURL}`;
 | |
| 
 | |
|     // Quickly peak to make sure we don't already have it
 | |
|     const conversationExists = ConversationController.getInstance().get(conversationId);
 | |
|     if (conversationExists) {
 | |
|       // We are already a member of this public chat
 | |
|       return new Promise((_resolve, reject) => {
 | |
|         reject(window.i18n('publicChatExists'));
 | |
|       });
 | |
|     }
 | |
| 
 | |
|     // Get server
 | |
|     const serverAPI = await window.lokiPublicChatAPI.findOrCreateServer(completeServerURL);
 | |
|     // SSL certificate failure or offline
 | |
|     if (!serverAPI) {
 | |
|       // Url incorrect or server not compatible
 | |
|       return new Promise((_resolve, reject) => {
 | |
|         reject(window.i18n('connectToServerFail'));
 | |
|       });
 | |
|     }
 | |
| 
 | |
|     // Create conversation
 | |
|     const conversation = await ConversationController.getInstance().getOrCreateAndWait(
 | |
|       conversationId,
 | |
|       ConversationTypeEnum.GROUP // keep a group for this one as this is an old open group
 | |
|     );
 | |
| 
 | |
|     // Convert conversation to a public one
 | |
|     await conversation.setPublicSource(completeServerURL, channelId);
 | |
| 
 | |
|     // and finally activate it
 | |
|     void conversation.getPublicSendData(); // may want "await" if you want to use the API
 | |
| 
 | |
|     return conversation;
 | |
|   }
 | |
| }
 |