瀏覽代碼

feat: prosody plugin for sending system chat messages (#14603)

* feat: prosody plugin for sending system chat messages

* code review changes

* code review changes

* update module name

* update comment
factor2
Avram Tudor 1 年之前
父節點
當前提交
097d51ce10
No account linked to committer's email address

+ 1
- 0
lang/main.json 查看文件

@@ -128,6 +128,7 @@
128 128
         "privateNotice": "Private message to {{recipient}}",
129 129
         "sendButton": "Send",
130 130
         "smileysPanel": "Emoji panel",
131
+        "systemDisplayName": "System",
131 132
         "tabs": {
132 133
             "chat": "Chat",
133 134
             "polls": "Polls"

+ 6
- 4
react/features/chat/components/native/ChatMessage.tsx 查看文件

@@ -9,6 +9,7 @@ import Linkify from '../../../base/react/components/native/Linkify';
9 9
 import { isGifMessage } from '../../../gifs/functions.native';
10 10
 import { MESSAGE_TYPE_ERROR, MESSAGE_TYPE_LOCAL } from '../../constants';
11 11
 import {
12
+    getCanReplyToMessage,
12 13
     getFormattedTimestamp,
13 14
     getMessageText,
14 15
     getPrivateNoticeMessage,
@@ -163,10 +164,10 @@ class ChatMessage extends Component<IChatMessageProps> {
163 164
      * @returns {React$Element<*> | null}
164 165
      */
165 166
     _renderPrivateReplyButton() {
166
-        const { message, knocking } = this.props;
167
-        const { messageType, privateMessage, lobbyChat } = message;
167
+        const { message, canReply } = this.props;
168
+        const { lobbyChat } = message;
168 169
 
169
-        if (!(privateMessage || lobbyChat) || messageType === MESSAGE_TYPE_LOCAL || knocking) {
170
+        if (!canReply) {
170 171
             return null;
171 172
         }
172 173
 
@@ -206,8 +207,9 @@ class ChatMessage extends Component<IChatMessageProps> {
206 207
  * @param {Object} state - The Redux state.
207 208
  * @returns {IProps}
208 209
  */
209
-function _mapStateToProps(state: IReduxState) {
210
+function _mapStateToProps(state: IReduxState, { message }: IChatMessageProps) {
210 211
     return {
212
+        canReply: getCanReplyToMessage(state, message),
211 213
         knocking: state['features/lobby'].knocking
212 214
     };
213 215
 }

+ 5
- 5
react/features/chat/components/web/ChatMessage.tsx 查看文件

@@ -7,8 +7,7 @@ import { IReduxState } from '../../../app/types';
7 7
 import { translate } from '../../../base/i18n/functions';
8 8
 import Message from '../../../base/react/components/web/Message';
9 9
 import { withPixelLineHeight } from '../../../base/styles/functions.web';
10
-import { MESSAGE_TYPE_LOCAL } from '../../constants';
11
-import { getFormattedTimestamp, getMessageText, getPrivateNoticeMessage } from '../../functions';
10
+import { getCanReplyToMessage, getFormattedTimestamp, getMessageText, getPrivateNoticeMessage } from '../../functions';
12 11
 import { IChatMessageProps } from '../../types';
13 12
 
14 13
 import PrivateMessageButton from './PrivateMessageButton';
@@ -117,6 +116,7 @@ const useStyles = makeStyles()((theme: Theme) => {
117 116
  * @returns {JSX}
118 117
  */
119 118
 const ChatMessage = ({
119
+    canReply,
120 120
     knocking,
121 121
     message,
122 122
     showDisplayName,
@@ -191,8 +191,7 @@ const ChatMessage = ({
191 191
                         {(message.privateMessage || (message.lobbyChat && !knocking))
192 192
                             && _renderPrivateNotice()}
193 193
                     </div>
194
-                    {(message.privateMessage || (message.lobbyChat && !knocking))
195
-                        && message.messageType !== MESSAGE_TYPE_LOCAL
194
+                    {canReply
196 195
                         && (
197 196
                             <div
198 197
                                 className = { classes.replyButtonContainer }>
@@ -214,10 +213,11 @@ const ChatMessage = ({
214 213
  * @param {Object} state - The Redux state.
215 214
  * @returns {IProps}
216 215
  */
217
-function _mapStateToProps(state: IReduxState) {
216
+function _mapStateToProps(state: IReduxState, { message }: IProps) {
218 217
     const { knocking } = state['features/lobby'];
219 218
 
220 219
     return {
220
+        canReply: getCanReplyToMessage(state, message),
221 221
         knocking
222 222
     };
223 223
 }

+ 5
- 0
react/features/chat/constants.ts 查看文件

@@ -43,3 +43,8 @@ export const CHAT_TABS = {
43 43
  * Formatter string to display the message timestamp.
44 44
  */
45 45
 export const TIMESTAMP_FORMAT = 'H:mm';
46
+
47
+/**
48
+ * The namespace for system messages.
49
+ */
50
+export const MESSAGE_TYPE_SYSTEM = 'system_chat_message';

+ 18
- 0
react/features/chat/functions.ts 查看文件

@@ -7,6 +7,7 @@ import emojiAsciiAliases from 'react-emoji-render/data/asciiAliases';
7 7
 import { IReduxState } from '../app/types';
8 8
 import { getLocalizedDateFormatter } from '../base/i18n/dateUtil';
9 9
 import i18next from '../base/i18n/i18next';
10
+import { getParticipantById } from '../base/participants/functions';
10 11
 import { escapeRegexp } from '../base/util/helpers';
11 12
 
12 13
 import { MESSAGE_TYPE_ERROR, MESSAGE_TYPE_LOCAL, TIMESTAMP_FORMAT } from './constants';
@@ -161,6 +162,23 @@ export function getMessageText(message: IMessage) {
161 162
         : message.message;
162 163
 }
163 164
 
165
+
166
+/**
167
+ * Returns whether a message can be replied to.
168
+ *
169
+ * @param {IReduxState} state - The redux state.
170
+ * @param {IMessage} message - The message to be checked.
171
+ * @returns {boolean}
172
+ */
173
+export function getCanReplyToMessage(state: IReduxState, message: IMessage) {
174
+    const { knocking } = state['features/lobby'];
175
+    const participant = getParticipantById(state, message.id);
176
+
177
+    return Boolean(participant)
178
+        && (message.privateMessage || (message.lobbyChat && !knocking))
179
+        && message.messageType !== MESSAGE_TYPE_LOCAL;
180
+}
181
+
164 182
 /**
165 183
  * Returns the message that is displayed as a notice for private messages.
166 184
  *

+ 24
- 2
react/features/chat/middleware.ts 查看文件

@@ -2,7 +2,11 @@ import { AnyAction } from 'redux';
2 2
 
3 3
 import { IReduxState, IStore } from '../app/types';
4 4
 import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app/actionTypes';
5
-import { CONFERENCE_JOINED, ENDPOINT_MESSAGE_RECEIVED } from '../base/conference/actionTypes';
5
+import {
6
+    CONFERENCE_JOINED,
7
+    ENDPOINT_MESSAGE_RECEIVED,
8
+    NON_PARTICIPANT_MESSAGE_RECEIVED
9
+} from '../base/conference/actionTypes';
6 10
 import { getCurrentConference } from '../base/conference/functions';
7 11
 import { IJitsiConference } from '../base/conference/reducer';
8 12
 import { openDialog } from '../base/dialog/actions';
@@ -40,7 +44,8 @@ import {
40 44
     LOBBY_CHAT_MESSAGE,
41 45
     MESSAGE_TYPE_ERROR,
42 46
     MESSAGE_TYPE_LOCAL,
43
-    MESSAGE_TYPE_REMOTE
47
+    MESSAGE_TYPE_REMOTE,
48
+    MESSAGE_TYPE_SYSTEM
44 49
 } from './constants';
45 50
 import { getUnreadCount } from './functions';
46 51
 import { INCOMING_MSG_SOUND_FILE } from './sounds';
@@ -131,6 +136,23 @@ MiddlewareRegistry.register(store => next => action => {
131 136
         break;
132 137
     }
133 138
 
139
+    case NON_PARTICIPANT_MESSAGE_RECEIVED: {
140
+        const { id, json: data } = action;
141
+
142
+        if (data?.type === MESSAGE_TYPE_SYSTEM && data.message) {
143
+            _handleReceivedMessage(store, {
144
+                displayName: data.displayName ?? i18next.t('chat.systemDisplayName'),
145
+                id,
146
+                lobbyChat: false,
147
+                message: data.message,
148
+                privateMessage: true,
149
+                timestamp: Date.now()
150
+            });
151
+        }
152
+
153
+        break;
154
+    }
155
+
134 156
     case OPEN_CHAT:
135 157
         unreadCount = 0;
136 158
 

+ 6
- 1
react/features/chat/types.ts 查看文件

@@ -39,10 +39,15 @@ export interface IChatProps extends WithTranslation {
39 39
 
40 40
 export interface IChatMessageProps extends WithTranslation {
41 41
 
42
+    /**
43
+     * Whether the message can be replied to.
44
+     */
45
+    canReply?: boolean;
46
+
42 47
     /**
43 48
      * Whether current participant is currently knocking in the lobby room.
44 49
      */
45
-    knocking: boolean;
50
+    knocking?: boolean;
46 51
 
47 52
     /**
48 53
      * The representation of a chat message.

+ 124
- 0
resources/prosody-plugins/mod_system_chat_message.lua 查看文件

@@ -0,0 +1,124 @@
1
+-- Module which can be used as an http endpoint to send system chat messages to meeting participants. The provided token
2
+--- in the request is verified whether it has the right to do so. This module should be loaded under the virtual host.
3
+-- Copyright (C) 2024-present 8x8, Inc.
4
+
5
+-- curl https://{host}/send-system-message  -d '{"message": "testmessage", "to": "{connection_jid}", "room": "{room_jid}"}' -H "content-type: application/json" -H "authorization: Bearer {token}"
6
+
7
+local util = module:require "util";
8
+local token_util = module:require "token/util".new(module);
9
+
10
+local async_handler_wrapper = util.async_handler_wrapper;
11
+local room_jid_match_rewrite = util.room_jid_match_rewrite;
12
+local starts_with = util.starts_with;
13
+local get_room_from_jid = util.get_room_from_jid;
14
+
15
+local st = require "util.stanza";
16
+local json = require "cjson.safe";
17
+
18
+local muc_domain_base = module:get_option_string("muc_mapper_domain_base");
19
+local asapKeyServer = module:get_option_string("prosody_password_public_key_repo_url", "");
20
+
21
+if asapKeyServer then
22
+    -- init token util with our asap keyserver
23
+    token_util:set_asap_key_server(asapKeyServer)
24
+end
25
+
26
+function verify_token(token)
27
+    if token == nil then
28
+        module:log("warn", "no token provided");
29
+        return false;
30
+    end
31
+
32
+    local session = {};
33
+    session.auth_token = token;
34
+    local verified, reason, msg = token_util:process_and_verify_token(session);
35
+    if not verified then
36
+        module:log("warn", "not a valid token %s %s", tostring(reason), tostring(msg));
37
+        return false;
38
+    end
39
+    return true;
40
+end
41
+
42
+function handle_send_system_message (event)
43
+    local request = event.request;
44
+
45
+    module:log("debug", "Request for sending a system message received: reqid %s", request.headers["request_id"])
46
+
47
+    -- verify payload
48
+    if request.headers.content_type ~= "application/json"
49
+            or (not request.body or #request.body == 0) then
50
+        module:log("error", "Wrong content type: %s or missing payload", request.headers.content_type);
51
+        return { status_code = 400; }
52
+    end
53
+
54
+    local payload = json.decode(request.body);
55
+
56
+    if not payload then
57
+        module:log("error", "Request body is missing");
58
+        return { status_code = 400; }
59
+    end
60
+
61
+    local displayName = payload["displayName"];
62
+    local message = payload["message"];
63
+    local to = payload["to"];
64
+    local payload_room = payload["room"];
65
+
66
+    if not message or not to or not payload_room then
67
+        module:log("error", "One of [message, to, room] was not provided");
68
+        return { status_code = 400; }
69
+    end
70
+
71
+    local room_jid = room_jid_match_rewrite(payload_room);
72
+    local room = get_room_from_jid(room_jid);
73
+
74
+    if not room then
75
+        module:log("error", "Room %s not found", room_jid);
76
+        return { status_code = 404; }
77
+    end
78
+
79
+    -- verify access
80
+    local token = request.headers["authorization"]
81
+    if not token then
82
+        module:log("error", "Authorization header was not provided for conference %s", room_jid)
83
+        return { status_code = 401 };
84
+    end
85
+    if starts_with(token, 'Bearer ') then
86
+        token = token:sub(8, #token)
87
+    else
88
+        module:log("error", "Authorization header is invalid")
89
+        return { status_code = 401 };
90
+    end
91
+
92
+    if not verify_token(token, room_jid) then
93
+        return { status_code = 401 };
94
+    end
95
+
96
+    local data = {
97
+        displayName = displayName,
98
+        type = "system_chat_message",
99
+        message = message,
100
+    };
101
+
102
+    local stanza = st.message({
103
+        from = room.jid,
104
+        to = to
105
+    })
106
+    :tag('json-message', { xmlns = 'http://jitsi.org/jitmeet' })
107
+    :text(json.encode(data))
108
+    :up();
109
+
110
+    room:route_stanza(stanza);
111
+
112
+    return { status_code = 200 };
113
+end
114
+
115
+module:log("info", "Adding http handler for /send-system-chat-message on %s", module.host);
116
+module:depends("http");
117
+module:provides("http", {
118
+    default_path = "/";
119
+    route = {
120
+        ["POST send-system-chat-message"] = function(event)
121
+            return async_handler_wrapper(event, handle_send_system_message)
122
+        end;
123
+    };
124
+});

Loading…
取消
儲存