Merge pull request #3601 from matrix-org/bwindels/verification-over-dm

Show verification requests in the timeline
This commit is contained in:
David Baker 2019-11-08 16:20:01 +00:00 committed by GitHub
commit 43c6298bea
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 668 additions and 11 deletions

View file

@ -24,6 +24,10 @@ import sdk from '../../../index';
import * as FormattingUtils from '../../../utils/FormattingUtils';
import { _t } from '../../../languageHandler';
import {verificationMethods} from 'matrix-js-sdk/lib/crypto';
import DMRoomMap from '../../../utils/DMRoomMap';
import createRoom from "../../../createRoom";
import dis from "../../../dispatcher";
import SettingsStore from '../../../settings/SettingsStore';
const MODE_LEGACY = 'legacy';
const MODE_SAS = 'sas';
@ -86,25 +90,37 @@ export default class DeviceVerifyDialog extends React.Component {
this.props.onFinished(confirm);
}
_onSasRequestClick = () => {
_onSasRequestClick = async () => {
this.setState({
phase: PHASE_WAIT_FOR_PARTNER_TO_ACCEPT,
});
this._verifier = MatrixClientPeg.get().beginKeyVerification(
verificationMethods.SAS, this.props.userId, this.props.device.deviceId,
);
this._verifier.on('show_sas', this._onVerifierShowSas);
this._verifier.verify().then(() => {
const client = MatrixClientPeg.get();
const verifyingOwnDevice = this.props.userId === client.getUserId();
try {
if (!verifyingOwnDevice && SettingsStore.getValue("feature_dm_verification")) {
const roomId = await ensureDMExistsAndOpen(this.props.userId);
// throws upon cancellation before having started
this._verifier = await client.requestVerificationDM(
this.props.userId, roomId, [verificationMethods.SAS],
);
} else {
this._verifier = client.beginKeyVerification(
verificationMethods.SAS, this.props.userId, this.props.device.deviceId,
);
}
this._verifier.on('show_sas', this._onVerifierShowSas);
// throws upon cancellation
await this._verifier.verify();
this.setState({phase: PHASE_VERIFIED});
this._verifier.removeListener('show_sas', this._onVerifierShowSas);
this._verifier = null;
}).catch((e) => {
} catch (e) {
console.log("Verification failed", e);
this.setState({
phase: PHASE_CANCELLED,
});
this._verifier = null;
});
}
}
_onSasMatchesClick = () => {
@ -299,3 +315,30 @@ export default class DeviceVerifyDialog extends React.Component {
}
}
async function ensureDMExistsAndOpen(userId) {
const client = MatrixClientPeg.get();
const roomIds = DMRoomMap.shared().getDMRoomsForUserId(userId);
const rooms = roomIds.map(id => client.getRoom(id));
const suitableDMRooms = rooms.filter(r => {
if (r && r.getMyMembership() === "join") {
const member = r.getMember(userId);
return member && (member.membership === "invite" || member.membership === "join");
}
return false;
});
let roomId;
if (suitableDMRooms.length) {
const room = suitableDMRooms[0];
roomId = room.roomId;
} else {
roomId = await createRoom({dmUserId: userId, spinner: false, andView: false});
}
// don't use andView and spinner in createRoom, together, they cause this dialog to close and reopen,
// we causes us to loose the verifier and restart, and we end up having two verification requests
dis.dispatch({
action: 'view_room',
room_id: roomId,
should_peek: false,
});
return roomId;
}

View file

@ -0,0 +1,132 @@
/*
Copyright 2019 The Matrix.org Foundation C.I.C.
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 classNames from 'classnames';
import PropTypes from 'prop-types';
import MatrixClientPeg from '../../../MatrixClientPeg';
import { _t } from '../../../languageHandler';
import KeyVerificationStateObserver, {getNameForEventRoom, userLabelForEventRoom}
from '../../../utils/KeyVerificationStateObserver';
export default class MKeyVerificationConclusion extends React.Component {
constructor(props) {
super(props);
this.keyVerificationState = null;
this.state = {
done: false,
cancelled: false,
otherPartyUserId: null,
cancelPartyUserId: null,
};
const rel = this.props.mxEvent.getRelation();
if (rel) {
const client = MatrixClientPeg.get();
const room = client.getRoom(this.props.mxEvent.getRoomId());
const requestEvent = room.findEventById(rel.event_id);
if (requestEvent) {
this._createStateObserver(requestEvent, client);
this.state = this._copyState();
} else {
const findEvent = event => {
if (event.getId() === rel.event_id) {
this._createStateObserver(event, client);
this.setState(this._copyState());
room.removeListener("Room.timeline", findEvent);
}
};
room.on("Room.timeline", findEvent);
}
}
}
_createStateObserver(requestEvent, client) {
this.keyVerificationState = new KeyVerificationStateObserver(requestEvent, client, () => {
this.setState(this._copyState());
});
}
_copyState() {
const {done, cancelled, otherPartyUserId, cancelPartyUserId} = this.keyVerificationState;
return {done, cancelled, otherPartyUserId, cancelPartyUserId};
}
componentDidMount() {
if (this.keyVerificationState) {
this.keyVerificationState.attach();
}
}
componentWillUnmount() {
if (this.keyVerificationState) {
this.keyVerificationState.detach();
}
}
_getName(userId) {
const roomId = this.props.mxEvent.getRoomId();
const client = MatrixClientPeg.get();
const room = client.getRoom(roomId);
const member = room.getMember(userId);
return member ? member.name : userId;
}
_userLabel(userId) {
const name = this._getName(userId);
if (name !== userId) {
return _t("%(name)s (%(userId)s)", {name, userId});
} else {
return userId;
}
}
render() {
const {mxEvent} = this.props;
const client = MatrixClientPeg.get();
const myUserId = client.getUserId();
let title;
if (this.state.done) {
title = _t("You verified %(name)s", {name: getNameForEventRoom(this.state.otherPartyUserId, mxEvent)});
} else if (this.state.cancelled) {
if (mxEvent.getSender() === myUserId) {
title = _t("You cancelled verifying %(name)s",
{name: getNameForEventRoom(this.state.otherPartyUserId, mxEvent)});
} else if (mxEvent.getSender() === this.state.otherPartyUserId) {
title = _t("%(name)s cancelled verifying",
{name: getNameForEventRoom(this.state.otherPartyUserId, mxEvent)});
}
}
if (title) {
const subtitle = userLabelForEventRoom(this.state.otherPartyUserId, mxEvent);
const classes = classNames("mx_EventTile_bubble", "mx_KeyVerification", "mx_KeyVerification_icon", {
mx_KeyVerification_icon_verified: this.state.done,
});
return (<div className={classes}>
<div className="mx_KeyVerification_title">{title}</div>
<div className="mx_KeyVerification_subtitle">{subtitle}</div>
</div>);
}
return null;
}
}
MKeyVerificationConclusion.propTypes = {
/* the MatrixEvent to show */
mxEvent: PropTypes.object.isRequired,
};

