Skip to content

Instantly share code, notes, and snippets.

@frostbtn
Last active June 26, 2025 15:11
Show Gist options
  • Save frostbtn/ad2d079aaecfde3ae97c27c8d3167a30 to your computer and use it in GitHub Desktop.
Save frostbtn/ad2d079aaecfde3ae97c27c8d3167a30 to your computer and use it in GitHub Desktop.
rocket.chat web-hook to post messages silently

This is a Rocket.Chat incoming web hook. Hook gets an array of "messages" and silently creates chat messages directly in the Rocket's database without disturbing users with notifications or alerts - messages just appear in the channels. Messages appear silently even if the user has the channel openned: no refresh or re-enter is required (this is not this script's feature, it's how Rocket works).

This script can post messages to channels and groups by name (if message destination set to #name), or by api roomId (no prefixes in destination). And it can post DM to a user (if destination is set to @username). Please note, in this case DM between message author and destination user must already be created.

Note. Rocket.Chat's server version 6 has undergone significant changes. As a result, now there are two script versions: silent-post-whs-v5.js for server version 5 and silent-post-whs-v6.js for version 6. However, these scripts use an undocumented server API, which unfortunately could result in compatibility issues with even minor future Rocket.Chat updates.

This hook expects request.content: ISilentMessage[];

ISilentMessage {
  // Message body.
  text: string;

  // User to set as message author.
  // No leading @, user must be registered.
  author: string;

  // Channel to post message to.
  // It may be "#channeg_or_group_name", "@username", or "room_id".
  // NOTE: in case of "@username", the DM between this user and message
  // author must exists, this script doesn't create one
  // (greatly complicates everything).
  destination: string;

  // An array of message attachments. Optional, may be omitted.
  attachments: [];
}

Pyhton

with requests.sessions.Session() as session:
  session.post(
    'https://CHAT.URL/hooks/WEBHOOK/TOKEN',
    json=[
      {
        'text': 'Multiline\nmessage\nto #channel_by_name',
        'author': 'admin',
        'destination': '#channel_by_name',
      },
      {
        'text': 'Message to abc123abc123abc123 (roomId)',
        'author': 'admin',
        'destination': 'abc123abc123abc123',
      },
      {
        'text': 'DM to user `user` (by username)',
        'author': 'admin',
        'destination': '@user',
      },
      {
        'text': 'Message with attachments to #channel_by_name',
        'author': 'admin',
        'destination': '#channel_by_name',
        'attachments': [
          {
            "title": "Rocket.Chat",
            "title_link": "https://rocket.chat",
            "text": "Rocket.Chat, the best open source chat",
            "image_url": "/images/integration-attachment-example.png",
            "color": "#764FA5"
          }
        ]
      },
    ])

curl

curl -X POST -H 'Content-Type: application/json' \
     --data '[ { "text": "Multiline\\nmessage\\nto #channel_by_name", "author": "admin", "destination": "#channel_by_name" }, { "text": "Message to abc123abc123abc123 (roomId)", "author": "admin", "destination": "abc123abc123abc123" }, { "text": "DM to user `user` (by username)", "author": "admin", "destination": "@user" }]' \
     https://chat.url/hooks/WEBHOOK/TOKEN
class Script {
knownRoomIds = new Map();
knownUserIds = new Map();
process_incoming_request({request}) {
/*
* This hook expects
* request.content: ISilentMessage[];
* ISilentMessage {
* // Message body.
* text: string;
*
* // User to set as message author.
* // No leading @, user must be registered.
* author: string;
*
* // Channel to post message to.
* // It may be "#channeg_or_group_name", "@username", or "room_id".
* // NOTE: in case of "@username", the DM between this user and message
* // author must exists, this script doesn't create one
* // (greatly complicates everything).
* destination: string;
*
* // An array of message attachments. Optional, may be omitted.
* attachments: [];
* }
* */
for (const message of request.content) {
authorId = this.findUser(message.author)
if (!authorId) {
continue;
}
rid = this.findDestination(message.destination, authorId);
if (!rid) {
continue;
}
this.postMessageSilent(
rid, authorId, message.author,
message.text, message.attachments);
}
return {
content: null,
};
}
findDestination(dest, authorId) {
if (this.knownRoomIds.has(dest)) {
return this.knownRoomIds.get(dest);
}
let rid = null;
if (dest[0] === '#') {
const room = Rooms.findOneByName(dest.slice(1));
if (!room) {
return null;
}
rid = room._id;
} else if (dest[0] === '@') {
userId = this.findUser(dest.slice(1), knownUserIds);
if (!userId) {
return null;
}
rid = [authorId, userId].sort().join('');
const room = Rooms.findOneById(rid);
if (!room) {
return null;
}
rid = room._id;
} else {
rid = dest;
}
this.knownRoomIds.set(dest, rid);
return rid;
}
findUser(username) {
if (this.knownUserIds.has(username)) {
return this.knownUserIds.get(username);
}
const user = Users.findOneByUsername(username);
if (!user) {
return null;
}
this.knownUserIds.set(username, user._id);
return user._id;
}
postMessageSilent(rid, authorId, authorName, text, attachments) {
const record = {
t: 'p',
rid: rid,
ts: new Date(),
msg: text,
u: {
_id: authorId,
username: authorName,
},
groupable: false,
unread: true,
};
if (attachments && attachments.length) {
record.attachments = attachments;
}
Messages.insertOrUpsert(record);
}
}
/* jshint esversion: 2020 */
/* global console, globalThis, Rooms, Users, Messages */
class Script {
knownRoomIds = new Map();
knownUserIds = new Map();
process_incoming_request({request}) {
/*
* This hook expects
* request.content: ISilentMessage[];
* ISilentMessage {
* // Message body.
* text: string;
*
* // User to set as message author.
* // No leading @, user must be registered.
* author: string;
*
* // Channel to post message to.
* // It may be "#channeg_or_group_name", "@username", or "room_id".
* // NOTE: in case of "@username", the DM between this user and message
* // author must exists, this script doesn't create one
* // (greatly complicates everything).
* destination: string;
*
* // An array of message attachments. Optional, may be omitted.
* attachments: [];
* }
* */
this.log('SILENT_POST: Processing', request.content);
this.log('SILENT_POST: Globals', Object.keys(globalThis));
const posts = request.content.map((message) => {
this.log('SILENT_POST: Processing a message', message);
return this.findUser(message.author).then((authorId) => {
this.log('SILENT_POST: Author ID', authorId);
if (!authorId) {
return null;
}
return this.findDestination(message.destination, authorId).then((rid) => {
this.log('SILENT_POST: Destination ID', rid);
if (!rid) {
return null;
}
this.log('SILENT_POST: Go with message');
return this.postMessageSilent(
rid, authorId, message.author,
message.text, message.attachments)
.then((messageId) => {
this.log('SILENT_POST: Done with message');
return messageId;
});
});
});
});
return Promise.all(posts)
.then(() => {
this.log('SILENT_POST: All messages done');
return {
content: null,
};
});
}
findDestination(dest, authorId) {
if (this.knownRoomIds.has(dest)) {
return Promise.resolve(this.knownRoomIds.get(dest));
}
if (dest[0] === '#') {
return Rooms.findOne({name: dest.slice(1)}).then((room) => {
if (!room) {
return null;
}
this.knownRoomIds.set(dest, room._id);
return room._id;
});
}
if (dest[0] === '@') {
return this.findUser(dest.slice(1)).then((userId) => {
if (!userId) {
return null;
}
const rid = [authorId, userId].sort().join('');
return Rooms.findOne({_id: rid}).then((room) => {
if (!room) {
return null;
}
this.knownRoomIds.set(dest, room._id);
return room._id;
});
});
}
this.knownRoomIds.set(dest, dest);
return Promise.resolve(dest);
}
findUser(username) {
if (this.knownUserIds.has(username)) {
return Promise.resolve(this.knownUserIds.get(username));
}
return Users
.findOne({username})
.then((user) => {
if (!user) {
return null;
}
this.knownUserIds.set(username, user._id);
return user._id;
});
}
postMessageSilent(rid, authorId, authorName, text, attachments) {
const record = {
rid: rid,
ts: new Date(),
msg: text,
u: {
_id: authorId,
username: authorName,
},
groupable: false,
unread: true,
};
if (attachments && attachments.length) {
record.attachments = attachments;
}
return Messages.insertOne(record);
}
log(...args) {
// Uncomment to debug
// console.log(...args);
}
}
@gelpiu-developers
Copy link

This script is not working anymore as Users.findOneByUsername returns a Promise and because Messages.insertOrUpsert method does not exist now.

@frostbtn
Copy link
Author

Yeah, just updated my instance to v6. A big thanks to @gelpiu-developers for the hint about their switch to async. It saved me a ton of time.

@gelpiu-developers
Copy link

Happy to help!

@frostbtn
Copy link
Author

To any random walker seeking silence:

In version 7, the Rocket.Chat server completely isolated integration scripts from anything. Therefore, this script no longer works, and it is not possible to fix it.

As far as I understand, they now suggest using their Marketplace Apps, which allegedly support the required API. However, the Apps-Engine seems to be specifically crippled and tied to their premium plans.

At the moment, my workaround is to use API to set the global setting Troubleshoot_Disable_Notifications to true, quickly send the messages that are supposed to be silent, and then change the setting back to false. This prevents sounds, pop-ups, and push notifications, but the channels unfortunately become marked as "unread."

@reetp
Copy link

reetp commented Jun 26, 2025

As far as I understand, they now suggest using their Marketplace Apps, which allegedly support the required API. However, the Apps-Engine seems to be specifically crippled and tied to their premium plans.

Not sure what makes you say that?

Yup there are limits on how many apps you can use depending on what version you run, but they have to make money to keep themselves in business somehow.

I suggest you ask here specifically about what issues you face and we may be able to get a dev to look at it.

https://open.rocket.chat/channel/support

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment