Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Commit

Permalink
Merge pull request #2434 from matrix-org/travis/invite-better
Browse files Browse the repository at this point in the history
Give a route for retrying invites for users which may not exist
  • Loading branch information
turt2live authored Jan 14, 2019
2 parents c11d0bd + a05c0f9 commit a324035
Show file tree
Hide file tree
Showing 5 changed files with 217 additions and 51 deletions.
1 change: 1 addition & 0 deletions src/components/structures/UserSettings.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ const SIMPLE_SETTINGS = [
{ id: "pinMentionedRooms" },
{ id: "pinUnreadRooms" },
{ id: "showDeveloperTools" },
{ id: "alwaysRetryInvites" },
];

// These settings must be defined in SettingsStore
Expand Down
81 changes: 81 additions & 0 deletions src/components/views/dialogs/AskInviteAnywayDialog.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
Copyright 2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import {SettingLevel} from "../../../settings/SettingsStore";
import SettingsStore from "../../../settings/SettingsStore";

export default React.createClass({
propTypes: {
unknownProfileUsers: PropTypes.array.isRequired, // [ {userId, errorText}... ]
onInviteAnyways: PropTypes.func.isRequired,
onGiveUp: PropTypes.func.isRequired,
onFinished: PropTypes.func.isRequired,
},

_onInviteClicked: function() {
this.props.onInviteAnyways();
this.props.onFinished(true);
},

_onInviteNeverWarnClicked: function() {
SettingsStore.setValue("alwaysInviteUnknownUsers", null, SettingLevel.ACCOUNT, true);
this.props.onInviteAnyways();
this.props.onFinished(true);
},

_onGiveUpClicked: function() {
this.props.onGiveUp();
this.props.onFinished(false);
},

render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');

const errorList = this.props.unknownProfileUsers
.map(address => <li key={address.userId}>{address.userId}: {address.errorText}</li>);

return (
<BaseDialog className='mx_RetryInvitesDialog'
onFinished={this._onGiveUpClicked}
title={_t('The following users may not exist')}
contentId='mx_Dialog_content'
>
<div id='mx_Dialog_content'>
<p>{_t("The following users may not exist - would you like to invite them anyways?")}</p>
<ul>
{ errorList }
</ul>
</div>

<div className="mx_Dialog_buttons">
<button onClick={this._onGiveUpClicked}>
{ _t('Close') }
</button>
<button onClick={this._onInviteNeverWarnClicked}>
{ _t('Invite anyways and never warn me again') }
</button>
<button onClick={this._onInviteClicked} autoFocus="true">
{ _t('Invite anyways') }
</button>
</div>
</BaseDialog>
);
},
});
7 changes: 7 additions & 0 deletions src/i18n/strings/en_EN.json
Original file line number Diff line number Diff line change
Expand Up @@ -222,8 +222,10 @@
"Your browser does not support the required cryptography extensions": "Your browser does not support the required cryptography extensions",
"Not a valid Riot keyfile": "Not a valid Riot keyfile",
"Authentication check failed: incorrect password?": "Authentication check failed: incorrect password?",
"Unrecognised address": "Unrecognised address",
"You do not have permission to invite people to this room.": "You do not have permission to invite people to this room.",
"User %(user_id)s does not exist": "User %(user_id)s does not exist",
"User %(user_id)s may or may not exist": "User %(user_id)s may or may not exist",
"Unknown server error": "Unknown server error",
"Use a few words, avoid common phrases": "Use a few words, avoid common phrases",
"No need for symbols, digits, or uppercase letters": "No need for symbols, digits, or uppercase letters",
Expand Down Expand Up @@ -291,6 +293,7 @@
"Pin unread rooms to the top of the room list": "Pin unread rooms to the top of the room list",
"Enable widget screenshots on supported widgets": "Enable widget screenshots on supported widgets",
"Show empty room list headings": "Show empty room list headings",
"Always invite users which may not exist": "Always invite users which may not exist",
"Show developer tools": "Show developer tools",
"Collecting app version information": "Collecting app version information",
"Collecting logs": "Collecting logs",
Expand Down Expand Up @@ -881,6 +884,10 @@
"That doesn't look like a valid email address": "That doesn't look like a valid email address",
"You have entered an invalid address.": "You have entered an invalid address.",
"Try using one of the following valid address types: %(validTypesList)s.": "Try using one of the following valid address types: %(validTypesList)s.",
"The following users may not exist": "The following users may not exist",
"The following users may not exist - would you like to invite them anyways?": "The following users may not exist - would you like to invite them anyways?",
"Invite anyways and never warn me again": "Invite anyways and never warn me again",
"Invite anyways": "Invite anyways",
"Preparing to send logs": "Preparing to send logs",
"Logs sent": "Logs sent",
"Thank you!": "Thank you!",
Expand Down
5 changes: 5 additions & 0 deletions src/settings/Settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,11 @@ export const SETTINGS = {
displayName: _td('Show empty room list headings'),
default: true,
},
"alwaysInviteUnknownUsers": {
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
displayName: _td('Always invite users which may not exist'),
default: false,
},
"showDeveloperTools": {
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
displayName: _td('Show developer tools'),
Expand Down
174 changes: 123 additions & 51 deletions src/utils/MultiInviter.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,15 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

import React from "react";
import MatrixClientPeg from '../MatrixClientPeg';
import {getAddressType} from '../UserAddress';
import GroupStore from '../stores/GroupStore';
import Promise from 'bluebird';
import {_t} from "../languageHandler";
import sdk from "../index";
import Modal from "../Modal";
import SettingsStore from "../settings/SettingsStore";

/**
* Invites multiple addresses to a room or group, handling rate limiting from the server
Expand All @@ -41,7 +45,7 @@ export default class MultiInviter {
this.addrs = [];
this.busy = false;
this.completionStates = {}; // State of each address (invited or error)
this.errorTexts = {}; // Textual error per address
this.errors = {}; // { address: {errorText, errcode} }
this.deferred = null;
}

Expand All @@ -61,7 +65,10 @@ export default class MultiInviter {
for (const addr of this.addrs) {
if (getAddressType(addr) === null) {
this.completionStates[addr] = 'error';
this.errorTexts[addr] = 'Unrecognised address';
this.errors[addr] = {
errcode: 'M_INVALID',
errorText: _t('Unrecognised address'),
};
}
}
this.deferred = Promise.defer();
Expand All @@ -85,18 +92,28 @@ export default class MultiInviter {
}

getErrorText(addr) {
return this.errorTexts[addr];
return this.errors[addr] ? this.errors[addr].errorText : null;
}

async _inviteToRoom(roomId, addr) {
async _inviteToRoom(roomId, addr, ignoreProfile) {
const addrType = getAddressType(addr);

if (addrType === 'email') {
return MatrixClientPeg.get().inviteByEmail(roomId, addr);
} else if (addrType === 'mx-user-id') {
const profile = await MatrixClientPeg.get().getProfileInfo(addr);
if (!profile) {
return Promise.reject({errcode: "M_NOT_FOUND", error: "User does not have a profile."});
if (!ignoreProfile && !SettingsStore.getValue("alwaysInviteUnknownUsers", this.roomId)) {
try {
const profile = await MatrixClientPeg.get().getProfileInfo(addr);
if (!profile) {
// noinspection ExceptionCaughtLocallyJS
throw new Error("User has no profile");
}
} catch (e) {
throw {
errcode: "RIOT.USER_NOT_FOUND",
error: "User does not have a profile or does not exist."
};
}
}

return MatrixClientPeg.get().invite(roomId, addr);
Expand All @@ -105,14 +122,109 @@ export default class MultiInviter {
}
}

_doInvite(address, ignoreProfile) {
return new Promise((resolve, reject) => {
console.log(`Inviting ${address}`);

_inviteMore(nextIndex) {
let doInvite;
if (this.groupId !== null) {
doInvite = GroupStore.inviteUserToGroup(this.groupId, address);
} else {
doInvite = this._inviteToRoom(this.roomId, address, ignoreProfile);
}

doInvite.then(() => {
if (this._canceled) {
return;
}

this.completionStates[address] = 'invited';
delete this.errors[address];

resolve();
}).catch((err) => {
if (this._canceled) {
return;
}

let errorText;
let fatal = false;
if (err.errcode === 'M_FORBIDDEN') {
fatal = true;
errorText = _t('You do not have permission to invite people to this room.');
} else if (err.errcode === 'M_LIMIT_EXCEEDED') {
// we're being throttled so wait a bit & try again
setTimeout(() => {
this._doInvite(address, ignoreProfile).then(resolve, reject);
}, 5000);
return;
} else if (['M_NOT_FOUND', 'M_USER_NOT_FOUND', 'RIOT.USER_NOT_FOUND'].includes(err.errcode)) {
errorText = _t("User %(user_id)s does not exist", {user_id: address});
} else if (err.errcode === 'M_PROFILE_UNDISCLOSED') {
errorText = _t("User %(user_id)s may or may not exist", {user_id: address});
} else if (err.errcode === 'M_PROFILE_NOT_FOUND' && !ignoreProfile) {
// Invite without the profile check
console.warn(`User ${address} does not have a profile - inviting anyways automatically`);
this._doInvite(address, true).then(resolve, reject);
} else {
errorText = _t('Unknown server error');
}

this.completionStates[address] = 'error';
this.errors[address] = {errorText, errcode: err.errcode};

this.busy = !fatal;
this.fatal = fatal;

if (fatal) {
reject();
} else {
resolve();
}
});
});
}

_inviteMore(nextIndex, ignoreProfile) {
if (this._canceled) {
return;
}

if (nextIndex === this.addrs.length) {
this.busy = false;
if (Object.keys(this.errors).length > 0 && !this.groupId) {
// There were problems inviting some people - see if we can invite them
// without caring if they exist or not.
const unknownProfileErrors = ['M_NOT_FOUND', 'M_USER_NOT_FOUND', 'M_PROFILE_UNDISCLOSED', 'M_PROFILE_NOT_FOUND', 'RIOT.USER_NOT_FOUND'];
const unknownProfileUsers = Object.keys(this.errors).filter(a => unknownProfileErrors.includes(this.errors[a].errcode));

if (unknownProfileUsers.length > 0) {
const inviteUnknowns = () => {
const promises = unknownProfileUsers.map(u => this._doInvite(u, true));
Promise.all(promises).then(() => this.deferred.resolve(this.completionStates));
};

if (SettingsStore.getValue("alwaysInviteUnknownUsers", this.roomId)) {
inviteUnknowns();
return;
}

const AskInviteAnywayDialog = sdk.getComponent("dialogs.AskInviteAnywayDialog");
console.log("Showing failed to invite dialog...");
Modal.createTrackedDialog('Failed to invite the following users to the room', '', AskInviteAnywayDialog, {
unknownProfileUsers: unknownProfileUsers.map(u => {return {userId: u, errorText: this.errors[u].errorText};}),
onInviteAnyways: () => inviteUnknowns(),
onGiveUp: () => {
// Fake all the completion states because we already warned the user
for (const addr of unknownProfileUsers) {
this.completionStates[addr] = 'invited';
}
this.deferred.resolve(this.completionStates);
},
});
return;
}
}
this.deferred.resolve(this.completionStates);
return;
}
Expand All @@ -134,48 +246,8 @@ export default class MultiInviter {
return;
}

let doInvite;
if (this.groupId !== null) {
doInvite = GroupStore.inviteUserToGroup(this.groupId, addr);
} else {
doInvite = this._inviteToRoom(this.roomId, addr);
}

doInvite.then(() => {
if (this._canceled) { return; }

this.completionStates[addr] = 'invited';

this._inviteMore(nextIndex + 1);
}).catch((err) => {
if (this._canceled) { return; }

let errorText;
let fatal = false;
if (err.errcode === 'M_FORBIDDEN') {
fatal = true;
errorText = _t('You do not have permission to invite people to this room.');
} else if (err.errcode === 'M_LIMIT_EXCEEDED') {
// we're being throttled so wait a bit & try again
setTimeout(() => {
this._inviteMore(nextIndex);
}, 5000);
return;
} else if(err.errcode === "M_NOT_FOUND") {
errorText = _t("User %(user_id)s does not exist", {user_id: addr});
} else {
errorText = _t('Unknown server error');
}
this.completionStates[addr] = 'error';
this.errorTexts[addr] = errorText;
this.busy = !fatal;
this.fatal = fatal;

if (!fatal) {
this._inviteMore(nextIndex + 1);
} else {
this.deferred.resolve(this.completionStates);
}
});
this._doInvite(addr, ignoreProfile).then(() => {
this._inviteMore(nextIndex + 1, ignoreProfile);
}).catch(() => this.deferred.resolve(this.completionStates));
}
}

0 comments on commit a324035

Please sign in to comment.