Skip to content

Commit 7c27a19

Browse files
committed
first commit
Adds chat commands proccessor with an extensible design to allow for easy addition of new commands. There is only one command implemented which is `/demote` which demotes all moderators - except for the sender - to viewers.
1 parent 7512ade commit 7c27a19

File tree

10 files changed

+241
-14
lines changed

10 files changed

+241
-14
lines changed

README.md

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,14 @@
22

33
## Description
44

5-
A brief description of the plugin including a screenshot and/or a short video.
5+
This is an experimental internal plugin developed by mconf for BigBlueButton. Its main purpose is to allow the inclusion and execution of custom chat commands in meetings. The plugin is designed to be easily extensible, enabling developers to add new commands with minimal effort.
6+
7+
### Features
8+
- Easily add new chat commands by extending the configuration.
9+
- Commands can trigger custom mutations and actions in the meeting context.
10+
- Example command: `/demote` (see below for details).
11+
12+
A screenshot and/or a short video can be added here to illustrate usage.
613

714
## Building the Plugin
815

@@ -26,6 +33,39 @@ pluginManifests=[{"url":"<your-domain>/path/to/manifest.json"}]
2633

2734
Or additionally, you can add this same configuration in the `.properties` file from `bbb-web` in `/usr/share/bbb-web/WEB-INF/classes/bigbluebutton.properties`
2835

36+
## How to Add New Commands
37+
38+
Commands are defined in the plugin's configuration as objects with a name, description, mutation (optional), and an `execute` function. To add a new command:
39+
40+
1. Open the file where commands are configured (usually `component.tsx` or a dedicated config file).
41+
2. Add a new entry to the `CommandConfig` object, specifying:
42+
- `name`: The command name (used after `/` in chat).
43+
- `description`: A brief description of the command.
44+
- `mutation`: (Optional) The GraphQL mutation string to be used.
45+
- `execute`: The function that will be called when the command is triggered. It receives context such as the current user, user list, arguments, and mutation trigger.
46+
3. Ensure the mutation is mapped in the `mutationMap` if your command uses a custom mutation.
47+
48+
Example:
49+
```typescript
50+
const DEFAULT_COMMANDS: CommandConfig = {
51+
demote: { ... },
52+
myCommand: {
53+
name: 'myCommand',
54+
description: 'Does something special',
55+
mutation: MY_MUTATION,
56+
execute: ({ mutation, users, senderId, args }) => {
57+
// Your logic here
58+
},
59+
},
60+
};
61+
```
62+
63+
## Implemented Commands
64+
65+
### `/demote`
66+
- **Description:** Demotes all users in the meeting to the viewer role, except for the user who issued the command.
67+
- **Usage:** Type `/demote` in the chat as a moderator. The command will change the role of all other moderators to viewers.
68+
- **Restrictions:** Only users with moderator privileges can execute this command. The command will not affect the sender or users who are already viewers.
2969

3070
## Development mode
3171

manifest.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
2-
"requiredSdkVersion": "~0.0.73",
3-
"name": "<plugin-name>",
4-
"javascriptEntrypointUrl": "<plugin-name>.js",
5-
"localesBaseUrl": "https://cdn.dominio.com/pluginabc/"
2+
"requiredSdkVersion": "0.1.x",
3+
"name": "ChatCommandsPlugin",
4+
"javascriptEntrypointUrl": "ChatCommandsPlugin.js",
5+
"localesBaseUrl": "ChatCommandsPlugin.js"
66
}

package-lock.json

