snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s

This commit is contained in:
upstream-mirror
2026-09-16 01:21:02 +05:00
commit 30810eecd7
10161 changed files with 987110 additions and 0 deletions

View File

@@ -0,0 +1,54 @@
# Rocket.chat bot helpers
This package provides some extra shortcut Meteor methods intended to help Hubot scripts.
## Approach
This was an experiment in how to extend Hubot and Rocket.chat integration.
Keeping controller logic out of Hubot scripts made sense to me, but its not necessarily the best approach.
For bots to use these methods they obviously need this package added to Rocket.chat as well as the [rocketchat adapter](https://github.com/RocketChat/hubot-rocketchat).
## Usage
In Hubot scripts, call bot helper methods using Meteor.call with the first argument as the helper type, then the method name as second argument.
At present, there's only one type of helper (`botRequest`) for simple getters, more utility methods may be added in future.
e.g. Below uses `onlineNames` method, which returns array of names of online users (not including bots).
Listens for *"who is online"* and replies with something like *"Robert, Desmond and Billie are currently online"*.
```
# Use Bot Helpers class to check who's online
robot.hear /who is online/i, (res) ->
promise = robot.adapter.callMethod('botRequest', 'onlineNames')
promise.then (result) ->
if result.length > 0
names = result.join(', ').replace(/,(?=[^,]*$)/, ' and ') # convert last comma to 'and'
res.send "#{ names } #{ if result.length == 1 then 'is' else 'are' } currently online"
else
res.send "Nobody is currently online... \*cricket sound\*"
return
, (error) ->
res.send "Uh, sorry I don't know, something's not working"
return
```
Method names that include fields like `allUsernames` return an 1D array of just that property.
First two methods return 2D array with properties defined in a defaults object (currently id, name, username, status, emails).
- `allUsers`
- `onlineUsers`
- `allUsernames`
- `onlineUsernames`
- `allNames`
- `onlineNames`
- `allIDs`
- `onlineIDs`
## Tests
None yet, PR adding tests would be much apreciated.

View File

@@ -0,0 +1,214 @@
import type { IUser } from '@rocket.chat/core-typings';
import { UserStatus } from '@rocket.chat/core-typings';
import type { ServerMethods } from '@rocket.chat/ddp-client';
import { Rooms, Users } from '@rocket.chat/models';
import { Meteor } from 'meteor/meteor';
import type { Filter, FindCursor } from 'mongodb';
import { addUserToRole } from '../../meteor-methods/auth/addUserToRole';
import { removeUserFromRole } from '../../meteor-methods/auth/removeUserFromRole';
import { addUsersToRoomMethod } from '../../meteor-methods/rooms/addUsersToRoom';
import { removeUserFromRoomMethod } from '../../meteor-methods/rooms/removeUserFromRoom';
import { settings } from '../../settings';
import { hasRoleAsync } from '../authorization/hasRole';
/**
* BotHelpers helps bots
* "private" properties use meteor collection cursors, so they stay reactive
* "public" properties use getters to fetch and filter collections as array
*/
class BotHelpers {
private queries: {
online: Filter<IUser>;
users: Filter<IUser>;
};
private userFields: Record<string, 1>;
private _allUsers: FindCursor<IUser>;
private _onlineUsers: FindCursor<IUser>;
constructor() {
this.queries = {
online: { status: { $ne: UserStatus.OFFLINE } },
users: { roles: { $not: { $all: ['bot'] } } },
};
}
// setup collection cursors with array of fields from setting
setupCursors(fieldsSetting: string | string[]) {
this.userFields = {};
if (typeof fieldsSetting === 'string') {
fieldsSetting = fieldsSetting.split(',');
}
fieldsSetting.forEach((n) => {
this.userFields[n.trim()] = 1;
});
this._allUsers = Users.find(this.queries.users, { projection: this.userFields });
this._onlineUsers = Users.find({ $and: [this.queries.users, this.queries.online] }, { projection: this.userFields });
}
// request methods or props as arguments to Meteor.call
async request(prop: keyof this, ...params: unknown[]): Promise<any> {
const p = this[prop];
if (typeof p === 'undefined') {
return null;
}
if (typeof p === 'function') {
return p(...params);
}
return p;
}
async addUserToRole(userName: string, roleId: string, userId: string): Promise<void> {
await addUserToRole(userId, roleId, userName);
}
async removeUserFromRole(userName: string, roleId: string, userId: string): Promise<void> {
await removeUserFromRole(userId, roleId, userName);
}
async addUserToRoom(userName: string, room: string): Promise<void> {
const foundRoom = await Rooms.findOneByIdOrName(room);
if (!foundRoom) {
throw new Meteor.Error('invalid-channel');
}
const userId = Meteor.userId();
if (!userId) {
throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'addUserToRoom' });
}
await addUsersToRoomMethod(userId, {
rid: foundRoom._id,
users: [userName],
});
}
async removeUserFromRoom(userName: string, room: string) {
const foundRoom = await Rooms.findOneByIdOrName(room);
if (!foundRoom) {
throw new Meteor.Error('invalid-channel');
}
const userId = Meteor.userId();
if (!userId) {
throw new Meteor.Error('error-invalid-user', 'Invalid user');
}
await removeUserFromRoomMethod(userId, { rid: foundRoom._id, username: userName });
}
// generic error whenever property access insufficient to fill request
requestError() {
throw new Meteor.Error('error-not-allowed', 'Bot request not allowed', {
method: 'botRequest',
action: 'bot_request',
});
}
// "public" properties accessed by getters
// allUsers / onlineUsers return whichever properties are enabled by settings
get allUsers() {
if (!Object.keys(this.userFields).length) {
this.requestError();
return false;
}
return this._allUsers.toArray();
}
get onlineUsers() {
if (!Object.keys(this.userFields).length) {
this.requestError();
return false;
}
return this._onlineUsers.toArray();
}
get allUsernames() {
return (async () => {
if (!this.userFields.hasOwnProperty('username')) {
this.requestError();
return false;
}
return (await this._allUsers.toArray()).map((user) => user.username);
})();
}
get onlineUsernames() {
return (async () => {
if (!this.userFields.hasOwnProperty('username')) {
this.requestError();
return false;
}
return (await this._onlineUsers.toArray()).map((user) => user.username);
})();
}
get allNames() {
return (async () => {
if (!this.userFields.hasOwnProperty('name')) {
this.requestError();
return false;
}
return (await this._allUsers.toArray()).map((user) => user.name);
})();
}
get onlineNames() {
return (async () => {
if (!this.userFields.hasOwnProperty('name')) {
this.requestError();
return false;
}
return (await this._onlineUsers.toArray()).map((user) => user.name);
})();
}
get allIDs() {
return (async () => {
if (!this.userFields.hasOwnProperty('_id') || !this.userFields.hasOwnProperty('username')) {
this.requestError();
return false;
}
return (await this._allUsers.toArray()).map((user) => ({ id: user._id, name: user.username }));
})();
}
get onlineIDs() {
return (async () => {
if (!this.userFields.hasOwnProperty('_id') || !this.userFields.hasOwnProperty('username')) {
this.requestError();
return false;
}
return (await this._onlineUsers.toArray()).map((user) => ({ id: user._id, name: user.username }));
})();
}
}
// add class to meteor methods
const botHelpers = new BotHelpers();
// init cursors with fields setting and update on setting change
settings.watch<string>('BotHelpers_userFields', (value) => {
botHelpers.setupCursors(value);
});
declare module '@rocket.chat/ddp-client' {
// eslint-disable-next-line @typescript-eslint/naming-convention
interface ServerMethods {
botRequest: (prop: keyof BotHelpers, ...params: unknown[]) => Promise<unknown>;
}
}
Meteor.methods<ServerMethods>({
async botRequest(...args) {
const userID = Meteor.userId();
if (userID && (await hasRoleAsync(userID, 'bot'))) {
return botHelpers.request(...args, userID);
}
throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'botRequest' });
},
});