// 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.

using namespace ircd;

mapi::header
IRCD_MODULE
{
	"Web hook Handler"
};

conf::item<std::string>
webhook_secret
{
	{ "name", "webhook.secret" }
};

conf::item<std::string>
webhook_user
{
	{ "name", "webhook.user" }
};

conf::item<std::string>
webhook_room
{
	{ "name", "webhook.room" }
};

conf::item<std::string>
webhook_url
{
	{ "name", "webhook.url" },
	{ "default", "/webhook" }
};

conf::item<bool>
webhook_status_verbose
{
	{ "name",     "webhook.github.status.verbose" },
	{ "default",  true                            },
};

conf::item<bool>
webhook_status_errors
{
	{ "name",     "webhook.github.status.errors" },
	{ "default",  true                           },
};

conf::item<std::string>
webhook_github_token
{
	{ "name", "webhook.github.token" }
};

resource
webhook_resource
{
	string_view{webhook_url},
	{
		"Webhook Resource",
		webhook_resource.DIRECTORY
	}
};

static resource::response
post__webhook(client &,
              const resource::request &);

resource::method
webhook_post
{
	webhook_resource, "POST", post__webhook
};

static void
github_handle(client &,
              const resource::request &);

static void
appveyor_handle(client &,
                const resource::request &);

static void
dockerhub_handle(client &,
                 const resource::request &);

resource::response
post__webhook(client &client,
              const resource::request &request)
{
	const http::headers &headers
	{
		request.head.headers
	};

	if(http::has(headers, "X-GitHub-Event"))
		github_handle(client, request);

	else if(http::has(headers, "X-Appveyor-Secret"))
		appveyor_handle(client, request);

	else if(startswith(request.head.content_type, "application/json"))
		dockerhub_handle(client, request);

	return resource::response
	{
		client, http::OK
	};
}

static bool
github_validate(const string_view &sig,
                const const_buffer &content,
                const string_view &secret);

static std::string
github_url(const json::string &url);

static json::string
github_find_commit_hash(const json::object &content);

static json::string
github_find_issue_number(const json::object &content);

static std::pair<json::string, json::string>
github_find_party(const json::object &content);

static std::pair<json::string, json::string>
github_find_repo(const json::object &content);

static ircd::m::event::id::buf
github_find_push_event_id(const m::room &, const m::user::id &, const string_view &);

static bool
github_handle__dependabot_alert(std::ostream &,
                                const json::object &content);

static bool
github_handle__check_suite(std::ostream &,
                           const json::object &content);

static bool
github_handle__check_run(std::ostream &,
                         const json::object &content);

static bool
github_handle__workflow_job(std::ostream &,
                            std::ostream &,
                            const json::object &content);

static bool
github_handle__workflow_run(std::ostream &,
                            std::ostream &,
                            const json::object &content);

static bool
github_handle__milestone(std::ostream &,
                         const json::object &content);

static bool
github_handle__gollum(std::ostream &,
                      const json::object &content);

static bool
github_handle__push(std::ostream &,
                    const json::object &content);

static bool
github_handle__pull_request(std::ostream &,
                            const json::object &content);

static bool
github_handle__issue_comment(std::ostream &,
                             const json::object &content);

static bool
github_handle__commit_comment(std::ostream &,
                              const json::object &content);

static bool
github_handle__issues(std::ostream &,
                      const json::object &content);

static bool
github_handle__watch(std::ostream &,
                     const json::object &content);

static bool
github_handle__star(std::ostream &,
                    const json::object &content);

static bool
github_handle__label(std::ostream &,
                     const json::object &content);

static bool
github_handle__organization(std::ostream &,
                            const json::object &content);

static bool
github_handle__status(std::ostream &,
                      const json::object &content);

static bool
github_handle__repository(std::ostream &,
                          const json::object &content);

static bool
github_handle__delete(std::ostream &,
                      const json::object &content);

static bool
github_handle__create(std::ostream &,
                      const json::object &content);

static bool
github_handle__ping(std::ostream &,
                    const json::object &content);

static std::ostream &
github_heading(std::ostream &,
               const string_view &type,
               const json::object &content);

void
github_handle(client &client,
              const resource::request &request)
{
	if(!string_view(webhook_room))
		return;

	if(!string_view(webhook_user))
		return;

	const http::headers &headers
	{
		request.head.headers
	};

	const auto sig
	{
		headers.at("X-Hub-Signature")
	};

	if(!github_validate(sig, request.content, webhook_secret))
		throw http::error
		{
			http::UNAUTHORIZED, "X-Hub-Signature verification failed"
		};

	const string_view &type
	{
		headers.at("X-GitHub-Event")
	};

	const string_view &delivery
	{
		headers.at("X-GitHub-Delivery")
	};

	const unique_buffer<mutable_buffer> buf[2]
	{
		{ 48_KiB },
		{ 4_KiB  },
	};

	std::stringstream out, alt;
	pubsetbuf(out, buf[0]);
	pubsetbuf(alt, buf[1]);

	github_heading(out, type, request.content);

	alt
	<< type
	<< " by "
	<< github_find_party(request.content).first
	<< " to "
	<< github_find_repo(request.content).first
	<< " at "
	<< github_find_commit_hash(request.content)
	;

	const bool ok
	{
		type == "ping"?
			github_handle__ping(out, request.content):
		type == "push"?
			github_handle__push(out, request.content):
		type == "pull_request"?
			github_handle__pull_request(out, request.content):
		type == "issues"?
			github_handle__issues(out, request.content):
		type == "issue_comment"?
			github_handle__issue_comment(out, request.content):
		type == "commit_comment"?
			github_handle__commit_comment(out, request.content):
		type == "watch"?
			github_handle__watch(out, request.content):
		type == "star"?
			github_handle__star(out, request.content):
		type == "label"?
			github_handle__label(out, request.content):
		type == "organization"?
			github_handle__organization(out, request.content):
		type == "status"?
			github_handle__status(out, request.content):
		type == "repository"?
			github_handle__repository(out, request.content):
		type == "create"?
			github_handle__create(out, request.content):
		type == "delete"?
			github_handle__delete(out, request.content):
		type == "gollum"?
			github_handle__gollum(out, request.content):
		type == "milestone"?
			github_handle__milestone(out, request.content):
		type == "dependabot_alert"?
			github_handle__dependabot_alert(out, request.content):
		type == "workflow_run"?
			github_handle__workflow_run(out, alt, request.content):
		type == "workflow_job"?
			github_handle__workflow_job(out, alt, request.content):
		type == "check_run"?
			github_handle__check_run(out, request.content):
		type == "check_suite"?
			github_handle__check_suite(out, request.content):

		true // unhandled will just show heading
	};

	if(!ok)
		return;

	const auto room_id
	{
		m::room_id(string_view(webhook_room))
	};

	const m::user::id::buf user_id
	{
		string_view(webhook_user), my_host()
	};

	const auto evid
	{
		m::msghtml(room_id, user_id, view(out, buf[0]), view(alt, buf[1]), "m.notice")
	};

	log::info
	{
		"Webhook [%s] '%s' delivered to %s %s",
		delivery,
		type,
		string_view{room_id},
		string_view{evid}
	};
}

