mirror of
https://github.com/matrix-construct/construct
synced 2024-12-30 17:34:04 +01:00
1961 lines
33 KiB
C++
1961 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 at %s",
|
|
type,
|
|
github_find_party(request.content).first,
|
|
github_find_repo(request.content).first,
|
|
github_find_commit_hash(request.content),
|
|
};
|
|
|
|
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
|
|
<< " <b>"
|
|
<< state
|
|
<< "</b> "
|
|
<< "</font>"
|
|
;
|
|
|
|
out
|
|
<< ' '
|
|
<< "<pre><code>"
|
|
<< json::string(milestone["description"])
|
|
<< "</code></pre>"
|
|
;
|
|
|
|
out
|
|
<< ' '
|
|
<< "Issues"
|
|
<< ' '
|
|
<< "open"
|
|
<< " "
|
|
<< "<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>"
|
|
<< " "
|
|
<< ' '
|
|
;
|
|
|
|
const json::object head
|
|
{
|
|
pr["head"]
|
|
};
|
|
|
|
const json::object base
|
|
{
|
|
pr["base"]
|
|
};
|
|
|
|
for(const json::object label : json::array(pr["labels"]))
|
|
{
|
|
out << " ";
|
|
out << "<font color=\"#FFFFFF\""
|
|
<< "data-mx-bg-color=\"#"
|
|
<< json::string(label["color"])
|
|
<< "\">";
|
|
|
|
out << "<b>";
|
|
out << " ";
|
|
out << json::string(label["name"]);
|
|
out << " ";
|
|
out << "</b>";
|
|
|
|
out << "</font>";
|
|
}
|
|
|
|
if(pr["merged"] == "true")
|
|
out << ' '
|
|
<< "<font color=\"#FFFFFF\""
|
|
<< "data-mx-bg-color=\"#6f42c1\">"
|
|
<< " <b>"
|
|
<< "merged"
|
|
<< "</b> "
|
|
<< "</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 << ' '
|
|
<< " "
|
|
<< "<b>"
|
|
<< pr["commits"]
|
|
<< ' '
|
|
<< "<a href="
|
|
<< github_url(pr["commits_url"])
|
|
<< ">"
|
|
<< "commits"
|
|
<< "</a>"
|
|
<< "</b>"
|
|
;
|
|
|
|
if(pr.has("comments"))
|
|
out << ' '
|
|
<< " "
|
|
<< "<b>"
|
|
<< pr["comments"]
|
|
<< ' '
|
|
<< "<a href="
|
|
<< github_url(pr["comments_url"])
|
|
<< ">"
|
|
<< "comments"
|
|
<< "</a>"
|
|
<< "</b>"
|
|
;
|
|
|
|
if(pr.has("changed_files"))
|
|
out << ' '
|
|
<< " "
|
|
<< "<b>"
|
|
<< pr["changed_files"]
|
|
<< ' '
|
|
<< "<a href=\""
|
|
<< json::string(pr["html_url"])
|
|
<< "/files"
|
|
<< "\">"
|
|
<< "files"
|
|
<< "</a>"
|
|
<< "</b>"
|
|
;
|
|
|
|
if(pr.has("additions"))
|
|
out << ' '
|
|
<< " "
|
|
<< "<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>"
|
|
<< " "
|
|
<< "NO CONFLICTS"
|
|
<< " "
|
|
<< "</b>"
|
|
<< "</font>"
|
|
;
|
|
break;
|
|
|
|
case "false"_:
|
|
out << ' '
|
|
<< "<font color=\"#FFFFFF\""
|
|
<< "data-mx-bg-color=\"#CC0000\">"
|
|
<< "<b>"
|
|
<< " "
|
|
<< "MERGE CONFLICT"
|
|
<< " "
|
|
<< "</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 << " ";
|
|
out << "<font color=\"#FFFFFF\""
|
|
<< "data-mx-bg-color=\"#"
|
|
<< json::string(label["color"])
|
|
<< "\">";
|
|
|
|
out << "<b>";
|
|
out << " ";
|
|
out << json::string(label["name"]);
|
|
out << " ";
|
|
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 << " ";
|
|
out << json::string(label["name"]);
|
|
out << " ";
|
|
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 << " ";
|
|
out << json::string(label["name"]);
|
|
out << " ";
|
|
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
|
|
<< " <b>"
|
|
<< state
|
|
<< "</b> "
|
|
<< "</font>"
|
|
;
|
|
|
|
out
|
|
<< ' '
|
|
<< " "
|
|
<< "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>"
|
|
<< " "
|
|
<< json::string(label["name"])
|
|
<< " "
|
|
<< "</b>"
|
|
<< "</font>"
|
|
<< " "
|
|
;
|
|
|
|
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 << " ";
|
|
out << json::string(label["name"]);
|
|
out << " ";
|
|
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 << " ";
|
|
out << name;
|
|
out << " ";
|
|
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 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] { 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 << " "
|
|
<< "<a href="
|
|
<< url
|
|
<< ">";
|
|
|
|
out << ""
|
|
<< "<font color=\"#FFFFFF\">"
|
|
<< "<b>"
|
|
<< description
|
|
<< "</b>"
|
|
<< "</font>"
|
|
<< "</a>"
|
|
<< " "
|
|
<< "</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>"
|
|
;
|
|
}
|