construct/modules/web_hook.cc

1962 lines
33 KiB
C++

// Matrix Construct
//
// Copyright (C) Matrix Construct Developers, Authors & Contributors
// Copyright (C) 2016-2018 Jason Volk <jason@zemos.net>
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice is present in all copies. The
// full license for this software is available in the LICENSE file.
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 },
};
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 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
{
48_KiB
};
std::stringstream out;
pubsetbuf(out, buf);
github_heading(out, type, 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):
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 fmt::bsprintf<512> alt_msg
{
"%s by %s to %s",
type,
github_find_party(request.content).first,
github_find_repo(request.content).first,
};
const auto evid
{
m::msghtml(room_id, user_id, view(out, buf), alt_msg, "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)
};
if(issue_number)
out << " <b>#" << issue_number << "</b>";
else
out << " " << type;
const auto party
{
github_find_party(content)
};
out << " by "
<< "<a href=\""
<< party.second
<< "\">"
<< party.first
<< "</a>";
return out;
}
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><code>";
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 << "<br />";
}
out << "</code></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"]
};
// Find the message resulting from the push and react with the status.
m::event::id::buf push_event_id;
{
const json::string &commit_hash
{
content["sha"]
};
m::room::events it
{
_webhook_room
};
static const auto type_match
{
[](const string_view &type) noexcept
{
return type == "m.room.message";
}
};
const auto user_match
{
[&_webhook_user](const string_view &sender) noexcept
{
return sender && sender == _webhook_user;
}
};
const auto content_match
{
[&commit_hash](const json::object &content)
{
const json::string &body
{
content["body"]
};
return 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] { 512, 32 };
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;
push_event_id = m::event_id(std::nothrow, it.event_idx());
break;
}
}
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;
}
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"];
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 void
dockerhub_handle_push(std::ostream &out,
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
{
48_KiB
};
std::stringstream out;
pubsetbuf(out, buf);
if(request.has("push_data"))
dockerhub_handle_push(out, 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)
};
const auto evid
{
m::msghtml(room_id, user_id, msg, "dockerhub shot", "m.notice")
};
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;
}
void
dockerhub_handle_push(std::ostream &out,
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>"
;
}