static std::ostream &
github_heading(std::ostream &out,
               const string_view &type,
               const json::object &content)
{
	const json::object repository
	{
		content["repository"]
	};

	const json::object organization
	{
		content["organization"]
	};

	if(empty(repository))
	{
		const auto url
		{
			github_url(organization["url"])
		};

		out << "<a href=\"" << url << "\">"
		    << json::string(organization["login"])
		    << "</a>";
	}
	else
		out << "<a href=" << repository["html_url"] << ">"
		    << json::string(repository["full_name"])
		    << "</a>";

	const auto commit_hash
	{
		github_find_commit_hash(content)
	};

	if(commit_hash && type == "push")
		out << " <b><font color=\"#FF5733\">";
	else if(commit_hash && type == "pull_request")
		out << " <b><font color=\"#FF5733\">";
	else if(commit_hash)
		out << " <b>";

	if(commit_hash)
		out << commit_hash.substr(0, 8)
		    << "</font></b>";

	const string_view issue_number
	{
		github_find_issue_number(content)
	};

	const json::object workflow
	{
		content.has("workflow_run")?
			content["workflow_run"]:
			content["workflow_job"]
	};

	const json::string
	workflow_name{workflow["workflow_name"]},
	job_name{workflow["name"]};

	if(issue_number)
		out << " <b>#" << issue_number << "</b>";
	else if(workflow_name && job_name)
		out << " job <b>" << workflow_name << "</b>";
	else if(job_name)
		out << " job <b>" << job_name << "</b>";
	else
		out << " " << type;

	const auto party
	{
		github_find_party(content)
	};

	out << " by "
	    << "<a href=\""
	    << party.second
	    << "\">"
	    << party.first
	    << "</a>";

	return out;
}

static string_view
github_markdown(unique_const_buffer &buf,
                const string_view &text);

bool
github_handle__dependabot_alert(std::ostream &out,
                                const json::object &content)
{
	const json::string
	action{content["action"]},
	url{content["html_url"]};

	const json::object
	alert{content["alert"]},
	advise{alert["security_advisory"]},
	vuln{alert["security_vulnerability"]},
	dep{alert["dependency"]},
	pkg{dep["package"]};

	const json::string
	ghsa{advise["ghsa_id"]},
	cve{advise["cve_id"]},
	summary{advise["summary"]},
	desc{advise["description"]},
	severity{advise["severity"]},
	name{pkg["name"]},
	path{dep["manifest_path"]};

	out
	<< " <a href=" << alert["html_url"] << ">"
	<< "<b>"
	<< summary
	<< "</b>"
	<< "</a>"
	<< "<br>🚨 "
	<< "<b>"
	<< cve
	<< "</b>"
	<< " "
	<< "<b>"
	<< ghsa
	<< "</b>"
	<< " severity "
	<< severity
	<< " 🚨<br>"
	;

	unique_const_buffer buf;
	const string_view markup
	{
		github_markdown(buf, desc)
	};

	out
	//<< "<blockquote>"
	<< markup
	//<< "</blockquote>"
	<< "<br>"
	;

	if(path)
		out
		<< "<pre>"
		<< path
		<< "</pre>"
		<< "<br>"
		;

	return true;
}

static size_t
clear_reactions(const m::room &room,
                const m::user::id &user_id,
                const m::event::id &event_id)
{
	const m::relates relations
	{
		index(event_id)
	};

	const auto user_match
	{
		[&user_id](const auto &sender)
		{
			return sender == user_id;
		}
	};

	size_t ret(0);
	relations.for_each("m.annotation", [&]
	(const auto &ref_idx, const json::object &content, const m::relates_to &relates)
	{
		if(!m::query(ref_idx, "sender", user_match))
			return true;

		if(m::redacted(ref_idx))
			return true;

		const auto ref_id(m::event_id(ref_idx));
		m::redact(room, user_id, ref_id, "cleared");
		++ret;
		return true;
	});

	return ret;
}

template<class closure>
static ircd::m::event::id::buf
_find_reaction_id(const m::room &room,
                 const m::user::id &user_id,
                 const m::event::id &event_id,
                 closure&& func)
{
	const m::relates relations
	{
		index(event_id)
	};

	const auto user_match
	{
		[&user_id](const auto &sender)
		{
			return sender == user_id;
		}
	};

	m::event::id::buf ret;
	relations.for_each("m.annotation", [&func, &user_match, &ret]
	(const auto &ref_idx, const json::object &content, const m::relates_to &relates)
	{
		if(!m::query(ref_idx, "sender", user_match))
			return true;

		if(m::redacted(ref_idx))
			return true;

		if(func(relates.source))
		{
			ret = m::event_id(ref_idx);
			return false;
		}
		else return true;
	});

	return ret;
}

static ircd::m::event::id::buf
find_reaction_id(const m::room &room,
                 const m::user::id &user_id,
                 const m::event::id &event_id,
                 const string_view &label)
{
	return _find_reaction_id(room, user_id, event_id, [&label]
	(const auto &relates)
	{
		const json::string key
		{
			relates["key"]
		};

		return key == label;
	});
}

static ircd::m::event::id::buf
find_reaction_id_contains(const m::room &room,
                          const m::user::id &user_id,
                          const m::event::id &event_id,
                          const string_view &label)
{
	return _find_reaction_id(room, user_id, event_id, [&]
	(const auto &relates)
	{
		const json::string key
		{
			relates["key"]
		};

		return has(key, label);
	});
}

static bool
clear_reaction(const m::room &room,
               const m::user::id &user_id,
               const m::event::id &event_id,
               const string_view &label)
{
	const auto reaction_id
	{
		find_reaction_id(room, user_id, event_id, label)
	};

	if(!reaction_id)
		return false;

	m::redact(room, user_id, reaction_id, "cleared");
	return true;
}

static bool
clear_reaction_contains(const m::room &room,
                        const m::user::id &user_id,
                        const m::event::id &event_id,
                        const string_view &label)
{
	const auto reaction_id
	{
		find_reaction_id_contains(room, user_id, event_id, label)
	};

	if(!reaction_id)
		return false;

	m::redact(room, user_id, reaction_id, "cleared");
	return true;
}

static ircd::m::event::id::buf
github_find_job_table(const m::room &room,
                      const m::user::id &user_id,
                      const string_view &str)
{
	const auto type_match
	{
		[](const string_view &type) noexcept
		{
			return type == "m.room.message";
		}
	};

	const auto user_match
	{
		[&user_id](const string_view &sender) noexcept
		{
			return sender && sender == user_id;
		}
	};

	const auto content_match
	{
		[&str](const json::object &content)
		{
			const json::string &body
			{
				content["body"]
			};

			return has(body, str);
		}
	};

	// Limit the search to a maximum of recent messages from the
	// webhook user and total messages so we don't run out of control
	// and scan the whole room history.
	int lim[2] { 768, 384 };
	m::room::events it{room};
	for(; it && lim[0] > 0 && lim[1] > 0; --it, --lim[0])
	{
		if(!m::query(std::nothrow, it.event_idx(), "sender", user_match))
			continue;

		--lim[1];
		if(!m::query(std::nothrow, it.event_idx(), "type", type_match))
			continue;

		if(!m::query(std::nothrow, it.event_idx(), "content", content_match))
			continue;

		return m::event_id(std::nothrow, it.event_idx());
	}

	return {};
}

static json::string
github_repopath(const json::object &content)
{
	const json::object repository
	{
		content.at("repository")
	};

	const json::string full_name
	{
		repository.at("full_name")
	};

	return full_name;
}

static json::object
_github_request(unique_const_buffer &out,
                const string_view &method,
                const string_view &url,
                const string_view &content)
{
	char authorization_buf[128];
	const string_view authorization
	{
		fmt::sprintf
		{
			authorization_buf, "Bearer %s",
			string_view{webhook_github_token},
		}
	};

	const http::header headers[]
	{
		{ "Accept", "application/json; charset=utf-8"  },
		{ "X-GitHub-Api-Version", "2022-11-28"         },
		{ "Authorization", authorization               },
	};

	const auto num_headers
	{
		sizeof(headers) / sizeof(http::header)
		- empty(webhook_github_token)
	};

	return string_view
	{
		rest::request
		{
			url,
			{
				.method = method,
				.content = content,
				.content_type = "application/json; charset=utf-8",
				.headers = vector_view(headers, num_headers),
				.out = &out,
			}
		}
	};
}

template<class... args>
static json::object
github_request(const string_view &content,
               unique_const_buffer &out,
               const string_view &method,
               const string_view &repo,
               const string_view &fmt,
               args&&... a)
{
	char path_buf[384];
	const string_view path{fmt::sprintf
	{
		path_buf, fmt,
		std::forward<args>(a)...
	}};

	char url_buf[512];
	const string_view url{fmt::sprintf
	{
		url_buf, "https://api.github.com/repos/%s/%s",
		repo,
		path,
	}};

	return _github_request(out, method, url, content);
}

