add start of OpenGroup Pollers v2 to start of the app

This commit is contained in:
Audric Ackermann 2021-04-23 14:59:08 +10:00
parent 193fb2a101
commit 9d825dc2d2
No known key found for this signature in database
GPG Key ID: 999F434D76324AD4
16 changed files with 237 additions and 83 deletions

View File

@ -49,7 +49,8 @@ module.exports = {
updateConversation,
removeConversation,
getAllConversations,
getAllPublicConversations,
getAllOpenGroupV1Conversations,
getAllOpenGroupV2Conversations,
getPubkeysInPublicConversation,
getAllConversationIds,
getAllGroupsInvolvingId,
@ -1591,11 +1592,25 @@ async function getAllConversationIds() {
return map(rows, row => row.id);
}
async function getAllPublicConversations() {
async function getAllOpenGroupV1Conversations() {
const rows = await db.all(
`SELECT json FROM ${CONVERSATIONS_TABLE} WHERE
type = 'group' AND
id LIKE 'publicChat:%'
id LIKE 'publicChat:1@%'
ORDER BY id ASC;`
);
return map(rows, row => jsonToObject(row.json));
}
async function getAllOpenGroupV2Conversations() {
// first _ matches all opengroupv1,
// second _ force a second char to be there, so it can only be opengroupv2 convos
const rows = await db.all(
`SELECT json FROM ${CONVERSATIONS_TABLE} WHERE
type = 'group' AND
id LIKE 'publicChat:__%@%'
ORDER BY id ASC;`
);

View File

@ -106,7 +106,7 @@
if (specialConvInited) {
return;
}
const publicConversations = await window.Signal.Data.getAllPublicConversations();
const publicConversations = await window.Signal.Data.getAllOpenGroupV1Conversations();
publicConversations.forEach(conversation => {
// weird but create the object and does everything we need
conversation.getPublicSendData();

View File

@ -30,6 +30,7 @@ import { clearSearch } from '../../state/ducks/search';
import { showLeftPaneSection } from '../../state/ducks/section';
import { cleanUpOldDecryptedMedias } from '../../session/crypto/DecryptedAttachmentsManager';
import { OpenGroupManagerV2 } from '../../opengroup/opengroupV2/OpenGroupManagerV2';
// tslint:disable-next-line: no-import-side-effect no-submodule-imports
export enum SectionType {
@ -178,16 +179,8 @@ export const ActionsPanel = () => {
// 'http://sessionopengroup.com/main?public_key=658d29b91892a2389505596b135e76a53db6e11d613a51dbd3d0816adffb231b'
// 'https://sog.ibolpap.finance/main?public_key=b464aa186530c97d6bcf663a3a3b7465a5f782beaa67c83bee99468824b4aa10'
// 'https://opengroup.bilb.us/main?public_key=1352534ba73d4265973280431dbc72e097a3e43275d1ada984f9805b4943047d'
void OpenGroupManagerV2.getInstance().startPolling();
void syncConfiguration();
// const parsedRoom = parseOpenGroupV2(
// 'https://opengroup.bilb.us/main?public_key=1352534ba73d4265973280431dbc72e097a3e43275d1ada984f9805b4943047d'
// );
// if (parsedRoom) {
// setTimeout(async () => {
// }, 6000);
// }
}, []);
// wait for cleanUpMediasInterval and then start cleaning up medias

View File

@ -25,6 +25,11 @@ import { LeftPaneSectionHeader } from './LeftPaneSectionHeader';
import { ConversationController } from '../../session/conversations';
import { OpenGroup } from '../../opengroup/opengroupV1/OpenGroup';
import { ConversationType } from '../../models/conversation';
import {
getOpenGroupV2ConversationId,
openGroupV2CompleteURLRegex,
} from '../../opengroup/utils/OpenGroupUtils';
import { joinOpenGroupV2, parseOpenGroupV2 } from '../../opengroup/opengroupV2/JoinOpenGroupV2';
export interface Props {
searchTerm: string;
@ -377,34 +382,25 @@ export class LeftPaneMessageSection extends React.Component<Props, State> {
}
}
private async handleJoinChannelButtonClick(serverUrl: string) {
const { loading } = this.state;
if (loading) {
return;
}
// guess if this is an open
private async handleOpenGroupJoinV1(serverUrlV1: string) {
// Server URL valid?
if (serverUrl.length === 0 || !OpenGroup.validate(serverUrl)) {
if (serverUrlV1.length === 0 || !OpenGroup.validate(serverUrlV1)) {
ToastUtils.pushToastError('connectToServer', window.i18n('invalidOpenGroupUrl'));
return;
}
// Already connected?
if (OpenGroup.getConversation(serverUrl)) {
if (OpenGroup.getConversation(serverUrlV1)) {
ToastUtils.pushToastError('publicChatExists', window.i18n('publicChatExists'));
return;
}
// Connect to server
try {
ToastUtils.pushToastInfo('connectingToServer', window.i18n('connectingToServer'));
this.setState({ loading: true });
await OpenGroup.join(serverUrl);
if (await OpenGroup.serverExists(serverUrl)) {
await OpenGroup.join(serverUrlV1);
if (await OpenGroup.serverExists(serverUrlV1)) {
ToastUtils.pushToastSuccess(
'connectToServerSuccess',
window.i18n('connectToServerSuccess')
@ -413,7 +409,7 @@ export class LeftPaneMessageSection extends React.Component<Props, State> {
throw new Error('Open group joined but the corresponding server does not exist');
}
this.setState({ loading: false });
const openGroupConversation = OpenGroup.getConversation(serverUrl);
const openGroupConversation = OpenGroup.getConversation(serverUrlV1);
if (!openGroupConversation) {
window.log.error('Joined an opengroup but did not find ther corresponding conversation');
@ -426,6 +422,51 @@ export class LeftPaneMessageSection extends React.Component<Props, State> {
}
}
private async handleOpenGroupJoinV2(serverUrlV2: string) {
const parsedRoom = parseOpenGroupV2(serverUrlV2);
if (!parsedRoom) {
ToastUtils.pushToastError('connectToServer', window.i18n('invalidOpenGroupUrl'));
return;
}
try {
const conversationID = getOpenGroupV2ConversationId(parsedRoom.serverUrl, parsedRoom.roomId);
ToastUtils.pushToastInfo('connectingToServer', window.i18n('connectingToServer'));
this.setState({ loading: true });
await joinOpenGroupV2(parsedRoom, false);
const isConvoCreated = ConversationController.getInstance().get(conversationID);
if (isConvoCreated) {
ToastUtils.pushToastSuccess(
'connectToServerSuccess',
window.i18n('connectToServerSuccess')
);
} else {
ToastUtils.pushToastError('connectToServerFail', window.i18n('connectToServerFail'));
}
} catch (error) {
window.log.warn('got error while joining open group:', error);
ToastUtils.pushToastError('connectToServerFail', window.i18n('connectToServerFail'));
} finally {
this.setState({ loading: false });
}
}
private async handleJoinChannelButtonClick(serverUrl: string) {
const { loading } = this.state;
if (loading) {
return;
}
// guess if this is an open
if (serverUrl.match(openGroupV2CompleteURLRegex)) {
await this.handleOpenGroupJoinV2(serverUrl);
} else {
// this is an open group v1
await this.handleOpenGroupJoinV1(serverUrl);
}
}
private async onCreateClosedGroup(groupName: string, groupMembers: Array<ContactType>) {
if (this.state.loading) {
window.log.warn('Closed group creation already in progress');

View File

@ -93,7 +93,7 @@ const channelsToMake = {
getAllConversations,
getAllConversationIds,
getAllPublicConversations,
getAllOpenGroupV1Conversations,
getPubkeysInPublicConversation,
savePublicServerToken,
getPublicServerTokenByServerUrl,
@ -584,8 +584,8 @@ export async function getAllConversationIds(): Promise<Array<string>> {
return ids;
}
export async function getAllPublicConversations(): Promise<ConversationCollection> {
const conversations = await channels.getAllPublicConversations();
export async function getAllOpenGroupV1Conversations(): Promise<ConversationCollection> {
const conversations = await channels.getAllOpenGroupV1Conversations();
const collection = new ConversationCollection();
collection.add(conversations);

View File

@ -1,3 +1,4 @@
import { ConversationCollection } from '../models/conversation';
import { OpenGroupRequestCommonType } from '../opengroup/opengroupV2/ApiUtil';
import { isOpenGroupV2 } from '../opengroup/utils/OpenGroupUtils';
import { channels } from './channels';
@ -15,7 +16,6 @@ export type OpenGroupV2Room = {
};
export async function getAllV2OpenGroupRooms(): Promise<Map<string, OpenGroupV2Room> | undefined> {
// TODO sql
const opengroupsv2Rooms = (await channels.getAllV2OpenGroupRooms()) as Array<OpenGroupV2Room>;
if (!opengroupsv2Rooms) {
@ -86,4 +86,13 @@ export const channelsToMake = {
getV2OpenGroupRoomByRoomId,
saveV2OpenGroupRoom,
removeV2OpenGroupRoom,
getAllOpenGroupV2Conversations,
};
export async function getAllOpenGroupV2Conversations(): Promise<ConversationCollection> {
const conversations = await channels.getAllOpenGroupV2Conversations();
const collection = new ConversationCollection();
collection.add(conversations);
return collection;
}

View File

@ -0,0 +1,37 @@
import {
getCompleteUrlFromRoom,
openGroupPrefixRegex,
openGroupV2ConversationIdRegex,
} from '../opengroup/utils/OpenGroupUtils';
import { getV2OpenGroupRoom } from '../data/opengroups';
import { ToastUtils } from '../session/utils';
export async function copyPublicKey(convoId: string) {
if (convoId.match(openGroupPrefixRegex)) {
// open group v1 or v2
if (convoId.match(openGroupV2ConversationIdRegex)) {
// this is a v2 group, just build the url
const roomInfos = await getV2OpenGroupRoom(convoId);
if (roomInfos) {
const fullUrl = getCompleteUrlFromRoom(roomInfos);
window.clipboard.writeText(fullUrl);
ToastUtils.pushCopiedToClipBoard();
return;
}
window.log.warn('coy to pubkey no roomInfo');
return;
}
// this is a v1
const atIndex = convoId.indexOf('@');
const openGroupUrl = convoId.substr(atIndex + 1);
window.clipboard.writeText(openGroupUrl);
ToastUtils.pushCopiedToClipBoard();
return;
}
window.clipboard.writeText(convoId);
ToastUtils.pushCopiedToClipBoard();
}

View File

@ -1,3 +1,4 @@
import * as MessageInteraction from './message';
import * as ConversationInteraction from './conversation';
export { MessageInteraction };
export { MessageInteraction, ConversationInteraction };

View File

@ -42,6 +42,8 @@ import {
openGroupV1ConversationIdRegex,
openGroupV2ConversationIdRegex,
} from '../opengroup/utils/OpenGroupUtils';
import { getV2OpenGroupRoom } from '../data/opengroups';
import { ConversationInteraction } from '../interactions';
export enum ConversationType {
GROUP = 'group',
@ -1275,17 +1277,7 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
}
public copyPublicKey() {
if (this.isPublic()) {
const atIndex = this.id.indexOf('@') as number;
const openGroupUrl = this.id.substr(atIndex + 1);
window.clipboard.writeText(openGroupUrl);
ToastUtils.pushCopiedToClipBoard();
return;
}
window.clipboard.writeText(this.id);
ToastUtils.pushCopiedToClipBoard();
void ConversationInteraction.copyPublicKey(this.id);
}
public changeNickname() {

View File

@ -99,7 +99,6 @@ export const parseMessages = async (
rawMessages.map(async r => {
try {
const opengroupMessage = OpenGroupMessageV2.fromJson(r);
console.warn('opengroupMessage', opengroupMessage);
if (
!opengroupMessage?.serverId ||
!opengroupMessage.sentTimestamp ||

View File

@ -60,7 +60,7 @@ export function parseOpenGroupV2(urlWithPubkey: string): OpenGroupV2Room | undef
*/
export async function joinOpenGroupV2(
room: OpenGroupV2Room,
fromSyncMessage: boolean = false
fromSyncMessage: boolean
): Promise<void> {
if (!room.serverUrl || !room.roomId || room.roomId.length < 2 || !room.serverPublicKey) {
return;
@ -79,8 +79,9 @@ export async function joinOpenGroupV2(
window.log.warn('Skipping join opengroupv2: already exists');
return;
} else if (alreadyExist) {
// we don't have a convo associated with it. Remove the room in db
await removeV2OpenGroupRoom(conversationId);
// we don't have a convo associated with it. Remove everything related to it so we start fresh
window.log.warn('leaving before rejoining open group v2 room', conversationId);
await ConversationController.getInstance().deleteContact(conversationId);
}
// Try to connect to server

View File

@ -25,11 +25,7 @@ export const compactFetchEverything = async (
}
const result = await sendOpenGroupV2RequestCompactPoll(compactPollRequest, abortSignal);
const statusCode = parseStatusCodeFromOnionRequest(result);
if (statusCode !== 200) {
return null;
}
return result;
return result ? result : null;
};
/**
@ -119,8 +115,6 @@ async function sendOpenGroupV2RequestCompactPoll(
// this will throw if the url is not valid
const builtUrl = new URL(`${serverUrl}/${endpoint}`);
console.warn(`sending compactPoll request: ${request.body}`);
const res = await sendViaOnion(
serverPubKey,
builtUrl,
@ -144,11 +138,14 @@ async function sendOpenGroupV2RequestCompactPoll(
return null;
}
// get all roomIds which needs a refreshed token
const roomTokensToRefresh = results.filter(ret => ret.statusCode === 401).map(r => r.roomId);
const roomWithTokensToRefresh = results.filter(ret => ret.statusCode === 401).map(r => r.roomId);
if (roomTokensToRefresh) {
// this holds only the poll results which are valid
const roomPollValidResults = results.filter(ret => ret.statusCode === 200);
if (roomWithTokensToRefresh) {
await Promise.all(
roomTokensToRefresh.map(async roomId => {
roomWithTokensToRefresh.map(async roomId => {
const roomDetails = await getV2OpenGroupRoomByRoomId({
serverUrl,
roomId,
@ -165,11 +162,7 @@ async function sendOpenGroupV2RequestCompactPoll(
);
}
throw new Error(
'See how we handle needs of new tokens, and save stuff to db (last deleted, ... conversation commit, etc'
);
return results;
return roomPollValidResults;
}
type ParsedRoomCompactPollResults = {

View File

@ -1,12 +1,20 @@
import { OpenGroupV2Room, removeV2OpenGroupRoom, saveV2OpenGroupRoom } from '../../data/opengroups';
import {
getAllOpenGroupV2Conversations,
getAllV2OpenGroupRooms,
OpenGroupV2Room,
removeV2OpenGroupRoom,
saveV2OpenGroupRoom,
} from '../../data/opengroups';
import { ConversationModel, ConversationType } 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 { deleteAuthToken, openGroupV2GetRoomInfo } from './OpenGroupAPIV2';
import { OpenGroupServerPoller } from './OpenGroupServerPoller';
import _ from 'lodash';
/**
* 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.
@ -93,9 +101,10 @@ export class OpenGroupManagerV2 {
private readonly pollers: Map<string, OpenGroupServerPoller> = new Map();
private isPolling = false;
private wasStopped = false;
private constructor() {}
private constructor() {
this.startPollingBouncy = this.startPollingBouncy.bind(this);
}
public static getInstance() {
if (!OpenGroupManagerV2.instance) {
@ -104,14 +113,8 @@ export class OpenGroupManagerV2 {
return OpenGroupManagerV2.instance;
}
public startPolling() {
if (this.isPolling) {
return;
}
if (this.wasStopped) {
throw new Error('OpengroupManager is not supposed to be starting again after being stopped.');
}
this.isPolling = true;
public async startPolling() {
await allowOnlyOneAtATime('V2ManagerStartPolling', this.startPollingBouncy);
}
/**
@ -121,7 +124,13 @@ export class OpenGroupManagerV2 {
if (!this.isPolling) {
return;
}
this.wasStopped = true;
// 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;
}
@ -149,4 +158,47 @@ export class OpenGroupManagerV2 {
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;
}
}

View File

@ -43,6 +43,7 @@ export class OpenGroupServerPoller {
});
this.abortController = new AbortController();
this.compactPoll = this.compactPoll.bind(this);
this.pollForEverythingTimer = global.setInterval(this.compactPoll, pollForEverythingInterval);
}
@ -143,6 +144,8 @@ export class OpenGroupServerPoller {
this.roomIdsToPoll.has(result.roomId)
);
window.log.warn(`compactFetchResults for ${this.serverUrl}:`, compactFetchResults);
// ==> At this point all those results need to trigger conversation updates, so update what we have to update
} catch (e) {
window.log.warn('Got error while compact fetch:', e);
} finally {

View File

@ -1,23 +1,39 @@
import { default as insecureNodeFetch } from 'node-fetch';
import { OpenGroupV2Room } from '../../data/opengroups';
import { sendViaOnion } from '../../session/onions/onionSend';
import { OpenGroup } from '../opengroupV1/OpenGroup';
export const protocolRegex = '(https?://)?';
export const hostnameRegex =
'(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]).)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])';
export const portRegex = '(:[0-9]+)?';
const protocolRegex = new RegExp('(https?://)?');
const dot = '\\.';
const qMark = '\\?';
const hostnameRegex = new RegExp(
`(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])${dot})*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])`
);
const portRegex = '(:[0-9]+)?';
// roomIds allows between 2 and 64 of '0-9' or 'a-z' or '_' chars
export const roomIdV2Regex = '[0-9a-z_]{2,64}';
export const publicKeyRegex = '[0-9a-z]{64}';
export const publicKeyParam = 'public_key=';
export const openGroupV2ServerUrlRegex = new RegExp(`${protocolRegex}${hostnameRegex}${portRegex}`);
export const openGroupV2ServerUrlRegex = new RegExp(
`${protocolRegex.source}${hostnameRegex.source}${portRegex}`
);
export const openGroupV2CompleteURLRegex = new RegExp(
`^${openGroupV2ServerUrlRegex}/${roomIdV2Regex}\\?${publicKeyParam}${publicKeyRegex}$`,
`^${openGroupV2ServerUrlRegex.source}\/${roomIdV2Regex}${qMark}${publicKeyParam}${publicKeyRegex}$`,
'gm'
);
/**
* This function returns a full url on an open group v2 room used for sync messages for instance.
* This is basically what the QRcode encodes
*
*/
export function getCompleteUrlFromRoom(roomInfos: OpenGroupV2Room) {
// serverUrl has the port and protocol already
return `${roomInfos.serverUrl}/${roomInfos.roomId}?${publicKeyParam}${roomInfos.serverPublicKey}`;
}
/**
* Just a constant to have less `publicChat:` everywhere.
* This is the prefix used to identify our open groups in the conversation database (v1 or v2)

View File

@ -17,6 +17,7 @@ import { actions as conversationActions } from '../../state/ducks/conversations'
import { getV2OpenGroupRoom, removeV2OpenGroupRoom } from '../../data/opengroups';
import { deleteAuthToken } from '../../opengroup/opengroupV2/OpenGroupAPIV2';
import _ from 'lodash';
import { OpenGroupManagerV2 } from '../../opengroup/opengroupV2/OpenGroupManagerV2';
export class ConversationController {
private static instance: ConversationController | null;
@ -200,6 +201,7 @@ export class ConversationController {
window.log.info('leaving open group v2', conversation.id);
const roomInfos = await getV2OpenGroupRoom(conversation.id);
if (roomInfos) {
OpenGroupManagerV2.getInstance().removeRoomFromPolledRooms(roomInfos);
// leave the group on the remote server
await deleteAuthToken(_.pick(roomInfos, 'serverUrl', 'roomId'));
// remove the roomInfos locally for this open group room