2016-09-13 16:47:56 +03:00
|
|
|
/*
|
|
|
|
Copyright 2016 OpenMarket Ltd
|
2018-11-30 01:05:53 +03:00
|
|
|
Copyright 2017, 2018 New Vector Ltd
|
2016-09-13 16:47:56 +03:00
|
|
|
|
|
|
|
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.
|
|
|
|
*/
|
|
|
|
|
2019-12-21 00:13:46 +03:00
|
|
|
import {MatrixClientPeg} from '../MatrixClientPeg';
|
2017-08-15 15:42:23 +03:00
|
|
|
import {getAddressType} from '../UserAddress';
|
2018-05-01 13:18:45 +03:00
|
|
|
import GroupStore from '../stores/GroupStore';
|
2018-11-30 01:05:53 +03:00
|
|
|
import {_t} from "../languageHandler";
|
2019-12-20 04:19:56 +03:00
|
|
|
import * as sdk from "../index";
|
2019-01-11 07:43:21 +03:00
|
|
|
import Modal from "../Modal";
|
|
|
|
import SettingsStore from "../settings/SettingsStore";
|
2019-11-12 14:45:28 +03:00
|
|
|
import {defer} from "./promise";
|
2016-09-13 16:47:56 +03:00
|
|
|
|
|
|
|
/**
|
2017-08-16 16:58:30 +03:00
|
|
|
* Invites multiple addresses to a room or group, handling rate limiting from the server
|
2016-09-13 16:47:56 +03:00
|
|
|
*/
|
|
|
|
export default class MultiInviter {
|
2017-08-16 16:58:30 +03:00
|
|
|
/**
|
|
|
|
* @param {string} targetId The ID of the room or group to invite to
|
|
|
|
*/
|
|
|
|
constructor(targetId) {
|
|
|
|
if (targetId[0] === '+') {
|
|
|
|
this.roomId = null;
|
|
|
|
this.groupId = targetId;
|
|
|
|
} else {
|
|
|
|
this.roomId = targetId;
|
|
|
|
this.groupId = null;
|
|
|
|
}
|
2016-09-13 16:47:56 +03:00
|
|
|
|
|
|
|
this.canceled = false;
|
|
|
|
this.addrs = [];
|
|
|
|
this.busy = false;
|
|
|
|
this.completionStates = {}; // State of each address (invited or error)
|
2019-01-11 07:43:21 +03:00
|
|
|
this.errors = {}; // { address: {errorText, errcode} }
|
2016-09-13 16:47:56 +03:00
|
|
|
this.deferred = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Invite users to this room. This may only be called once per
|
|
|
|
* instance of the class.
|
|
|
|
*
|
2018-11-30 01:05:53 +03:00
|
|
|
* @param {array} addrs Array of addresses to invite
|
2021-02-27 01:09:54 +03:00
|
|
|
* @param {string} reason Reason for inviting (optional)
|
2016-09-13 16:47:56 +03:00
|
|
|
* @returns {Promise} Resolved when all invitations in the queue are complete
|
|
|
|
*/
|
2021-02-27 01:09:54 +03:00
|
|
|
invite(addrs, reason) {
|
2016-09-13 16:47:56 +03:00
|
|
|
if (this.addrs.length > 0) {
|
|
|
|
throw new Error("Already inviting/invited");
|
|
|
|
}
|
|
|
|
this.addrs.push(...addrs);
|
2021-02-27 01:09:54 +03:00
|
|
|
this.reason = reason;
|
2016-09-13 16:47:56 +03:00
|
|
|
|
|
|
|
for (const addr of this.addrs) {
|
|
|
|
if (getAddressType(addr) === null) {
|
|
|
|
this.completionStates[addr] = 'error';
|
2019-01-11 07:43:21 +03:00
|
|
|
this.errors[addr] = {
|
|
|
|
errcode: 'M_INVALID',
|
|
|
|
errorText: _t('Unrecognised address'),
|
|
|
|
};
|
2016-09-13 16:47:56 +03:00
|
|
|
}
|
|
|
|
}
|
2019-11-12 14:45:28 +03:00
|
|
|
this.deferred = defer();
|
2016-09-13 16:47:56 +03:00
|
|
|
this._inviteMore(0);
|
|
|
|
|
|
|
|
return this.deferred.promise;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Stops inviting. Causes promises returned by invite() to be rejected.
|
|
|
|
*/
|
|
|
|
cancel() {
|
|
|
|
if (!this.busy) return;
|
|
|
|
|
|
|
|
this._canceled = true;
|
|
|
|
this.deferred.reject(new Error('canceled'));
|
|
|
|
}
|
|
|
|
|
|
|
|
getCompletionState(addr) {
|
|
|
|
return this.completionStates[addr];
|
|
|
|
}
|
|
|
|
|
|
|
|
getErrorText(addr) {
|
2019-01-11 07:43:21 +03:00
|
|
|
return this.errors[addr] ? this.errors[addr].errorText : null;
|
2016-09-13 16:47:56 +03:00
|
|
|
}
|
|
|
|
|
2019-01-11 07:43:21 +03:00
|
|
|
async _inviteToRoom(roomId, addr, ignoreProfile) {
|
2018-11-30 01:05:53 +03:00
|
|
|
const addrType = getAddressType(addr);
|
|
|
|
|
|
|
|
if (addrType === 'email') {
|
|
|
|
return MatrixClientPeg.get().inviteByEmail(roomId, addr);
|
|
|
|
} else if (addrType === 'mx-user-id') {
|
2019-03-01 23:36:24 +03:00
|
|
|
const room = MatrixClientPeg.get().getRoom(roomId);
|
|
|
|
if (!room) throw new Error("Room not found");
|
|
|
|
|
|
|
|
const member = room.getMember(addr);
|
|
|
|
if (member && ['join', 'invite'].includes(member.membership)) {
|
|
|
|
throw {errcode: "RIOT.ALREADY_IN_ROOM", error: "Member already invited"};
|
|
|
|
}
|
|
|
|
|
2019-01-16 18:07:30 +03:00
|
|
|
if (!ignoreProfile && SettingsStore.getValue("promptBeforeInviteUnknownUsers", this.roomId)) {
|
2019-01-12 01:46:03 +03:00
|
|
|
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."
|
|
|
|
};
|
2019-01-11 07:43:21 +03:00
|
|
|
}
|
2018-11-30 01:05:53 +03:00
|
|
|
}
|
|
|
|
|
2021-02-27 01:09:54 +03:00
|
|
|
return MatrixClientPeg.get().invite(roomId, addr, undefined, this.reason);
|
2018-11-30 01:05:53 +03:00
|
|
|
} else {
|
|
|
|
throw new Error('Unsupported address');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-01-11 07:43:21 +03:00
|
|
|
_doInvite(address, ignoreProfile) {
|
|
|
|
return new Promise((resolve, reject) => {
|
2019-01-12 01:46:03 +03:00
|
|
|
console.log(`Inviting ${address}`);
|
|
|
|
|
2019-01-11 07:43:21 +03:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2019-11-28 06:29:11 +03:00
|
|
|
console.error(err);
|
|
|
|
|
2019-01-11 07:43:21 +03:00
|
|
|
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.');
|
2019-03-01 23:36:24 +03:00
|
|
|
} else if (err.errcode === "RIOT.ALREADY_IN_ROOM") {
|
|
|
|
errorText = _t("User %(userId)s is already in the room", {userId: address});
|
2019-01-11 07:43:21 +03:00
|
|
|
} 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;
|
2019-01-12 01:46:03 +03:00
|
|
|
} else if (['M_NOT_FOUND', 'M_USER_NOT_FOUND', 'RIOT.USER_NOT_FOUND'].includes(err.errcode)) {
|
2019-01-11 07:43:21 +03:00
|
|
|
errorText = _t("User %(user_id)s does not exist", {user_id: address});
|
2019-01-12 01:46:03 +03:00
|
|
|
} else if (err.errcode === 'M_PROFILE_UNDISCLOSED') {
|
2019-01-11 07:43:21 +03:00
|
|
|
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
|
2019-01-12 01:46:03 +03:00
|
|
|
console.warn(`User ${address} does not have a profile - inviting anyways automatically`);
|
2019-01-11 07:43:21 +03:00
|
|
|
this._doInvite(address, true).then(resolve, reject);
|
2019-03-01 23:36:24 +03:00
|
|
|
} else if (err.errcode === "M_BAD_STATE") {
|
|
|
|
errorText = _t("The user must be unbanned before they can be invited.");
|
2019-04-10 04:03:38 +03:00
|
|
|
} else if (err.errcode === "M_UNSUPPORTED_ROOM_VERSION") {
|
|
|
|
errorText = _t("The user's homeserver does not support the version of the room.");
|
2019-01-11 07:43:21 +03:00
|
|
|
} 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();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
2018-11-30 01:05:53 +03:00
|
|
|
|
2019-01-11 07:43:21 +03:00
|
|
|
_inviteMore(nextIndex, ignoreProfile) {
|
2016-09-13 16:47:56 +03:00
|
|
|
if (this._canceled) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-11-30 01:05:53 +03:00
|
|
|
if (nextIndex === this.addrs.length) {
|
2016-09-13 16:47:56 +03:00
|
|
|
this.busy = false;
|
2019-01-11 07:43:21 +03:00
|
|
|
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.
|
2019-01-12 01:46:03 +03:00
|
|
|
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));
|
2019-01-11 07:43:21 +03:00
|
|
|
|
2019-01-12 01:46:03 +03:00
|
|
|
if (unknownProfileUsers.length > 0) {
|
|
|
|
const inviteUnknowns = () => {
|
|
|
|
const promises = unknownProfileUsers.map(u => this._doInvite(u, true));
|
2019-01-11 07:43:21 +03:00
|
|
|
Promise.all(promises).then(() => this.deferred.resolve(this.completionStates));
|
|
|
|
};
|
|
|
|
|
2019-01-16 18:07:30 +03:00
|
|
|
if (!SettingsStore.getValue("promptBeforeInviteUnknownUsers", this.roomId)) {
|
2019-01-12 01:46:03 +03:00
|
|
|
inviteUnknowns();
|
2019-01-11 07:43:21 +03:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2019-01-12 01:46:03 +03:00
|
|
|
const AskInviteAnywayDialog = sdk.getComponent("dialogs.AskInviteAnywayDialog");
|
2019-01-11 07:43:21 +03:00
|
|
|
console.log("Showing failed to invite dialog...");
|
2019-01-12 01:46:03 +03:00
|
|
|
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(),
|
2019-01-11 07:43:21 +03:00
|
|
|
onGiveUp: () => {
|
|
|
|
// Fake all the completion states because we already warned the user
|
2019-01-12 01:46:03 +03:00
|
|
|
for (const addr of unknownProfileUsers) {
|
2019-01-11 07:43:21 +03:00
|
|
|
this.completionStates[addr] = 'invited';
|
|
|
|
}
|
|
|
|
this.deferred.resolve(this.completionStates);
|
|
|
|
},
|
|
|
|
});
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
2016-09-13 16:47:56 +03:00
|
|
|
this.deferred.resolve(this.completionStates);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const addr = this.addrs[nextIndex];
|
|
|
|
|
|
|
|
// don't try to invite it if it's an invalid address
|
|
|
|
// (it will already be marked as an error though,
|
|
|
|
// so no need to do so again)
|
|
|
|
if (getAddressType(addr) === null) {
|
|
|
|
this._inviteMore(nextIndex + 1);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// don't re-invite (there's no way in the UI to do this, but
|
|
|
|
// for sanity's sake)
|
2018-11-30 01:05:53 +03:00
|
|
|
if (this.completionStates[addr] === 'invited') {
|
2016-09-13 16:47:56 +03:00
|
|
|
this._inviteMore(nextIndex + 1);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2019-01-11 07:43:21 +03:00
|
|
|
this._doInvite(addr, ignoreProfile).then(() => {
|
|
|
|
this._inviteMore(nextIndex + 1, ignoreProfile);
|
|
|
|
}).catch(() => this.deferred.resolve(this.completionStates));
|
2016-09-13 16:47:56 +03:00
|
|
|
}
|
|
|
|
}
|