Skip to content

Conversation

@sreeshanth-soma
Copy link

Summary

Implements XEP-0231: Bits of Binary, enabling Converse.js to receive and display inline binary data such as custom smileys from clients like Pidgin.
Closes #3611

Implementation

New converse-bob Plugin

  • In-memory cache with TTL and max-age expiration
  • API: api.bob.get(), api.bob.store(), api.bob.has()
  • IQ handling: Fetches uncached BOB data via <iq type="get">
  • Message parsing: Extracts <data xmlns="urn:xmpp:bob"> elements
  • Disco feature: Registers urn:xmpp:bob
  • Security: Image MIME types only, 8KB size limit per spec

Test Results

TOTAL: 94 SUCCESS

New Tests (7)

  • Cache storage, retrieval, expiration
  • Size and MIME validation
  • Message parsing and IQ requests
  • Error handling

Capability Hash

Adding urn:xmpp:bob updates XEP-0115 verification string from 1T0pIfIxYO645OaT9gpXVXOvb9s= to ZXDrJD54GWZYlZif9IuMxum/u+g=

import _converse from '../../shared/_converse.js';
import log from '@converse/log';

const { Strophe, $iq } = converse.env;

Check notice

Code scanning / CodeQL

Unused variable, import, function or class Note

Unused variable Strophe.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please pay attention to these security bot messages.

Copy link
Member

@jcbrand jcbrand left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your pull request @sreeshanth-soma

You noted that the headless tests failed, but there are also non-headless tests which now fail due to the new urn:xmpp:bob feature supported.

Please fix those as well.

- Add converse-bob plugin for receiving BOB images (custom smileys)
- Implements cache, API, IQ handling, and message parsing
- Register urn:xmpp:bob disco feature
- Add comprehensive test suite (7 tests)
- Update capability hashes for new feature
Update XEP-0115 entity capability verification hashes in test files
to account for the added urn:xmpp:bob feature.

Headless tests now use: 9zuZdgDXVE0fkcAjeaokq9JxHJw=
Non-headless tests now use: KjyaPNsNXajTrXVlYimRsIvje2g=
RAI tests now use: 3Tday6wfQnY1R8zGPMO3CBYFrik=
@sreeshanth-soma sreeshanth-soma force-pushed the xep-0231-bits-of-binary branch from 06c6529 to 6deb51b Compare January 27, 2026 02:14
@sreeshanth-soma
Copy link
Author

Hello @jcbrand . I've addressed your feedback. The non-headless tests that were failing due to the urn:xmpp:bob feature have been fixed:

  • Updated all capability hashes in the non-headless test files
  • Added TypeScript type definitions for the BOB plugin
    All tests are now passing locally (matching upstream/master behavior). Ready for re-review!

Copy link
Member

@jcbrand jcbrand left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @sreeshanth-soma, please see my review comments.

const MAX_BOB_SIZE = 8192;

// In-memory cache for BOB data
const bob_cache = new Map();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not convinced that an in-memory cache is the right way to go here.

Every time you reload the tab the cache will be wiped and then you lose all the images.
If you then try to refetch the images, you might not get all of them due to the sender being offline.

So instead, we should probably create a persistent collection of BOB images, similar to what we do with VCards: https://github.com/conversejs/converse.js/blob/master/src/headless/plugins/vcard/vcards.js

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the feedback! I've replaced the in-memory Map with a persistent Skeletor Collection, following the VCards pattern:

  • Created bob.js model with isExpired() and getBlobURL() methods.
  • Created bobs.js collection using initStorage() for IndexedDB persistence
  • Collection is initialized on connected event and cleaned up on clearSession

BOB images now persist across page reloads and remain available even if the sender goes offline.

* @param {number} offset - The index of the passed in text relative to
* the start of the message body text.
*/
async addBOBImages(text, offset) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function should go into a bob-views plugin and then in that plugin you should listen for the afterMessageBodyTransformed event in order to call this function.

See here:

api.listen.on('afterMessageBodyTransformed', handleEncryptedFiles);

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done! Created a new bob-views plugin that:

  • Listens to afterMessageBodyTransformed event (following the omemo-views pattern)
  • Moved the BOB image handling logic from texture.js to the new plugin
  • Registered it in src/index.js and added to VIEW_PLUGINS whitelist

This properly separates the headless BOB storage from the view-layer rendering.

import { until } from "lit/directives/until.js";
import { Directive, directive } from "lit/directive.js";
import { api, u } from "@converse/headless";
import log from "@converse/log";

Check notice

Code scanning / CodeQL

Unused variable, import, function or class Note

Unused import log.
Comment on lines 37 to 41
const binary = atob(data);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const binary = atob(data);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
const bytes = Uint8Array.from(atob(data), c => c.charCodeAt(0));

await this.fetchBOBs();

// Clean up expired entries
this.cleanupExpired();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove comments like this. It's already clear from the code what's happening.

Comment on lines 51 to 52
const expired = this.filter((bob) => bob.isExpired());
expired.forEach((bob) => bob.destroy());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const expired = this.filter((bob) => bob.isExpired());
expired.forEach((bob) => bob.destroy());
this.forEach((bob) => if (bob.isExpired()) bob.destroy());

import _converse from '../../shared/_converse.js';
import log from '@converse/log';

const { Strophe, $iq } = converse.env;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please pay attention to these security bot messages.

const { Strophe, $iq } = converse.env;

// Maximum size for BOB data (8KB as per XEP-0231)
const MAX_BOB_SIZE = 8192;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this can be made configurable.

Where you init the plugin:

        // Configure plugin settings
        api.settings.extend({
            max_bob_size: 8192
        });


// Export classes
const exports = { BOB, BOBs };
Object.assign(_converse, exports); // XXX DEPRECATED
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please don't add deprecated lines to new files.

Object.assign(_converse, exports); // XXX DEPRECATED
Object.assign(_converse.exports, exports);

// Register namespace
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unnecessary comment

media_urls: this.model.get('media_urls'),
mentions: this.model.get('references'),
nick: this.model.chatbox.get('nick'),
from_jid: this.model.get('from'),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line is no longer necessary

* which create new Texture instances (as happens with XEP-393 styling directives).
* @param {Object} [options]
* @param {string} [options.nick] - The current user's nickname (only relevant if the message is in a XEP-0045 MUC)
* @param {string} [options.from_jid] - The JID of the message sender (needed for fetching BOB images)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The changes in this file are no longer necessary. Please look through your changes yourself before asking me to review. You could easily have seen this.

if (!bob) return false;

if (bob.isExpired()) {
bob.destroy();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think calling has should have the side effect of deleting expired blobs.

If should return true, even if the blob is expired.

Instead you can use setInterval to call a function every 10 minutes, to check for expired blobs and then delete them.

…eanup

Use configurable max_bob_size setting, add setInterval for expired blob cleanup, and update test fixtures for new caps hashes
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

XEP-0231: Bits of Binary - for custom smileys and CAPTCHA

2 participants