// Matrix Construct // // Copyright (C) Matrix Construct Developers, Authors & Contributors // Copyright (C) 2016-2019 Jason Volk // // 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 using namespace ircd; mapi::header IRCD_MODULE { "Server Command" }; static void handle_command(const m::event &event, m::vm::eval &eval); m::hookfn 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 watch_limit { { "name", "ircd.m.command.watch.limit" }, { "default", 256 }, }; static conf::item watch_opers { { "name", "ircd.m.command.watch.opers" }, { "default", true }, }; static conf::item 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(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 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 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 {" "}; out << "
" << "" << "" //<< sp << sp << e.what() << sp << sp << "" << " " << "" << "" << sp << sp << unquote(error.get("errcode")) << sp << sp << "" << " " << "
" << "
"
	    << unquote(error.get("error"))
	    << "
" ; 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 {" "}; out << "
" << "" << "" << sp << sp << e.what() << sp << sp << "" << " " << "
" << "
"
	    << e.content
	    << "
" ; 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 {" "}; out << "

" << info::name << "

" << "
"
	    << info::version
	    << "
" ; 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()) }; 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 << "
"
	<< ""
	<< ""
	;

	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
			<< ""
			<< ""
			<< ""
			<< ""
			;
		}};

		// 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
	<< "
" << "" << room_id << "" << "" << event_id << "
" << "
" << "
*** Marked " << matched << " rooms as read.
" << "
" ; 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 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() }; static const string_view sp{" "}, 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 << " " << " " << sp << sp << status << sp << sp << " " << " " << " " << sp << sp << " " << target << " " << sp << " " ; if(!eptr) out << " " << pretty(tmbuf, time) << " " << " application layer round-trip time."; if(eptr) out << "
"
		     << what(eptr)
		     << "
"; 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{" "}, 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 << " " << " " << sp << sp << status << sp << sp << " " << " " << " " << sp << sp << " " << result.origin << " " << sp << " " ; if(!result.eptr) out << " " << pretty(tmbuf, timer.at()) << " " << " application layer round-trip time." << "
"; if(result.eptr) out << "" << what(result.eptr) << "" << "
"; 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()) }}; 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 {" "}; out << "
" << "" << "" << sp << sp << " CONSTRUCT STATUS " << sp << sp << "" << " " << "" << "" << sp << sp << " OK " << sp << sp << "" << " " << "
" << "
"
	    << " "
	    << "
" ; 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 edit_hook { handle_edit, { { "_site", "vm.eval" }, { "type", "m.room.message" }, { "content", { { "msgtype", "m.text" } }} } }; conf::item edit_path { { "name", "ircd.m.cmd.edit.path" }, { "default", string_view{} }, }; conf::item 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, "
"_sv));
	consume(buf, size(replace(buf, content, '<', "<"_sv)));
	consume(buf, copy(buf, "
"_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 << "" << "" << 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 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 << "
";
	command(out, cmd, opts);
    out << "
"; 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 }; }