template<class... args>
static json::object
github_request(unique_const_buffer &out,
               const string_view &method,
               const string_view &repo,
               const string_view &fmt,
               args&&... a)
{
	const auto &content(json::empty_object);
	return github_request(content, out, method, repo, fmt, std::forward<args>(a)...);
}

static string_view
github_markdown(unique_const_buffer &buf,
                const string_view &text)
{
	const json::strung content
	{
		json::members
		{
			{ "text", text }
		}
	};

	return _github_request
	(
		buf, "POST", "https://api.github.com/markdown", content
	);
}

static bool
github_hook_for_each(const string_view &repo,
                     const function_bool<json::object> &closure)
{
	unique_const_buffer buf;
	const json::array response
	{
		github_request
		(
			buf, "GET", repo, "hooks"
		)
	};

	for(const json::object hook : response)
		if(!closure(hook))
			return false;

	return true;
}

static void
github_hook_ping(const string_view &repo,
                 const string_view &hook)
{
	unique_const_buffer buf;
	github_request
	(
		buf, "POST", repo, "hooks/%s/pings",
		hook
	);
}

static void
github_hook_ping(const string_view &repo)
{
	github_hook_for_each(repo, [&repo]
	(const json::object &hook)
	{
		const json::string id
		{
			hook["id"]
		};

		github_hook_ping(repo, id);
	});
}

static bool
github_hook_shot_for_each(const string_view &repo,
                          const string_view &hook,
                          const bool &redelivery,
                          const function_bool<json::object> &closure)
{
	unique_const_buffer buf;
	const json::array response
	{
		github_request
		(
			//TODO: pagination token
			buf, "GET", repo, "hooks/%s/deliveries?per_page=100",
			hook
		)
	};

	for(const json::object shot : response)
		if(!closure(shot))
			return false;

	return true;
}

static void
github_hook_shot_retry(const string_view &repo,
                       const string_view &hook,
                       const string_view &id)
{
	unique_const_buffer buf;
	github_request
	(
		buf, "POST", repo, "hooks/%s/deliveries/%s/attempts",
		hook,
		id
	);
}

static bool
github_run_for_each_jobs(const string_view &repo,
                         const string_view &run_id,
                         const function_bool<json::object> &closure)
{
	for(size_t page(1), i(50); i >= 50; ++page)
	{
		unique_const_buffer buf;
		const json::object response
		{
			github_request
			(
				buf, "GET", repo, "actions/runs/%s/jobs?per_page=%zu&page=%zu", run_id, i, page
			)
		};

		i = 0;
		for(const json::object job : json::array(response["jobs"]))
			if(!closure(job))
				return false;
			else
				++i;
	}

	return true;
}

static void
github_run_delete(const string_view &repo,
                  const string_view &run_id)
{
	unique_const_buffer buf;
	github_request(buf, "DELETE", repo, "actions/runs/%s", run_id);
}

static void
github_run_cancel(const string_view &repo,
                  const string_view &run_id)
{
	unique_const_buffer buf;
	github_request(buf, "POST", repo, "actions/runs/%s/cancel", run_id);
}

static void
github_run_rerun(const string_view &repo,
                 const string_view &run_id)
{
	unique_const_buffer buf;
	github_request(buf, "POST", repo, "actions/runs/%s/rerun", run_id);
}

static void
github_run_rerun_failed(const string_view &repo,
                        const string_view &run_id)
{
	unique_const_buffer buf;
	github_request(buf, "POST", repo, "actions/runs/%s/rerun-failed-jobs", run_id);
}

bool
github_handle__workflow_run(std::ostream &out,
                            std::ostream &alt,
                            const json::object &content)
{
	const json::object
	workflow{content["workflow"]},
	workflow_run{content["workflow_run"]};

	const json::string
	action{content["action"]},
	title{workflow_run["display_title"]},
	status{workflow_run["status"]},
	conclusion{workflow_run["conclusion"]},
	url{workflow_run["html_url"]},
	name{workflow_run["name"]},
	head_sha{workflow_run["head_sha"]},
	created_at{workflow_run["created_at"]},
	updated_at{workflow_run["updated_at"]},
	run_started_at{workflow_run["run_started_at"]},
	attempt{workflow_run["run_attempt"]},
	run_id{workflow_run["id"]};

	const auto _webhook_room_id
	{
		m::room_id(string_view(webhook_room))
	};

	const m::user::id::buf _webhook_user
	{
		string_view{webhook_user}, my_host()
	};

	const m::room _webhook_room
	{
		_webhook_room_id
	};

	const auto push_event_id
	{
		github_find_push_event_id(_webhook_room, _webhook_user, head_sha)
	};

	const auto &stage
	{
		workflow_run["conclusion"] == json::literal_null?
			status: conclusion
	};

	string_view annote;
	switch(hash(stage))
	{
		case "queued"_:        annote = "🔵"_sv;  break;
		case "in_progress"_:   annote = "🟡"_sv;  break;
		case "success"_:       annote = "🟢"_sv;  break;
		case "failure"_:       annote = "🔴"_sv;  break;
		case "skipped"_:       annote = "⭕"_sv;  break;
		case "cancelled"_:     annote = "⭕"_sv;  break;
		default:               annote = "❓️"_sv;  break;
	}

	char buf[64] {0};
	annote = ircd::strlcpy(buf, annote);
	annote = ircd::strlcat(buf, " "_sv);
	annote = ircd::strlcat(buf, name);

	if(push_event_id && action != "requested") // skip search on first action
		while(clear_reaction_contains(_webhook_room, _webhook_user, push_event_id, name));

	m::annotate(_webhook_room, _webhook_user, push_event_id, annote);

	if(status == "completed")
	{
		const fmt::bsprintf<128> alt
		{
			"job status table %s %s %s",
			github_repopath(content),
			run_id,
			attempt,
		};

		const auto job_table_id
		{
			github_find_job_table(_webhook_room, _webhook_user, alt)
		};

		if(job_table_id)
			switch(hash(conclusion))
			{
				case "success"_:
				case "skipped"_:
					clear_reactions(_webhook_room, _webhook_user, job_table_id);
					break;

				default:
					clear_reaction(_webhook_room, _webhook_user, job_table_id, "⭕"_sv);
					break;
			}
	}

	bool outputs{false};
	if(action == "requested" && conclusion == "failure" && webhook_status_errors)
	{
		outputs = true;
		out
		<< "<br>"
		<< "<font data-mx-bg-color=\"#CC0000\" color=\"#FFFFFF\">"
		<< "&nbsp;"
		<< "&nbsp;"
		<< "<b>"
		<< name
		<< "</b>"
		<< "&nbsp;"
		<< "&nbsp;"
		<< "</font>"
		<< " failed "
		<< "<a href=\""
		<< url
		<< "\">"
		<< "</a>"
		;

		alt
		<< ' '
		<< name
		<< ' '
		<< "failed"
		;
	}

	return outputs;
}

