remove most of sogsv1 and fsv1 and update version fetch url
version fetch is used to know when we have a new updatepull/1671/head
parent
3456162402
commit
6f8c8ee1b3
@ -0,0 +1,69 @@
|
|||||||
|
|
||||||
|
A TESTER
|
||||||
|
* MENTIONS opengroupv2
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
lokiPublicChatAPI
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
dot_net_api
|
||||||
|
loki_public_chat_api
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
OpenGroupUtils
|
||||||
|
|
||||||
|
OpenGroup
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
channelAPI.sendMessage
|
||||||
|
|
||||||
|
|
||||||
|
getAllOpenGroupV1Conversations => migration to remove opengroupsv1 matching urls
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
DELETED
|
||||||
|
tokenlessFileServerAdnAPI
|
||||||
|
channelId
|
||||||
|
publicChat with only a cahnnel of 1
|
||||||
|
loki_file_server_api
|
||||||
|
LokiAppDotNetApi
|
||||||
|
LokiPublicChatFactoryAPI
|
||||||
|
lastPublicMessage
|
||||||
|
getLastRetrievedMessage
|
||||||
|
setLastRetrievedMessage
|
||||||
|
attemptConnection
|
||||||
|
attemptConnectionOneAtATime
|
||||||
|
|
||||||
|
getPublicSendData
|
||||||
|
setPublicSource
|
||||||
|
getPublicSource
|
||||||
|
findOrCreateServer
|
||||||
|
initAPIs
|
||||||
|
OpenGroup.
|
||||||
|
findOrCreateChannel
|
||||||
|
initSpecialConversations
|
||||||
|
updateOpenGroupV1
|
||||||
|
partChannel
|
||||||
|
acceptOpenGroupInvitationV1
|
||||||
|
pollForChannelOnce
|
||||||
|
publicConversatio
|
||||||
|
handleOpenGroupJoinV1
|
||||||
|
setChannelName
|
||||||
|
setChannelAvatar
|
||||||
|
isOpenGroupV1
|
||||||
|
toOpenGroupV1
|
||||||
|
setListOfMembers
|
||||||
|
|
||||||
|
|
||||||
|
lokiFileServerAPI
|
||||||
|
lokiFileServerAPIFactory
|
File diff suppressed because it is too large
Load Diff
@ -1,3 +0,0 @@
|
|||||||
interface LokiFileServerInstance {
|
|
||||||
downloadAttachment(url: string): Promise<ArrayBuffer>;
|
|
||||||
}
|
|
@ -1,89 +0,0 @@
|
|||||||
/* global log, window */
|
|
||||||
/* global log: false */
|
|
||||||
|
|
||||||
const LokiAppDotNetAPI = require('./loki_app_dot_net_api');
|
|
||||||
|
|
||||||
// can have multiple of these instances as each user can have a
|
|
||||||
// different home server
|
|
||||||
class LokiFileServerInstance {
|
|
||||||
constructor(ourKey) {
|
|
||||||
this.ourKey = ourKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME: this is not file-server specific
|
|
||||||
// and is currently called by LokiAppDotNetAPI.
|
|
||||||
// LokiAppDotNetAPI (base) should not know about LokiFileServer.
|
|
||||||
async establishConnection(serverUrl, options) {
|
|
||||||
// why don't we extend this?
|
|
||||||
this._server = new LokiAppDotNetAPI(this.ourKey, serverUrl);
|
|
||||||
|
|
||||||
// make sure pubKey & pubKeyHex are set in _server
|
|
||||||
this.pubKey = this._server.getPubKeyForUrl();
|
|
||||||
|
|
||||||
if (options !== undefined && options.skipToken) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// get a token for multidevice
|
|
||||||
const gotToken = await this._server.getOrRefreshServerToken();
|
|
||||||
// TODO: Handle this failure gracefully
|
|
||||||
if (!gotToken) {
|
|
||||||
log.error('You are blacklisted form this home server');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// for files
|
|
||||||
async downloadAttachment(url) {
|
|
||||||
return this._server.downloadAttachment(url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// extends LokiFileServerInstance with functions we'd only perform on our own home server
|
|
||||||
// so we don't accidentally send info to the wrong file server
|
|
||||||
class LokiHomeServerInstance extends LokiFileServerInstance {
|
|
||||||
// you only upload to your own home server
|
|
||||||
// you can download from any server...
|
|
||||||
uploadAvatar(data) {
|
|
||||||
if (!this._server.token) {
|
|
||||||
log.warn('uploadAvatar no token yet');
|
|
||||||
}
|
|
||||||
return this._server.uploadAvatar(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
static uploadPrivateAttachment(data) {
|
|
||||||
return window.tokenlessFileServerAdnAPI.uploadData(data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// this will be our instance factory
|
|
||||||
class LokiFileServerFactoryAPI {
|
|
||||||
constructor(ourKey) {
|
|
||||||
this.ourKey = ourKey;
|
|
||||||
this.servers = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
establishHomeConnection(serverUrl) {
|
|
||||||
let thisServer = this.servers.find(server => server._server.baseServerUrl === serverUrl);
|
|
||||||
if (!thisServer) {
|
|
||||||
thisServer = new LokiHomeServerInstance(this.ourKey);
|
|
||||||
log.info(`Registering HomeServer ${serverUrl}`);
|
|
||||||
// not await, so a failure or slow connection doesn't hinder loading of the app
|
|
||||||
thisServer.establishConnection(serverUrl);
|
|
||||||
this.servers.push(thisServer);
|
|
||||||
}
|
|
||||||
return thisServer;
|
|
||||||
}
|
|
||||||
|
|
||||||
async establishConnection(serverUrl) {
|
|
||||||
let thisServer = this.servers.find(server => server._server.baseServerUrl === serverUrl);
|
|
||||||
if (!thisServer) {
|
|
||||||
thisServer = new LokiFileServerInstance(this.ourKey);
|
|
||||||
log.info(`Registering FileServer ${serverUrl}`);
|
|
||||||
await thisServer.establishConnection(serverUrl, { skipToken: true });
|
|
||||||
this.servers.push(thisServer);
|
|
||||||
}
|
|
||||||
return thisServer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = LokiFileServerFactoryAPI;
|
|
@ -1,20 +0,0 @@
|
|||||||
import { LokiAppDotNetServerInterface, LokiPublicChannelAPI } from './loki_app_dot_net_api';
|
|
||||||
|
|
||||||
export interface LokiPublicChatFactoryInterface {
|
|
||||||
ourKey: string;
|
|
||||||
openGroupPubKeys: { [key: string]: string };
|
|
||||||
findOrCreateServer(url: string): Promise<LokiAppDotNetServerInterface | null>;
|
|
||||||
findOrCreateChannel(
|
|
||||||
url: string,
|
|
||||||
channelId: number,
|
|
||||||
conversationId: string
|
|
||||||
): Promise<LokiPublicChannelAPI | null>;
|
|
||||||
getListOfMembers(): Promise<Array<{ authorPhoneNumber: string; authorProfileName?: string }>>;
|
|
||||||
setListOfMembers(members: Array<{ authorPhoneNumber: string; authorProfileName?: string }>);
|
|
||||||
}
|
|
||||||
|
|
||||||
declare class LokiPublicChatFactoryAPI implements LokiPublicChatFactoryInterface {
|
|
||||||
constructor(ourKey: string);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default LokiPublicChatFactoryAPI;
|
|
@ -1,149 +0,0 @@
|
|||||||
/* global log, process */
|
|
||||||
const EventEmitter = require('events');
|
|
||||||
const LokiAppDotNetAPI = require('./loki_app_dot_net_api');
|
|
||||||
const OpenGroupUtils = require('../../ts/opengroup/utils/OpenGroupUtils');
|
|
||||||
|
|
||||||
class LokiPublicChatFactoryAPI extends EventEmitter {
|
|
||||||
constructor(ourKey) {
|
|
||||||
super();
|
|
||||||
this.ourKey = ourKey;
|
|
||||||
this.servers = [];
|
|
||||||
this.allMembers = [];
|
|
||||||
this.openGroupPubKeys = {};
|
|
||||||
// Multidevice states
|
|
||||||
}
|
|
||||||
|
|
||||||
// MessageReceiver.connect calls this
|
|
||||||
// start polling in all existing registered channels
|
|
||||||
async open() {
|
|
||||||
await Promise.all(this.servers.map(server => server.open()));
|
|
||||||
}
|
|
||||||
|
|
||||||
// MessageReceiver.close
|
|
||||||
async close() {
|
|
||||||
await Promise.all(this.servers.map(server => server.close()));
|
|
||||||
}
|
|
||||||
|
|
||||||
// server getter/factory
|
|
||||||
async findOrCreateServer(serverUrl) {
|
|
||||||
let thisServer = this.servers.find(server => server.baseServerUrl === serverUrl);
|
|
||||||
if (!thisServer) {
|
|
||||||
log.info(`loki_public_chat::findOrCreateServer - creating ${serverUrl}`);
|
|
||||||
|
|
||||||
const serverIsValid = await OpenGroupUtils.validOpenGroupServer(serverUrl);
|
|
||||||
if (!serverIsValid) {
|
|
||||||
// FIXME: add toast?
|
|
||||||
log.error(`loki_public_chat::findOrCreateServer - error: ${serverUrl} is not valid`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// after verification then we can start up all the pollers
|
|
||||||
if (process.env.USE_STUBBED_NETWORK) {
|
|
||||||
// eslint-disable-next-line global-require
|
|
||||||
const StubAppDotNetAPI = require('../.././ts/test/session/integration/stubs/stub_app_dot_net_api');
|
|
||||||
thisServer = new StubAppDotNetAPI(this.ourKey, serverUrl);
|
|
||||||
} else {
|
|
||||||
thisServer = new LokiAppDotNetAPI(this.ourKey, serverUrl);
|
|
||||||
if (this.openGroupPubKeys[serverUrl]) {
|
|
||||||
thisServer.getPubKeyForUrl();
|
|
||||||
if (!thisServer.pubKeyHex) {
|
|
||||||
log.warn(`loki_public_chat::findOrCreateServer - failed to set public key`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const gotToken = await thisServer.getOrRefreshServerToken();
|
|
||||||
if (!gotToken) {
|
|
||||||
log.warn(`loki_public_chat::findOrCreateServer - Invalid server ${serverUrl}`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.servers.push(thisServer);
|
|
||||||
}
|
|
||||||
return thisServer;
|
|
||||||
}
|
|
||||||
|
|
||||||
// channel getter/factory
|
|
||||||
async findOrCreateChannel(serverUrl, channelId, conversationId) {
|
|
||||||
const server = await this.findOrCreateServer(serverUrl);
|
|
||||||
if (!server) {
|
|
||||||
log.error(`Failed to create server for: ${serverUrl}`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return server.findOrCreateChannel(this, channelId, conversationId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// deallocate resources server uses
|
|
||||||
unregisterChannel(serverUrl, channelId) {
|
|
||||||
const i = this.servers.findIndex(server => server.baseServerUrl === serverUrl);
|
|
||||||
if (i === -1) {
|
|
||||||
log.warn(`Tried to unregister from nonexistent server ${serverUrl}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const thisServer = this.servers[i];
|
|
||||||
if (!thisServer) {
|
|
||||||
log.warn(`Tried to unregister from nonexistent server ${i}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
thisServer.unregisterChannel(channelId);
|
|
||||||
this.servers.splice(i, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// shouldn't this be scoped per conversation?
|
|
||||||
async getListOfMembers() {
|
|
||||||
// enable in the next release
|
|
||||||
/*
|
|
||||||
let members = [];
|
|
||||||
await Promise.all(this.servers.map(async server => {
|
|
||||||
await Promise.all(server.channels.map(async channel => {
|
|
||||||
const newMembers = await channel.getSubscribers();
|
|
||||||
members = [...members, ...newMembers];
|
|
||||||
}));
|
|
||||||
}));
|
|
||||||
const results = members.map(member => {
|
|
||||||
return { authorPhoneNumber: member.username };
|
|
||||||
});
|
|
||||||
*/
|
|
||||||
return this.allMembers;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: make this private (or remove altogether) when
|
|
||||||
// we switch to polling the server for group members
|
|
||||||
setListOfMembers(members) {
|
|
||||||
this.allMembers = members;
|
|
||||||
}
|
|
||||||
|
|
||||||
async setProfileName(profileName) {
|
|
||||||
await Promise.all(
|
|
||||||
this.servers.map(async server => {
|
|
||||||
await server.setProfileName(profileName);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async setHomeServer(homeServer) {
|
|
||||||
await Promise.all(
|
|
||||||
this.servers.map(async server => {
|
|
||||||
// this may fail
|
|
||||||
// but we can't create a sql table to remember to retry forever
|
|
||||||
// I think we just silently fail for now
|
|
||||||
await server.setHomeServer(homeServer);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async setAvatar(url, profileKey) {
|
|
||||||
await Promise.all(
|
|
||||||
this.servers.map(async server => {
|
|
||||||
// this may fail
|
|
||||||
// but we can't create a sql table to remember to retry forever
|
|
||||||
// I think we just silently fail for now
|
|
||||||
await server.setAvatar(url, profileKey);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// These files are expected to be in commonjs so we can't use es6 syntax :(
|
|
||||||
// If we move these to TS then we should be able to use es6
|
|
||||||
module.exports = LokiPublicChatFactoryAPI;
|
|
@ -1,98 +0,0 @@
|
|||||||
/* global window: false */
|
|
||||||
/* global callWorker: false */
|
|
||||||
/* global textsecure: false */
|
|
||||||
/* global Event: false */
|
|
||||||
/* global dcodeIO: false */
|
|
||||||
/* global lokiPublicChatAPI: false */
|
|
||||||
|
|
||||||
/* eslint-disable more/no-then */
|
|
||||||
/* eslint-disable no-unreachable */
|
|
||||||
|
|
||||||
let openGroupBound = false;
|
|
||||||
|
|
||||||
function MessageReceiver() {
|
|
||||||
this.pending = Promise.resolve();
|
|
||||||
|
|
||||||
// only do this once to prevent duplicates
|
|
||||||
if (lokiPublicChatAPI) {
|
|
||||||
window.log.info('Binding open group events handler', openGroupBound);
|
|
||||||
if (!openGroupBound) {
|
|
||||||
openGroupBound = true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
window.log.warn('Can not handle open group data, API is not available');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MessageReceiver.stringToArrayBuffer = string =>
|
|
||||||
Promise.resolve(dcodeIO.ByteBuffer.wrap(string, 'binary').toArrayBuffer());
|
|
||||||
MessageReceiver.arrayBufferToString = arrayBuffer =>
|
|
||||||
Promise.resolve(dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('binary'));
|
|
||||||
MessageReceiver.arrayBufferToStringBase64 = arrayBuffer =>
|
|
||||||
callWorker('arrayBufferToStringBase64', arrayBuffer);
|
|
||||||
|
|
||||||
MessageReceiver.prototype = new textsecure.EventTarget();
|
|
||||||
MessageReceiver.prototype.extend({
|
|
||||||
constructor: MessageReceiver,
|
|
||||||
connect() {
|
|
||||||
if (this.calledClose) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.hasConnected) {
|
|
||||||
const ev = new Event('reconnect');
|
|
||||||
this.dispatchEvent(ev);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.hasConnected = true;
|
|
||||||
|
|
||||||
// start polling all open group rooms you have registered
|
|
||||||
// if not registered yet, they'll get started when they're created
|
|
||||||
if (lokiPublicChatAPI) {
|
|
||||||
lokiPublicChatAPI.open();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensures that an immediate 'empty' event from the websocket will fire only after
|
|
||||||
// all cached envelopes are processed.
|
|
||||||
this.incoming = [this.pending];
|
|
||||||
},
|
|
||||||
stopProcessing() {
|
|
||||||
window.log.info('MessageReceiver: stopProcessing requested');
|
|
||||||
this.stoppingProcessing = true;
|
|
||||||
return this.close();
|
|
||||||
},
|
|
||||||
shutdown() {},
|
|
||||||
async close() {
|
|
||||||
window.log.info('MessageReceiver.close()');
|
|
||||||
this.calledClose = true;
|
|
||||||
|
|
||||||
// stop polling all open group rooms
|
|
||||||
if (lokiPublicChatAPI) {
|
|
||||||
await lokiPublicChatAPI.close();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onopen() {},
|
|
||||||
onerror() {},
|
|
||||||
onclose() {},
|
|
||||||
});
|
|
||||||
|
|
||||||
window.textsecure = window.textsecure || {};
|
|
||||||
|
|
||||||
textsecure.MessageReceiver = function MessageReceiverWrapper() {
|
|
||||||
const messageReceiver = new MessageReceiver();
|
|
||||||
this.addEventListener = messageReceiver.addEventListener.bind(messageReceiver);
|
|
||||||
this.removeEventListener = messageReceiver.removeEventListener.bind(messageReceiver);
|
|
||||||
this.close = messageReceiver.close.bind(messageReceiver);
|
|
||||||
|
|
||||||
this.stopProcessing = messageReceiver.stopProcessing.bind(messageReceiver);
|
|
||||||
|
|
||||||
messageReceiver.connect();
|
|
||||||
};
|
|
||||||
|
|
||||||
textsecure.MessageReceiver.prototype = {
|
|
||||||
constructor: textsecure.MessageReceiver,
|
|
||||||
};
|
|
||||||
|
|
||||||
textsecure.MessageReceiver.stringToArrayBuffer = MessageReceiver.stringToArrayBuffer;
|
|
||||||
textsecure.MessageReceiver.arrayBufferToString = MessageReceiver.arrayBufferToString;
|
|
||||||
textsecure.MessageReceiver.arrayBufferToStringBase64 = MessageReceiver.arrayBufferToStringBase64;
|
|
@ -1,346 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,157 +0,0 @@
|
|||||||
/* global clearTimeout, Buffer, TextDecoder, process */
|
|
||||||
|
|
||||||
import LokiAppDotNetServerAPI from '../../../../../js/modules/loki_app_dot_net_api';
|
|
||||||
|
|
||||||
const samplesGetMessages = {
|
|
||||||
meta: { code: 200 },
|
|
||||||
data: [
|
|
||||||
{
|
|
||||||
channel_id: 1,
|
|
||||||
created_at: '2020-03-18T04:48:44.000Z',
|
|
||||||
entities: {
|
|
||||||
mentions: [],
|
|
||||||
hashtags: [],
|
|
||||||
links: [],
|
|
||||||
},
|
|
||||||
id: 3662,
|
|
||||||
machine_only: false,
|
|
||||||
num_replies: 0,
|
|
||||||
source: {},
|
|
||||||
thread_id: 3662,
|
|
||||||
reply_to: null,
|
|
||||||
text: 'hgt',
|
|
||||||
html: '<span itemscope="https://app.net/schemas/Post">hgt</span>',
|
|
||||||
annotations: [
|
|
||||||
{
|
|
||||||
type: 'network.loki.messenger.publicChat',
|
|
||||||
value: {
|
|
||||||
timestamp: 1584506921361,
|
|
||||||
sig:
|
|
||||||
'262ab113810564d7ff6474dea264e10e2143d91c004903d06d8d9fddb5b74b2c6245865544d5cf76ee16a3fca045bc028a48c51f8a290508a29b6013d014dc83',
|
|
||||||
sigver: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
user: {
|
|
||||||
id: 2448,
|
|
||||||
username: '050cd79763303bcc251bd489a6f7da823a2b8555402b01a7959ebca550d048600f',
|
|
||||||
created_at: '2020-03-18T02:42:05.000Z',
|
|
||||||
canonical_url: null,
|
|
||||||
type: null,
|
|
||||||
timezone: null,
|
|
||||||
locale: null,
|
|
||||||
avatar_image: {
|
|
||||||
url: null,
|
|
||||||
width: null,
|
|
||||||
height: null,
|
|
||||||
is_default: false,
|
|
||||||
},
|
|
||||||
cover_image: {
|
|
||||||
url: null,
|
|
||||||
width: null,
|
|
||||||
height: null,
|
|
||||||
is_default: false,
|
|
||||||
},
|
|
||||||
counts: {
|
|
||||||
following: 0,
|
|
||||||
posts: 0,
|
|
||||||
followers: 0,
|
|
||||||
stars: 0,
|
|
||||||
},
|
|
||||||
name: 'asdf',
|
|
||||||
annotations: [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
class StubAppDotNetAPI extends LokiAppDotNetServerAPI {
|
|
||||||
// make a request to the server
|
|
||||||
public async serverRequest(endpoint: string, options: { method?: string } = {}) {
|
|
||||||
const { method } = options;
|
|
||||||
|
|
||||||
if (endpoint === 'channels/1/messages') {
|
|
||||||
if (!method) {
|
|
||||||
return {
|
|
||||||
statusCode: 200,
|
|
||||||
response: samplesGetMessages,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
statusCode: 200,
|
|
||||||
response: {
|
|
||||||
data: [],
|
|
||||||
meta: {
|
|
||||||
max_id: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (endpoint === 'loki/v1/channel/1/deletes' || endpoint === 'loki/v1/channel/1/moderators') {
|
|
||||||
return {
|
|
||||||
statusCode: 200,
|
|
||||||
response: {
|
|
||||||
data: [],
|
|
||||||
meta: {
|
|
||||||
max_id: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (endpoint === 'files') {
|
|
||||||
return {
|
|
||||||
statusCode: 200,
|
|
||||||
response: {
|
|
||||||
data: {
|
|
||||||
url: 'fakeurl',
|
|
||||||
id: 12345,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (endpoint === 'channels/1') {
|
|
||||||
let name = 'Unknown group';
|
|
||||||
if (this.baseServerUrl.includes('/chat-dev.lokinet.org')) {
|
|
||||||
name = 'Loki Dev Chat';
|
|
||||||
} else if (this.baseServerUrl.includes('/chat.getsession.org')) {
|
|
||||||
name = 'Session Public Chat';
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
statusCode: 200,
|
|
||||||
response: {
|
|
||||||
data: {
|
|
||||||
annotations: [
|
|
||||||
{
|
|
||||||
type: 'net.patter-app.settings',
|
|
||||||
value: {
|
|
||||||
name,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (endpoint === 'token') {
|
|
||||||
return {
|
|
||||||
statusCode: 200,
|
|
||||||
response: {
|
|
||||||
data: {
|
|
||||||
user: {
|
|
||||||
name: 'unknown name',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
statusCode: 200,
|
|
||||||
response: {},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = StubAppDotNetAPI;
|
|
Loading…
Reference in New Issue