View file

@ -0,0 +1,141 @@
/*
Copyright 2019 The Matrix.org Foundation C.I.C.
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 MatrixClientPeg from '../../../MatrixClientPeg';
import {verificationMethods} from 'matrix-js-sdk/lib/crypto';
import sdk from '../../../index';
import Modal from "../../../Modal";
import { _t } from '../../../languageHandler';
import KeyVerificationStateObserver, {getNameForEventRoom, userLabelForEventRoom}
from '../../../utils/KeyVerificationStateObserver';
export default class MKeyVerificationRequest extends React.Component {
constructor(props) {
super(props);
this.keyVerificationState = new KeyVerificationStateObserver(this.props.mxEvent, MatrixClientPeg.get(), () => {
this.setState(this._copyState());
});
this.state = this._copyState();
}
_copyState() {
const {accepted, done, cancelled, cancelPartyUserId, otherPartyUserId} = this.keyVerificationState;
return {accepted, done, cancelled, cancelPartyUserId, otherPartyUserId};
}
componentDidMount() {
this.keyVerificationState.attach();
}
componentWillUnmount() {
this.keyVerificationState.detach();
}
_onAcceptClicked = () => {
const IncomingSasDialog = sdk.getComponent('views.dialogs.IncomingSasDialog');
// todo: validate event, for example if it has sas in the methods.
const verifier = MatrixClientPeg.get().acceptVerificationDM(this.props.mxEvent, verificationMethods.SAS);
Modal.createTrackedDialog('Incoming Verification', '', IncomingSasDialog, {
verifier,
});
};
_onRejectClicked = () => {
// todo: validate event, for example if it has sas in the methods.
const verifier = MatrixClientPeg.get().acceptVerificationDM(this.props.mxEvent, verificationMethods.SAS);
verifier.cancel("User declined");
};
_acceptedLabel(userId) {
const client = MatrixClientPeg.get();
const myUserId = client.getUserId();
if (userId === myUserId) {
return _t("You accepted");
} else {
return _t("%(name)s accepted", {name: getNameForEventRoom(userId, this.props.mxEvent)});
}
}
_cancelledLabel(userId) {
const client = MatrixClientPeg.get();
const myUserId = client.getUserId();
if (userId === myUserId) {
return _t("You cancelled");
} else {
return _t("%(name)s cancelled", {name: getNameForEventRoom(userId, this.props.mxEvent)});
}
}
render() {
const {mxEvent} = this.props;
const fromUserId = mxEvent.getSender();
const content = mxEvent.getContent();
const toUserId = content.to;
const client = MatrixClientPeg.get();
const myUserId = client.getUserId();
const isOwn = fromUserId === myUserId;
let title;
let subtitle;
let stateNode;
if (this.state.accepted || this.state.cancelled) {
let stateLabel;
if (this.state.accepted) {
stateLabel = this._acceptedLabel(toUserId);
} else if (this.state.cancelled) {
stateLabel = this._cancelledLabel(this.state.cancelPartyUserId);
}
stateNode = (<div className="mx_KeyVerification_state">{stateLabel}</div>);
}
if (toUserId === myUserId) { // request sent to us
title = (<div className="mx_KeyVerification_title">{
_t("%(name)s wants to verify", {name: getNameForEventRoom(fromUserId, mxEvent)})}</div>);
subtitle = (<div className="mx_KeyVerification_subtitle">{
userLabelForEventRoom(fromUserId, mxEvent)}</div>);
const isResolved = !(this.state.accepted || this.state.cancelled || this.state.done);
if (isResolved) {
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
stateNode = (<div className="mx_KeyVerification_buttons">
<AccessibleButton kind="decline" onClick={this._onRejectClicked}>{_t("Decline")}</AccessibleButton>
<AccessibleButton kind="accept" onClick={this._onAcceptClicked}>{_t("Accept")}</AccessibleButton>
</div>);
}
} else if (isOwn) { // request sent by us
title = (<div className="mx_KeyVerification_title">{
_t("You sent a verification request")}</div>);
subtitle = (<div className="mx_KeyVerification_subtitle">{
userLabelForEventRoom(this.state.otherPartyUserId, mxEvent)}</div>);
}
if (title) {
return (<div className="mx_EventTile_bubble mx_KeyVerification mx_KeyVerification_icon">
{title}
{subtitle}
{stateNode}
</div>);
}
return null;
}
}
MKeyVerificationRequest.propTypes = {
/* the MatrixEvent to show */
mxEvent: PropTypes.object.isRequired,
};