bool
github_handle__workflow_job(std::ostream &out,
                            std::ostream &alt,
                            const json::object &content)
{
	const json::object workflow_job
	{
		content["workflow_job"]
	};

	const json::string
	action{content["action"]};

	// Ignore queued actions. Instead on the first in_progress we'll pull
	// all jobs from github at once.
	if(action == "queued")
		return false;

	const json::string
	flow_name{workflow_job["workflow_name"]},
	job_name{workflow_job["name"]},
	url{workflow_job["html_url"]},
	status{workflow_job["status"]},
	conclusion{workflow_job["conclusion"]},
	head_sha{workflow_job["head_sha"]},
	started_at{workflow_job["started_at"]},
	completed_at{workflow_job["completed_at"]},
	attempt{workflow_job["run_attempt"]},
	run_id{workflow_job["run_id"]},
	job_id{workflow_job["id"]};

	const json::array steps
	{
		workflow_job["steps"]
	};

	const auto _webhook_room_id
	{
		m::room_id(string_view(webhook_room))
	};

	const m::user::id::buf _webhook_user
	{
		string_view{webhook_user}, my_host()
	};

	const m::room _webhook_room
	{
		_webhook_room_id
	};

	static const auto annote{[]
	(const json::object &workflow_job)
	{
		const json::string stage
		{
			workflow_job["conclusion"] == json::literal_null?
				workflow_job["status"]:
				workflow_job["conclusion"]
		};

		switch(hash(stage))
		{
			case "queued"_:        return "🟦"_sv;
			case "in_progress"_:   return "🟨"_sv;
			case "success"_:       return "🟩"_sv;
			case "failure"_:       return "🟥"_sv;
			case "skipped"_:       return "⬜️"_sv;
			case "cancelled"_:     return "⬛️"_sv;
			default:               return "❓️"_sv;
		}
	}};

	const fmt::bsprintf<128> alt_tab
	{
		"job status table %s %s %s",
		github_repopath(content),
		run_id,
		attempt,
	};

	const fmt::bsprintf<128> alt_up
	{
		"job status update %s %s %s",
		github_repopath(content),
		run_id,
		attempt,
	};

	// slow this bird down
	static ctx::mutex mutex;
	const std::unique_lock lock
	{
		mutex
	};

	const auto orig_table_id
	{
		github_find_job_table(_webhook_room, _webhook_user, alt_tab)
	};

	const auto last_table_id
	{
		github_find_job_table(_webhook_room, _webhook_user, alt_up)
	};

	const unique_mutable_buffer buf
	{
		32_KiB
	};

	char headbuf[512] {0};
	std::stringstream heading;
	pubsetbuf(heading, headbuf);
	github_heading(heading, "push", content);

	if(orig_table_id)
	{
		const auto old_content
		{
			m::get(last_table_id?: orig_table_id, "content")
		};

		const json::string old_tab
		{
			json::object(old_content)["formatted_body"]
		};

		const string_view td
		{
			between(old_tab, "<td>", "</td>")
		};

		const fmt::bsprintf<512> expect
		{
			"<a href=\\\"%s\\\">", url
		};

		const bool exists
		{
			!tokens(td, "​"_sv, [&]
			(const string_view &cell)
			{
				return !startswith(cell, expect); // return false for found
			})
		};

		const fmt::bsprintf<512> expect_unmodified
		{
			"%s%s</a>",
			string_view{expect},
			annote(workflow_job),
		};

		bool modified
		{
			!tokens(td, "​"_sv, [&]
			(const string_view &cell)
			{
				if(!startswith(cell, expect))
					return true;

				return cell == expect_unmodified; // return false for found
			})
		};

		const bool cancelled
		{
			json::string(workflow_job["conclusion"]) == "cancelled"
		};

		string_view tab;
		tab = ircd::strlcpy(buf, view(heading, headbuf));
		tab = ircd::strlcat(buf, "<table><tr><td>");

		if(exists && modified && !cancelled)
			tokens(td, "​"_sv, [&]
			(const string_view &cell)
			{
				if(!startswith(cell, expect))
				{
					tab = ircd::strlcat(buf, cell);
					tab = ircd::strlcat(buf, "​"_sv);
					return;
				}

				tab = ircd::strlcat(buf, "<a href=\"");
				tab = ircd::strlcat(buf, url);
				tab = ircd::strlcat(buf, "\">");
				tab = ircd::strlcat(buf, annote(workflow_job));
				tab = ircd::strlcat(buf, "</a>");
				tab = ircd::strlcat(buf, "​"_sv);
			});

		if(!exists || (modified && cancelled))
			github_run_for_each_jobs(github_repopath(content), run_id, [&]
			(const json::object &workflow_job)
			{
				const json::string
				url{workflow_job["html_url"]};

				tab = ircd::strlcat(buf, "<a href=");
				tab = ircd::strlcat(buf, workflow_job["html_url"]);
				tab = ircd::strlcat(buf, ">");
				tab = ircd::strlcat(buf, annote(workflow_job));
				tab = ircd::strlcat(buf, "</a>");
				tab = ircd::strlcat(buf, "​"_sv);
				modified = true;
				return true;
			});

		if(modified)
		{
			tab = ircd::strlcat(buf, "</td></tr></table>");

			m::message(_webhook_room, _webhook_user, json::members
			{
				{ "body", alt_up },
				{ "msgtype", "m.notice" },
				{ "format", "org.matrix.custom.html" },
				{ "formatted_body", tab },
				{ "m.new_content", json::members
				{
					{ "body", alt_up },
					{ "msgtype", "m.notice" },
					{ "format", "org.matrix.custom.html" },
					{ "formatted_body", tab },
				}},
				{ "m.relates_to", json::members
				{
					{ "event_id", orig_table_id },
					{ "rel_type", "m.replace" },
				}}
			});
		}
	}
	else if(json::string(workflow_job["conclusion"]) != "skipped")
	{
		string_view tab;
		tab = ircd::strlcpy(buf, view(heading, headbuf));
		tab = ircd::strlcat(buf, "<table><tr><td>");

		github_run_for_each_jobs(github_repopath(content), run_id, [&]
		(const json::object &workflow_job)
		{
			const json::string
			url{workflow_job["html_url"]};

			tab = ircd::strlcat(buf, "<a href=");
			tab = ircd::strlcat(buf, workflow_job["html_url"]);
			tab = ircd::strlcat(buf, ">");
			tab = ircd::strlcat(buf, annote(workflow_job));
			tab = ircd::strlcat(buf, "</a>");
			tab = ircd::strlcat(buf, "​"_sv);
			return true;
		});

		tab = ircd::strlcat(buf, "</td></tr></table>");

		const auto table_event_id
		{
			m::msghtml(_webhook_room, _webhook_user, tab, alt_tab)
		};

		if(table_event_id)
		{
			m::annotate(_webhook_room, _webhook_user, table_event_id, "⭕"_sv);
			m::annotate(_webhook_room, _webhook_user, table_event_id, "🔄"_sv);
			m::annotate(_webhook_room, _webhook_user, table_event_id, "↪️"_sv);
			m::annotate(_webhook_room, _webhook_user, table_event_id, "🚮"_sv);
		}

		if(lex_cast<uint>(attempt) > 1)
		{
			const fmt::bsprintf<128> prior_alt
			{
				"job status table %s %s %u",
				github_repopath(content),
				run_id,
				lex_cast<uint>(attempt) - 1,
			};

			const auto prior_table_id
			{
				github_find_job_table(_webhook_room, _webhook_user, prior_alt)
			};

			if(prior_table_id)
				clear_reactions(_webhook_room, _webhook_user, prior_table_id);
		}
	}

	bool outputs{false};
	if(conclusion == "failure" && webhook_status_errors)
	{
		outputs = true;
		out
		<< "<br>"
		<< "<font data-mx-bg-color=\"#CC0000\" color=\"#FFFFFF\">"
		<< "&nbsp;"
		<< "&nbsp;"
		<< "<b>"
		<< flow_name
		<< "</b>"
		<< "&nbsp;"
		<< "&nbsp;"
		<< "</font>"
		<< " failed "
		<< "<a href=\""
		<< url
		<< "\">"
		<< "<b>"
		<< job_name
		<< "</b>"
		<< "</a>"
		;

		alt
		<< ' '
		<< flow_name
		<< ':'
		<< job_name
		<< ' '
		<< "failed"
		;
	}

	return outputs;
}

static void
github_react_handle(const m::event &event,
                    m::vm::eval &)
