Skip to content

Instantly share code, notes, and snippets.

@frostbtn
Last active June 28, 2023 09:38
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • 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!

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