View file

@ -33,12 +33,15 @@ import dis from '../../../dispatcher';
import SettingsStore from "../../../settings/SettingsStore";
import {EventStatus, MatrixClient} from 'matrix-js-sdk';
import {formatTime} from "../../../DateUtils";
import MatrixClientPeg from '../../../MatrixClientPeg';
const ObjectUtils = require('../../../ObjectUtils');
const eventTileTypes = {
'm.room.message': 'messages.MessageEvent',
'm.sticker': 'messages.MessageEvent',
'm.key.verification.cancel': 'messages.MKeyVerificationConclusion',
'm.key.verification.done': 'messages.MKeyVerificationConclusion',
'm.call.invite': 'messages.TextualEvent',
'm.call.answer': 'messages.TextualEvent',
'm.call.hangup': 'messages.TextualEvent',
@ -68,6 +71,31 @@ const stateEventTileTypes = {
function getHandlerTile(ev) {
const type = ev.getType();
// don't show verification requests we're not involved in,
// not even when showing hidden events
if (type === "m.room.message") {
const content = ev.getContent();
if (content && content.msgtype === "m.key.verification.request") {
const client = MatrixClientPeg.get();
const me = client && client.getUserId();
if (ev.getSender() !== me && content.to !== me) {
return undefined;
} else {
return "messages.MKeyVerificationRequest";
}
}
}
// these events are sent by both parties during verification, but we only want to render one
// tile once the verification concludes, so filter out the one from the other party.
if (type === "m.key.verification.done") {
const client = MatrixClientPeg.get();
const me = client && client.getUserId();
if (ev.getSender() !== me) {
return undefined;
}
}
return ev.isState() ? stateEventTileTypes[type] : eventTileTypes[type];
}
@ -527,8 +555,11 @@ module.exports = createReactClass({
const eventType = this.props.mxEvent.getType();
// Info messages are basically information about commands processed on a room
const isBubbleMessage = eventType.startsWith("m.key.verification") ||
(eventType === "m.room.message" && msgtype.startsWith("m.key.verification"));
let isInfoMessage = (
eventType !== 'm.room.message' && eventType !== 'm.sticker' && eventType != 'm.room.create'
!isBubbleMessage && eventType !== 'm.room.message' &&
eventType !== 'm.sticker' && eventType != 'm.room.create'
);
let tileHandler = getHandlerTile(this.props.mxEvent);
@ -561,6 +592,7 @@ module.exports = createReactClass({
const isEditing = !!this.props.editState;
const classes = classNames({
mx_EventTile_bubbleContainer: isBubbleMessage,
mx_EventTile: true,
mx_EventTile_isEditing: isEditing,
mx_EventTile_info: isInfoMessage,
@ -596,7 +628,7 @@ module.exports = createReactClass({
if (this.props.tileShape === "notif") {
avatarSize = 24;
needsSenderProfile = true;
} else if (tileHandler === 'messages.RoomCreate') {
} else if (tileHandler === 'messages.RoomCreate' || isBubbleMessage) {
avatarSize = 0;
needsSenderProfile = false;
} else if (isInfoMessage) {
@ -794,7 +826,7 @@ module.exports = createReactClass({
{ readAvatars }
</div>
{ sender }
<div className="mx_EventTile_line">
<div className={classNames("mx_EventTile_line", {mx_EventTile_bubbleLine: isBubbleMessage})}>
<a
href={permalink}
onClick={this.onPermalinkClicked}