try
{
	if(!webhook_room)
		return;

	// XXX alias?
	if(json::get<"room_id"_>(event) != webhook_room)
		return;

	const m::room room
	{
		at<"room_id"_>(event)
	};

	const m::user::id user_id
	{
		at<"sender"_>(event)
	};

	const m::room::power power
	{
		room
	};

	// XXX ???
	if(power.level_user(user_id) < 50)
		return;

	const json::object relates_to
	{
		json::get<"content"_>(event).get("m.relates_to")
	};

	const json::string relates_event_id
	{
		relates_to["event_id"]
	};

	const json::string key
	{
		relates_to["key"]
	};

	const auto relates_content
	{
		m::get(relates_event_id, "content")
	};

	const json::string relates_body
	{
		json::object{relates_content}.get("body")
	};

	if(!startswith(relates_body, "job status table "))
		return;

	const auto suffix
	{
		lstrip(relates_body, "job status table ")
	};

	string_view token[3];
	ircd::tokens(suffix, ' ', token);
	const auto &[repopath, run_id, attempt] {token};
	assert(repopath);
	assert(run_id);
	if(!repopath || !run_id)
		return;

	switch(hash(key))
	{
		case hash("🚮"_sv):
			github_run_delete(repopath, run_id);
			m::redact(room, user_id, relates_event_id, "deleted");
			break;

		case hash("⭕"_sv):
			github_run_cancel(repopath, run_id);
			break;

		case hash("🔄"_sv):
			github_run_rerun_failed(repopath, run_id);
			break;

		case hash("↪️"_sv):
			github_run_rerun(repopath, run_id);
			break;
	}
}
catch(const ctx::interrupted &)
{
	throw;
}
catch(const std::exception &e)
{
	log::error
	{
		"github react handle hook :%s",
		e.what(),
	};
}

static m::hookfn<m::vm::eval &>
github_react_hook
{
	github_react_handle,
	{
		{ "_site",   "vm.effect"   },
		{ "type",    "m.reaction"  },
	}
};

bool
github_handle__check_run(std::ostream &out,
                         const json::object &content)
{
	const json::string action
	{
		content["action"]
	};

	const json::object check_run
	{
		content["check_run"]
	};

	const json::object check_suite
	{
		check_run["check_suite"]
	};

	return false;
}

bool
github_handle__check_suite(std::ostream &out,
                           const json::object &content)
{
	const json::string action
	{
		content["action"]
	};

	const json::object check_suite
	{
		content["check_suite"]
	};

	return false;
}

bool
github_handle__gollum(std::ostream &out,
                      const json::object &content)
{
	const json::array pages
	{
		content["pages"]
	};

	const auto count
	{
		size(pages)
	};

	out
	<< " to "
	<< "<b>"
	<< count
	<< "</b>"
	<< " page" << (count != 1? "s" : "")
	<< ":"
	;

	for(const json::object page : pages)
	{
		const json::string &action
		{
			page["action"]
		};

		const json::string sha
		{
			page["sha"]
		};

		out
		<< "<br />"
		<< "<b>"
		<< sha.substr(0, 8)
		<< "</b>"
		<< " " << action << " "
		<< "<a href=" << page["html_url"] << ">"
		<< "<b>"
		<< json::string(page["title"])
		<< "</b>"
		<< "</a>"
		;

		if(page["summary"] && page["summary"] != "null")
		{
			out
			<< " "
			<< "<blockquote>"
			<< "<pre>"
			;

			static const auto delim("\\r\\n");
			const json::string body(page["summary"]);
			ircd::tokens(body, delim, [&out]
			(const string_view &line)
			{
				out << line << "<br />";
			});

			out
			<< ""
			<< "</pre>"
			<< "</blockquote>"
			;
		}
	}

	return true;
}

bool
github_handle__milestone(std::ostream &out,
                         const json::object &content)
{
	const json::string &action
	{
		content["action"]
	};

	const json::object milestone
	{
		content["milestone"]
	};

	out
	<< " "
	<< action
	<< " "
	<< "<a href="
	<< milestone["html_url"]
	<< ">"
	<< "<b>"
	<< json::string(milestone["title"])
	<< "</b>"
	<< "</a>"
	<< ' '
	;

	const json::string &state
	{
		milestone["state"]
	};

	if(state == "open")
		out
	    << "<font color=\"#FFFFFF\""
	    << "data-mx-bg-color=\"#2cbe4e\">"
		;
	else if(state == "closed")
		out
	    << "<font color=\"#FFFFFF\""
	    << "data-mx-bg-color=\"#cb2431\">"
	    ;

	out
	<< "&nbsp;<b>"
	<< state
	<< "</b>&nbsp;"
	<< "</font>"
	;

	out
	<< ' '
	<< "<pre><code>"
	<< json::string(milestone["description"])
	<< "</code></pre>"
	;

	out
	<< ' '
	<< "Issues"
	<< ' '
	<< "open"
	<< "&nbsp;"
    << "<font color=\"#2cbe4e\">"
	<< "<b>"
	<< milestone["open_issues"]
	<< "</b>"
	<< "</font>"
	<< ' '
	<< "closed"
	<< ' '
    << "<font color=\"#cb2431\">"
	<< "<b>"
	<< milestone["closed_issues"]
	<< "</b>"
	<< "</font>"
	;

	return true;
}

bool
github_handle__push(std::ostream &out,
                    const json::object &content)
{
	const bool created
	{
		content.get("created", false)
	};

	const bool deleted
	{
		content.get("deleted", false)
	};

	const bool forced
	{
		content.get("forced", false)
	};

	const json::array commits
	{
		content["commits"]
	};

	const auto count
	{
		size(commits)
	};

	if(!count && deleted)
	{
		out << " <font color=\"#FF0000\">";
		if(content["ref"])
			out << " " << json::string(content["ref"]);

		out << " deleted</font>";
		return true;
	}

	if(!count && !webhook_status_verbose)
		return false;

	if(content["ref"])
	{
		const json::string ref(content["ref"]);
		out << " "
		    << " "
		    << token_last(ref, '/');
	}

	out << " <a href=\"" << json::string(content["compare"]) << "\">"
	    << "<b>" << count << " commits</b>"
	    << "</a>";

	if(content["forced"] == "true")
		out << " (rebase)";

	out << "<pre>";
	for(ssize_t i(count - 1); i >= 0; --i)
	{
		const json::object &commit(commits.at(i));
		const json::string url(commit["url"]);
		const json::string id(commit["id"]);
		const auto sid(id.substr(0, 8));
		out << " <a href=\"" << url << "\">"
		    << "<b>" << sid << "</b>"
		    << "</a>";

		const json::object author(commit["author"]);
		out << " <b>"
		    << json::string(author["name"])
		    << "</b>"
		    ;

		const json::object committer(commit["committer"]);
		if(committer["email"] != author["email"])
			out << " via <b>"
			    << json::string(committer["name"])
			    << "</b>"
			    ;

		const json::string message(commit["message"]);
		const auto summary
		{
			split(message, "\\n").first
		};

		out << " "
		    << summary
		    ;

		out << "\n";
	}

	out << "</pre>";
	return true;
}

