Skip to content

Commit

Permalink
feat: prosody plugin for sending system chat messages (#14603)
Browse files Browse the repository at this point in the history
* feat: prosody plugin for sending system chat messages

* code review changes

* code review changes

* update module name

* update comment
  • Loading branch information
quitrk authored Apr 8, 2024
1 parent 9b16296 commit 097d51c
Show file tree
Hide file tree
Showing 8 changed files with 189 additions and 12 deletions.
1 change: 1 addition & 0 deletions lang/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@
"privateNotice": "Private message to {{recipient}}",
"sendButton": "Send",
"smileysPanel": "Emoji panel",
"systemDisplayName": "System",
"tabs": {
"chat": "Chat",
"polls": "Polls"
Expand Down
10 changes: 6 additions & 4 deletions react/features/chat/components/native/ChatMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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
};
}
Expand Down
10 changes: 5 additions & 5 deletions react/features/chat/components/web/ChatMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -117,6 +116,7 @@ const useStyles = makeStyles()((theme: Theme) => {
* @returns {JSX}
*/
const ChatMessage = ({
canReply,
knocking,
message,
showDisplayName,
Expand Down Expand Up @@ -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 }>
Expand All @@ -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
};
}
Expand Down
5 changes: 5 additions & 0 deletions react/features/chat/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
18 changes: 18 additions & 0 deletions react/features/chat/functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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.
*
Expand Down
26 changes: 24 additions & 2 deletions react/features/chat/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -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;

Expand Down
7 changes: 6 additions & 1 deletion react/features/chat/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
124 changes: 124 additions & 0 deletions resources/prosody-plugins/mod_system_chat_message.lua
Original file line number Diff line number Diff line change
@@ -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;
};
});

0 comments on commit 097d51c

Please sign in to comment.