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
pull/14606/head jitsi-meet_9425
Avram Tudor 1 year ago committed by GitHub
parent 9b16296581
commit 097d51ce10
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 1
      lang/main.json
  2. 10
      react/features/chat/components/native/ChatMessage.tsx
  3. 10
      react/features/chat/components/web/ChatMessage.tsx
  4. 5
      react/features/chat/constants.ts
  5. 18
      react/features/chat/functions.ts
  6. 26
      react/features/chat/middleware.ts
  7. 7
      react/features/chat/types.ts
  8. 124
      resources/prosody-plugins/mod_system_chat_message.lua

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

@ -9,6 +9,7 @@ import Linkify from '../../../base/react/components/native/Linkify';
import { isGifMessage } from '../../../gifs/functions.native';
import { MESSAGE_TYPE_ERROR, MESSAGE_TYPE_LOCAL } from '../../constants';
import {
getCanReplyToMessage,
getFormattedTimestamp,
getMessageText,
getPrivateNoticeMessage,
@ -163,10 +164,10 @@ class ChatMessage extends Component<IChatMessageProps> {
* @returns {React$Element<*> | null}
*/
_renderPrivateReplyButton() {
const { message, knocking } = this.props;
const { messageType, privateMessage, lobbyChat } = message;
const { message, canReply } = this.props;
const { lobbyChat } = message;
if (!(privateMessage || lobbyChat) || messageType === MESSAGE_TYPE_LOCAL || knocking) {
if (!canReply) {
return null;
}
@ -206,8 +207,9 @@ class ChatMessage extends Component<IChatMessageProps> {
* @param {Object} state - The Redux state.
* @returns {IProps}
*/
function _mapStateToProps(state: IReduxState) {
function _mapStateToProps(state: IReduxState, { message }: IChatMessageProps) {
return {
canReply: getCanReplyToMessage(state, message),
knocking: state['features/lobby'].knocking
};
}

@ -7,8 +7,7 @@ import { IReduxState } from '../../../app/types';
import { translate } from '../../../base/i18n/functions';
import Message from '../../../base/react/components/web/Message';
import { withPixelLineHeight } from '../../../base/styles/functions.web';
import { MESSAGE_TYPE_LOCAL } from '../../constants';
import { getFormattedTimestamp, getMessageText, getPrivateNoticeMessage } from '../../functions';
import { getCanReplyToMessage, getFormattedTimestamp, getMessageText, getPrivateNoticeMessage } from '../../functions';
import { IChatMessageProps } from '../../types';
import PrivateMessageButton from './PrivateMessageButton';
@ -117,6 +116,7 @@ const useStyles = makeStyles()((theme: Theme) => {
* @returns {JSX}
*/
const ChatMessage = ({
canReply,
knocking,
message,
showDisplayName,
@ -191,8 +191,7 @@ const ChatMessage = ({
{(message.privateMessage || (message.lobbyChat && !knocking))
&& _renderPrivateNotice()}
</div>
{(message.privateMessage || (message.lobbyChat && !knocking))
&& message.messageType !== MESSAGE_TYPE_LOCAL
{canReply
&& (
<div
className = { classes.replyButtonContainer }>
@ -214,10 +213,11 @@ const ChatMessage = ({
* @param {Object} state - The Redux state.
* @returns {IProps}
*/
function _mapStateToProps(state: IReduxState) {
function _mapStateToProps(state: IReduxState, { message }: IProps) {
const { knocking } = state['features/lobby'];
return {
canReply: getCanReplyToMessage(state, message),
knocking
};
}

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

@ -7,6 +7,7 @@ import emojiAsciiAliases from 'react-emoji-render/data/asciiAliases';
import { IReduxState } from '../app/types';
import { getLocalizedDateFormatter } from '../base/i18n/dateUtil';
import i18next from '../base/i18n/i18next';
import { getParticipantById } from '../base/participants/functions';
import { escapeRegexp } from '../base/util/helpers';
import { MESSAGE_TYPE_ERROR, MESSAGE_TYPE_LOCAL, TIMESTAMP_FORMAT } from './constants';
@ -161,6 +162,23 @@ export function getMessageText(message: IMessage) {
: message.message;
}
/**
* Returns whether a message can be replied to.
*
* @param {IReduxState} state - The redux state.
* @param {IMessage} message - The message to be checked.
* @returns {boolean}
*/
export function getCanReplyToMessage(state: IReduxState, message: IMessage) {
const { knocking } = state['features/lobby'];
const participant = getParticipantById(state, message.id);
return Boolean(participant)
&& (message.privateMessage || (message.lobbyChat && !knocking))
&& message.messageType !== MESSAGE_TYPE_LOCAL;
}
/**
* Returns the message that is displayed as a notice for private messages.
*

@ -2,7 +2,11 @@ import { AnyAction } from 'redux';
import { IReduxState, IStore } from '../app/types';
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app/actionTypes';
import { CONFERENCE_JOINED, ENDPOINT_MESSAGE_RECEIVED } from '../base/conference/actionTypes';
import {
CONFERENCE_JOINED,
ENDPOINT_MESSAGE_RECEIVED,
NON_PARTICIPANT_MESSAGE_RECEIVED
} from '../base/conference/actionTypes';
import { getCurrentConference } from '../base/conference/functions';
import { IJitsiConference } from '../base/conference/reducer';
import { openDialog } from '../base/dialog/actions';
@ -40,7 +44,8 @@ import {
LOBBY_CHAT_MESSAGE,
MESSAGE_TYPE_ERROR,
MESSAGE_TYPE_LOCAL,
MESSAGE_TYPE_REMOTE
MESSAGE_TYPE_REMOTE,
MESSAGE_TYPE_SYSTEM
} from './constants';
import { getUnreadCount } from './functions';
import { INCOMING_MSG_SOUND_FILE } from './sounds';
@ -131,6 +136,23 @@ MiddlewareRegistry.register(store => next => action => {
break;
}
case NON_PARTICIPANT_MESSAGE_RECEIVED: {
const { id, json: data } = action;
if (data?.type === MESSAGE_TYPE_SYSTEM && data.message) {
_handleReceivedMessage(store, {
displayName: data.displayName ?? i18next.t('chat.systemDisplayName'),
id,
lobbyChat: false,
message: data.message,
privateMessage: true,
timestamp: Date.now()
});
}
break;
}
case OPEN_CHAT:
unreadCount = 0;

@ -39,10 +39,15 @@ export interface IChatProps extends WithTranslation {
export interface IChatMessageProps extends WithTranslation {
/**
* Whether the message can be replied to.
*/
canReply?: boolean;
/**
* Whether current participant is currently knocking in the lobby room.
*/
knocking: boolean;
knocking?: boolean;
/**
* The representation of a chat message.

@ -0,0 +1,124 @@
-- Module which can be used as an http endpoint to send system chat messages to meeting participants. The provided token
--- in the request is verified whether it has the right to do so. This module should be loaded under the virtual host.
-- Copyright (C) 2024-present 8x8, Inc.
-- curl https://{host}/send-system-message -d '{"message": "testmessage", "to": "{connection_jid}", "room": "{room_jid}"}' -H "content-type: application/json" -H "authorization: Bearer {token}"
local util = module:require "util";
local token_util = module:require "token/util".new(module);
local async_handler_wrapper = util.async_handler_wrapper;
local room_jid_match_rewrite = util.room_jid_match_rewrite;
local starts_with = util.starts_with;
local get_room_from_jid = util.get_room_from_jid;
local st = require "util.stanza";
local json = require "cjson.safe";
local muc_domain_base = module:get_option_string("muc_mapper_domain_base");
local asapKeyServer = module:get_option_string("prosody_password_public_key_repo_url", "");
if asapKeyServer then
-- init token util with our asap keyserver
token_util:set_asap_key_server(asapKeyServer)
end
function verify_token(token)
if token == nil then
module:log("warn", "no token provided");
return false;
end
local session = {};
session.auth_token = token;
local verified, reason, msg = token_util:process_and_verify_token(session);
if not verified then
module:log("warn", "not a valid token %s %s", tostring(reason), tostring(msg));
return false;
end
return true;
end
function handle_send_system_message (event)
local request = event.request;
module:log("debug", "Request for sending a system message received: reqid %s", request.headers["request_id"])
-- verify payload
if request.headers.content_type ~= "application/json"
or (not request.body or #request.body == 0) then
module:log("error", "Wrong content type: %s or missing payload", request.headers.content_type);
return { status_code = 400; }
end
local payload = json.decode(request.body);
if not payload then
module:log("error", "Request body is missing");
return { status_code = 400; }
end
local displayName = payload["displayName"];
local message = payload["message"];
local to = payload["to"];
local payload_room = payload["room"];
if not message or not to or not payload_room then
module:log("error", "One of [message, to, room] was not provided");
return { status_code = 400; }
end
local room_jid = room_jid_match_rewrite(payload_room);
local room = get_room_from_jid(room_jid);
if not room then
module:log("error", "Room %s not found", room_jid);
return { status_code = 404; }
end
-- verify access
local token = request.headers["authorization"]
if not token then
module:log("error", "Authorization header was not provided for conference %s", room_jid)
return { status_code = 401 };
end
if starts_with(token, 'Bearer ') then
token = token:sub(8, #token)
else
module:log("error", "Authorization header is invalid")
return { status_code = 401 };
end
if not verify_token(token, room_jid) then
return { status_code = 401 };
end
local data = {
displayName = displayName,
type = "system_chat_message",
message = message,
};
local stanza = st.message({
from = room.jid,
to = to
})
:tag('json-message', { xmlns = 'http://jitsi.org/jitmeet' })
:text(json.encode(data))
:up();
room:route_stanza(stanza);
return { status_code = 200 };
end
module:log("info", "Adding http handler for /send-system-chat-message on %s", module.host);
module:depends("http");
module:provides("http", {
default_path = "/";
route = {
["POST send-system-chat-message"] = function(event)
return async_handler_wrapper(event, handle_send_system_message)
end;
};
});
Loading…
Cancel
Save