Skip to content

Commit

Permalink
Inform user if message couldn't be delivered because they're blocked
Browse files Browse the repository at this point in the history
  • Loading branch information
jcbrand committed Jan 5, 2025
1 parent 76b2ad5 commit 70a707a
Show file tree
Hide file tree
Showing 15 changed files with 152 additions and 55 deletions.
15 changes: 15 additions & 0 deletions src/headless/plugins/blocklist/plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,21 @@ converse.plugins.add('converse-blocking', {

api.promises.add(['blocklistInitialized']);

api.listen.on(
'getErrorAttributesForMessage',
/**
* @param {import('plugins/chat/types').MessageAttributes} attrs
* @param {import('plugins/chat/types').MessageErrorAttributes} new_attrs
*/
(attrs, new_attrs) => {
if (attrs.errors.find((e) => e.name === 'blocked' && e.xmlns === `${Strophe.NS.BLOCKING}:errors`)) {
const { __ } = _converse;
new_attrs.error = __('You are blocked from sending messages.');
}
return new_attrs;
}
);

api.listen.on('connected', () => {
const connection = api.connection.get();
connection.addHandler(
Expand Down
60 changes: 50 additions & 10 deletions src/headless/plugins/blocklist/tests/blocklist.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/*global mock, converse */
const { u, stx } = converse.env;

fdescribe('A blocklist', function () {
describe('A blocklist', function () {
beforeEach(() => {
jasmine.addMatchers({ toEqualStanza: jasmine.toEqualStanza });
window.sessionStorage.removeItem('[email protected]');
Expand Down Expand Up @@ -125,15 +125,18 @@ fdescribe('A blocklist', function () {
const IQ_stanzas = api.connection.get().IQ_stanzas;
let sent_stanza = await u.waitUntil(() => IQ_stanzas.find((s) => s.querySelector('iq blocklist')));

_converse.api.connection.get()._dataRecv(mock.createRequest(
stx`<iq xmlns="jabber:client"
_converse.api.connection.get()._dataRecv(
mock.createRequest(
stx`<iq xmlns="jabber:client"
to="${api.connection.get().jid}"
type="result"
id="${sent_stanza.getAttribute('id')}">
<blocklist xmlns='urn:xmpp:blocking'>
<item jid='[email protected]'/>
</blocklist>
</iq>`));
</iq>`
)
);

const blocklist = await api.waitUntil('blocklistInitialized');
expect(blocklist.length).toBe(1);
Expand All @@ -148,9 +151,13 @@ fdescribe('A blocklist', function () {
</block>
</iq>`);

_converse.api.connection.get()._dataRecv(mock.createRequest(
stx`<iq xmlns="jabber:client" type="result" id="${sent_stanza.getAttribute('id')}"/>`)
);
_converse.api.connection
.get()
._dataRecv(
mock.createRequest(
stx`<iq xmlns="jabber:client" type="result" id="${sent_stanza.getAttribute('id')}"/>`
)
);

await u.waitUntil(() => blocklist.length === 2);
expect(blocklist.models.map((m) => m.get('jid'))).toEqual(['[email protected]', '[email protected]']);
Expand All @@ -165,12 +172,45 @@ fdescribe('A blocklist', function () {
</unblock>
</iq>`);

_converse.api.connection.get()._dataRecv(mock.createRequest(
stx`<iq xmlns="jabber:client" type="result" id="${sent_stanza.getAttribute('id')}"/>`)
);
_converse.api.connection
.get()
._dataRecv(
mock.createRequest(
stx`<iq xmlns="jabber:client" type="result" id="${sent_stanza.getAttribute('id')}"/>`
)
);

await u.waitUntil(() => blocklist.length === 1);
expect(blocklist.models.map((m) => m.get('jid'))).toEqual(['[email protected]']);
})
);
});

describe('A Chat Message', function () {
fit(
"will show an error message if it's rejected due to being banned",
mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) {
const { api } = _converse;
await mock.waitForRoster(_converse, 'current', 1);
const sender_jid = mock.cur_names[0].replace(/ /g, '.').toLowerCase() + '@montague.lit';
const chat = await api.chats.open(sender_jid);
const msg_text = 'This message will not be sent, due to an error';
const message = await chat.sendMessage({ body: msg_text });

api.connection.get()._dataRecv(mock.createRequest(stx`
<message xmlns="jabber:client"
to="${api.connection.get().jid}"
type="error"
id="${message.get('msgid')}"
from="${sender_jid}">
<error type="cancel">
<not-acceptable xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
<blocked xmlns='urn:xmpp:blocking:errors'/>
</error>
</message>`));

await u.waitUntil(() => message.get('is_error') === true);
expect(message.get('error')).toBe('You are blocked from sending messages.');
})
);
});
16 changes: 10 additions & 6 deletions src/headless/plugins/chat/types.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,27 @@
import {EncryptionAttrs} from "../../shared/types";

export type MessageAttributes = EncryptionAttrs & {
export type MessageErrorAttributes = {
is_error: boolean; // Whether an error was received for this message
error: string; // The error name
errors: { name: string; xmlns: string }[];
error_condition: string; // The defined error condition
error_text: string; // The error text received from the server
error_type: string; // The type of error received from the server
}

export type MessageAttributes = EncryptionAttrs & MessageErrorAttributes & {
body: string; // The contents of the <body> tag of the message stanza
chat_state: string; // The XEP-0085 chat state notification contained in this message
contact_jid: string; // The JID of the other person or entity
editable: boolean; // Is this message editable via XEP-0308?
edited: string; // An ISO8601 string recording the time that the message was edited per XEP-0308
error: string; // The error name
error_condition: string; // The defined error condition
error_text: string; // The error text received from the server
error_type: string; // The type of error received from the server
from: string; // The sender JID
message?: string; // Used with info and error messages
fullname: string; // The full name of the sender
is_archived: boolean; // Is this message from a XEP-0313 MAM archive?
is_carbon: boolean; // Is this message a XEP-0280 Carbon?
is_delayed: boolean; // Was delivery of this message was delayed as per XEP-0203?
is_encrypted: boolean; // Is this message XEP-0384 encrypted?
is_error: boolean; // Whether an error was received for this message
is_headline: boolean; // Is this a "headline" message?
is_markable: boolean; // Can this message be marked with a XEP-0333 chat marker?
is_marker: boolean; // Is this message a XEP-0333 Chat Marker?
Expand Down
4 changes: 2 additions & 2 deletions src/headless/plugins/emoji/plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import emojis from './api.js';
import { isOnlyEmojis } from './utils.js';

converse.emojis = {
'initialized': false,
'initialized_promise': getOpenPromise(),
initialized: false,
initialized_promise: getOpenPromise(),
};

converse.plugins.add('converse-emoji', {
Expand Down
68 changes: 42 additions & 26 deletions src/headless/shared/model-with-messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { isNewMessage } from '../plugins/chat/utils.js';
import _converse from './_converse.js';
import { MethodNotImplementedError } from './errors.js';
import { sendMarker, sendReceiptStanza, sendRetractionMessage } from './actions.js';
import {parseMessage} from '../plugins/chat/parsers';
import { parseMessage } from '../plugins/chat/parsers';

const { Strophe, $msg, u } = converse.env;

Expand Down Expand Up @@ -274,6 +274,8 @@ export default function ModelWithMessages(BaseModel) {
* chat.sendMessage({'body': 'hello world'});
*/
async sendMessage(attrs) {
await converse.emojis?.initialized_promise;

if (!this.canPostMessages()) {
log.warn('sendMessage was called but canPostMessages is false');
return;
Expand Down Expand Up @@ -748,11 +750,48 @@ export default function ModelWithMessages(BaseModel) {
}
}

/**
* @param {Message} message
* @param {MessageAttributes} attrs
*/
async getErrorAttributesForMessage(message, attrs) {
const { __ } = _converse;
const new_attrs = {
editable: false,
error: attrs.error,
error_condition: attrs.error_condition,
error_text: attrs.error_text,
error_type: attrs.error_type,
is_error: true,
};
if (attrs.msgid === message.get('retraction_id')) {
// The error message refers to a retraction
new_attrs.retraction_id = undefined;
if (!attrs.error) {
if (attrs.error_condition === 'forbidden') {
new_attrs.error = __("You're not allowed to retract your message.");
} else {
new_attrs.error = __('Sorry, an error occurred while trying to retract your message.');
}
}
} else if (!attrs.error) {
if (attrs.error_condition === 'forbidden') {
new_attrs.error = __("You're not allowed to send a message.");
} else {
new_attrs.error = __('Sorry, an error occurred while trying to send your message.');
}
}
/**
* *Hook* which allows plugins to add application-specific attributes
* @event _converse#getErrorAttributesForMessage
*/
return await api.hook('getErrorAttributesForMessage', attrs, new_attrs);
}

/**
* @param {Element} stanza
*/
async handleErrorMessageStanza(stanza) {
const { __ } = _converse;
const attrs_or_error = await parseMessage(stanza);
if (u.isErrorObject(attrs_or_error)) {
const { stanza, message } = /** @type {StanzaParseError} */ (attrs_or_error);
Expand All @@ -767,30 +806,7 @@ export default function ModelWithMessages(BaseModel) {

const message = this.getMessageReferencedByError(attrs);
if (message) {
const new_attrs = {
'error': attrs.error,
'error_condition': attrs.error_condition,
'error_text': attrs.error_text,
'error_type': attrs.error_type,
'editable': false,
};
if (attrs.msgid === message.get('retraction_id')) {
// The error message refers to a retraction
new_attrs.retraction_id = undefined;
if (!attrs.error) {
if (attrs.error_condition === 'forbidden') {
new_attrs.error = __("You're not allowed to retract your message.");
} else {
new_attrs.error = __('Sorry, an error occurred while trying to retract your message.');
}
}
} else if (!attrs.error) {
if (attrs.error_condition === 'forbidden') {
new_attrs.error = __("You're not allowed to send a message.");
} else {
new_attrs.error = __('Sorry, an error occurred while trying to send your message.');
}
}
const new_attrs = await this.getErrorAttributesForMessage(message, attrs);
message.save(new_attrs);
} else {
this.createMessage(attrs);
Expand Down
9 changes: 5 additions & 4 deletions src/headless/shared/parsers.js
Original file line number Diff line number Diff line change
Expand Up @@ -259,10 +259,11 @@ export function getErrorAttributes (stanza) {
const error = stanza.querySelector('error');
const text = sizzle(`text[xmlns="${Strophe.NS.STANZAS}"]`, error).pop();
return {
'is_error': true,
'error_text': text?.textContent,
'error_type': error.getAttribute('type'),
'error_condition': error.firstElementChild.nodeName
is_error: true,
error_text: text?.textContent,
error_type: error.getAttribute('type'),
error_condition: error.firstElementChild.nodeName,
errors: Array.from(error.children).map((e) => ({ name: e.nodeName, xmlns: e.getAttribute('xmlns') })),
};
}
return {};
Expand Down
1 change: 1 addition & 0 deletions src/headless/types/plugins/chat/model.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ declare const ChatBox_base: {
};
sendMarkerForMessage(msg: import("./message.js").default, type?: ("received" | "displayed" | "acknowledged"), force?: boolean): Promise<void>;
handleUnreadMessage(message: import("./message.js").default): void;
getErrorAttributesForMessage(message: import("./message.js").default, attrs: import("./types").MessageAttributes): Promise<any>;
handleErrorMessageStanza(stanza: Element): Promise<void>;
incrementUnreadMsgsCounter(message: import("./message.js").default): void;
clearUnreadMsgCounter(): void;
Expand Down
18 changes: 12 additions & 6 deletions src/headless/types/plugins/chat/types.d.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,28 @@
import { EncryptionAttrs } from "../../shared/types";
export type MessageAttributes = EncryptionAttrs & {
export type MessageErrorAttributes = {
is_error: boolean;
error: string;
errors: {
name: string;
xmlns: string;
}[];
error_condition: string;
error_text: string;
error_type: string;
};
export type MessageAttributes = EncryptionAttrs & MessageErrorAttributes & {
body: string;
chat_state: string;
contact_jid: string;
editable: boolean;
edited: string;
error: string;
error_condition: string;
error_text: string;
error_type: string;
from: string;
message?: string;
fullname: string;
is_archived: boolean;
is_carbon: boolean;
is_delayed: boolean;
is_encrypted: boolean;
is_error: boolean;
is_headline: boolean;
is_markable: boolean;
is_marker: boolean;
Expand Down
1 change: 1 addition & 0 deletions src/headless/types/plugins/muc/muc.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ declare const MUC_base: {
};
sendMarkerForMessage(msg: import("../chat/message.js").default, type?: ("received" | "displayed" | "acknowledged"), force?: boolean): Promise<void>;
handleUnreadMessage(message: import("../chat/message.js").default): void;
getErrorAttributesForMessage(message: import("../chat/message.js").default, attrs: import("../chat/types").MessageAttributes): Promise<any>;
handleErrorMessageStanza(stanza: Element): Promise<void>;
incrementUnreadMsgsCounter(message: import("../chat/message.js").default): void;
clearUnreadMsgCounter(): void;
Expand Down
1 change: 1 addition & 0 deletions src/headless/types/plugins/muc/occupant.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ declare const MUCOccupant_base: {
};
sendMarkerForMessage(msg: import("../chat").Message, type?: ("received" | "displayed" | "acknowledged"), force?: boolean): Promise<void>;
handleUnreadMessage(message: import("../chat").Message): void;
getErrorAttributesForMessage(message: import("../chat").Message, attrs: import("../chat/types").MessageAttributes): Promise<any>;
handleErrorMessageStanza(stanza: Element): Promise<void>;
incrementUnreadMsgsCounter(message: import("../chat").Message): void;
clearUnreadMsgCounter(): void;
Expand Down
1 change: 1 addition & 0 deletions src/headless/types/shared/chatbox.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ declare const ChatBoxBase_base: {
};
sendMarkerForMessage(msg: import("../index.js").Message, type?: ("received" | "displayed" | "acknowledged"), force?: boolean): Promise<void>;
handleUnreadMessage(message: import("../index.js").Message): void;
getErrorAttributesForMessage(message: import("../index.js").Message, attrs: import("../plugins/chat/types.js").MessageAttributes): Promise<any>;
handleErrorMessageStanza(stanza: Element): Promise<void>;
incrementUnreadMsgsCounter(message: import("../index.js").Message): void;
clearUnreadMsgCounter(): void;
Expand Down
5 changes: 5 additions & 0 deletions src/headless/types/shared/model-with-messages.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,11 @@ export default function ModelWithMessages<T extends import("./types").ModelExten
* @param {Message} message
*/
handleUnreadMessage(message: import("../plugins/chat/message").default): void;
/**
* @param {Message} message
* @param {MessageAttributes} attrs
*/
getErrorAttributesForMessage(message: import("../plugins/chat/message").default, attrs: import("../plugins/chat/types.ts").MessageAttributes): Promise<any>;
/**
* @param {Element} stanza
*/
Expand Down
5 changes: 5 additions & 0 deletions src/headless/types/shared/parsers.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,16 @@ export function getErrorAttributes(stanza: Element): {
error_text: string;
error_type: string;
error_condition: string;
errors: {
name: string;
xmlns: string;
}[];
} | {
is_error?: undefined;
error_text?: undefined;
error_type?: undefined;
error_condition?: undefined;
errors?: undefined;
};
/**
* Given a message stanza, find and return any XEP-0372 references
Expand Down
2 changes: 1 addition & 1 deletion src/shared/chat/templates/message-text.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export default (el) => {
const is_groupchat_message = (el.model.get('type') === 'groupchat');
const i18n_show_less = __('Show less');
const error_text = el.model.get('error_text') || el.model.get('error');
const i18n_error = __('Message delivery failed: "%1$s"', error_text);
const i18n_error = `${__('Message delivery failed.')}\n${error_text}`;

const tplSpoilerHint = html`
<div class="chat-msg__spoiler-hint">
Expand Down
1 change: 1 addition & 0 deletions src/shared/styles/messages.scss
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@

.chat-msg__error {
color: var(--error-color);
white-space: pre-wrap;
}

.chat-msg__media {
Expand Down

0 comments on commit 70a707a

Please sign in to comment.