0
0
Fork 0
mirror of https://github.com/matrix-construct/construct synced 2024-10-31 19:08:59 +01:00
construct/modules/m_command.cc

1536 lines
29 KiB
C++

// Matrix Construct
//
// Copyright (C) Matrix Construct Developers, Authors & Contributors
// Copyright (C) 2016-2019 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 <ircd/util/params.h>
using namespace ircd;
mapi::header
IRCD_MODULE
{
"Server Command"
};
static void
handle_command(const m::event &event,
m::vm::eval &eval);
m::hookfn<m::vm::eval &>
command_hook
{
handle_command,
{
{ "_site", "vm.effect" },
{ "type", "ircd.cmd" },
{ "origin", my_host() },
}
};
struct command_result
{
string_view html;
string_view alt;
string_view msgtype {"m.notice"};
};
static conf::item<size_t>
watch_limit
{
{ "name", "ircd.m.command.watch.limit" },
{ "default", 256 },
};
static conf::item<bool>
watch_opers
{
{ "name", "ircd.m.command.watch.opers" },
{ "default", true },
};
static conf::item<bool>
command_typing
{
{ "name", "ircd.m.command.typing" },
{ "default", false },
};
static command_result
execute_command(const mutable_buffer &buf,
const m::user &user,
const m::room &room,
const string_view &cmd,
const m::event::id &reply_to,
const bool public_response);
static void
watch_command(const m::user::id &user_id,
const m::room::id &room_id,
const m::event::id &reply_id,
const m::event::id &response_id,
const m::room::id &response_room,
const m::user::id &response_sender,
const string_view &response_type,
const string_view &cmd,
const bool &public_response,
const milliseconds &watch_delay);
static const json::value
undef_val
{
string_view{nullptr}, json::STRING
};
static const json::value
html_format
{
"org.matrix.custom.html", json::STRING
};
void
handle_command(const m::event &event,
m::vm::eval &eval)
try
{
const m::user user
{
at<"sender"_>(event)
};
if(!m::my(user.user_id))
return;
const json::object &content
{
json::get<"content"_>(event)
};
const m::user::room user_room
{
user
};
if(json::get<"room_id"_>(event) != user_room.room_id)
return;
const m::room::id &room_id
{
unquote(content.at("room_id"))
};
const json::string &event_id
{
content["event_id"]
};
const json::string &body
{
content.at("body")
};
const bool is_command
{
!content.has("input")
&& startswith(body, '\\')
};
if(!is_command)
return;
const json::string reply_to
{
content["reply_id"]
};
const auto reply_id
{
reply_to?
m::event::id::buf{reply_to}:
m::event::id::buf{}
};
// View of the command string without prefix
string_view input
{
lstrip(body, '\\')
};
// Determine if there's a bang after the prefix; if so the response's
// sender will be the user, and will be broadcast publicly to the room.
// Otherwise the response comes from the server and is only visible in
// the user's timeline.
const bool public_response
{
startswith(input, '!')
};
if(public_response)
input = lstrip(input, '!');
const bool command_watch
{
startswith(input, "watch ")
&& (!watch_opers || is_oper(user))
};
milliseconds watch_delay {0ms};
if(command_watch)
{
const auto delay
{
lex_cast<float>(token(input, ' ', 1))
};
watch_delay = milliseconds
{
long(delay * 1000.0)
};
input = tokens_after(input, ' ', 1);
}
const string_view cmd
{
input
};
log::debug
{
m::log, "Server command from %s in %s public:%b watch:%ld replying:%s :%s",
string_view{room_id},
string_view{user.user_id},
public_response,
watch_delay.count(),
string_view{reply_id}?: "false"_sv,
cmd
};
const unique_buffer<mutable_buffer> buf
{
56_KiB
};
const auto &[html, alt, msgtype]
{
execute_command(buf, user, room_id, cmd, reply_id, public_response)
};
if(!html && !alt)
return;
const auto &response_sender
{
public_response? user : m::user(m::me())
};
const auto &response_room
{
public_response? room_id : user_room
};
const auto &response_type
{
public_response? "m.room.message" : "ircd.cmd.result"
};
const auto &command_event_id
{
public_response?
string_view{event_id}:
string_view{event.event_id}
};
const json::value html_val
{
html, json::STRING
};
const json::value content_body
{
alt?: "no alt text"_sv, json::STRING
};
char relates_buf[576], reply_buf[288];
json::object in_reply_to, relates_to {json::empty_object};
if(event_id)
{
in_reply_to = json::stringify(reply_buf, json::members
{
{ "event_id", event_id },
});
relates_to = json::stringify(relates_buf, json::members
{
{ "event_id", event_id },
{ "rel_type", "ircd.cmd" },
{ "m.in_reply_to", in_reply_to },
});
}
const auto response_id
{
m::send(response_room, response_sender, response_type,
{
{ "msgtype", msgtype?: "m.notice" },
{ "format", html? html_format: undef_val },
{ "body", content_body },
{ "formatted_body", html? html_val: undef_val },
{ "room_id", room_id },
{ "input", cmd },
{ "m.relates_to", relates_to },
})
};
if(command_watch)
ctx::context
{
"watch", ctx::context::POST | ctx::context::DETACH,
[
user_id(m::user::id::buf(user.user_id)),
room_id(m::room::id::buf(room_id)),
reply_id(m::event::id::buf(reply_id)),
response_id(m::event::id::buf(response_id)),
response_room(m::room::id::buf(response_room)),
response_sender(m::user::id::buf(response_sender)),
response_type(std::string(response_type)),
cmd(std::string(cmd)),
public_response,
watch_delay
]
{
watch_command
(
user_id,
room_id,
reply_id,
response_id,
response_room,
response_sender,
response_type,
cmd,
public_response,
watch_delay
);
}
};
}
catch(const std::exception &e)
{
const json::object &content
{
json::get<"content"_>(event)
};
const json::string &room_id
{
content["room_id"]
};
const json::string &input
{
content["body"]
};
log::error
{
m::log, "Command in %s in %s by %s '%s' :%s",
string_view{event.event_id},
room_id,
at<"sender"_>(event),
input,
e.what(),
};
}
void
watch_command(const m::user::id &user_id,
const m::room::id &room_id,
const m::event::id &reply_id,
const m::event::id &response_id,
const m::room::id &response_room,
const m::user::id &response_sender,
const string_view &response_type,
const string_view &cmd,
const bool &public_response,
const milliseconds &watch_delay)
{
const auto annotation_id
{
public_response?
m::annotate(response_room, response_sender, response_id, "▶️"_sv):
m::event::id::buf{}
};
const unwind deannotate{[&]
{
if(annotation_id && !m::redacted(annotation_id))
m::redact(response_room, response_sender, annotation_id, "cleared");
}};
const unique_buffer<mutable_buffer> buf
{
56_KiB
};
for(size_t i(0); i < size_t(watch_limit); ++i)
{
sleep(watch_delay);
if(m::redacted(response_id))
break;
if(annotation_id && m::redacted(annotation_id))
break;
const auto &[html, alt, msgtype]
{
execute_command(buf, user_id, room_id, cmd, reply_id, public_response)
};
if(!html && !alt)
break;
const json::value html_val
{
html, json::STRING
};
const json::value content_body
{
alt?: "no alt text"_sv, json::STRING
};
m::send(response_room, response_sender, response_type,
{
{ "body", content_body },
{ "format", html? html_format: undef_val },
{ "formatted_body", html? html_val: undef_val },
{ "input", cmd },
{ "msgtype", msgtype?: "m.notice" },
{ "room_id", room_id },
{ "m.new_content", json::members
{
{ "body", content_body },
{ "format", html? html_format: undef_val },
{ "formatted_body", html? html_val: undef_val },
{ "input", cmd },
{ "msgtype", msgtype?: "m.notice" },
{ "room_id", room_id },
}},
{ "m.relates_to", json::members
{
{ "event_id", response_id },
{ "rel_type", "m.replace" },
}}
});
}
}
static command_result
command__summarize(const mutable_buffer &buf,
const m::user &user,
const m::room &room,
const string_view &cmd,
const m::event::id &reply_id);
static command_result
command__caption(const mutable_buffer &buf,
const m::user &user,
const m::room &room,
const string_view &cmd);
static command_result
command__edit(const mutable_buffer &buf,
const m::user &user,
const m::room &room,
const string_view &cmd);
static command_result
command__ping(const mutable_buffer &buf,
const m::user &user,
const m::room &room,
const string_view &cmd);
static command_result
command__dash(const mutable_buffer &buf,
const m::user &user,
const m::room &room,
const string_view &cmd);
static command_result
command__read(const mutable_buffer &buf,
const m::user &user,
const m::room &room,
const string_view &cmd);
static command_result
command__version(const mutable_buffer &buf,
const m::user &user,
const m::room &room,
const string_view &cmd);
static command_result
command__control(const mutable_buffer &buf,
const m::user &user,
const m::room &room,
const string_view &cmd);
command_result
execute_command(const mutable_buffer &buf,
const m::user &user,
const m::room &room,
const string_view &cmd,
const m::event::id &reply_id,
const bool public_response)
try
{
m::typing::edu typing
{
{ "room_id", room.room_id },
{ "typing", true },
{ "user_id", user.user_id },
{ "timeout", (30000ms).count() },
};
if(public_response && command_typing)
m::typing::commit{typing};
const unwind done_typing{[&]
{
if(public_response && command_typing)
{
json::get<"typing"_>(typing) = false;
m::typing::commit{typing};
}
}};
if(startswith(cmd, '#'))
return command__control(buf, user, room, lstrip(cmd, '#'));
switch(hash(token(cmd, ' ', 0)))
{
case "version"_:
return command__version(buf, user, room, cmd);
case "read"_:
return command__read(buf, user, room, cmd);
case "dash"_:
return command__dash(buf, user, room, cmd);
case "ping"_:
return command__ping(buf, user, room, cmd);
case "edit"_:
return command__edit(buf, user, room, cmd);
case "caption"_:
return command__caption(buf, user, room, cmd);
case "summarize"_:
return command__summarize(buf, user, room, cmd, reply_id);
case "control"_:
{
const auto subcmd
{
tokens_after(cmd, ' ', 0)
};
return command__control(buf, user, room, subcmd);
}
default:
break;
}
const string_view out{fmt::sprintf
{
buf, "unknown command :%s", cmd
}};
const string_view alt
{
out
};
return { out, alt };
}
catch(const m::error &e)
{
const json::object &error
{
e.content
};
log::error
{
m::log, "Server command from %s in %s '%s' :%s :%s :%s",
string_view{room.room_id},
string_view{user.user_id},
cmd,
e.what(),
unquote(error.get("errcode")),
unquote(error.get("error")),
};
std::ostringstream out;
pubsetbuf(out, buf);
const string_view fg[] {"#FCFCFC", "#FFFFFF"};
const string_view bg[] {"#A01810", "#C81810"};
const string_view sp {"&nbsp;"};
out
<< "<h5>"
<< "<font color=\"" << fg[0] << "\" data-mx-bg-color=\"" << bg[0] << "\">"
<< "<b>"
//<< sp << sp << e.what() << sp << sp
<< "</b>"
<< "</font> "
<< "<font color=\"" << fg[1] << "\" data-mx-bg-color=\"" << bg[1] << "\">"
<< "<b>"
<< sp << sp << unquote(error.get("errcode")) << sp << sp
<< "</b>"
<< "</font> "
<< "</h5>"
<< "<pre>"
<< unquote(error.get("error"))
<< "</pre>"
;
return
{
view(out, buf), e.what()
};
}
catch(const http::error &e)
{
log::error
{
m::log, "Server command from %s in %s '%s' :%s :%s",
string_view{room.room_id},
string_view{user.user_id},
cmd,
e.what(),
e.content
};
std::ostringstream out;
pubsetbuf(out, buf);
const string_view fg[] {"#FCFCFC"};
const string_view bg[] {"#A01810"};
const string_view sp {"&nbsp;"};
out
<< "<h5>"
<< "<font color=\"" << fg[0] << "\" data-mx-bg-color=\"" << bg[0] << "\">"
<< "<b>"
<< sp << sp << e.what() << sp << sp
<< "</b>"
<< "</font> "
<< "</h5>"
<< "<pre>"
<< e.content
<< "</pre>"
;
return
{
view(out, buf), e.what()
};
}
catch(const std::exception &e)
{
log::error
{
m::log, "Server command from %s in %s '%s' :%s",
string_view{room.room_id},
string_view{user.user_id},
cmd,
e.what()
};
const size_t len
{
copy(buf, string_view(e.what()))
};
return
{
{ data(buf), len },
{ data(buf), len }
};
}
command_result
command__version(const mutable_buffer &buf,
const m::user &user,
const m::room &room,
const string_view &cmd)
{
std::ostringstream out;
pubsetbuf(out, buf);
const string_view sp {"&nbsp;"};
out
<< "<h1>"
<< info::name
<< "</h1>"
<< "<pre><code>"
<< info::version
<< "</code></pre>"
;
const string_view alt
{
info::version
};
return { view(out, buf), alt };
}
command_result
command__read(const mutable_buffer &buf,
const m::user &user,
const m::room &room,
const string_view &cmd)
{
const params param{tokens_after(cmd, ' ', 0), " ",
{
"arg", "[time]"
}};
const string_view &arg
{
param["arg"]
};
const time_t &ms
{
param.at("[time]", ircd::time<milliseconds>())
};
std::ostringstream out;
pubsetbuf(out, buf);
if(m::valid(m::id::EVENT, arg))
{
const m::event::id::buf event_id
{
arg?
m::event::id::buf{arg}:
m::head(room)
};
const json::strung content{json::members
{
{ "ts", ms },
}};
m::receipt::read(room, user, event_id, json::object(content));
return {};
}
else if(m::valid(m::id::ROOM, arg) || m::valid(m::id::ROOM_ALIAS, arg))
{
const auto room_id
{
m::room_id(arg)
};
const m::room room
{
room_id
};
const m::event::id::buf event_id
{
m::head(room)
};
const json::strung content{json::members
{
{ "ts", ms },
}};
m::receipt::read(room, user, event_id, json::object(content));
return {};
}
// We don't accept no-argument as a wildcard to prevent a naive user
// just probing the command interface from performing the following large
// operation on all their rooms...
if(!arg)
return {};
// The string argument can be a globular expression of room tags, like
// `m.*` or just `*`.
const globular_imatch match
{
arg
};
// Iterate all joined rooms for a user. For each room we'll test if the
// room's tag matches the arg string globular expression.
const m::user::rooms user_rooms
{
user
};
const string_view fg[] {"#FFFFFF"};
const string_view bg[] {"#000000"};
out
<< "<pre>"
<< "<font color=\"" << fg[0] << "\" data-mx-bg-color=\"" << bg[0] << "\">"
<< "<table>"
;
size_t matched(0);
user_rooms.for_each("join", [&user, &ms, &match, &out, &matched]
(const m::room::id &room_id, const string_view &)
{
const m::user::room_tags room_tags
{
user, room_id
};
// return true if the expression is not matched for this room.
const auto without_match{[&match]
(const string_view &key, const json::object &object) noexcept
{
return !match(key);
}};
// for_each returns true if it didn't break from the loop, which means
// no match and skip actions for this room.
if(match.a != "*")
if(room_tags.for_each(without_match))
return;
// Get the room head (if there are multiple, the best is selected for
// us) which will be the target of our receipt.
const m::event::id::buf event_id
{
m::head(std::nothrow, room_id)
};
// Nothing to send a receipt for.
if(!event_id)
return;
const auto put{[&out]
(const string_view &room_id, const string_view &event_id)
{
out
<< "<tr>"
<< "<td>"
<< "<b>"
<< room_id
<< "</b>"
<< "</td>"
<< "<td>"
<< event_id
<< "</td>"
<< "</tr>"
;
}};
// Check if event_id is more recent than the last receipt's event_id.
if(!m::receipt::freshest(room_id, user, event_id))
{
put(room_id, "You already read this or a later event in the room.");
return;
}
// Check if user wants to prevent sending receipts to this room.
if(m::receipt::ignoring(user, room_id))
{
put(room_id, "You have configured to not send receipts to this room.");
return;
}
// Check if user wants to prevent based on this event's specifics.
if(m::receipt::ignoring(user, event_id))
{
put(room_id, "You have configured to not send receipts for this event.");
return;
}
// Commit the receipt.
const json::strung content{json::members
{
{ "ts", ms },
{ "m.hidden", true },
}};
m::receipt::read(room_id, user, event_id, json::object(content));
put(room_id, event_id);
++matched;
});
out
<< "</table>"
<< "</font>"
<< "<br />*** Marked "
<< matched
<< " rooms as read.<br />"
<< "</pre>"
;
return
{
view(out, buf), "TODO: alt text."
};
}
static command_result
command__ping__room(const mutable_buffer &buf,
const m::user &user,
const m::room &room,
const string_view &cmd);
command_result
command__ping(const mutable_buffer &buf,
const m::user &user,
const m::room &room,
const string_view &cmd)
{
const params param{tokens_after(cmd, ' ', 0), " ",
{
"target"
}};
const string_view &target
{
param["target"]
};
const bool room_ping
{
!target
|| m::valid(m::id::ROOM, target)
|| m::valid(m::id::ROOM_ALIAS, target)
};
if(room_ping)
return command__ping__room(buf, user, room, cmd);
m::fed::version::opts opts;
if(m::valid(m::id::USER, target))
opts.remote = m::user::id(target).host();
else
opts.remote = target;
const unique_buffer<mutable_buffer> http_buf
{
8_KiB
};
util::timer timer;
m::fed::version request
{
http_buf, std::move(opts)
};
std::exception_ptr eptr; try
{
request.wait(seconds(10));
const auto code(request.get());
}
catch(const std::exception &e)
{
eptr = std::current_exception();
}
const auto time
{
timer.at<milliseconds>()
};
static const string_view
sp{"&nbsp;"}, fg{"#e8e8e8"}, host_bg{"#181b21"},
online_bg{"#008000"}, offline_bg{"#A01810"};
const string_view
bg{eptr? offline_bg : online_bg},
status{eptr? "FAILED " : "ONLINE"};
std::ostringstream out;
pubsetbuf(out, buf);
char tmbuf[32];
out
<< " <font color=\"" << fg << "\" data-mx-bg-color=\"" << bg << "\">"
<< " <b>"
<< sp << sp << status << sp << sp
<< " </b>"
<< " </font>"
<< " <font color=\"" << fg << "\" data-mx-bg-color=\"" << host_bg << "\">"
<< sp << sp << " " << target << " " << sp
<< " </font> "
;
if(!eptr)
out << " <b>"
<< pretty(tmbuf, time)
<< " </b>"
<< " application layer round-trip time.";
if(eptr)
out << "<pre>"
<< what(eptr)
<< "</pre>";
const string_view rich
{
view(out, buf)
};
const string_view alt{fmt::sprintf
{
buf + size(rich), "response in %s", pretty(tmbuf, time)
}};
return { view(out, buf), alt };
}
command_result
command__ping__room(const mutable_buffer &buf,
const m::user &user,
const m::room &room,
const string_view &cmd)
{
const params param{tokens_after(cmd, ' ', 0), " ",
{
"target"
}};
const string_view &target
{
param["target"]
};
m::feds::opts opts;
opts.op = m::feds::op::version;
opts.room_id = room.room_id;
opts.closure_cached_errors = true;
opts.timeout = seconds(10); //TODO: conf
char tmbuf[32];
std::ostringstream out;
pubsetbuf(out, buf);
util::timer timer;
size_t responses{0};
m::feds::execute(opts, [&tmbuf, &timer, &responses, &buf, &out]
(const auto &result)
{
++responses;
static const string_view
sp{"&nbsp;"}, fg{"#e8e8e8"}, host_bg{"#181b21"},
online_bg{"#008000"}, offline_bg{"#A01810"};
const string_view
bg{result.eptr? offline_bg : online_bg},
status{result.eptr? "FAILED " : "ONLINE"};
assert(result.origin);
out
<< " <font color=\"" << fg << "\" data-mx-bg-color=\"" << bg << "\">"
<< " <b>"
<< sp << sp << status << sp << sp
<< " </b>"
<< " </font>"
<< " <font color=\"" << fg << "\" data-mx-bg-color=\"" << host_bg << "\">"
<< sp << sp << " " << result.origin << " " << sp
<< " </font> "
;
if(!result.eptr)
out << " <b>"
<< pretty(tmbuf, timer.at<milliseconds>())
<< " </b>"
<< " application layer round-trip time."
<< "<br />";
if(result.eptr)
out << "<code>"
<< what(result.eptr)
<< "</code>"
<< "<br />";
return true;
});
const string_view rich
{
view(out, buf)
};
const string_view alt{fmt::sprintf
{
buf + size(rich), "%zu responses in %s",
responses,
pretty(tmbuf, timer.at<milliseconds>())
}};
return { view(out, buf), alt };
}
command_result
command__dash(const mutable_buffer &buf,
const m::user &user,
const m::room &room,
const string_view &cmd)
{
std::ostringstream out;
pubsetbuf(out, buf);
const string_view fg[] {"#3EA6FF", "#FFFFFF"};
const string_view bg[] {"#000000", "#008000"};
const string_view sp {"&nbsp;"};
out
<< "<h5>"
<< "<font color=\"" << fg[0] << "\" data-mx-bg-color=\"" << bg[0] << "\">"
<< "<b>"
<< sp << sp << " CONSTRUCT STATUS " << sp << sp
<< "</b>"
<< "</font> "
<< "<font color=\"" << fg[1] << "\" data-mx-bg-color=\"" << bg[1] << "\">"
<< "<b>"
<< sp << sp << " OK " << sp << sp
<< "</b>"
<< "</font> "
<< "</h5>"
<< "<pre>"
<< " "
<< "</pre>"
;
const string_view alt
{
"no alt text"
};
return { view(out, buf), alt };
}
static void
handle_edit(const m::event &event,
m::vm::eval &eval);
m::hookfn<m::vm::eval &>
edit_hook
{
handle_edit,
{
{ "_site", "vm.eval" },
{ "type", "m.room.message" },
{ "content",
{
{ "msgtype", "m.text" }
}}
}
};
conf::item<std::string>
edit_path
{
{ "name", "ircd.m.cmd.edit.path" },
{ "default", string_view{} },
};
conf::item<std::string>
edit_whitelist
{
{ "name", "ircd.m.cmd.edit.whitelist" },
{ "default", string_view{} },
};
static bool
edit_whitelisted(const m::user::id &user_id)
{
bool ret(false);
ircd::tokens(edit_whitelist, ' ', [&user_id, &ret]
(const string_view &whitelisted_user_id)
{
ret |= whitelisted_user_id == user_id;
});
return ret;
}
command_result
command__edit(const mutable_buffer &buf_,
const m::user &user,
const m::room &room,
const string_view &cmd)
{
const params param
{
tokens_after(cmd, ' ', 0), " ",
{
"path"
}
};
if(!edit_path)
throw m::UNAVAILABLE
{
"Configure the 'ircd.m.cmd.edit.path' to use this feature.",
};
if(!edit_whitelisted(user))
throw m::ACCESS_DENIED
{
"'%s' is not listed in the 'ircd.m.cmd.edit.whitelist'.",
string_view{user.user_id},
};
const string_view parts[2]
{
edit_path, param["path"]
};
const string_view path
{
fs::path(buf_, edit_path, parts)
};
const fs::fd fd
{
path
};
const std::string content
{
fs::read(fd)
};
mutable_buffer buf(buf_);
consume(buf, copy(buf, "<pre><code>"_sv));
consume(buf, size(replace(buf, content, '<', "&lt;"_sv)));
consume(buf, copy(buf, "</code></pre>"_sv));
if(empty(buf))
return
{
{}, "File too large.", "m.notice",
};
const string_view html
{
data(buf_), data(buf)
};
return
{
html, {}, "m.text"
};
}
static void
handle_edit(const m::event &event,
m::vm::eval &eval)
try
{
if(!edit_path)
return;
if(!edit_whitelisted(json::get<"sender"_>(event)))
return;
const json::object &content
{
json::get<"content"_>(event)
};
const m::relates_to relates_to
{
content["m.relates_to"]
};
if(json::get<"rel_type"_>(relates_to) != "m.replace")
return;
if(!m::valid(m::id::EVENT, json::get<"event_id"_>(relates_to)))
return;
const m::event::fetch relates_event
{
std::nothrow, json::get<"event_id"_>(relates_to)
};
if(!relates_event.valid)
return;
if(json::get<"sender"_>(relates_event) != json::get<"sender"_>(event))
return;
if(json::get<"room_id"_>(relates_event) != json::get<"room_id"_>(event))
return;
const json::object &relates_content
{
json::get<"content"_>(relates_event)
};
const json::string &input
{
relates_content["input"]
};
const string_view cmd_input
{
lstrip(input, '!')
};
if(!startswith(cmd_input, "edit"))
return;
const auto &[cmd, args]
{
split(cmd_input, ' ')
};
const bool pub_cmd
{
startswith(input, '!')
};
const json::object &new_content
{
content["m.new_content"]
};
const json::string new_body
{
new_content["body"]
};
const unique_mutable_buffer body_buf
{
size(new_body), simd::alignment
};
string_view body(new_body);
body = strip(body, "```");
body = lstrip(body, "\\n"_sv);
body = json::unescape(body_buf, body);
if(!body)
return;
const string_view path_parts[2]
{
edit_path, args
};
const std::string path
{
fs::path(fs::path_scratch, edit_path, path_parts)
};
fs::write_opts wopts;
const const_buffer written
{
fs::overwrite(path, body, wopts)
};
log::info
{
m::log, "Edit %s in %s by %s to `%s' wrote %zu/%zu bytes",
string_view{event.event_id},
json::get<"room_id"_>(event),
json::get<"sender"_>(event),
path,
size(written),
size(body),
};
}
catch(const ctx::interrupted &)
{
throw;
}
catch(const std::exception &e)
{
log::error
{
m::log, "Edit %s in %s by %s failed :%s",
string_view{event.event_id},
json::get<"room_id"_>(event),
json::get<"sender"_>(event),
e.what(),
};
}
command_result
command__caption(const mutable_buffer &buf,
const m::user &user,
const m::room &room,
const string_view &cmd)
{
const params param{tokens_after(cmd, ' ', 0), " ",
{
"url",
}};
const string_view caption
{
tokens_after(cmd, ' ', 1)
};
std::ostringstream out;
pubsetbuf(out, buf);
out
<< "<img"
<< " src=\"" << param.at("url") << "\""
<< " height=\"100%\""
<< " width=\"100%\""
<< " />"
<< "<caption>"
<< caption
<< "</caption>"
;
const string_view html
{
view(out, buf)
};
return
{
html, caption
};
}
command_result
command__control(const mutable_buffer &buf,
const m::user &user,
const m::room &room,
const string_view &cmd)
{
if(!is_oper(user))
throw m::ACCESS_DENIED
{
"You do not have access to the !control room."
};
const ircd::module console_module
{
"console"
};
using prototype = int (std::ostream &, const string_view &, const string_view &);
mods::import<prototype> command
{
console_module, "console_command"s
};
std::ostringstream out;
out.exceptions(out.badbit | out.failbit | out.eofbit);
pubsetbuf(out, buf);
static const string_view opts
{
"html"
};
out << "<pre>";
command(out, cmd, opts);
out << "</pre>";
const string_view html
{
view(out, buf)
};
const string_view alt //TODO: X
{
"no alt text"
};
return
{
html, alt
};
}
command_result
command__summarize(const mutable_buffer &buf,
const m::user &user,
const m::room &room,
const string_view &cmd,
const m::event::id &reply_id)
{
const params param{cmd, " ",
{
"cmd", "top_p", "top_k"
}};
const auto reply_idx
{
index(reply_id)
};
const m::relates relates
{
.refs = reply_idx,
.match_sender = true,
};
const auto replace_idx
{
relates.latest("m.replace")
};
const auto content_idx
{
replace_idx?: reply_idx
};
const json::object content
{
m::get(content_idx, "content", buf)
};
const m::room::message msg
{
content
};
const auto body
{
msg.body()
};
std::string text(body);
text += "\n\nTL;DR:"s;
gpt::opts opts alignas(4096);
opts.top_k = param.at("top_k", opts.top_k);
opts.top_p = param.at("top_p", opts.top_p);
opts.debug |= 0x1;
opts.debug |= 0x2;
gpt::ctrl ctrl alignas(4096) {0};
gpt::task task
{
&opts, &ctrl
};
auto output
{
task(buf, text)
};
output = lstrip(output, ": "_sv);
output = rstrip(output, '\n', 2);
return
{
output, output
};
}