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

pull/1230/head
Audric Ackermann 5 years ago committed by GitHub
commit 32bf5cd83f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

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

@ -1104,16 +1104,30 @@
window.setMediaPermissions(!mediaPermissions); window.setMediaPermissions(!mediaPermissions);
}; };
// attempts a connection to an open group server // Attempts a connection to an open group server
window.attemptConnection = async (serverURL, channelId) => { 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(/^https?:\/\//i, '')
.replace(/[/\\]+$/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( const conversationExists = window.ConversationController.get(
conversationId conversationId
); );
@ -1124,9 +1138,9 @@
}); });
} }
// get server // Get server
const serverAPI = await window.lokiPublicChatAPI.findOrCreateServer( const serverAPI = await window.lokiPublicChatAPI.findOrCreateServer(
sslServerURL completeServerURL
); );
// SSL certificate failure or offline // SSL certificate failure or offline
if (!serverAPI) { if (!serverAPI) {
@ -1136,14 +1150,14 @@
}); });
} }
// create conversation // Create conversation
const conversation = await window.ConversationController.getOrCreateAndWait( const conversation = await window.ConversationController.getOrCreateAndWait(
conversationId, conversationId,
'group' 'group'
); );
// convert conversation to a public one // Convert conversation to a public one
await conversation.setPublicSource(sslServerURL, channelId); await conversation.setPublicSource(completeServerURL, channelId);
// and finally activate it // and finally activate it
conversation.getPublicSendData(); // may want "await" if you want to use the API conversation.getPublicSendData(); // may want "await" if you want to use the API

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

@ -7,7 +7,6 @@ import {
} from './session/settings/SessionSettings'; } from './session/settings/SessionSettings';
export const MainViewController = { export const MainViewController = {
joinChannelStateManager,
createClosedGroup, createClosedGroup,
renderMessageView, renderMessageView,
renderSettingsView, renderSettingsView,
@ -40,71 +39,6 @@ export class MessageView extends React.Component {
// //////////// Management ///////////// // //////////// 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( async function createClosedGroup(
groupName: string, groupName: string,
groupMembers: Array<ContactType>, groupMembers: Array<ContactType>,

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

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

@ -66,20 +66,10 @@ export class SessionClosableOverlay extends React.Component<Props, State> {
} }
public getContacts() { 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) => { return contactsList.map((d: any) => {
const lokiProfile = d.getLokiProfile(); const name = d.name ?? window.i18n('anonymous');
const name = lokiProfile ? lokiProfile.displayName : 'Anonymous';
// TODO: should take existing members into account // TODO: should take existing members into account
const existingMember = false; const existingMember = false;
@ -90,7 +80,7 @@ export class SessionClosableOverlay extends React.Component<Props, State> {
authorProfileName: name, authorProfileName: name,
selected: false, selected: false,
authorName: name, authorName: name,
authorColor: d.getColor(), authorColor: d.color,
checkmarked: false, checkmarked: false,
existingMember, existingMember,
}; };

@ -1,4 +1,4 @@
// This is the Open Group equivalent to the PubKey type. import { ConversationModel } from '../../../js/models/conversations';
interface OpenGroupParams { interface OpenGroupParams {
server: string; server: string;
@ -8,7 +8,7 @@ interface OpenGroupParams {
export class OpenGroup { export class OpenGroup {
private static readonly serverRegex = new RegExp( 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( private static readonly groupIdRegex = new RegExp(
'^publicChat:[0-9]*@([\\w-]{2,}.){1,2}[\\w-]{2,}$' '^publicChat:[0-9]*@([\\w-]{2,}.){1,2}[\\w-]{2,}$'
@ -18,9 +18,17 @@ export class OpenGroup {
public readonly groupId?: string; public readonly groupId?: string;
public readonly conversationId: 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) { constructor(params: OpenGroupParams) {
const strippedServer = params.server.replace('https://', ''); this.server = OpenGroup.prefixify(params.server.toLowerCase());
this.server = strippedServer;
// Validate server format // Validate server format
const isValid = OpenGroup.serverRegex.test(this.server); const isValid = OpenGroup.serverRegex.test(this.server);
@ -33,14 +41,29 @@ export class OpenGroup {
this.groupId = OpenGroup.getGroupId(this.server, this.channel); 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( public static from(
groupId: string, groupId: string,
conversationId: string conversationId: string,
hasSSL: boolean = true
): OpenGroup | undefined { ): OpenGroup | undefined {
// Returns a new instance from a groupId if it's valid const server = this.getServer(groupId, hasSSL);
// eg. groupId = 'publicChat:1@chat.getsession.org'
const server = this.getServer(groupId);
const channel = this.getChannel(groupId); const channel = this.getChannel(groupId);
// Was groupId successfully utilized? // Was groupId successfully utilized?
@ -55,17 +78,121 @@ export class OpenGroup {
conversationId, conversationId,
} as OpenGroupParams; } as OpenGroupParams;
if (this.serverRegex.test(server)) { const isValid = OpenGroup.serverRegex.test(server);
if (!isValid) {
return; return;
} }
return new OpenGroup(openGroupParams); return new OpenGroup(openGroupParams);
} }
private static getServer(groupId: string): string | undefined { /**
* 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;
}
// 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 isValid = this.groupIdRegex.test(groupId);
const strippedServer = isValid ? groupId.split('@')[1] : undefined;
return 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 { private static getChannel(groupId: string): number | undefined {
@ -76,7 +203,22 @@ export class OpenGroup {
} }
private static getGroupId(server: string, channel: number): string { private static getGroupId(server: string, channel: number): string {
// server is already validated in constructor; no need to re-check // Server is already validated in constructor; no need to re-check
return `publicChat:${channel}@${server}`;
// 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}`;
} }
} }

@ -120,8 +120,26 @@ export const _getLeftPaneLists = (
}; };
} }
// Remove all invalid conversations and conversatons of devices associated with cancelled attempted links // Add Open Group to list as soon as the name has been set
if (!conversation.timestamp) { 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; continue;
} }
@ -133,7 +151,7 @@ export const _getLeftPaneLists = (
unreadCount += conversation.unreadCount; unreadCount += conversation.unreadCount;
} }
if (!conversation.activeAt) { if (!conversation.isPublic && !conversation.activeAt) {
continue; continue;
} }

5
ts/window.d.ts vendored

@ -7,12 +7,15 @@ import { LokiPublicChatFactoryInterface } from '../js/modules/loki_public_chat_a
import { LokiAppDotNetServerInterface } from '../js/modules/loki_app_dot_net_api'; import { LokiAppDotNetServerInterface } from '../js/modules/loki_app_dot_net_api';
import { LokiMessageInterface } from '../js/modules/loki_message_api'; import { LokiMessageInterface } from '../js/modules/loki_message_api';
import { SwarmPolling } from './session/snode_api/swarmPolling'; import { SwarmPolling } from './session/snode_api/swarmPolling';
import { LibTextsecure } from '../libtextsecure'; 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. 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. If you import anything in global.d.ts, the type system won't work correctly.
*/ */
declare global { declare global {
interface Window { interface Window {
CONSTANTS: any; CONSTANTS: any;
@ -33,7 +36,7 @@ declare global {
StubMessageAPI: any; StubMessageAPI: any;
WebAPI: any; WebAPI: any;
Whisper: any; Whisper: any;
attemptConnection: any; attemptConnection: ConversationType;
clearLocalData: any; clearLocalData: any;
clipboard: any; clipboard: any;
confirmationDialog: any; confirmationDialog: any;

Loading…
Cancel
Save