0
0
Fork 0
mirror of https://github.com/matrix-construct/construct synced 2024-12-11 08:02:59 +01:00
construct/modules/client/rooms/send.cc

342 lines
7.2 KiB
C++

// Matrix Construct
//
// Copyright (C) Matrix Construct Developers, Authors & Contributors
// Copyright (C) 2016-2018 Jason Volk <jason@zemos.net>
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice is present in all copies. The
// full license for this software is available in the LICENSE file.
#include "rooms.h"
using namespace ircd::m;
using namespace ircd;
static bool
check_transaction_id(const m::user::id &user_id,
const string_view &transaction_id);
static void
save_transaction_id(const m::event &,
m::vm::eval &);
static m::event::id::buf
handle_command(client &,
const m::resource::request &,
const m::room &,
const m::room::message &,
const string_view & = {});
m::hookfn<m::vm::eval &>
save_transaction_id_hookfn
{
save_transaction_id,
{
{ "_site", "vm.post" },
{ "origin", my_host() },
}
};
conf::item<bool>
new_content_workaround
{
{ "name", "ircd.client.rooms.send.new_content_workaround" },
{ "default", true },
};
static_assert
(
m::event::MAX_SIZE >= 1_KiB
);
static const size_t
content_max
{
m::event::MAX_SIZE - 1_KiB
};
m::resource::response
put__send(client &client,
const m::resource::request &request,
const room::id &room_id)
{
if(request.parv.size() < 3)
throw NEED_MORE_PARAMS
{
"type parameter missing"
};
char type_buf[m::event::TYPE_MAX_SIZE];
const string_view &type
{
url::decode(type_buf, request.parv[2])
};
if(request.parv.size() < 4)
throw NEED_MORE_PARAMS
{
"txnid parameter missing"
};
char transaction_id_buf[64];
const string_view &transaction_id
{
url::decode(transaction_id_buf, request.parv[3])
};
if(!check_transaction_id(request.user_id, transaction_id))
throw m::error
{
http::CONFLICT, "M_DUPLICATE_TXNID",
"Already processed request with txnid '%s'",
transaction_id
};
json::object content
{
request
};
// Workaround for the quadruplication of content by certain clients
// supporting message edits via `m.new_content`. The skinny is that
// there's effectively four copies of the user's message data which can
// be found within the content here. For this reason if the content size
// with all four copies will exceed the threshold, we truncate two of the
// four copies which won't be used by supporting clients.
const bool workaround_new_content
{
// This functionality is enabled by the configuration
new_content_workaround
// For good-faith compatibility this functionality is enabled iff the
// content size is excessive.
&& size(string_view(content)) > content_max
// This workaround is only effective for m.new_content edits.
&& content.has("m.new_content")
};
json::strung shorter_content;
if(workaround_new_content)
content = shorter_content = json::replace(content, json::members
{
{ "body", " * "_sv },
{ "formatted_body", " * "_sv },
});
// This is only a preliminary check that the content size is sane and
// will fit. There may still be a rejection at a deeper stage.
if(size(string_view(content)) > content_max)
throw m::error
{
http::PAYLOAD_TOO_LARGE, "M_TOO_LARGE",
"Message of %zu bytes exceeds maximum of %zu bytes.",
size(request.content),
content_max,
};
const m::vm::copts copts
{
.client_txnid = transaction_id,
// Bridges are authorized to set their own time.
.ts = request.bridge_id?
request.query.get("ts", milliseconds::min()):
milliseconds::min(),
};
const room room
{
room_id, &copts
};
bool command_echo {false};
if(type == "m.room.message")
{
const m::room::message message
{
content
};
const auto body
{
message.body()
};
const bool command
{
json::get<"msgtype"_>(message) == "m.text"
&& startswith(body, "\\\\")
};
const bool echo
{
startswith(lstrip(body, "\\\\"), '!')
};
if(command && !echo)
{
const auto event_id
{
handle_command(client, request, room, message)
};
return m::resource::response
{
client, json::members
{
{ "event_id", event_id },
{ "cmd", true },
}
};
}
else command_echo = command && echo;
}
const auto event_id
{
m::send(room, request.user_id, type, content)
};
// For public echo, run the command after sending the command to the room.
if(command_echo)
{
const auto cmd_event_id
{
handle_command(client, request, room, content, event_id)
};
}
return m::resource::response
{
client, json::members
{
{ "event_id", event_id }
}
};
}
m::event::id::buf
handle_command(client &client,
const m::resource::request &request,
const m::room &room,
const m::room::message &msg,
const string_view &echo_id)
{
const user::room user_room
{
request.user_id, room.copts
};
const string_view reply_id
{
msg.reply_to_event()
};
const string_view body
{
msg.body()
};
const auto event_id
{
send(user_room, request.user_id, "ircd.cmd",
{
{ "msgtype", "m.text" },
{ "body", body },
{ "room_id", room.room_id },
{ "event_id", echo_id },
{ "reply_id", reply_id },
})
};
return event_id;
}
void
save_transaction_id(const m::event &event,
m::vm::eval &eval)
{
if(!eval.copts)
return;
if(!eval.copts->client_txnid)
return;
if(!event.event_id)
return;
assert(my_host(at<"origin"_>(event)));
const m::user::room user_room
{
at<"sender"_>(event)
};
static const string_view &type
{
"ircd.client.txnid"
};
const auto &state_key
{
event.event_id
};
send(user_room, at<"sender"_>(event), type, state_key,
{
{ "transaction_id", eval.copts->client_txnid }
});
}
// Using a linear search here because we have no index on txnids as this
// is the only codepath where we'd perform that lookup; in contrast the
// event_id -> txnid query is made far more often for client sync.
//
// This means we have to set some arbitrary limits on the linear search:
// lim[0] is a total limit of events to iterate, so if the user's room
// has a lot of activity we might return a false non-match and allow a
// duplicate txnid; this is highly unlikely. lim[1] allows the user to
// have several /sends in flight at the same time, also unlikely but we
// avoid that case for false non-match.
static bool
check_transaction_id(const m::user::id &user_id,
const string_view &transaction_id)
{
static const auto type_match
{
[](const string_view &type) noexcept
{
return type == "ircd.client.txnid";
}
};
const auto content_match
{
[&transaction_id](const json::object &content)
{
const json::string &value
{
content["transaction_id"]
};
return value == transaction_id;
}
};
ssize_t lim[] { 128, 3 };
const m::user::room user_room{user_id};
for(m::room::events it(user_room); it && lim[0] > 0 && lim[1] > 0; --it, --lim[0])
{
if(!m::query(std::nothrow, it.event_idx(), "type", type_match))
continue;
--lim[1];
if(!m::query(std::nothrow, it.event_idx(), "content", content_match))
continue;
return false;
}
return true;
}