Give a route for retrying invites for users which may not exist

Fixes https://github.com/vector-im/riot-web/issues/7922

This supports the current style of errors (M_NOT_FOUND) as well as the errors presented by MSC1797: https://github.com/matrix-org/matrix-doc/pull/1797
This commit is contained in:
Travis Ralston 2019-01-10 21:43:21 -07:00
parent c11d0bdf0c
commit 5333114d7b
5 changed files with 207 additions and 51 deletions

View file

@ -86,6 +86,7 @@ const SIMPLE_SETTINGS = [
{ id: "pinMentionedRooms" },
{ id: "pinUnreadRooms" },
{ id: "showDeveloperTools" },
{ id: "alwaysRetryInvites" },
];
// These settings must be defined in SettingsStore

View file

@ -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>
);
},
});

View file

@ -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",
@ -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",
@ -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",

View file

@ -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'),

View file

@ -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
@ -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;
}
@ -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();
@ -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') {
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."});
return Promise.reject({
errcode: "M_NOT_FOUND",
error: "User does not have a profile or does not exist.",
});
}
}
return MatrixClientPeg.get().invite(roomId, addr);
@ -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);
}
_inviteMore(nextIndex) {
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, 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,
@ -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));
}
}