Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c6b500bc09 | |||
| c3972015c7 | |||
| 3c18c57857 | |||
| f562a06707 | |||
| c4fe15400c | |||
| 6e3e8f7310 | |||
| fadb4d9219 | |||
| b63149b36a | |||
| 79f92abcfa | |||
| 0137fb468b | |||
| 70ef8760cc | |||
| c74d2d831b | |||
| 0415f821eb | |||
| 779fe35255 | |||
| 11382d2cd7 | |||
| ef31131a5d | |||
| 76feabe32b | |||
| 7fe3e2f90a | |||
| c0b2151929 | |||
| 5e3b1bf6b0 | |||
| fdf4523c2a | |||
| c6cf76f345 | |||
| f16e544691 | |||
| df101217fc | |||
| 5d90dc16cc | |||
| bbfc1a1cd6 | |||
| b7208c12ac | |||
| eaa2fdec44 | |||
| 7099adfe79 | |||
| cb17a2bcb0 | |||
| 172044a1cb | |||
| 4b203b6b63 | |||
| 1bf1ce7070 | |||
| 260e7b529f | |||
| b027089fc1 | |||
| 95d582ccee |
@@ -1,3 +1,52 @@
|
||||
Changes in [0.10.6](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.10.6) (2018-07-09)
|
||||
==================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.10.6-rc.1...v0.10.6)
|
||||
|
||||
* No changes since rc.1
|
||||
|
||||
Changes in [0.10.6-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.10.6-rc.1) (2018-07-06)
|
||||
============================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.10.5...v0.10.6-rc.1)
|
||||
|
||||
* Expose event decryption error via Event.decrypted event
|
||||
[\#665](https://github.com/matrix-org/matrix-js-sdk/pull/665)
|
||||
* Add decryption error codes to base.DecryptionError
|
||||
[\#663](https://github.com/matrix-org/matrix-js-sdk/pull/663)
|
||||
|
||||
Changes in [0.10.5](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.10.5) (2018-06-29)
|
||||
==================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.10.5-rc.1...v0.10.5)
|
||||
|
||||
* No changes since rc.1
|
||||
|
||||
Changes in [0.10.5-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.10.5-rc.1) (2018-06-21)
|
||||
============================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.10.4...v0.10.5-rc.1)
|
||||
|
||||
* fix auth header and filename=undefined
|
||||
[\#659](https://github.com/matrix-org/matrix-js-sdk/pull/659)
|
||||
* allow setting the output device for webrtc calls
|
||||
[\#650](https://github.com/matrix-org/matrix-js-sdk/pull/650)
|
||||
* arguments true and false are actually invalid
|
||||
[\#596](https://github.com/matrix-org/matrix-js-sdk/pull/596)
|
||||
* fix typo where `headers` was not being used and thus sent wrong content-type
|
||||
[\#643](https://github.com/matrix-org/matrix-js-sdk/pull/643)
|
||||
* fix some documentation typos
|
||||
[\#642](https://github.com/matrix-org/matrix-js-sdk/pull/642)
|
||||
|
||||
Changes in [0.10.4](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.10.4) (2018-06-12)
|
||||
==================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.10.4-rc.1...v0.10.4)
|
||||
|
||||
* No changes since rc.1
|
||||
|
||||
Changes in [0.10.4-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.10.4-rc.1) (2018-06-06)
|
||||
============================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.10.3...v0.10.4-rc.1)
|
||||
|
||||
* check whether notif level is undefined, because `0` is falsey
|
||||
[\#651](https://github.com/matrix-org/matrix-js-sdk/pull/651)
|
||||
|
||||
Changes in [0.10.3](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.10.3) (2018-05-25)
|
||||
==================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.10.3-rc.1...v0.10.3)
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "0.10.3",
|
||||
"version": "0.10.6",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -1071,6 +1071,10 @@ MatrixBaseApis.prototype.searchUserDirectory = function(opts) {
|
||||
* @param {string=} opts.name Name to give the file on the server. Defaults
|
||||
* to <tt>file.name</tt>.
|
||||
*
|
||||
* @param {boolean=} opts.includeFilename if false will not send the filename,
|
||||
* e.g for encrypted file uploads where filename leaks are undesirable.
|
||||
* Defaults to true.
|
||||
*
|
||||
* @param {string=} opts.type Content-type for the upload. Defaults to
|
||||
* <tt>file.type</tt>, or <tt>applicaton/octet-stream</tt>.
|
||||
*
|
||||
|
||||
+2
-2
@@ -627,7 +627,7 @@ MatrixClient.prototype.isEventSenderVerified = async function(event) {
|
||||
/**
|
||||
* Cancel a room key request for this event if one is ongoing and resend the
|
||||
* request.
|
||||
* @param {MatrxEvent} event event of which to cancel and resend the room
|
||||
* @param {MatrixEvent} event event of which to cancel and resend the room
|
||||
* key request.
|
||||
*/
|
||||
MatrixClient.prototype.cancelAndResendEventRoomKeyRequest = function(event) {
|
||||
@@ -721,7 +721,7 @@ MatrixClient.prototype.getGroup = function(groupId) {
|
||||
|
||||
/**
|
||||
* Retrieve all known groups.
|
||||
* @return {Groups[]} A list of groups, or an empty list if there is no data store.
|
||||
* @return {Group[]} A list of groups, or an empty list if there is no data store.
|
||||
*/
|
||||
MatrixClient.prototype.getGroups = function() {
|
||||
return this.store.getGroups();
|
||||
|
||||
@@ -176,8 +176,9 @@ export {DecryptionAlgorithm}; // https://github.com/jsdoc3/jsdoc/issues/1272
|
||||
* @extends Error
|
||||
*/
|
||||
class DecryptionError extends Error {
|
||||
constructor(msg, details) {
|
||||
constructor(code, msg, details) {
|
||||
super(msg);
|
||||
this.code = code;
|
||||
this.name = 'DecryptionError';
|
||||
this.detailedString = _detailedStringForDecryptionError(this, details);
|
||||
}
|
||||
|
||||
@@ -618,7 +618,10 @@ MegolmDecryption.prototype.decryptEvent = async function(event) {
|
||||
if (!content.sender_key || !content.session_id ||
|
||||
!content.ciphertext
|
||||
) {
|
||||
throw new base.DecryptionError("Missing fields in input");
|
||||
throw new base.DecryptionError(
|
||||
"MEGOLM_MISSING_FIELDS",
|
||||
"Missing fields in input",
|
||||
);
|
||||
}
|
||||
|
||||
// we add the event to the pending list *before* we start decryption.
|
||||
@@ -635,10 +638,16 @@ MegolmDecryption.prototype.decryptEvent = async function(event) {
|
||||
event.getId(), event.getTs(),
|
||||
);
|
||||
} catch (e) {
|
||||
let errorCode = "OLM_DECRYPT_GROUP_MESSAGE_ERROR";
|
||||
|
||||
if (e.message === 'OLM.UNKNOWN_MESSAGE_INDEX') {
|
||||
this._requestKeysForEvent(event);
|
||||
|
||||
errorCode = 'OLM_UNKNOWN_MESSAGE_INDEX';
|
||||
}
|
||||
|
||||
throw new base.DecryptionError(
|
||||
errorCode,
|
||||
e.toString(), {
|
||||
session: content.sender_key + '|' + content.session_id,
|
||||
},
|
||||
@@ -655,6 +664,7 @@ MegolmDecryption.prototype.decryptEvent = async function(event) {
|
||||
// scheduled, so we needn't send out the request here.)
|
||||
this._requestKeysForEvent(event);
|
||||
throw new base.DecryptionError(
|
||||
"MEGOLM_UNKNOWN_INBOUND_SESSION_ID",
|
||||
"The sender's device has not sent us the keys for this message.",
|
||||
{
|
||||
session: content.sender_key + '|' + content.session_id,
|
||||
@@ -673,6 +683,7 @@ MegolmDecryption.prototype.decryptEvent = async function(event) {
|
||||
// room, so neither the sender nor a MITM can lie about the room_id).
|
||||
if (payload.room_id !== event.getRoomId()) {
|
||||
throw new base.DecryptionError(
|
||||
"MEGOLM_BAD_ROOM",
|
||||
"Message intended for room " + payload.room_id,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -168,11 +168,17 @@ OlmDecryption.prototype.decryptEvent = async function(event) {
|
||||
const ciphertext = content.ciphertext;
|
||||
|
||||
if (!ciphertext) {
|
||||
throw new base.DecryptionError("Missing ciphertext");
|
||||
throw new base.DecryptionError(
|
||||
"OLM_MISSING_CIPHERTEXT",
|
||||
"Missing ciphertext",
|
||||
);
|
||||
}
|
||||
|
||||
if (!(this._olmDevice.deviceCurve25519Key in ciphertext)) {
|
||||
throw new base.DecryptionError("Not included in recipients");
|
||||
throw new base.DecryptionError(
|
||||
"OLM_NOT_INCLUDED_IN_RECIPIENTS",
|
||||
"Not included in recipients",
|
||||
);
|
||||
}
|
||||
const message = ciphertext[this._olmDevice.deviceCurve25519Key];
|
||||
let payloadString;
|
||||
@@ -181,6 +187,7 @@ OlmDecryption.prototype.decryptEvent = async function(event) {
|
||||
payloadString = await this._decryptMessage(deviceKey, message);
|
||||
} catch (e) {
|
||||
throw new base.DecryptionError(
|
||||
"OLM_BAD_ENCRYPTED_MESSAGE",
|
||||
"Bad Encrypted Message", {
|
||||
sender: deviceKey,
|
||||
err: e,
|
||||
@@ -194,12 +201,14 @@ OlmDecryption.prototype.decryptEvent = async function(event) {
|
||||
// https://github.com/vector-im/vector-web/issues/2483
|
||||
if (payload.recipient != this._userId) {
|
||||
throw new base.DecryptionError(
|
||||
"OLM_BAD_RECIPIENT",
|
||||
"Message was intented for " + payload.recipient,
|
||||
);
|
||||
}
|
||||
|
||||
if (payload.recipient_keys.ed25519 != this._olmDevice.deviceEd25519Key) {
|
||||
throw new base.DecryptionError(
|
||||
"OLM_BAD_RECIPIENT_KEY",
|
||||
"Message not intended for this device", {
|
||||
intended: payload.recipient_keys.ed25519,
|
||||
our_key: this._olmDevice.deviceEd25519Key,
|
||||
@@ -213,6 +222,7 @@ OlmDecryption.prototype.decryptEvent = async function(event) {
|
||||
// which is checked elsewhere).
|
||||
if (payload.sender != event.getSender()) {
|
||||
throw new base.DecryptionError(
|
||||
"OLM_FORWARDED_MESSAGE",
|
||||
"Message forwarded from " + payload.sender, {
|
||||
reported_sender: event.getSender(),
|
||||
},
|
||||
@@ -222,6 +232,7 @@ OlmDecryption.prototype.decryptEvent = async function(event) {
|
||||
// Olm events intended for a room have a room_id.
|
||||
if (payload.room_id !== event.getRoomId()) {
|
||||
throw new base.DecryptionError(
|
||||
"OLM_BAD_ROOM",
|
||||
"Message intended for room " + payload.room_id, {
|
||||
reported_room: event.room_id,
|
||||
},
|
||||
|
||||
@@ -1275,6 +1275,7 @@ Crypto.prototype._getRoomDecryptor = function(roomId, algorithm) {
|
||||
const AlgClass = algorithms.DECRYPTION_CLASSES[algorithm];
|
||||
if (!AlgClass) {
|
||||
throw new algorithms.DecryptionError(
|
||||
'UNKNOWN_ENCRYPTION_ALGORITHM',
|
||||
'Unknown encryption algorithm "' + algorithm + '".',
|
||||
);
|
||||
}
|
||||
|
||||
+31
-6
@@ -118,6 +118,10 @@ module.exports.MatrixHttpApi.prototype = {
|
||||
* @param {string=} opts.name Name to give the file on the server. Defaults
|
||||
* to <tt>file.name</tt>.
|
||||
*
|
||||
* @param {boolean=} opts.includeFilename if false will not send the filename,
|
||||
* e.g for encrypted file uploads where filename leaks are undesirable.
|
||||
* Defaults to true.
|
||||
*
|
||||
* @param {string=} opts.type Content-type for the upload. Defaults to
|
||||
* <tt>file.type</tt>, or <tt>applicaton/octet-stream</tt>.
|
||||
*
|
||||
@@ -152,6 +156,9 @@ module.exports.MatrixHttpApi.prototype = {
|
||||
opts = {};
|
||||
}
|
||||
|
||||
// default opts.includeFilename to true (ignoring falsey values)
|
||||
const includeFilename = opts.includeFilename !== false;
|
||||
|
||||
// if the file doesn't have a mime type, use a default since
|
||||
// the HS errors if we don't supply one.
|
||||
const contentType = opts.type || file.type || 'application/octet-stream';
|
||||
@@ -272,10 +279,26 @@ module.exports.MatrixHttpApi.prototype = {
|
||||
}
|
||||
});
|
||||
let url = this.opts.baseUrl + "/_matrix/media/v1/upload";
|
||||
url += "?access_token=" + encodeURIComponent(this.opts.accessToken);
|
||||
url += "&filename=" + encodeURIComponent(fileName);
|
||||
|
||||
const queryArgs = [];
|
||||
|
||||
if (includeFilename && fileName) {
|
||||
queryArgs.push("filename=" + encodeURIComponent(fileName));
|
||||
}
|
||||
|
||||
if (!this.useAuthorizationHeader) {
|
||||
queryArgs.push("access_token="
|
||||
+ encodeURIComponent(this.opts.accessToken));
|
||||
}
|
||||
|
||||
if (queryArgs.length > 0) {
|
||||
url += "?" + queryArgs.join("&");
|
||||
}
|
||||
|
||||
xhr.open("POST", url);
|
||||
if (this.useAuthorizationHeader) {
|
||||
xhr.setRequestHeader("Authorization", "Bearer " + this.opts.accessToken);
|
||||
}
|
||||
xhr.setRequestHeader("Content-Type", contentType);
|
||||
xhr.send(body);
|
||||
promise = defer.promise;
|
||||
@@ -283,9 +306,11 @@ module.exports.MatrixHttpApi.prototype = {
|
||||
// dirty hack (as per _request) to allow the upload to be cancelled.
|
||||
promise.abort = xhr.abort.bind(xhr);
|
||||
} else {
|
||||
const queryParams = {
|
||||
filename: fileName,
|
||||
};
|
||||
const queryParams = {};
|
||||
|
||||
if (includeFilename && fileName) {
|
||||
queryParams.filename = fileName;
|
||||
}
|
||||
|
||||
promise = this.authedRequest(
|
||||
opts.callback, "POST", "/upload", queryParams, body, {
|
||||
@@ -730,7 +755,7 @@ module.exports.MatrixHttpApi.prototype = {
|
||||
body: data,
|
||||
json: false,
|
||||
timeout: localTimeoutMs,
|
||||
headers: opts.headers || {},
|
||||
headers: headers || {},
|
||||
_matrix_opts: this.opts,
|
||||
},
|
||||
function(err, response, body) {
|
||||
|
||||
@@ -38,6 +38,8 @@ module.exports.MatrixError = require("./http-api").MatrixError;
|
||||
module.exports.MatrixClient = require("./client").MatrixClient;
|
||||
/** The {@link module:models/room|Room} class. */
|
||||
module.exports.Room = require("./models/room");
|
||||
/** The {@link module:models/group|Group} class. */
|
||||
module.exports.Group = require("./models/group");
|
||||
/** The {@link module:models/event-timeline~EventTimeline} class. */
|
||||
module.exports.EventTimeline = require("./models/event-timeline");
|
||||
/** The {@link module:models/event-timeline-set~EventTimelineSet} class. */
|
||||
@@ -81,6 +83,13 @@ module.exports.IndexedDBCryptoStore =
|
||||
module.exports.createNewMatrixCall = require("./webrtc/call").createNewMatrixCall;
|
||||
|
||||
|
||||
/**
|
||||
* Set an audio output device to use for MatrixCalls
|
||||
* @function
|
||||
* @param {string=} deviceId the identifier for the device
|
||||
* undefined treated as unset
|
||||
*/
|
||||
module.exports.setMatrixCallAudioOutput = require('./webrtc/call').setAudioOutput;
|
||||
/**
|
||||
* Set an audio input device to use for MatrixCalls
|
||||
* @function
|
||||
|
||||
+9
-1
@@ -404,6 +404,7 @@ utils.extend(module.exports.MatrixEvent.prototype, {
|
||||
this._retryDecryption = false;
|
||||
|
||||
let res;
|
||||
let err;
|
||||
try {
|
||||
if (!crypto) {
|
||||
res = this._badEncryptedMessage("Encryption not enabled");
|
||||
@@ -422,6 +423,8 @@ utils.extend(module.exports.MatrixEvent.prototype, {
|
||||
return;
|
||||
}
|
||||
|
||||
err = e;
|
||||
|
||||
// see if we have a retry queued.
|
||||
//
|
||||
// NB: make sure to keep this check in the same tick of the
|
||||
@@ -467,6 +470,9 @@ utils.extend(module.exports.MatrixEvent.prototype, {
|
||||
this._decryptionPromise = null;
|
||||
this._retryDecryption = false;
|
||||
this._setClearData(res);
|
||||
|
||||
this.emit("Event.decrypted", this, err);
|
||||
|
||||
return;
|
||||
}
|
||||
},
|
||||
@@ -503,7 +509,6 @@ utils.extend(module.exports.MatrixEvent.prototype, {
|
||||
decryptionResult.claimedEd25519Key || null;
|
||||
this._forwardingCurve25519KeyChain =
|
||||
decryptionResult.forwardingCurve25519KeyChain || [];
|
||||
this.emit("Event.decrypted", this);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -708,4 +713,7 @@ const _REDACT_KEEP_CONTENT_MAP = {
|
||||
*
|
||||
* @param {module:models/event.MatrixEvent} event
|
||||
* The matrix event which has been decrypted
|
||||
* @param {module:crypto/algorithms/base.DecryptionError?} err
|
||||
* The error that occured during decryption, or `undefined` if no
|
||||
* error occured.
|
||||
*/
|
||||
|
||||
@@ -329,7 +329,7 @@ RoomState.prototype._hasSufficientPowerLevelFor = function(action, powerLevel) {
|
||||
}
|
||||
|
||||
let requiredLevel = 50;
|
||||
if (powerLevels[action] !== undefined) {
|
||||
if (utils.isNumber(powerLevels[action])) {
|
||||
requiredLevel = powerLevels[action];
|
||||
}
|
||||
|
||||
@@ -417,18 +417,18 @@ RoomState.prototype._maySendEventOfType = function(eventType, userId, state) {
|
||||
power_levels = power_levels_event.getContent();
|
||||
events_levels = power_levels.events || {};
|
||||
|
||||
if (power_levels.state_default !== undefined) {
|
||||
if (utils.isNumber(power_levels.state_default)) {
|
||||
state_default = power_levels.state_default;
|
||||
} else {
|
||||
state_default = 50;
|
||||
}
|
||||
if (power_levels.events_default !== undefined) {
|
||||
if (utils.isNumber(power_levels.events_default)) {
|
||||
events_default = power_levels.events_default;
|
||||
}
|
||||
}
|
||||
|
||||
let required_level = state ? state_default : events_default;
|
||||
if (events_levels[eventType] !== undefined) {
|
||||
if (utils.isNumber(events_levels[eventType])) {
|
||||
required_level = events_levels[eventType];
|
||||
}
|
||||
return member.powerLevel >= required_level;
|
||||
@@ -455,7 +455,7 @@ RoomState.prototype.mayTriggerNotifOfType = function(notifLevelKey, userId) {
|
||||
powerLevelsEvent &&
|
||||
powerLevelsEvent.getContent() &&
|
||||
powerLevelsEvent.getContent().notifications &&
|
||||
powerLevelsEvent.getContent().notifications[notifLevelKey]
|
||||
utils.isNumber(powerLevelsEvent.getContent().notifications[notifLevelKey])
|
||||
) {
|
||||
notifLevel = powerLevelsEvent.getContent().notifications[notifLevelKey];
|
||||
}
|
||||
|
||||
+2
-2
@@ -94,10 +94,10 @@ function synthesizeReceipt(userId, event, receiptType) {
|
||||
* event type and the values are the events.
|
||||
* @prop {RoomState} oldState The state of the room at the time of the oldest
|
||||
* event in the live timeline. Present for backwards compatibility -
|
||||
* prefer getLiveTimeline().getState(true).
|
||||
* prefer getLiveTimeline().getState(EventTimeline.BACKWARDS).
|
||||
* @prop {RoomState} currentState The state of the room at the time of the
|
||||
* newest event in the timeline. Present for backwards compatibility -
|
||||
* prefer getLiveTimeline().getState(false).
|
||||
* prefer getLiveTimeline().getState(EventTimeline.FORWARDS).
|
||||
* @prop {RoomSummary} summary The room summary.
|
||||
* @prop {*} storageToken A token which a data store can use to remember
|
||||
* the state of the room.
|
||||
|
||||
@@ -652,3 +652,13 @@ module.exports.inherits = function(ctor, superCtor) {
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns whether the given value is a finite number without type-coercion
|
||||
*
|
||||
* @param {*} value the value to test
|
||||
* @return {boolean} whether or not value is a finite number without type-coercion
|
||||
*/
|
||||
module.exports.isNumber = function(value) {
|
||||
return typeof value === 'number' && isFinite(value);
|
||||
};
|
||||
|
||||
+13
-1
@@ -1121,9 +1121,13 @@ const _tryPlayRemoteStream = function(self) {
|
||||
}
|
||||
};
|
||||
|
||||
const _tryPlayRemoteAudioStream = function(self) {
|
||||
const _tryPlayRemoteAudioStream = async function(self) {
|
||||
if (self.getRemoteAudioElement() && self.remoteAStream) {
|
||||
const player = self.getRemoteAudioElement();
|
||||
|
||||
// if audioOutput is non-default:
|
||||
if (audioOutput) await player.setSinkId(audioOutput);
|
||||
|
||||
player.autoplay = true;
|
||||
self.assignElement(player, self.remoteAStream, "remoteAudio");
|
||||
setTimeout(function() {
|
||||
@@ -1317,8 +1321,16 @@ const forAllTracksOnStream = function(s, f) {
|
||||
/** The MatrixCall class. */
|
||||
module.exports.MatrixCall = MatrixCall;
|
||||
|
||||
let audioOutput;
|
||||
let audioInput;
|
||||
let videoInput;
|
||||
/**
|
||||
* Set an audio output device to use for MatrixCalls
|
||||
* @function
|
||||
* @param {string=} deviceId the identifier for the device
|
||||
* undefined treated as unset
|
||||
*/
|
||||
module.exports.setAudioOutput = function(deviceId) { audioOutput = deviceId; };
|
||||
/**
|
||||
* Set an audio input device to use for MatrixCalls
|
||||
* @function
|
||||
|
||||
Reference in New Issue
Block a user