Merge pull request #1225 from vincentbavitz/https-open-group

This commit is contained in:
Audric Ackermann 2020-07-08 18:07:01 +10:00 committed by GitHub
commit 32bf5cd83f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 267 additions and 130 deletions

View file

@ -2189,13 +2189,16 @@
"message": "You have removed your password."
},
"publicChatExists": {
"message": "You are already connected to this public channel"
"message": "You are already connected to this open group"
},
"connectToServerFail": {
"message": "Failed to connect to server. Check URL"
},
"connectingToServer": {
"message": "Connecting to server..."
},
"connectToServerSuccess": {
"message": "Successfully connected to new open group server"
"message": "Successfully connected to open group"
},
"setPasswordFail": {
"message": "Failed to set password"
@ -2248,6 +2251,10 @@
"message": "Invalid Pubkey Format",
"description": "Error string shown when user types an invalid pubkey format"
},
"attemptedConnectionTimeout": {
"message": "Connection to open group timed out",
"description": "Shown in toast when attempted connection to OpenGroup times out"
},
"lnsMappingNotFound": {
"message": "There is no LNS mapping associated with this name",
"description": "Shown in toast if user enters an unknown LNS name"
@ -2370,6 +2377,10 @@
"displayName": {
"message": "Display Name"
},
"anonymous": {
"message": "Anonymous",
"description": "The name of currently unidentified users"
},
"enterDisplayName": {
"message": "Enter a display name"
},

View file

@ -1104,16 +1104,30 @@
window.setMediaPermissions(!mediaPermissions);
};
// attempts a connection to an open group server
// Attempts a connection to an open group server
window.attemptConnection = async (serverURL, channelId) => {
let rawserverURL = serverURL
let completeServerURL = serverURL.toLowerCase();
const valid = window.libsession.Types.OpenGroup.validate(
completeServerURL
);
if (!valid) {
return new Promise((_resolve, reject) => {
reject(window.i18n('connectToServerFail'));
});
}
// Add http or https prefix to server
completeServerURL = window.libsession.Types.OpenGroup.prefixify(
completeServerURL
);
const rawServerURL = serverURL
.replace(/^https?:\/\//i, '')
.replace(/[/\\]+$/i, '');
rawserverURL = rawserverURL.toLowerCase();
const sslServerURL = `https://${rawserverURL}`;
const conversationId = `publicChat:${channelId}@${rawserverURL}`;
// quickly peak to make sure we don't already have it
const conversationId = `publicChat:${channelId}@${rawServerURL}`;
// Quickly peak to make sure we don't already have it
const conversationExists = window.ConversationController.get(
conversationId
);
@ -1124,9 +1138,9 @@
});
}
// get server
// Get server
const serverAPI = await window.lokiPublicChatAPI.findOrCreateServer(
sslServerURL
completeServerURL
);
// SSL certificate failure or offline
if (!serverAPI) {
@ -1136,14 +1150,14 @@
});
}
// create conversation
// Create conversation
const conversation = await window.ConversationController.getOrCreateAndWait(
conversationId,
'group'
);
// convert conversation to a public one
await conversation.setPublicSource(sslServerURL, channelId);
// Convert conversation to a public one
await conversation.setPublicSource(completeServerURL, channelId);
// and finally activate it
conversation.getPublicSendData(); // may want "await" if you want to use the API

View file

