Răsfoiți Sursa

New config setting `message_limit`

for limiting messages to a certain number of characters.
JC Brand 6 ani în urmă
părinte
comite
38d0d8360b

+ 1 - 0
CHANGES.md

@@ -20,6 +20,7 @@
 - Replace `moment` with [DayJS](https://github.com/iamkun/dayjs).
 - New config option [enable_smacks](https://conversejs.org/docs/html/configuration.html#enable-smacks).
 - New config option [muc_show_join_leave_status](https://conversejs.org/docs/html/configuration.html#muc-show-join-leave-status)
+- New config option [message_limit](https://conversejs.org/docs/html/configuration.html#message-limit)
 - New config option [singleton](https://conversejs.org/docs/html/configuration.html#singleton).
   By setting this option to `false` and `view_mode` to `'embedded'`, it's now possible to
   "embed" the full app and not just a single chat. To embed just a single chat, it's now

+ 12 - 0
docs/source/configuration.rst

@@ -869,6 +869,18 @@ XEP-0280 requires server support, so make sure that message carbons are enabled
 on your server.
 
 
+message_limit
+-------------
+
+* Default:  ``0``
+
+Determines the allowed amount of characters in a chat message. A value of zero means there is no limit.
+Note, this limitation only applies to the Converse UX code running in the browser
+and it's trivial for an attacker to bypass this restriction.
+
+You should therefore also configure your XMPP server to limit message sizes.
+
+
 muc_disable_slash_commands
 --------------------------
 

+ 0 - 3
sass/_chatbox.scss

@@ -330,9 +330,6 @@
                 .private {
                     color: #4b7003;
                 }
-                .toggle-occupants {
-                    float: right;
-                }
                 li {
                     cursor: pointer;
                     display: inline-block;

+ 57 - 0
spec/chatbox.js

@@ -475,6 +475,63 @@
                     done();
                 }));
 
+                it("shows the remaining character count if a message_limit is configured",
+                    mock.initConverse(
+                        null, ['rosterGroupsFetched', 'chatBoxesFetched'], {'message_limit': 200},
+                        async function (done, _converse) {
+
+                    await test_utils.waitForRoster(_converse, 'current', 3);
+                    test_utils.openControlBox();
+                    const contact_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@montague.lit';
+                    await test_utils.openChatBoxFor(_converse, contact_jid);
+                    const view = _converse.chatboxviews.get(contact_jid);
+                    const toolbar = view.el.querySelector('.chat-toolbar');
+                    const counter = toolbar.querySelector('.message-limit');
+                    expect(counter.textContent).toBe('200');
+                    view.insertIntoTextArea('hello world');
+                    expect(counter.textContent).toBe('188');
+
+                    toolbar.querySelector('li.toggle-smiley').click();
+                    await test_utils.waitUntil(() => u.isVisible(view.el.querySelector('.toggle-smiley .emoji-picker-container')));
+                    var picker = view.el.querySelector('.toggle-smiley .emoji-picker-container');
+                    var items = picker.querySelectorAll('.emoji-picker li');
+                    items[0].click()
+                    expect(counter.textContent).toBe('177');
+
+                    const textarea = view.el.querySelector('.chat-textarea');
+                    const ev = {
+                        target: textarea,
+                        preventDefault: _.noop,
+                        keyCode: 13 // Enter
+                    };
+                    view.onKeyDown(ev);
+                    await new Promise((resolve, reject) => view.once('messageInserted', resolve));
+                    view.onKeyUp(ev);
+                    expect(counter.textContent).toBe('200');
+
+                    textarea.value = 'hello world';
+                    view.onKeyUp(ev);
+                    expect(counter.textContent).toBe('189');
+                    done();
+                }));
+
+
+                it("does not show a remaining character count if message_limit is zero",
+                    mock.initConverse(
+                        null, ['rosterGroupsFetched', 'chatBoxesFetched'], {'message_limit': 0},
+                        async function (done, _converse) {
+
+                    await test_utils.waitForRoster(_converse, 'current', 3);
+                    test_utils.openControlBox();
+                    const contact_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@montague.lit';
+                    await test_utils.openChatBoxFor(_converse, contact_jid);
+                    const view = _converse.chatboxviews.get(contact_jid);
+                    const counter = view.el.querySelector('.chat-toolbar .message-limit');
+                    expect(counter).toBe(null);
+                    done();
+                }));
+
+
                 it("can contain a button for starting a call",
                     mock.initConverse(
                         null, ['rosterGroupsFetched', 'chatBoxesFetched'], {},

+ 60 - 20
src/converse-chatview.js

@@ -57,6 +57,7 @@ converse.plugins.add('converse-chatview', {
 
         _converse.api.settings.update({
             'emoji_image_path': twemoji.default.base,
+            'message_limit': 0,
             'show_send_button': false,
             'show_toolbar': true,
             'time_format': 'HH:mm',
@@ -334,6 +335,8 @@ converse.plugins.add('converse-chatview', {
                 'click .upload-file': 'toggleFileUpload',
                 'input .chat-textarea': 'inputChanged',
                 'keydown .chat-textarea': 'onKeyDown',
+                'keyup .chat-textarea': 'onKeyUp',
+                'paste .chat-textarea': 'onPaste',
                 'dragover .chat-textarea': 'onDragOver',
                 'drop .chat-textarea': 'onDrop',
             },
@@ -381,16 +384,15 @@ converse.plugins.add('converse-chatview', {
                 return this;
             },
 
-            renderToolbar (toolbar, options) {
+            renderToolbar () {
                 if (!_converse.show_toolbar) {
                     return this;
                 }
-                toolbar = toolbar || tpl_toolbar;
-                options = _.assign(
+                const options = _.assign(
                     this.model.toJSON(),
-                    this.getToolbarOptions(options || {})
+                    this.getToolbarOptions()
                 );
-                this.el.querySelector('.chat-toolbar').innerHTML = toolbar(options);
+                this.el.querySelector('.chat-toolbar').innerHTML = tpl_toolbar(options);
                 this.addSpoilerButton(options);
                 this.addFileUploadButton();
                 /**
@@ -407,6 +409,7 @@ converse.plugins.add('converse-chatview', {
                 const form_container = this.el.querySelector('.bottom-panel');
                 form_container.innerHTML = tpl_chatbox_message_form(
                     Object.assign(this.model.toJSON(), {
+                        'message_limit': _converse.message_limit,
                         'hint_value': _.get(this.el.querySelector('.spoiler-hint'), 'value'),
                         'label_message': this.model.get('composing_spoiler') ? __('Hidden message') : __('Message'),
                         'label_send': __('Send'),
@@ -467,7 +470,7 @@ converse.plugins.add('converse-chatview', {
                 this.model.sendFiles(evt.dataTransfer.files);
             },
 
-            async addFileUploadButton (options) {
+            async addFileUploadButton () {
                 if (await _converse.api.disco.supports(Strophe.NS.HTTPUPLOAD, _converse.domain)) {
                     this.el.querySelector('.chat-toolbar').insertAdjacentHTML(
                         'beforeend',
@@ -475,11 +478,13 @@ converse.plugins.add('converse-chatview', {
                 }
             },
 
+            /**
+             * Asynchronously adds a button for writing spoiler
+             * messages, based on whether the contact's clients support it.
+             * @private
+             * @method _converse.ChatBoxView#addSpoilerButton
+             */
             async addSpoilerButton (options) {
-                /* Asynchronously adds a button for writing spoiler
-                 * messages, based on whether the contact's client supports
-                 * it.
-                 */
                 if (!options.show_spoiler_button || this.model.get('type') === _converse.CHATROOMS_TYPE) {
                     return;
                 }
@@ -516,22 +521,24 @@ converse.plugins.add('converse-chatview', {
                 return this;
             },
 
-            getToolbarOptions (options) {
+            getToolbarOptions () {
                 let label_toggle_spoiler;
                 if (this.model.get('composing_spoiler')) {
                     label_toggle_spoiler = __('Click to write as a normal (non-spoiler) message');
                 } else {
                     label_toggle_spoiler = __('Click to write your message as a spoiler');
                 }
-                return Object.assign(options || {}, {
+                return {
                     'label_clear': __('Clear all messages'),
-                    'tooltip_insert_smiley': __('Insert emojis'),
-                    'tooltip_start_call': __('Start a call'),
+                    'label_message_limit': __('Message characters remaining'),
                     'label_toggle_spoiler': label_toggle_spoiler,
+                    'message_limit': _converse.message_limit,
                     'show_call_button': _converse.visible_toolbar_buttons.call,
                     'show_spoiler_button': _converse.visible_toolbar_buttons.spoiler,
+                    'tooltip_insert_smiley': __('Insert emojis'),
+                    'tooltip_start_call': __('Start a call'),
                     'use_emoji': _converse.visible_toolbar_buttons.emoji,
-                });
+                }
             },
 
             async updateAfterMessagesFetched () {
@@ -905,9 +912,11 @@ converse.plugins.add('converse-chatview', {
 
             async onFormSubmitted (ev) {
                 ev.preventDefault();
-                const textarea = this.el.querySelector('.chat-textarea'),
-                      message = textarea.value;
-
+                const textarea = this.el.querySelector('.chat-textarea');
+                const message = textarea.value;
+                if (_converse.message_limit && message.length > _converse.message_limit) {
+                    return;
+                }
                 if (!message.replace(/\s/g, '').length) {
                     return;
                 }
@@ -949,9 +958,39 @@ converse.plugins.add('converse-chatview', {
                 this.setChatState(_converse.ACTIVE, {'silent': true});
             },
 
+            updateCharCounter (chars) {
+                if (_converse.message_limit) {
+                    const message_limit = this.el.querySelector('.message-limit');
+                    const counter = _converse.message_limit - chars.length;
+                    message_limit.textContent = counter;
+                    if (counter < 1) {
+                        u.addClass('error', message_limit);
+                    } else {
+                        u.removeClass('error', message_limit);
+                    }
+                }
+            },
+
+            onPaste (ev) {
+                this.updateCharCounter(ev.clipboardData.getData('text/plain'));
+            },
+
+            /**
+             * Event handler for when a depressed key goes up
+             * @private
+             * @method _converse.ChatBoxView#onKeyUp
+             */
+            onKeyUp (ev) {
+                this.updateCharCounter(ev.target.value);
+            },
+
+            /**
+             * Event handler for when a key is pressed down in a chat box textarea.
+             * @private
+             * @method _converse.ChatBoxView#onKeyDown
+             * @param { Event } ev
+             */
             onKeyDown (ev) {
-                /* Event handler for when a key is pressed in a chat box textarea.
-                 */
                 if (ev.ctrlKey) {
                     // When ctrl is pressed, no chars are entered into the textarea.
                     return;
@@ -1101,6 +1140,7 @@ converse.plugins.add('converse-chatview', {
                     textarea.value = '';
                     textarea.value = existing+value+' ';
                 }
+                this.updateCharCounter(textarea.value);
                 u.placeCaretAtEnd(textarea);
             },
 

+ 3 - 1
src/converse-muc-views.js

@@ -460,6 +460,7 @@ converse.plugins.add('converse-muc-views', {
                 'click .upload-file': 'toggleFileUpload',
                 'keydown .chat-textarea': 'onKeyDown',
                 'keyup .chat-textarea': 'onKeyUp',
+                'paste .chat-textarea': 'onPaste',
                 'input .chat-textarea': 'inputChanged',
                 'dragover .chat-textarea': 'onDragOver',
                 'drop .chat-textarea': 'onDrop',
@@ -583,11 +584,12 @@ converse.plugins.add('converse-muc-views', {
                 if (this.mention_auto_complete.onKeyDown(ev)) {
                     return;
                 }
-                return _converse.ChatBoxView.prototype.onKeyDown.apply(this, arguments);
+                return _converse.ChatBoxView.prototype.onKeyDown.call(this, ev);
             },
 
             onKeyUp (ev) {
                 this.mention_auto_complete.evaluate(ev);
+                return _converse.ChatBoxView.prototype.onKeyUp.call(this, ev);
             },
 
             showRoomDetailsModal (ev) {

+ 4 - 1
src/templates/toolbar.html

@@ -8,6 +8,9 @@
 <li class="toggle-call fa fa-phone" title="{{{o.label_start_call}}}"></li>
 {[ } ]}
 {[ if (o.show_occupants_toggle)  { ]}
-<li class="toggle-occupants fa {[ if (o.hidden_occupants)  { ]} fa-angle-double-left {[ } else { ]} fa-angle-double-right {[ } ]}"
+<li class="toggle-occupants float-right fa {[ if (o.hidden_occupants)  { ]} fa-angle-double-left {[ } else { ]} fa-angle-double-right {[ } ]}"
     title="{{{o.label_hide_occupants}}}"></li>
 {[ } ]}
+{[ if (o.message_limit)  { ]}
+<li class="message-limit font-weight-bold float-right" title="{{{o.label_message_limit}}}">{{{o.message_limit}}}</li>
+{[ } ]}