bool
github_handle__pull_request(std::ostream &out,
                            const json::object &content)
{
	const json::object pr
	{
		content["pull_request"]
	};

	if(pr["merged"] != "true")
		out << " "
		    << "<b>"
		    << json::string(content["action"])
		    << "</b>"
		    ;

	if(pr["title"])
		out << " "
		    << "<a href="
		    << pr["html_url"]
		    << ">"
		    << json::string(pr["title"])
		    << "</a>"
		    << "&nbsp;"
		    << ' '
		    ;

	const json::object head
	{
		pr["head"]
	};

	const json::object base
	{
		pr["base"]
	};

	for(const json::object label : json::array(pr["labels"]))
	{
		out << "&nbsp;";
		out << "<font color=\"#FFFFFF\""
		    << "data-mx-bg-color=\"#"
		    << json::string(label["color"])
		    << "\">";

		out << "<b>";
		out << "&nbsp;";
			out << json::string(label["name"]);
		out << "&nbsp;";
		out << "</b>";

		out << "</font>";
	}

	if(pr["merged"] == "true")
		out << ' '
		    << "<font color=\"#FFFFFF\""
		    << "data-mx-bg-color=\"#6f42c1\">"
		    << "&nbsp;<b>"
		    << "merged"
		    << "</b>&nbsp;"
		    << "</font>"
		    ;

	if(pr.has("merged_by") && pr["merged_by"] != "null")
	{
		const json::object merged_by{pr["merged_by"]};
		out << " "
		    << "by "
		    << "<a href=\""
		    << json::string(merged_by["html_url"])
		    << "\">"
		    << json::string(merged_by["login"])
		    << "</a>"
		    ;
	}

	const json::string &body
	{
		pr["body"]
	};

	if(!empty(body))
		out << ' '
		    << "<pre>"
		    << body
		    << "</pre>"
		    << ' '
		    ;
	else
		out << ' '
		    << "<br />"
		    ;

	if(pr.has("commits"))
		out << ' '
		    << "&nbsp;"
		    << "<b>"
		    << pr["commits"]
		    << ' '
		    << "<a href="
		    << github_url(pr["commits_url"])
		    << ">"
		    << "commits"
		    << "</a>"
		    << "</b>"
		    ;

	if(pr.has("comments"))
		out << ' '
		    << "&nbsp;"
		    << "<b>"
		    << pr["comments"]
		    << ' '
		    << "<a href="
		    << github_url(pr["comments_url"])
		    << ">"
		    << "comments"
		    << "</a>"
		    << "</b>"
		    ;

	if(pr.has("changed_files"))
		out << ' '
		    << "&nbsp;"
		    << "<b>"
		    << pr["changed_files"]
		    << ' '
		    << "<a href=\""
		    << json::string(pr["html_url"])
		    << "/files"
		    << "\">"
		    << "files"
		    << "</a>"
		    << "</b>"
		    ;

	if(pr.has("additions"))
		out << ' '
		    << "&nbsp;"
		    << "<b>"
		    << "<font color=\"#33CC33\">"
		    << "++"
		    << "</font>"
		    << pr["additions"]
		    << "</b>"
		    ;

	if(pr.has("deletions"))
		out << ' '
		    << "<b>"
		    << "<font color=\"#CC0000\">"
		    << "--"
		    << "</font>"
		    << pr["deletions"]
		    << "</b>"
		    ;

	if(pr["merged"] == "false") switch(hash(pr["mergeable"]))
	{
		default:
		case "null"_:
			break;

		case "true"_:
			out << ' '
			    << "<font color=\"#FFFFFF\""
			    << "data-mx-bg-color=\"#03B381\">"
			    << "<b>"
			    << "&nbsp;"
			    << "NO CONFLICTS"
			    << "&nbsp;"
			    << "</b>"
			    << "</font>"
			    ;
			break;

		case "false"_:
			out << ' '
			    << "<font color=\"#FFFFFF\""
			    << "data-mx-bg-color=\"#CC0000\">"
			    << "<b>"
			    << "&nbsp;"
			    << "MERGE CONFLICT"
			    << "&nbsp;"
			    << "</b>"
			    << "</font>"
			    ;
			break;
	}

	return true;
}

bool
github_handle__issues(std::ostream &out,
                      const json::object &content)
{
	const json::string &action
	{
		content["action"]
	};

	out << " "
	    << "<b>"
	    << action
	    << "</b>"
	    ;

	const json::object issue
	{
		content["issue"]
	};

	switch(hash(action))
	{
		case "assigned"_:
		case "unassigned"_:
		{
			const json::object assignee
			{
				content["assignee"]
			};

			out << " "
			    << "<a href=\""
			    << json::string(assignee["html_url"])
			    << "\">"
			    << json::string(assignee["login"])
			    << "</a>"
			    ;

			break;
		}
	}

	out << " "
	    << "<a href=\""
	    << json::string(issue["html_url"])
	    << "\">"
	    << "<b><u>"
	    << json::string(issue["title"])
	    << "</u></b>"
	    << "</a>"
	    ;

	for(const json::object label : json::array(issue["labels"]))
	{
		out << "&nbsp;";
		out << "<font color=\"#FFFFFF\""
		    << "data-mx-bg-color=\"#"
		    << json::string(label["color"])
		    << "\">";

		out << "<b>";
		out << "&nbsp;";
			out << json::string(label["name"]);
		out << "&nbsp;";
		out << "</b>";

		out << "</font>";
	}

	if(action == "opened")
	{
		out << " "
		    << "<blockquote>"
		    << "<pre>"
		    ;

		static const auto delim("\\r\\n");
		const json::string body(issue["body"]);
		ircd::tokens(body, delim, [&out]
		(const string_view &line)
		{
			out << line << "<br />";
		});

		out << ""
		    << "</pre>"
		    << "</blockquote>"
		    ;
	}
	else if(action == "labeled")
	{
		// quiet these messages for now until we can figure out how to reduce
		// noise around issue opens.
		return false;

		const json::object label
		{
			content["label"]
		};

		out << "<ul>";
		out << "<li>added: ";
		out << "<font color=\"#FFFFFF\""
		    << "data-mx-bg-color=\"#"
		    << json::string(label["color"])
		    << "\">";

		out << "<b>";
		out << "&nbsp;";
		out << json::string(label["name"]);
		out << "&nbsp;";
		out << "</b>";

		out << "</font>";
		out << "</li>";
		out << "</ul>";
	}
	else if(action == "unlabeled")
	{
		// quiet these messages for now until we can figure out how to reduce
		// noise around issue opens.
		return false;

		const json::object label
		{
			content["label"]
		};

		out << "<ul>";
		out << "<li>removed: ";
		out << "<font color=\"#FFFFFF\""
		    << "data-mx-bg-color=\"#"
		    << json::string(label["color"])
		    << "\">";

		out << "<b>";
		out << "&nbsp;";
		out << json::string(label["name"]);
		out << "&nbsp;";
		out << "</b>";

		out << "</font>";
		out << "</li>";
		out << "</ul>";
	}
	else if(action == "milestoned")
	{
		const json::object &milestone
		{
			content["milestone"]
		};

		out
		<< "<ul>"
		<< "<li>"
		<< "<a href="
		<< milestone["html_url"]
		<< ">"
		<< json::string(milestone["title"])
		<< "</a>"
		<< ' '
		;

		const json::string &state{milestone["state"]};
		if(state == "open")
			out
			<< "<font color=\"#FFFFFF\""
			<< "data-mx-bg-color=\"#2cbe4e\">"
			;
		else if(state == "closed")
			out
			<< "<font color=\"#FFFFFF\""
			<< "data-mx-bg-color=\"#cb2431\">"
			;

		out
		<< "&nbsp;<b>"
		<< state
		<< "</b>&nbsp;"
		<< "</font>"
		;

		out
		<< ' '
		<< "&nbsp;"
		<< "Issues"
		<< ' '
		<< "<font color=\"#2cbe4e\">"
		<< "<b>"
		<< milestone["open_issues"]
		<< "</b>"
		<< "</font>"
		<< ' '
		<< "open"
		<< ' '
		<< "<font color=\"#cb2431\">"
		<< "<b>"
		<< milestone["closed_issues"]
		<< "</b>"
		<< "</font>"
		<< ' '
		<< "closed"
		<< "</li>"
		<< "</ul>"
		;
	}

	return true;
}

bool
github_handle__issue_comment(std::ostream &out,
                             const json::object &content)
{
	const json::object issue
	{
		content["issue"]
	};

	const json::object comment
	{
		content["comment"]
	};

	const json::string action
	{
		content["action"]
	};

	out << " <b>";
	switch(hash(action))
	{
		case "created"_:
			out << "commented on";
			break;

		default:
			out << action;
			break;
	}
	out << "</b>";

	out << " "
	    << "<a href=\""
	    << json::string(issue["html_url"])
	    << "\">"
	    << "<b><u>"
	    << json::string(issue["title"])
	    << "</u></b>"
	    << "</a>"
	    ;

	if(action == "created")
	{
		out << " "
		    << "<blockquote>"
		    << "<pre>"
		    ;

		static const auto delim("\\r\\n");
		const json::string body(comment["body"]);
		ircd::tokens(body, delim, [&out]
		(const string_view &line)
		{
			out << line << "<br />";
		});

		out << ""
		    << "</pre>"
		    << "</blockquote>"
		    ;
	}

	for(const json::object label : json::array(issue["labels"]))
		out
		<< "<font color=\"#FFFFFF\""
		<< "data-mx-bg-color=\"#"
		<< json::string(label["color"])
		<< "\">"
		<< "<b>"
		<< "&nbsp;"
		<< json::string(label["name"])
		<< "&nbsp;"
		<< "</b>"
		<< "</font>"
		<< "&nbsp;"
		;

	return true;
}