@ -122,6 +122,7 @@ export class LeftPane extends React.Component<Props, State> {
return (
<LeftPaneMessageSection
contacts={this.props.contacts}
openConversationInternal={openConversationInternal}
conversations={conversations}
searchResults={searchResults}

View file

@ -7,7 +7,6 @@ import {
} from './session/settings/SessionSettings';
export const MainViewController = {
joinChannelStateManager,
createClosedGroup,
renderMessageView,
renderSettingsView,
@ -40,71 +39,6 @@ export class MessageView extends React.Component {
// //////////// Management /////////////
// /////////////////////////////////////
function joinChannelStateManager(
thisRef: any,
serverURL: string,
onSuccess?: any
) {
// Any component that uses this function MUST have the keys [loading, connectSuccess]
// in their State
// TODO: Make this not hard coded
const channelId = 1;
thisRef.setState({ loading: true });
const connectionResult = window.attemptConnection(serverURL, channelId);
// Give 5s maximum for promise to revole. Else, throw error.
const connectionTimeout = setTimeout(() => {
if (!thisRef.state.connectSuccess) {
thisRef.setState({ loading: false });
window.pushToast({
title: window.i18n('connectToServerFail'),
type: 'error',
id: 'connectToServerFail',
});
return;
}
}, window.CONSTANTS.MAX_CONNECTION_DURATION);
connectionResult
.then(() => {
clearTimeout(connectionTimeout);
if (thisRef.state.loading) {
thisRef.setState({
connectSuccess: true,
loading: false,
});
window.pushToast({
title: window.i18n('connectToServerSuccess'),
id: 'connectToServerSuccess',
type: 'success',
});
if (onSuccess) {
onSuccess();
}
}
})
.catch((connectionError: string) => {
clearTimeout(connectionTimeout);
thisRef.setState({
connectSuccess: true,
loading: false,
});
window.pushToast({
title: connectionError,
id: 'connectToServerFail',
type: 'error',
});
return false;
});
return true;
}
async function createClosedGroup(
groupName: string,
groupMembers: Array<ContactType>,

View file

@ -29,7 +29,6 @@ export interface Props {
conversations: Array<ConversationListItemPropsType>;
contacts: Array<ConversationType>;
searchResults?: SearchResultsProps;
updateSearchTerm: (searchTerm: string) => void;

View file

@ -7,6 +7,7 @@ import {
ConversationListItem,
PropsData as ConversationListItemPropsType,
} from '../ConversationListItem';
import { ConversationType } from '../../state/ducks/conversations';
import {
PropsData as SearchResultsProps,
SearchResults,
@ -28,11 +29,13 @@ import {
SessionButtonColor,
SessionButtonType,
} from './SessionButton';
import { OpenGroup } from '../../session/types';
export interface Props {
searchTerm: string;
isSecondaryDevice: boolean;
contacts: Array<ConversationType>;
conversations?: Array<ConversationListItemPropsType>;
searchResults?: SearchResultsProps;
@ -58,7 +61,6 @@ interface State {
loading: boolean;
overlay: false | SessionComposeToType;
valuePasted: string;
connectSuccess: boolean;
}
export class LeftPaneMessageSection extends React.Component<Props, State> {
@ -72,7 +74,6 @@ export class LeftPaneMessageSection extends React.Component<Props, State> {
loading: false,
overlay: false,
valuePasted: '',
connectSuccess: false,
};
const conversations = this.getCurrentConversations();
@ -314,6 +315,7 @@ export class LeftPaneMessageSection extends React.Component<Props, State> {
const closedGroupElement = (
<SessionClosableOverlay
contacts={this.props.contacts}
overlayMode={SessionClosableOverlayType.ClosedGroup}
onChangeSessionID={this.handleOnPaste}
onCloseClick={() => {
@ -438,41 +440,64 @@ export class LeftPaneMessageSection extends React.Component<Props, State> {
}
}
private handleJoinChannelButtonClick(groupUrl: string) {
private async handleJoinChannelButtonClick(serverUrl: string) {
const { loading } = this.state;
if (loading) {
return false;
return;
}
// longest TLD is now (20/02/06) 24 characters per https://jasontucker.blog/8945/what-is-the-longest-tld-you-can-get-for-a-domain-name
const regexURL = /(http:\/\/|https:\/\/)?[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,24}(:[0-9]{1,5})?(\/.*)?/;
// Server URL entered?
if (serverUrl.length === 0) {
return;
}
if (groupUrl.length <= 0) {
// Server URL valid?
if (!OpenGroup.validate(serverUrl)) {
window.pushToast({
title: window.i18n('noServerURL'),
type: 'error',
id: 'connectToServerFail',
type: 'error',
});
return false;
return;
}
if (!regexURL.test(groupUrl)) {
// Already connected?
if (Boolean(await OpenGroup.getConversation(serverUrl))) {
window.pushToast({
title: window.i18n('noServerURL'),
title: window.i18n('publicChatExists'),
id: 'publicChatExists',
type: 'error',
id: 'connectToServerFail',
});
return false;
return;
}
MainViewController.joinChannelStateManager(this, groupUrl, () => {
this.handleToggleOverlay(undefined);
// Connect to server
try {
await OpenGroup.join(serverUrl, async () => {
if (await OpenGroup.serverExists(serverUrl)) {
window.pushToast({
title: window.i18n('connectingToServer'),
id: 'connectToServerSuccess',
type: 'success',
});
return true;
this.setState({ loading: true });
}
});
} catch (e) {
window.pushToast({
title: window.i18n('connectToServerFail'),
id: 'connectToServerFail',
type: 'error',
});
} finally {
this.setState({
loading: false,
});
}
}
private async onCreateClosedGroup(

View file

@ -66,20 +66,10 @@ export class SessionClosableOverlay extends React.Component<Props, State> {
}
public getContacts() {
const conversations = window.getConversations() || [];
const contactsList = this.props.contacts ?? [];
const conversationList = conversations.filter((conversation: any) => {
return (
!conversation.isMe() &&
conversation.isPrivate() &&
!conversation.isSecondaryDevice() &&
!conversation.isBlocked()
);
});
return conversationList.map((d: any) => {
const lokiProfile = d.getLokiProfile();
const name = lokiProfile ? lokiProfile.displayName : 'Anonymous';
return contactsList.map((d: any) => {
const name = d.name ?? window.i18n('anonymous');
// TODO: should take existing members into account
const existingMember = false;
@ -90,7 +80,7 @@ export class SessionClosableOverlay extends React.Component<Props, State> {
authorProfileName: name,
selected: false,
authorName: name,
authorColor: d.getColor(),
authorColor: d.color,
checkmarked: false,
existingMember,
};

View file

@ -1,4 +1,4 @@
// This is the Open Group equivalent to the PubKey type.
import { ConversationModel } from '../../../js/models/conversations';
interface OpenGroupParams {
server: string;
@ -8,7 +8,7 @@ interface OpenGroupParams {
export class OpenGroup {
private static readonly serverRegex = new RegExp(
'^([\\w-]{2,}.){1,2}[\\w-]{2,}$'
'^((https?:\\/\\/){0,1})([\\w-]{2,}\\.){1,2}[\\w-]{2,}$'
);
private static readonly groupIdRegex = new RegExp(
'^publicChat:[0-9]*@([\\w-]{2,}.){1,2}[\\w-]{2,}$'
@ -18,9 +18,17 @@ export class OpenGroup {
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. `publicChat:1@chat.getsession.org`
* @param params.conversationId The conversation ID for the backbone model
*/
constructor(params: OpenGroupParams) {
const strippedServer = params.server.replace('https://', '');
this.server = strippedServer;
this.server = OpenGroup.prefixify(params.server.toLowerCase());
// Validate server format
const isValid = OpenGroup.serverRegex.test(this.server);
@ -33,14 +41,29 @@ export class OpenGroup {
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);
}
/**
* 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. `publicChat: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
conversationId: string,
hasSSL: boolean = true
): OpenGroup | undefined {
// Returns a new instance from a groupId if it's valid
// eg. groupId = 'publicChat:1@chat.getsession.org'
const server = this.getServer(groupId);
const server = this.getServer(groupId, hasSSL);
const channel = this.getChannel(groupId);
// Was groupId successfully utilized?
@ -55,17 +78,121 @@ export class OpenGroup {
conversationId,
} as OpenGroupParams;
if (this.serverRegex.test(server)) {
const isValid = OpenGroup.serverRegex.test(server);
if (!isValid) {
return;
}
return new OpenGroup(openGroupParams);
}
private static getServer(groupId: string): string | undefined {
const isValid = this.groupIdRegex.test(groupId);
/**
* 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,
onLoading?: any
): Promise<OpenGroup | undefined> {
const prefixedServer = OpenGroup.prefixify(server);
if (!OpenGroup.validate(server)) {
return;
}
return isValid ? groupId.split('@')[1] : undefined;
// Make this not hard coded
const channel = 1;
let conversation;
let conversationId;
// Return OpenGroup if we're already connected
conversation = await OpenGroup.getConversation(prefixedServer);
if (conversation) {
conversationId = conversation?.cid;
if (conversationId) {
return new OpenGroup({
server: prefixedServer,
channel: 1,
conversationId,
});
}
}
// Try to connect to server
try {
if (onLoading) {
onLoading();
}
conversation = await window.attemptConnection(prefixedServer, channel);
conversationId = conversation?.cid;
} catch (e) {
throw new Error(e);
}
// Do we want to add conversation as a property of OpenGroup?
return new OpenGroup({
server,
channel,
conversationId,
});
}
/**
* 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 async getConversation(
server: string
): Promise<ConversationModel | undefined> {
if (!OpenGroup.validate(server)) {
return;
}
const prefixedServer = this.prefixify(server);
const serverInfo = (await window.lokiPublicChatAPI.findOrCreateServer(
prefixedServer
)) as any;
if (!serverInfo?.channels?.length) {
return;
}
return serverInfo.channels[0].conversation;
}
/**
* 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 = this.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
? this.prefixify(strippedServer.toLowerCase(), hasSSL)
: undefined;
}
private static getChannel(groupId: string): number | undefined {
@ -76,7 +203,22 @@ export class OpenGroup {
}
private static getGroupId(server: string, channel: number): string {
// server is already validated in constructor; no need to re-check
return `publicChat:${channel}@${server}`;
// 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 `publicChat:${channel}@${strippedServer}`;
}
private static prefixify(server: string, hasSSL: boolean = true): string {
// Prefix server with https:// if it's not already prefixed with http or https.
const hasPrefix = server.match('^https?://');
if (hasPrefix) {
return server;
}
return `http${hasSSL ? 's' : ''}://${server}`;
}
}

View file

@ -120,8 +120,26 @@ export const _getLeftPaneLists = (
};
}
// Remove all invalid conversations and conversatons of devices associated with cancelled attempted links
if (!conversation.timestamp) {
// Add Open Group to list as soon as the name has been set
if (
conversation.isPublic &&
(!conversation.name || conversation.name === 'Unknown group')
) {
continue;
}
// Show loading icon while fetching messages
if (conversation.isPublic && !conversation.timestamp) {
conversation.lastMessage = {
status: 'sending',
text: '',
isRss: false,
};
}
// Remove all invalid conversations and conversatons of devices associated
// with cancelled attempted links
if (!conversation.isPublic && !conversation.timestamp) {
continue;
}
@ -133,7 +151,7 @@ export const _getLeftPaneLists = (
unreadCount += conversation.unreadCount;
}
if (!conversation.activeAt) {
if (!conversation.isPublic && !conversation.activeAt) {
continue;
}

5
ts/window.d.ts vendored
View file

@ -7,12 +7,15 @@ import { LokiPublicChatFactoryInterface } from '../js/modules/loki_public_chat_a
import { LokiAppDotNetServerInterface } from '../js/modules/loki_app_dot_net_api';
import { LokiMessageInterface } from '../js/modules/loki_message_api';
import { SwarmPolling } from './session/snode_api/swarmPolling';
import { LibTextsecure } from '../libtextsecure';
import { ConversationType } from '../js/modules/data';
/*
We declare window stuff here instead of global.d.ts because we are importing other declarations.
If you import anything in global.d.ts, the type system won't work correctly.
*/
declare global {
interface Window {
CONSTANTS: any;
@ -33,7 +36,7 @@ declare global {
StubMessageAPI: any;
WebAPI: any;
Whisper: any;
attemptConnection: any;
attemptConnection: ConversationType;
clearLocalData: any;
clipboard: any;
confirmationDialog: any;