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

Commit

Permalink
Give a route for retrying invites for users which may not exist
Browse files Browse the repository at this point in the history
Fixes element-hq/element-web#7922

This supports the current style of errors (M_NOT_FOUND) as well as the errors presented by MSC1797: matrix-org/matrix-spec-proposals#1797
  • Loading branch information
turt2live committed Jan 11, 2019
1 parent c11d0bd commit 5333114
Show file tree
Hide file tree
Showing 5 changed files with 207 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
78 changes: 78 additions & 0 deletions src/components/views/dialogs/RetryInvitesDialog.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
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: {
failedInvites: PropTypes.object.isRequired, // { address: { errcode, errorText } }
onTryAgain: PropTypes.func.isRequired,
onGiveUp: PropTypes.func.isRequired,
onFinished: PropTypes.func.isRequired,
},

_onTryAgainClicked: function() {
this.props.onTryAgain();
this.props.onFinished(true);
},

_onTryAgainNeverWarnClicked: function() {
SettingsStore.setValue("alwaysRetryInvites", null, SettingLevel.ACCOUNT, true);
this.props.onTryAgain();
this.props.onFinished(true);
},

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

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

const errorList = Object.keys(this.props.failedInvites)
.map(address => <p>{address}: {this.props.failedInvites[address].errorText}</p>);

return (
<BaseDialog className='mx_RetryInvitesDialog'
onFinished={this._onGiveUpClicked}
title={_t('Failed to invite the following users')}
contentId='mx_Dialog_content'
>
<div id='mx_Dialog_content'>
{ errorList }
</div>

<div className="mx_Dialog_buttons">
<button onClick={this._onGiveUpClicked}>
{ _t('Close') }
</button>
<button onClick={this._onTryAgainNeverWarnClicked}>
{ _t('Try again and never warn me again') }
</button>
<button onClick={this._onTryAgainClicked} autoFocus="true">
{ _t('Try again') }
</button>
</div>
</BaseDialog>
);
},
});
6 changes: 6 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 retry invites for unknown users": "Always retry invites for unknown users",
"Show developer tools": "Show developer tools",
"Collecting app version information": "Collecting app version information",
"Collecting logs": "Collecting logs",
Expand Down Expand Up @@ -965,6 +968,9 @@
"Clear cache and resync": "Clear cache and resync",
"Riot now uses 3-5x less memory, by only loading information about other users when needed. Please wait whilst we resynchronise with the server!": "Riot now uses 3-5x less memory, by only loading information about other users when needed. Please wait whilst we resynchronise with the server!",
"Updating Riot": "Updating Riot",
"Failed to invite the following users": "Failed to invite the following users",
"Try again and never warn me again": "Try again and never warn me again",
"Try again": "Try again",
"Failed to upgrade room": "Failed to upgrade room",
"The room upgrade could not be completed": "The room upgrade could not be completed",
"Upgrade this room to version %(version)s": "Upgrade this room to version %(version)s",
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,
},
"alwaysRetryInvites": {
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
displayName: _td('Always retry invites for unknown users'),
default: false,
},
"showDeveloperTools": {
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
displayName: _td('Show developer tools'),
Expand Down
168 changes: 117 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,23 @@ 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("alwaysRetryInvites", this.roomId)) {
const profile = await MatrixClientPeg.get().getProfileInfo(addr);
if (!profile) {
return Promise.reject({
errcode: "M_NOT_FOUND",
error: "User does not have a profile or does not exist.",
});
}
}

return MatrixClientPeg.get().invite(roomId, addr);
Expand All @@ -105,19 +117,113 @@ export default class MultiInviter {
}
}

_doInvite(address, ignoreProfile) {
return new Promise((resolve, reject) => {
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'].includes(err.errcode)) {
errorText = _t("User %(user_id)s does not exist", {user_id: address});
} else if (err.errcode === 'M_PROFILE_UNKNOWN') {
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 - trying invite again`);
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) {
_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 reinviteErrors = ['M_NOT_FOUND', 'M_USER_NOT_FOUND', 'M_PROFILE_UNKNOWN', 'M_PROFILE_NOT_FOUND'];
const reinvitableUsers = Object.keys(this.errors).filter(a => reinviteErrors.includes(this.errors[a].errcode));

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

if (SettingsStore.getValue("alwaysRetryInvites", this.roomId)) {
retryInvites();
return;
}

const RetryInvitesDialog = sdk.getComponent("dialogs.RetryInvitesDialog");
console.log("Showing failed to invite dialog...");
Modal.createTrackedDialog('Failed to invite the following users to the room', '', RetryInvitesDialog, {
failedInvites: this.errors,
onTryAgain: () => retryInvites(),
onGiveUp: () => {
// Fake all the completion states because we already warned the user
for (const addr of Object.keys(this.completionStates)) {
this.completionStates[addr] = 'invited';
}
this.deferred.resolve(this.completionStates);
},
});
return;
}
}
this.deferred.resolve(this.completionStates);
return;
}

const addr = this.addrs[nextIndex];
console.log(`Inviting ${addr}`);

// don't try to invite it if it's an invalid address
// (it will already be marked as an error though,
Expand All @@ -134,48 +240,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 5333114

Please sign in to comment.