bool
github_handle__commit_comment(std::ostream &out,
                              const json::object &content)
{
	const json::object comment
	{
		content["comment"]
	};

	const json::string action
	{
		content["action"]
	};

	const json::string commit
	{
		comment["commit_id"]
	};

	const json::string assoc
	{
		comment["author_association"]
	};

	char assoc_buf[32];
	if(assoc && assoc != "NONE")
		out
		<< " ["
		<< tolower(assoc_buf, assoc)
		<< "]"
		;

	out << " <b>";
	switch(hash(action))
	{
		case "created"_:
			out << "commented on";
			break;

		default:
			out << action;
			break;
	}
	out << "</b>";

	out << " "
	    << "<a href=\""
	    << json::string(comment["html_url"])
	    << "\">"
	    << "<b><u>"
	    << trunc(commit, 8)
	    << "</u></b>"
	    << "</a>"
	    ;

	if(action == "created")
	{
		out << " "
		    << "<blockquote>"
		    ;

		const json::string body
		{
			comment["body"]
		};

		static const auto delim("\\r\\n");
		ircd::tokens(body, delim, [&out]
		(const string_view &line)
		{
			out << line << "<br />";
		});

		out << ""
		    << "</blockquote>"
		    ;
	}

	return true;
}

bool
github_handle__label(std::ostream &out,
                     const json::object &content)
{
	const json::string &action
	{
		content["action"]
	};

	out << " "
	    << "<b>"
	    << action
	    << "</b>"
	    ;

	const json::object &label
	{
		content["label"]
	};

	out << "<ul>";
	out << "<li>";
	out << "<font color=\"#FFFFFF\""
	    << "data-mx-bg-color=\"#"
	    << json::string(label["color"])
	    << "\">";

	out << "<b>";
	out << " &nbsp; ";
	out << json::string(label["name"]);
	out << " &nbsp; ";
	out << "</b>";
	out << "</font>";
	out << "</li>";
	out << "</ul>";

	if(action == "edited")
	{
		const json::object &changes
		{
			content["changes"]
		};

		const json::object &color_obj
		{
			changes["color"]
		};

		const json::string &color
		{
			empty(color_obj)?
				label["color"]:
				color_obj["from"]
		};

		const json::object &name_obj
		{
			changes["name"]
		};

		const json::string &name
		{
			empty(name_obj)?
				label["name"]:
				name_obj["from"]
		};

		out << "from: ";
		out << "<ul>";
		out << "<li>";
		out << "<font color=\"#FFFFFF\""
		    << "data-mx-bg-color=\"#"
		    << color
		    << "\">";

		out << "<b>";
		out << " &nbsp; ";
		out << name;
		out << " &nbsp; ";
		out << "</b>";
		out << "</font>";
		out << "</li>";
		out << "</ul>";
	}

	return true;
}

bool
github_handle__organization(std::ostream &out,
                            const json::object &content)
{
	const json::string &action
	{
		content["action"]
	};

	const auto &action_words
	{
		split(action, '_')
	};

	out << " " << "<b>";

	if(action_words.second)
		out
		<< split(action, '_').second
		<< " ";

	out << action_words.first << "</b>";

	if(action == "member_added")
	{
		const json::object &membership
		{
			content["membership"]
		};

		const json::object &user
		{
			membership["user"]
		};

		out << " "
		    << "<a href=" << user["html_url"] << ">"
		    << json::string(user["login"])
		    << "</a>"
		    ;

		out << " with role "
		    << json::string(membership["role"])
		    ;
	}
	else if(action == "member_removed")
	{
		const json::object &membership
		{
			content["membership"]
		};

		const json::object &user
		{
			membership["user"]
		};

		out << " "
		    << "<a href=" << user["html_url"] << ">"
		    << json::string(user["login"])
		    << "</a>"
		    ;
	}
	else if(action == "member_invited")
	{
		const json::object &invitation
		{
			content["invitation"]
		};

		const json::object &user
		{
			invitation["user"]
		};

		out << " "
		    << "<a href=" << user["html_url"] << ">"
		    << json::string(user["login"])
		    << "</a>"
		    ;
	}

	return true;
}

bool
github_handle__status(std::ostream &out,
                      const json::object &content)
{
	const m::user::id::buf _webhook_user
	{
		string_view{webhook_user}, my_host()
	};

	const auto _webhook_room_id
	{
		m::room_id(string_view(webhook_room))
	};

	const m::room _webhook_room
	{
		_webhook_room_id
	};

	const json::string &state
	{
		content["state"]
	};

	const json::string &commit_hash
	{
		content["sha"]
	};

	const auto push_event_id
	{
		github_find_push_event_id(_webhook_room, _webhook_user, commit_hash)
	};

	if(push_event_id) switch(hash(state))
	{
		case "error"_:
			m::annotate(_webhook_room, _webhook_user, push_event_id, "⭕");
			break;

		case "failure"_:
			m::annotate(_webhook_room, _webhook_user, push_event_id, "🔴");
			break;

		case "pending"_:
			m::annotate(_webhook_room, _webhook_user, push_event_id, "🟡");
			break;

		case "success"_:
			m::annotate(_webhook_room, _webhook_user, push_event_id, "🟢");
			break;
	}

	if(!webhook_status_verbose) switch(hash(state))
	{
		case "error"_:
			return false;

		case "failure"_:
			break;

		case "pending"_:
			return false;

		case "success"_:
			return false;

		default:
			return false;
	}

	const json::string &description
	{
		content["description"]
	};

	const string_view &url
	{
		content["target_url"]
	};

	if(state == "success")
		out << " "
		    << "<font data-mx-bg-color=\"#03B381\">"
		    ;

	else if(state == "failure")
		out << " "
		    << "<font data-mx-bg-color=\"#CC0000\">"
		    ;

	else if(state == "error")
		out << " "
		    << "<font data-mx-bg-color=\"#280000\">"
		    ;

	out << "&nbsp;"
	    << "<a href="
	    << url
	    << ">";

	out << ""
	    << "<font color=\"#FFFFFF\">"
	    << "<b>"
	    << description
	    << "</b>"
	    << "</font>"
	    << "</a>"
	    << "&nbsp;"
	    << "</font>"
	    ;

	return true;
}

bool
github_handle__watch(std::ostream &out,
                     const json::object &content)
{
	const json::string &action
	{
		content["action"]
	};

	if(action != "started")
		return false;

	// There appears to be no way to distinguish between a genuine watch
	// button click and just a star; the watch event is sent for both.
	// Returning false just disables this event so there's no double-message.
	return false;
}

bool
github_handle__star(std::ostream &out,
                    const json::object &content)
{
	const json::string &action
	{
		content["action"]
	};

	if(action != "created")
		return false;

	return true;
}

bool
github_handle__repository(std::ostream &out,
                          const json::object &content)
{
	const json::string &action
	{
		content["action"]
	};

	out << ' ' << action;
	out << "<pre><code>"
	    << json::string(content["description"])
	    << "</code></pre>";

	return true;
}

bool
github_handle__create(std::ostream &out,
                      const json::object &content)
{
	const json::string &ref
	{
		content["ref"]
	};

	const json::string &ref_type
	{
		content["ref_type"]
	};

	out
	<< ' '
	<< ref_type
	<< ' '
	<< "<b>"
	<< ref
	<< "</b>"
	;

	if(ref_type == "tag")
		out << ' ' << "🎉";

	return true;
}