Lines changed: 5 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"main": "./src/index.tsx",
66
"dependencies": {
77
"babel-plugin-syntax-dynamic-import": "^6.18.0",
8-
"bigbluebutton-html-plugin-sdk": "0.0.73",
8+
"bigbluebutton-html-plugin-sdk": "0.1.x",
99
"path": "^0.12.7",
1010
"react": "^18.2.0",
1111
"react-dom": "^18.2.0",
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import React, { useEffect, useMemo, useRef } from 'react';
2+
import { pluginLogger } from 'bigbluebutton-html-plugin-sdk';
3+
import {
4+
CommandConfig,
5+
ChatCommandPluginProps,
6+
SetRoleMutation,
7+
MutationMap,
8+
} from './types';
9+
import { SET_ROLE } from './mutations';
10+
11+
const COMMAND_PREFIX = '/';
12+
13+
const VIEWER_ROLE = () => window.meetingClientSettings.public.user.role_viewer;
14+
const MODERATOR_ROLE = () => window.meetingClientSettings.public.user.role_moderator;
15+
16+
const DEFAULT_COMMANDS: CommandConfig = {
17+
demote: {
18+
name: 'demote',
19+
description: 'Demote all users to viewers except the sender',
20+
execute: ({
21+
currentUser,
22+
users,
23+
senderId,
24+
mutation,
25+
}) => {
26+
if (!currentUser || currentUser.role !== MODERATOR_ROLE()) {
27+
pluginLogger.warn('Current user is not a moderator. Cannot execute demote command.');
28+
return;
29+
}
30+
users
31+
.filter((user) => user.userId !== senderId && user.isModerator)
32+
.map((user) => mutation({
33+
variables: {
34+
userId: user.userId,
35+
role: VIEWER_ROLE(),
36+
},
37+
}));
38+
},
39+
},
40+
};
41+
42+
export function ChatCommandPlugin({
43+
pluginApi,
44+
commands = DEFAULT_COMMANDS,
45+
}: ChatCommandPluginProps): React.ReactElement {
46+
const loadedChatMessagesResponse = pluginApi.useLoadedChatMessages();
47+
const usersBasicInfoResponse = pluginApi.useUsersBasicInfo();
48+
const currentUserResponse = pluginApi.useCurrentUser();
49+
const [setRole] = pluginApi.useCustomMutation<SetRoleMutation>(SET_ROLE);
50+
const executedMessageIds = useRef<Set<string>>(new Set());
51+
52+
const mutationMap: MutationMap = useMemo(() => ({
53+
[DEFAULT_COMMANDS.demote.name]: setRole,
54+
}), [setRole]);
55+
56+
const currentUser = useMemo(() => {
57+
if (currentUserResponse.loading) return null;
58+
if (currentUserResponse.error || !currentUserResponse.data) {
59+
pluginLogger.error('Error loading current user', currentUserResponse.error);
60+
return null;
61+
}
62+
return currentUserResponse.data;
63+
}, [currentUserResponse]);
64+
65+
const usersList = useMemo(() => {
66+
if (usersBasicInfoResponse.loading) return [];
67+
if (usersBasicInfoResponse.error || !usersBasicInfoResponse.data) {
68+
pluginLogger.error('Error loading user list', usersBasicInfoResponse.error);
69+
return [];
70+
}
71+
return usersBasicInfoResponse.data.user;
72+
}, [usersBasicInfoResponse]);
73+
74+
const messages = useMemo(() => {
75+
if (loadedChatMessagesResponse.loading) return [];
76+
if (loadedChatMessagesResponse.error || !loadedChatMessagesResponse.data) {
77+
pluginLogger.error('Error loading chat messages', loadedChatMessagesResponse.error);
78+
return [];
79+
}
80+
return loadedChatMessagesResponse.data;
81+
}, [loadedChatMessagesResponse]);
82+
83+
useEffect(() => {
84+
if (messages && usersList && currentUser) {
85+
messages.forEach(async (message) => {
86+
if (message.message.startsWith(COMMAND_PREFIX)
87+
&& message.senderUserId === currentUser.userId
88+
&& !executedMessageIds.current.has(message.messageId)) {
89+
const [cmd, ...args] = message.message.slice(1).split(' ');
90+
if (commands[cmd]) {
91+
pluginLogger.info(`Executing command: ${cmd} with args: ${args.join(', ')}`);
92+
commands[cmd].execute({
93+
mutation: mutationMap[cmd],
94+
currentUser,
95+
users: usersList,
96+
senderId: message.senderUserId,
97+
args,
98+
});
99+
executedMessageIds.current.add(message.messageId);
100+
}
101+
}
102+
});
103+
}
104+
}, [messages, usersList, currentUser, commands]);
105+
106+
return null;
107+
}
108+
109+
export default ChatCommandPlugin;
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export const SET_ROLE = `
2+
mutation SetRole($userId: String!, $role: String!) {
3+
userSetRole(
4+
userId: $userId,
5+
role: $role,
6+
)
7+
}
8+
`;

src/ChatCommandsPlugin/types.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { CurrentUserData, PluginApi, UsersBasicInfoData } from 'bigbluebutton-html-plugin-sdk';
2+
import { TriggerMutationFunction } from 'bigbluebutton-html-plugin-sdk/dist/cjs/data-creation/types';
3+
4+
declare global {
5+
interface Window {
6+
meetingClientSettings: {
7+
public: {
8+
user: {
9+
role_viewer: string;
10+
role_moderator: string;
11+
};
12+
};
13+
};
14+
}
15+
}
16+
17+
export interface ChatMentionProps {
18+
pluginApi: PluginApi;
19+
}
20+
21+
export interface SetRoleMutation {
22+
userId: string;
23+
role: string;
24+
}
25+
26+
export interface MutationMap {
27+
[key: string]: TriggerMutationFunction<unknown>;
28+
}
29+
30+
export type CommandConfig = {
31+
[command: string]: {
32+
name: string;
33+
description: string;
34+
execute: (params: {
35+
mutation: TriggerMutationFunction<unknown>;
36+
currentUser: CurrentUserData;
37+
users: UsersBasicInfoData[];
38+
senderId: string;
39+
args?: string[];
40+
}) => void;
41+
};
42+
};
43+
44+
export interface ChatCommandPluginProps extends ChatMentionProps {
45+
commands?: CommandConfig;
46+
}

src/index.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import * as React from 'react';
2+
import * as ReactDOM from 'react-dom/client';
3+
import { BbbPluginSdk, PluginApi } from 'bigbluebutton-html-plugin-sdk';
4+
import ChatCommandsPlugin from './ChatCommandsPlugin/component';
5+
6+
const uuid = document.currentScript?.getAttribute('uuid') || 'root';
7+
8+
function PluginInitializer({ pluginUuid }: { pluginUuid: string }): React.ReactNode {
9+
BbbPluginSdk.initialize(pluginUuid);
10+
const pluginApi: PluginApi = BbbPluginSdk.getPluginApi(pluginUuid);
11+
12+
return <ChatCommandsPlugin pluginApi={pluginApi} />;
13+
}
14+
15+
const root = ReactDOM.createRoot(document.getElementById(uuid));
16+
root.render(
17+
<React.StrictMode>
18+
<PluginInitializer pluginUuid={uuid} />
19+
</React.StrictMode>,
20+
);

tsconfig.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
"target": "es5",
77
"jsx": "react",
88
"allowJs": true,
9-
"moduleResolution": "node"
9+
"moduleResolution": "node",
10+
"allowSyntheticDefaultImports": true,
1011
},
1112
"include": ["src/*"],
1213
"paths": {

webpack.config.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ const path = require('path');
55
module.exports = {
66
entry: './src/index.tsx',
77
output: {
8-
filename: '<plugin-name>.js',
9-
library: '<plugin-name>',
8+
filename: 'ChatCommandsPlugin.js',
9+
library: 'ChatCommandsPlugin',
1010
libraryTarget: 'umd',
1111
publicPath: '/',
1212
globalObject: 'this',
@@ -20,7 +20,7 @@ module.exports = {
2020
client: {
2121
overlay: false,
2222
},
23-
onBeforeSetupMiddleware: (devServer) => {
23+
setupMiddlewares: (middlewares, devServer) => {
2424
if (!devServer) {
2525
throw new Error('webpack-dev-server is not defined');
2626
}
@@ -29,6 +29,8 @@ module.exports = {
2929
devServer.app.get('/manifest.json', (req, res) => {
3030
res.sendFile(path.resolve(__dirname, 'manifest.json'));
3131
});
32+
33+
return middlewares;
3234
},
3335
},
3436
module: {

0 commit comments

Comments
 (0)