mirror of
https://github.com/matrix-construct/construct
synced 2024-12-11 08:02:59 +01:00
342 lines
7.2 KiB
C++
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;
|
|
}
|