bool
github_handle__delete(std::ostream &out,
                      const json::object &content)
{
	const json::string &ref
	{
		content["ref"]
	};

	const json::string &ref_type
	{
		content["ref_type"]
	};

	out
	<< ' '
	<< ref_type
	<< ' '
	<< "<b>"
	<< ref
	<< "</b>"
	;

	return true;
}

bool
github_handle__ping(std::ostream &out,
                    const json::object &content)
{
	out << "<pre><code>"
	    << json::string(content["zen"])
	    << "</code></pre>";

	return true;
}

// Find the message resulting from the push and react with the status.
ircd::m::event::id::buf
github_find_push_event_id(const m::room &room,
                          const m::user::id &user_id,
                          const string_view &commit_hash)
{
	const auto type_match
	{
		[](const string_view &type) noexcept
		{
			return type == "m.room.message";
		}
	};

	const auto user_match
	{
		[&user_id](const string_view &sender) noexcept
		{
			return sender && sender == user_id;
		}
	};

	const auto content_match
	{
		[&commit_hash](const json::object &content)
		{
			const json::string &body
			{
				content["body"]
			};

			return has(body, "push") && has(body, commit_hash);
		}
	};

	// Limit the search to a maximum of recent messages from the
	// webhook user and total messages so we don't run out of control
	// and scan the whole room history.
	int lim[2] { 768, 384 };
	m::room::events it{room};
	for(; it && lim[0] > 0 && lim[1] > 0; --it, --lim[0])
	{
		if(!m::query(std::nothrow, it.event_idx(), "sender", user_match))
			continue;

		--lim[1];
		if(!m::query(std::nothrow, it.event_idx(), "type", type_match))
			continue;

		if(!m::query(std::nothrow, it.event_idx(), "content", content_match))
			continue;

		return m::event_id(std::nothrow, it.event_idx());
	}

	return {};
}

std::pair<json::string, json::string>
github_find_repo(const json::object &content)
{
	const json::object repository
	{
		content["repository"]
	};

	if(!empty(repository))
		return
		{
		    repository["full_name"], repository["html_url"]
		};

	const json::object organization
	{
		content["organization"]
	};

	return
	{
	    organization["login"], organization["url"]
	};
}

/// Researched from yestifico bot
std::pair<json::string, json::string>
github_find_party(const json::object &content)
{
	const json::object pull_request
	{
		content["pull_request"]
	};

	const json::object user
	{
		pull_request["user"]
	};

	if(!empty(user))
		return
		{
			user["login"], user["html_url"]
		};

	const json::object sender
	{
		content["sender"]
	};

	return
	{
		sender["login"], sender["html_url"]
	};
}

/// Researched from yestifico bot
json::string
github_find_issue_number(const json::object &content)
{
	const json::object issue(content["issue"]);
	if(!empty(issue))
		return issue["number"];

	if(content["number"])
		return content["number"];

	return {};
}

/// Researched from yestifico bot
json::string
github_find_commit_hash(const json::object &content)
{
	if(content["sha"])
		return content["sha"];

	const json::object commit(content["commit"]);
	if(!empty(commit))
		return commit["sha"];

	const json::object head(content["head"]);
	if(!empty(head))
		return head["commit"];

	const json::object head_commit(content["head_commit"]);
	if(!empty(head_commit))
		return head_commit["id"];

	const json::object comment(content["comment"]);
	if(!empty(comment))
		return comment["commit_id"];

	if(content["commit"])
		return content["commit"];

	const json::object pr{content["pull_request"]};
	const json::object prhead{pr["head"]};
	if(prhead["sha"])
		return prhead["sha"];

	const json::object workflow_run{content["workflow_run"]};
	if(workflow_run["head_sha"])
		return workflow_run["head_sha"];

	const json::object workflow_job{content["workflow_job"]};
	if(workflow_job["head_sha"])
		return workflow_job["head_sha"];

	const json::object check_run{content["check_run"]};
	if(check_run["head_sha"])
		return check_run["head_sha"];

	const json::object check_suite{content["check_suite"]};
	if(check_suite["head_sha"])
		return check_suite["head_sha"];

	return {};
}

std::string
github_url(const json::string &url)
{
	std::string base("https://");
	return base + std::string(lstrip(url, "https://api."));
}

bool
github_validate(const string_view &sigheader,
                const const_buffer &content,
                const string_view &secret)
try
{
	const auto sig
	{
		split(sigheader, "=")
	};

	crh::hmac hmac
	{
		sig.first, secret
	};

	hmac.update(content);

	char ubuf[64], abuf[sizeof(ubuf) * 2];
	if(unlikely(sizeof(ubuf) < hmac.length()))
		throw ircd::panic
		{
			"HMAC algorithm '%s' digest exceeds buffer size.",
			sig.first
		};

	const const_buffer hmac_bin
	{
		hmac.finalize(ubuf)
	};

	static_assert(sizeof(abuf) >= sizeof(ubuf) * 2);
	const string_view hmac_hex
	{
		u2a(abuf, hmac_bin)
	};

	return hmac_hex == sig.second;
}
catch(const crh::error &e)
{
	throw http::error
	{
		http::NOT_IMPLEMENTED, "The signature algorithm is not supported.",
	};
}

///////////////////////////////////////////////////////////////////////////////
//
// appveyor
//

void
appveyor_handle(client &client,
                const resource::request &request)
{
	const http::headers &headers
	{
		request.head.headers
	};
}

///////////////////////////////////////////////////////////////////////////////
//
// dockerhub
//

static bool
dockerhub_handle_push(std::ostream &out,
                      std::ostream &alt,
                      client &client,
                      const resource::request &request);

void
dockerhub_handle(client &client,
                 const resource::request &request)
try
{
	if(!string_view(webhook_room))
		return;

	if(!string_view(webhook_user))
		return;

	const auto callback_url
	{
		request["callback_url"]
	};

	const unique_buffer<mutable_buffer> buf[2]
	{
		{ 48_KiB },
		{ 4_KiB  },
	};

	std::stringstream out, alt;
	pubsetbuf(out, buf[0]);
	pubsetbuf(alt, buf[1]);

	bool output {true};
	if(request.has("push_data"))
		output = dockerhub_handle_push(out, alt, client, request);

	const auto room_id
	{
		m::room_id(string_view(webhook_room))
	};

	const m::user::id::buf user_id
	{
		string_view(webhook_user), my_host()
	};

	const auto msg
	{
		view(out, buf[0])
	};

	const auto alt_msg
	{
		view(alt, buf[1])
	};

	const auto evid
	{
		output?
			m::msghtml(room_id, user_id, msg, alt_msg, "m.notice"):
			m::event::id::buf{}
	};

	log::info
	{
		"Webhook '%s' delivered to %s %s",
		"push"_sv,
		string_view{room_id},
		string_view{evid},
	};
}
catch(const std::exception &e)
{
	log::error
	{
		"dockerhub webhook :%s",
		e.what(),
	};

	throw;
}

bool
dockerhub_handle_push(std::ostream &out,
                      std::ostream &alt,
                      client &client,
                      const resource::request &request)
{
	const json::object push_data
	{
		request["push_data"]
	};

	const json::object repository
	{
		request["repository"]
	};

	const json::string pusher
	{
		push_data["pusher"]
	};

	const string_view pushed_at
	{
		push_data["pushed_at"]
	};

	const json::string tag
	{
		push_data["tag"]
	};

	const json::array images
	{
		push_data["images"]
	};

	out
	<< "<a href=" << repository["repo_url"] << ">"
	<< json::string(repository["repo_name"])
	<< "</a> push by <b>"
	<< pusher
	<< "</b> to <b>"
	<< tag
	<< "</b>"
	;

	alt
	<< json::string(repository["repo_name"])
	<< " push by "
	<< pusher
	<< " to "
	<< tag
	;

	return bool(webhook_status_verbose);
}
