mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2025-12-25 00:51:55 +00:00
Some rescaling since the stakes are 10x higher now:
Fix: +3
Tweak: -2
Performance: +12
Priority: High: +15
Priority: Critical: +20
Feedback: +2
Code Improvement: +2
The intention with the formula tweak is for the highest absolute value of the label set (favoring the negative) to be applied.
1028 lines
34 KiB
PHP
1028 lines
34 KiB
PHP
<?php
|
|
/*
|
|
* Github webhook In-game PR Announcer and Changelog Generator for /tg/Station13
|
|
* Author: MrStonedOne
|
|
* For documentation on the changelog generator see https://tgstation13.org/phpBB/viewtopic.php?f=5&t=5157
|
|
* To hide prs from being announced in game, place a [s] in front of the title
|
|
* All runtime errors are echo'ed to the webhook's logs in github
|
|
* Events to be sent via GitHub webhook: Pull Requests, Pushes
|
|
* Any other Event will result in a 404 returned to the webhook.
|
|
*/
|
|
|
|
/**CREDITS:
|
|
* GitHub webhook handler template.
|
|
*
|
|
* @see https://developer.github.com/webhooks/
|
|
* @author Miloslav Hula (https://github.com/milo)
|
|
*/
|
|
|
|
define('S_LINK_EMBED', 1<<0);
|
|
define('S_MENTIONS', 1<<1);
|
|
define('S_MARKDOWN', 1<<2);
|
|
define('S_HTML_COMMENTS', 1<<3);
|
|
|
|
define('F_UNVALIDATED_USER', 1<<0);
|
|
define('F_SECRET_PR', 1<<1);
|
|
|
|
//CONFIGS ARE IN SECRET.PHP, THESE ARE JUST DEFAULTS!
|
|
|
|
$hookSecret = '08ajh0qj93209qj90jfq932j32r';
|
|
$apiKey = '209ab8d879c0f987d06a09b9d879c0f987d06a09b9d8787d0a089c';
|
|
$repoOwnerAndName = "tgstation/tgstation";
|
|
$servers = array();
|
|
$enable_live_tracking = true;
|
|
$path_to_script = 'tools/WebhookProcessor/github_webhook_processor.php';
|
|
$tracked_branch = "master";
|
|
$trackPRBalance = true;
|
|
$prBalanceJson = '';
|
|
$startingPRBalance = 30;
|
|
$maintainer_team_id = 133041;
|
|
$validation = "org";
|
|
$validation_count = 1;
|
|
$tracked_branch = 'master';
|
|
$require_changelogs = false;
|
|
$discordWebHooks = array();
|
|
|
|
require_once 'secret.php';
|
|
|
|
//CONFIG END
|
|
function log_error($msg) {
|
|
echo htmlSpecialChars($msg);
|
|
file_put_contents('htwebhookerror.log', '['.date(DATE_ATOM).'] '.$msg.PHP_EOL, FILE_APPEND);
|
|
}
|
|
set_error_handler(function($severity, $message, $file, $line) {
|
|
throw new \ErrorException($message, 0, $severity, $file, $line);
|
|
});
|
|
set_exception_handler(function($e) {
|
|
header('HTTP/1.1 500 Internal Server Error');
|
|
log_error('Error on line ' . $e->getLine() . ': ' . $e->getMessage());
|
|
die();
|
|
});
|
|
$rawPost = NULL;
|
|
if (!$hookSecret || $hookSecret == '08ajh0qj93209qj90jfq932j32r')
|
|
throw new \Exception("Hook secret is required and can not be default");
|
|
if (!isset($_SERVER['HTTP_X_HUB_SIGNATURE'])) {
|
|
throw new \Exception("HTTP header 'X-Hub-Signature' is missing.");
|
|
} elseif (!extension_loaded('hash')) {
|
|
throw new \Exception("Missing 'hash' extension to check the secret code validity.");
|
|
}
|
|
list($algo, $hash) = explode('=', $_SERVER['HTTP_X_HUB_SIGNATURE'], 2) + array('', '');
|
|
if (!in_array($algo, hash_algos(), TRUE)) {
|
|
throw new \Exception("Hash algorithm '$algo' is not supported.");
|
|
}
|
|
$rawPost = file_get_contents('php://input');
|
|
if ($hash !== hash_hmac($algo, $rawPost, $hookSecret)) {
|
|
throw new \Exception('Hook secret does not match.');
|
|
}
|
|
|
|
$contenttype = null;
|
|
//apache and nginx/fastcgi/phpfpm call this two different things.
|
|
if (!isset($_SERVER['HTTP_CONTENT_TYPE'])) {
|
|
if (!isset($_SERVER['CONTENT_TYPE']))
|
|
throw new \Exception("Missing HTTP 'Content-Type' header.");
|
|
else
|
|
$contenttype = $_SERVER['CONTENT_TYPE'];
|
|
} else {
|
|
$contenttype = $_SERVER['HTTP_CONTENT_TYPE'];
|
|
}
|
|
if (!isset($_SERVER['HTTP_X_GITHUB_EVENT'])) {
|
|
throw new \Exception("Missing HTTP 'X-Github-Event' header.");
|
|
}
|
|
switch ($contenttype) {
|
|
case 'application/json':
|
|
$json = $rawPost ?: file_get_contents('php://input');
|
|
break;
|
|
case 'application/x-www-form-urlencoded':
|
|
$json = $_POST['payload'];
|
|
break;
|
|
default:
|
|
throw new \Exception("Unsupported content type: $contenttype");
|
|
}
|
|
# Payload structure depends on triggered event
|
|
# https://developer.github.com/v3/activity/events/types/
|
|
$payload = json_decode($json, true);
|
|
|
|
switch (strtolower($_SERVER['HTTP_X_GITHUB_EVENT'])) {
|
|
case 'ping':
|
|
echo 'pong';
|
|
break;
|
|
case 'pull_request':
|
|
handle_pr($payload);
|
|
break;
|
|
case 'pull_request_review':
|
|
if($payload['action'] == 'submitted'){
|
|
$lower_state = strtolower($payload['review']['state']);
|
|
if(($lower_state == 'approved' || $lower_state == 'changes_requested') && is_maintainer($payload, $payload['review']['user']['login']))
|
|
remove_ready_for_review($payload);
|
|
}
|
|
break;
|
|
default:
|
|
header('HTTP/1.0 404 Not Found');
|
|
echo "Event:$_SERVER[HTTP_X_GITHUB_EVENT] Payload:\n";
|
|
print_r($payload); # For debug only. Can be found in GitHub hook log.
|
|
die();
|
|
}
|
|
|
|
function apisend($url, $method = 'GET', $content = null, $authorization = null) {
|
|
if (is_array($content))
|
|
$content = json_encode($content);
|
|
|
|
$headers = array();
|
|
$headers[] = 'Content-type: application/json';
|
|
if ($authorization)
|
|
$headers[] = 'Authorization: ' . $authorization;
|
|
|
|
$scontext = array('http' => array(
|
|
'method' => $method,
|
|
'header' => implode("\r\n", $headers),
|
|
'ignore_errors' => true,
|
|
'user_agent' => 'tgstation13.org-Github-Automation-Tools'
|
|
));
|
|
|
|
if ($content)
|
|
$scontext['http']['content'] = $content;
|
|
|
|
return file_get_contents($url, false, stream_context_create($scontext));
|
|
|
|
}
|
|
|
|
function github_apisend($url, $method = 'GET', $content = NULL) {
|
|
global $apiKey;
|
|
return apisend($url, $method, $content, 'token ' . $apiKey);
|
|
}
|
|
|
|
function discord_webhook_send($webhook, $content) {
|
|
return apisend($webhook, 'POST', $content);
|
|
}
|
|
|
|
function validate_user($payload) {
|
|
global $validation, $validation_count;
|
|
$query = array();
|
|
if (empty($validation))
|
|
$validation = 'org';
|
|
switch (strtolower($validation)) {
|
|
case 'disable':
|
|
return TRUE;
|
|
case 'repo':
|
|
$query['repo'] = $payload['pull_request']['base']['repo']['full_name'];
|
|
break;
|
|
default:
|
|
$query['user'] = $payload['pull_request']['base']['repo']['owner']['login'];
|
|
break;
|
|
}
|
|
$query['author'] = $payload['pull_request']['user']['login'];
|
|
$query['is'] = 'merged';
|
|
$querystring = '';
|
|
foreach($query as $key => $value)
|
|
$querystring .= ($querystring == '' ? '' : '+') . urlencode($key) . ':' . urlencode($value);
|
|
$res = github_apisend('https://api.github.com/search/issues?q='.$querystring);
|
|
$res = json_decode($res, TRUE);
|
|
return $res['total_count'] >= (int)$validation_count;
|
|
|
|
}
|
|
|
|
function get_labels($payload){
|
|
$url = $payload['pull_request']['issue_url'] . '/labels';
|
|
$existing_labels = json_decode(github_apisend($url), true);
|
|
$existing = array();
|
|
foreach((array) $existing_labels as $label)
|
|
$existing[] = $label['name'];
|
|
return $existing;
|
|
}
|
|
|
|
function check_tag_and_replace($payload, $title_tag, $label, &$array_to_add_label_to){
|
|
$title = $payload['pull_request']['title'];
|
|
if(stripos($title, $title_tag) !== FALSE){
|
|
$array_to_add_label_to[] = $label;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function set_labels($payload, $labels, $remove) {
|
|
$existing = get_labels($payload);
|
|
$tags = array();
|
|
|
|
$tags = array_merge($labels, $existing);
|
|
$tags = array_unique($tags);
|
|
if($remove) {
|
|
$tags = array_diff($tags, $remove);
|
|
}
|
|
|
|
$final = array();
|
|
foreach($tags as $t)
|
|
$final[] = $t;
|
|
|
|
$url = $payload['pull_request']['issue_url'] . '/labels';
|
|
echo github_apisend($url, 'PUT', $final);
|
|
}
|
|
|
|
//rip bs-12
|
|
function tag_pr($payload, $opened) {
|
|
//get the mergeable state
|
|
$url = $payload['pull_request']['url'];
|
|
$payload['pull_request'] = json_decode(github_apisend($url), TRUE);
|
|
if($payload['pull_request']['mergeable'] == null) {
|
|
//STILL not ready. Give it a bit, then try one more time
|
|
sleep(10);
|
|
$payload['pull_request'] = json_decode(github_apisend($url), TRUE);
|
|
}
|
|
|
|
$tags = array();
|
|
$title = $payload['pull_request']['title'];
|
|
if($opened) { //you only have one shot on these ones so as to not annoy maintainers
|
|
$tags = checkchangelog($payload, false);
|
|
|
|
if(strpos(strtolower($title), 'refactor') !== FALSE)
|
|
$tags[] = 'Refactor';
|
|
|
|
if(strpos(strtolower($title), 'revert') !== FALSE)
|
|
$tags[] = 'Revert';
|
|
if(strpos(strtolower($title), 'removes') !== FALSE)
|
|
$tags[] = 'Removal';
|
|
}
|
|
|
|
$remove = array('Test Merge Candidate');
|
|
|
|
$mergeable = $payload['pull_request']['mergeable'];
|
|
if($mergeable === TRUE) //only look for the false value
|
|
$remove[] = 'Merge Conflict';
|
|
else if ($mergeable === FALSE)
|
|
$tags[] = 'Merge Conflict';
|
|
|
|
$treetags = array('_maps' => 'Map Edit', 'tools' => 'Tools', 'SQL' => 'SQL', '.github' => 'GitHub');
|
|
$addonlytags = array('icons' => 'Sprites', 'sound' => 'Sound', 'config' => 'Config Update', 'code/controllers/configuration/entries' => 'Config Update', 'code/modules/unit_tests' => 'Unit Tests', 'tgui' => 'UI');
|
|
foreach($treetags as $tree => $tag)
|
|
if(has_tree_been_edited($payload, $tree))
|
|
$tags[] = $tag;
|
|
else
|
|
$remove[] = $tag;
|
|
foreach($addonlytags as $tree => $tag)
|
|
if(has_tree_been_edited($payload, $tree))
|
|
$tags[] = $tag;
|
|
|
|
check_tag_and_replace($payload, '[dnm]', 'Do Not Merge', $tags);
|
|
if(!check_tag_and_replace($payload, '[wip]', 'Work In Progress', $tags) && check_tag_and_replace($payload, '[ready]', 'Work In Progress', $remove))
|
|
$tags[] = 'Needs Review';
|
|
|
|
return array($tags, $remove);
|
|
}
|
|
|
|
function remove_ready_for_review($payload, $labels = null){
|
|
if($labels == null)
|
|
$labels = get_labels($payload);
|
|
$index = array_search('Needs Review', $labels);
|
|
if($index !== FALSE)
|
|
unset($labels[$index]);
|
|
$url = $payload['pull_request']['issue_url'] . '/labels';
|
|
github_apisend($url, 'PUT', $labels);
|
|
}
|
|
|
|
function dismiss_review($payload, $id, $reason){
|
|
$content = array('message' => $reason);
|
|
github_apisend($payload['pull_request']['url'] . '/reviews/' . $id . '/dismissals', 'PUT', $content);
|
|
}
|
|
|
|
function get_reviews($payload){
|
|
return json_decode(github_apisend($payload['pull_request']['url'] . '/reviews'), true);
|
|
}
|
|
|
|
function check_ready_for_review($payload, $labels = null, $remove = array()){
|
|
$r4rlabel = 'Needs Review';
|
|
$labels_which_should_not_be_ready = array('Do Not Merge', 'Work In Progress', 'Merge Conflict');
|
|
$has_label_already = false;
|
|
$should_not_have_label = false;
|
|
if($labels == null)
|
|
$labels = get_labels($payload);
|
|
$returned = array($labels, $remove);
|
|
//if the label is already there we may need to remove it
|
|
foreach($labels as $L){
|
|
if(in_array($L, $labels_which_should_not_be_ready))
|
|
$should_not_have_label = true;
|
|
if($L == $r4rlabel)
|
|
$has_label_already = true;
|
|
}
|
|
|
|
if($has_label_already && $should_not_have_label){
|
|
$remove[] = $r4rlabel;
|
|
return $returned;
|
|
}
|
|
|
|
//find all reviews to see if changes were requested at some point
|
|
$reviews = get_reviews($payload);
|
|
|
|
$reviews_ids_with_changes_requested = array();
|
|
$dismissed_an_approved_review = false;
|
|
|
|
foreach($reviews as $R)
|
|
if(is_maintainer($payload, $R['user']['login'])){
|
|
$lower_state = strtolower($R['state']);
|
|
if($lower_state == 'changes_requested')
|
|
$reviews_ids_with_changes_requested[] = $R['id'];
|
|
else if ($lower_state == 'approved'){
|
|
dismiss_review($payload, $R['id'], 'Out of date review');
|
|
$dismissed_an_approved_review = true;
|
|
}
|
|
}
|
|
|
|
if(!$dismissed_an_approved_review && count($reviews_ids_with_changes_requested) == 0){
|
|
if($has_label_already)
|
|
$remove[] = $r4rlabel;
|
|
return $returned; //no need to be here
|
|
}
|
|
|
|
if(count($reviews_ids_with_changes_requested) > 0){
|
|
//now get the review comments for the offending reviews
|
|
|
|
$review_comments = json_decode(github_apisend($payload['pull_request']['review_comments_url']), true);
|
|
|
|
foreach($review_comments as $C){
|
|
//make sure they are part of an offending review
|
|
if(!in_array($C['pull_request_review_id'], $reviews_ids_with_changes_requested))
|
|
continue;
|
|
|
|
//review comments which are outdated have a null position
|
|
if($C['position'] !== null){
|
|
if($has_label_already)
|
|
$remove[] = $r4rlabel;
|
|
return $returned; //no need to tag
|
|
}
|
|
}
|
|
}
|
|
|
|
//finally, add it if necessary
|
|
if(!$has_label_already){
|
|
$labels[] = $r4rlabel;
|
|
}
|
|
return $returned;
|
|
}
|
|
|
|
function check_dismiss_changelog_review($payload){
|
|
global $require_changelog;
|
|
global $no_changelog;
|
|
|
|
if(!$require_changelog)
|
|
return;
|
|
|
|
if(!$no_changelog)
|
|
checkchangelog($payload, false);
|
|
|
|
$review_message = 'Your changelog for this PR is either malformed or non-existent. Please create one to document your changes.';
|
|
|
|
$reviews = get_reviews($payload);
|
|
if($no_changelog){
|
|
//check and see if we've already have this review
|
|
foreach($reviews as $R)
|
|
if($R['body'] == $review_message && strtolower($R['state']) == 'changes_requested')
|
|
return;
|
|
//otherwise make it ourself
|
|
github_apisend($payload['pull_request']['url'] . '/reviews', 'POST', array('body' => $review_message, 'event' => 'REQUEST_CHANGES'));
|
|
}
|
|
else
|
|
//kill previous reviews
|
|
foreach($reviews as $R)
|
|
if($R['body'] == $review_message && strtolower($R['state']) == 'changes_requested')
|
|
dismiss_review($payload, $R['id'], 'Changelog added/fixed.');
|
|
}
|
|
|
|
function handle_pr($payload) {
|
|
global $no_changelog;
|
|
$action = 'opened';
|
|
$validated = validate_user($payload);
|
|
switch ($payload["action"]) {
|
|
case 'opened':
|
|
list($labels, $remove) = tag_pr($payload, true);
|
|
set_labels($payload, $labels, $remove);
|
|
if($no_changelog)
|
|
check_dismiss_changelog_review($payload);
|
|
if(get_pr_code_friendliness($payload) <= 0){
|
|
$balances = pr_balances();
|
|
$author = $payload['pull_request']['user']['login'];
|
|
if(isset($balances[$author]) && $balances[$author] < 0 && !is_maintainer($payload, $author))
|
|
create_comment($payload, 'You currently have a negative Fix/Feature pull request delta of ' . $balances[$author] . '. Maintainers may close this PR at will. Fixing issues or improving the codebase will improve this score.');
|
|
}
|
|
break;
|
|
case 'edited':
|
|
check_dismiss_changelog_review($payload);
|
|
case 'synchronize':
|
|
list($labels, $remove) = tag_pr($payload, false);
|
|
if($payload['action'] == 'synchronize')
|
|
list($labels, $remove) = check_ready_for_review($payload, $labels, $remove);
|
|
set_labels($payload, $labels, $remove);
|
|
return;
|
|
case 'reopened':
|
|
$action = $payload['action'];
|
|
break;
|
|
case 'closed':
|
|
if (!$payload['pull_request']['merged']) {
|
|
$action = 'closed';
|
|
}
|
|
else {
|
|
$action = 'merged';
|
|
auto_update($payload);
|
|
checkchangelog($payload, true);
|
|
update_pr_balance($payload);
|
|
$validated = TRUE; //pr merged events always get announced.
|
|
}
|
|
break;
|
|
default:
|
|
return;
|
|
}
|
|
|
|
$pr_flags = 0;
|
|
if (strpos(strtolower($payload['pull_request']['title']), '[s]') !== false) {
|
|
$pr_flags |= F_SECRET_PR;
|
|
}
|
|
if (!$validated) {
|
|
$pr_flags |= F_UNVALIDATED_USER;
|
|
}
|
|
discord_announce($action, $payload, $pr_flags);
|
|
game_announce($action, $payload, $pr_flags);
|
|
|
|
}
|
|
|
|
function filter_announce_targets($targets, $owner, $repo, $action, $pr_flags) {
|
|
foreach ($targets as $i=>$target) {
|
|
if (isset($target['exclude_events']) && in_array($action, array_map('strtolower', $target['exclude_events']))) {
|
|
unset($targets[$i]);
|
|
continue;
|
|
}
|
|
|
|
if (isset($target['announce_secret']) && $target['announce_secret']) {
|
|
if (!($pr_flags & F_SECRET_PR) && $target['announce_secret'] === 'only') {
|
|
unset($targets[$i]);
|
|
continue;
|
|
}
|
|
} else if ($pr_flags & F_SECRET_PR) {
|
|
unset($targets[$i]);
|
|
continue;
|
|
}
|
|
|
|
if (isset($target['announce_unvalidated']) && $target['announce_unvalidated']) {
|
|
if (!($pr_flags & F_UNVALIDATED_USER) && $target['announce_unvalidated'] === 'only') {
|
|
unset($targets[$i]);
|
|
continue;
|
|
}
|
|
} else if ($pr_flags & F_UNVALIDATED_USER) {
|
|
unset($targets[$i]);
|
|
continue;
|
|
}
|
|
|
|
$wildcard = false;
|
|
if (isset($target['include_repos'])) {
|
|
foreach ($target['include_repos'] as $match_string) {
|
|
$owner_repo_pair = explode('/', strtolower($match_string));
|
|
if (count($owner_repo_pair) != 2) {
|
|
log_error('Bad include repo: `'. $match_string.'`');
|
|
continue;
|
|
}
|
|
if (strtolower($owner) == $owner_repo_pair[0]) {
|
|
if (strtolower($repo) == $owner_repo_pair[1])
|
|
continue 2; //don't parse excludes when we have an exact include match
|
|
if ($owner_repo_pair[1] == '*') {
|
|
$wildcard = true;
|
|
continue; //do parse excludes when we have a wildcard match (but check the other entries for exact matches first)
|
|
}
|
|
}
|
|
}
|
|
if (!$wildcard) {
|
|
unset($targets[$i]);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (isset($target['exclude_repos']))
|
|
foreach ($target['exclude_repos'] as $match_string) {
|
|
$owner_repo_pair = explode('/', strtolower($match_string));
|
|
if (count($owner_repo_pair) != 2) {
|
|
log_error('Bad exclude repo: `'. $match_string.'`');
|
|
continue;
|
|
}
|
|
if (strtolower($owner) == $owner_repo_pair[0]) {
|
|
if (strtolower($repo) == $owner_repo_pair[1]) {
|
|
unset($targets[$i]);
|
|
continue 2;
|
|
}
|
|
if ($owner_repo_pair[1] == '*') {
|
|
if ($wildcard)
|
|
log_error('Identical wildcard include and exclude: `'.$match_string.'`. Excluding.');
|
|
unset($targets[$i]);
|
|
continue 2;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return $targets;
|
|
}
|
|
|
|
function game_announce($action, $payload, $pr_flags) {
|
|
global $servers;
|
|
|
|
$msg = '['.$payload['pull_request']['base']['repo']['full_name'].'] Pull Request '.$action.' by '.htmlSpecialChars($payload['sender']['login']).': <a href="'.$payload['pull_request']['html_url'].'">'.htmlSpecialChars('#'.$payload['pull_request']['number'].' '.$payload['pull_request']['user']['login'].' - '.$payload['pull_request']['title']).'</a>';
|
|
|
|
$game_servers = filter_announce_targets($servers, $payload['pull_request']['base']['repo']['owner']['login'], $payload['pull_request']['base']['repo']['name'], $action, $pr_flags);
|
|
|
|
$msg = '?announce='.urlencode($msg).'&payload='.urlencode(json_encode($payload));
|
|
|
|
foreach ($game_servers as $serverid => $server) {
|
|
$server_message = $msg;
|
|
if (isset($server['comskey']))
|
|
$server_message .= '&key='.urlencode($server['comskey']);
|
|
game_server_send($server['address'], $server['port'], $server_message);
|
|
}
|
|
|
|
}
|
|
|
|
function discord_announce($action, $payload, $pr_flags) {
|
|
global $discordWebHooks;
|
|
$color;
|
|
switch ($action) {
|
|
case 'reopened':
|
|
case 'opened':
|
|
$color = 0x2cbe4e;
|
|
break;
|
|
case 'closed':
|
|
$color = 0xcb2431;
|
|
break;
|
|
case 'merged':
|
|
$color = 0x6f42c1;
|
|
break;
|
|
default:
|
|
return;
|
|
}
|
|
$data = array(
|
|
'username' => 'GitHub',
|
|
'avatar_url' => $payload['pull_request']['base']['user']['avatar_url'],
|
|
);
|
|
|
|
$content = 'Pull Request #'.$payload['pull_request']['number'].' *'.$action.'* by '.discord_sanitize($payload['sender']['login'])."\n".discord_sanitize($payload['pull_request']['user']['login']).' - __**'.discord_sanitize($payload['pull_request']['title']).'**__'."\n".'<'.$payload['pull_request']['html_url'].'>';
|
|
|
|
$embeds = array(
|
|
array(
|
|
'title' => '__**'.discord_sanitize($payload['pull_request']['title'], S_MARKDOWN).'**__',
|
|
'description' => discord_sanitize(str_replace(array("\r\n", "\n"), array(' ', ' '), substr($payload['pull_request']['body'], 0, 320)), S_HTML_COMMENTS),
|
|
'url' => $payload['pull_request']['html_url'],
|
|
'color' => $color,
|
|
'author' => array(
|
|
'name' => discord_sanitize($payload['pull_request']['user']['login'], S_MARKDOWN),
|
|
'url' => $payload['pull_request']['user']['html_url'],
|
|
'icon_url' => $payload['pull_request']['user']['avatar_url']
|
|
),
|
|
'footer' => array(
|
|
'text' => '#'.$payload['pull_request']['number'].' '.discord_sanitize($payload['pull_request']['base']['repo']['full_name'], S_MARKDOWN).' '.discord_sanitize($payload['pull_request']['head']['ref'], S_MARKDOWN).' -> '.discord_sanitize($payload['pull_request']['base']['ref'], S_MARKDOWN),
|
|
'icon_url' => $payload['pull_request']['base']['user']['avatar_url']
|
|
)
|
|
)
|
|
);
|
|
$discordWebHook_targets = filter_announce_targets($discordWebHooks, $payload['pull_request']['base']['repo']['owner']['login'], $payload['pull_request']['base']['repo']['name'], $action, $pr_flags);
|
|
foreach ($discordWebHook_targets as $discordWebHook) {
|
|
$sending_data = $data;
|
|
if (isset($discordWebHook['embed']) && $discordWebHook['embed']) {
|
|
$sending_data['embeds'] = $embeds;
|
|
if (!isset($discordWebHook['no_text']) || !$discordWebHook['no_text'])
|
|
$sending_data['content'] = $content;
|
|
} else {
|
|
$sending_data['content'] = $content;
|
|
}
|
|
discord_webhook_send($discordWebHook['url'], $sending_data);
|
|
}
|
|
|
|
}
|
|
|
|
function discord_sanitize($text, $flags = S_MENTIONS|S_LINK_EMBED|S_MARKDOWN) {
|
|
if ($flags & S_MARKDOWN)
|
|
$text = str_ireplace(array('\\', '*', '_', '~', '`', '|'), (array('\\\\', '\\*', '\\_', '\\~', '\\`', '\\|')), $text);
|
|
|
|
if ($flags & S_HTML_COMMENTS)
|
|
$text = preg_replace('/<!--(.*)-->/Uis', '', $text);
|
|
|
|
if ($flags & S_MENTIONS)
|
|
$text = str_ireplace(array('@everyone', '@here', '<@'), array('`@everyone`', '`@here`', '@<'), $text);
|
|
|
|
if ($flags & S_LINK_EMBED)
|
|
$text = preg_replace("/((https?|ftp|byond)\:\/\/)([a-z0-9-.]*)\.([a-z]{2,3})(\:[0-9]{2,5})?(\/(?:[a-z0-9+\$_-]\.?)+)*\/?(\?[a-z+&\$_.-][a-z0-9;:@&%=+\/\$_.-]*)?(#[a-z_.-][a-z0-9+\$_.-]*)?/mi", '<$0>', $text);
|
|
|
|
return $text;
|
|
}
|
|
|
|
//creates a comment on the payload issue
|
|
function create_comment($payload, $comment){
|
|
github_apisend($payload['pull_request']['comments_url'], 'POST', json_encode(array('body' => $comment)));
|
|
}
|
|
|
|
//returns the payload issue's labels as a flat array
|
|
function get_pr_labels_array($payload){
|
|
$url = $payload['pull_request']['issue_url'] . '/labels';
|
|
$issue = json_decode(github_apisend($url), true);
|
|
$result = array();
|
|
foreach($issue as $l)
|
|
$result[] = $l['name'];
|
|
return $result;
|
|
}
|
|
|
|
//helper for getting the path the the balance json file
|
|
function pr_balance_json_path(){
|
|
global $prBalanceJson;
|
|
return $prBalanceJson != '' ? $prBalanceJson : 'pr_balances.json';
|
|
}
|
|
|
|
//return the assoc array of login -> balance for prs
|
|
function pr_balances(){
|
|
$path = pr_balance_json_path();
|
|
if(file_exists($path))
|
|
return json_decode(file_get_contents($path), true);
|
|
else
|
|
return array();
|
|
}
|
|
|
|
//returns the difference in PR balance a pull request would cause
|
|
function get_pr_code_friendliness($payload, $oldbalance = null){
|
|
global $startingPRBalance;
|
|
if($oldbalance == null)
|
|
$oldbalance = $startingPRBalance;
|
|
$labels = get_pr_labels_array($payload);
|
|
//anything not in this list defaults to 0
|
|
$label_values = array(
|
|
'Fix' => 3,
|
|
'Refactor' => 10,
|
|
'Code Improvement' => 2,
|
|
'Grammar and Formatting' => 1,
|
|
'Priority: High' => 15,
|
|
'Priority: CRITICAL' => 20,
|
|
'Unit Tests' => 6,
|
|
'Logging' => 1,
|
|
'Feedback' => 2,
|
|
'Performance' => 12,
|
|
'Feature' => -10,
|
|
'Balance/Rebalance' => -8,
|
|
'Tweak' => -2,
|
|
'GBP: Reset' => $startingPRBalance - $oldbalance,
|
|
);
|
|
|
|
$maxNegative = 0;
|
|
$maxPositive = 0;
|
|
foreach($labels as $l){
|
|
if($l == 'GBP: No Update') { //no effect on balance
|
|
return 0;
|
|
}
|
|
else if(isset($label_values[$l])) {
|
|
$friendliness = $label_values[$l];
|
|
if($friendliness > 0)
|
|
$maxPositive = max($friendliness, $maxPositive);
|
|
else
|
|
$maxNegative = min($friendliness, $maxNegative);
|
|
}
|
|
}
|
|
|
|
$affecting = abs($maxNegative) >= $maxPositive ? $maxNegative : $maxPositive;
|
|
return $affecting;
|
|
}
|
|
|
|
function is_maintainer($payload, $author){
|
|
global $maintainer_team_id;
|
|
$repo_is_org = $payload['pull_request']['base']['repo']['owner']['type'] == 'Organization';
|
|
if($maintainer_team_id == null || !$repo_is_org) {
|
|
$collaburl = str_replace('{/collaborator}', '/' . $author, $payload['pull_request']['base']['repo']['collaborators_url']) . '/permission';
|
|
$perms = json_decode(github_apisend($collaburl), true);
|
|
$permlevel = $perms['permission'];
|
|
return $permlevel == 'admin' || $permlevel == 'write';
|
|
}
|
|
else {
|
|
$check_url = 'https://api.github.com/teams/' . $maintainer_team_id . '/memberships/' . $author;
|
|
$result = json_decode(github_apisend($check_url), true);
|
|
return isset($result['state']) && $result['state'] == 'active';
|
|
}
|
|
}
|
|
|
|
//payload is a merged pull request, updates the pr balances file with the correct positive or negative balance based on comments
|
|
function update_pr_balance($payload) {
|
|
global $startingPRBalance;
|
|
global $trackPRBalance;
|
|
if(!$trackPRBalance)
|
|
return;
|
|
$author = $payload['pull_request']['user']['login'];
|
|
$balances = pr_balances();
|
|
if(!isset($balances[$author]))
|
|
$balances[$author] = $startingPRBalance;
|
|
$friendliness = get_pr_code_friendliness($payload, $balances[$author]);
|
|
$balances[$author] += $friendliness;
|
|
if(!is_maintainer($payload, $author)){ //immune
|
|
if($balances[$author] < 0 && $friendliness < 0)
|
|
create_comment($payload, 'Your Fix/Feature pull request delta is currently below zero (' . $balances[$author] . '). Maintainers may close future Feature/Tweak/Balance PRs. Fixing issues or helping to improve the codebase will raise this score.');
|
|
else if($balances[$author] >= 0 && ($balances[$author] - $friendliness) < 0)
|
|
create_comment($payload, 'Your Fix/Feature pull request delta is now above zero (' . $balances[$author] . '). Feel free to make Feature/Tweak/Balance PRs.');
|
|
}
|
|
$balances_file = fopen(pr_balance_json_path(), 'w');
|
|
fwrite($balances_file, json_encode($balances));
|
|
fclose($balances_file);
|
|
}
|
|
|
|
$github_diff = null;
|
|
|
|
function get_diff($payload) {
|
|
global $github_diff;
|
|
if ($github_diff === null && $payload['pull_request']['diff_url']) {
|
|
//go to the diff url
|
|
$url = $payload['pull_request']['diff_url'];
|
|
$github_diff = file_get_contents($url);
|
|
}
|
|
return $github_diff;
|
|
}
|
|
|
|
function auto_update($payload){
|
|
global $enable_live_tracking;
|
|
global $path_to_script;
|
|
global $repoOwnerAndName;
|
|
global $tracked_branch;
|
|
global $github_diff;
|
|
if(!$enable_live_tracking || !has_tree_been_edited($payload, $path_to_script) || $payload['pull_request']['base']['ref'] != $tracked_branch)
|
|
return;
|
|
|
|
get_diff($payload);
|
|
$content = file_get_contents('https://raw.githubusercontent.com/' . $repoOwnerAndName . '/' . $tracked_branch . '/'. $path_to_script);
|
|
$content_diff = "### Diff not available. :slightly_frowning_face:";
|
|
if($github_diff && preg_match('/(diff --git a\/' . preg_quote($path_to_script, '/') . '.+?)(?:\Rdiff|$)/s', $github_diff, $matches)) {
|
|
$script_diff = $matches[1];
|
|
if($script_diff) {
|
|
$content_diff = "``" . "`DIFF\n" . $script_diff ."\n``" . "`";
|
|
}
|
|
}
|
|
create_comment($payload, "Edit detected. Self updating... \n<details><summary>Here are my changes:</summary>\n\n" . $content_diff . "\n</details>\n<details><summary>Here is my new code:</summary>\n\n``" . "`HTML+PHP\n" . $content . "\n``" . '`\n</details>');
|
|
|
|
$code_file = fopen(basename($path_to_script), 'w');
|
|
fwrite($code_file, $content);
|
|
fclose($code_file);
|
|
}
|
|
|
|
function has_tree_been_edited($payload, $tree){
|
|
global $github_diff;
|
|
get_diff($payload);
|
|
//find things in the _maps/map_files tree
|
|
//e.g. diff --git a/_maps/map_files/Cerestation/cerestation.dmm b/_maps/map_files/Cerestation/cerestation.dmm
|
|
return ($github_diff !== FALSE) && (preg_match('/^diff --git a\/' . preg_quote($tree, '/') . '/m', $github_diff) !== 0);
|
|
}
|
|
|
|
$no_changelog = false;
|
|
function checkchangelog($payload, $compile = true) {
|
|
global $no_changelog;
|
|
if (!isset($payload['pull_request']) || !isset($payload['pull_request']['body'])) {
|
|
return;
|
|
}
|
|
if (!isset($payload['pull_request']['user']) || !isset($payload['pull_request']['user']['login'])) {
|
|
return;
|
|
}
|
|
$body = $payload['pull_request']['body'];
|
|
|
|
$tags = array();
|
|
|
|
if(preg_match('/(?i)(fix|fixes|fixed|resolve|resolves|resolved)\s*#[0-9]+/',$body)) //github autoclose syntax
|
|
$tags[] = 'Fix';
|
|
|
|
$body = str_replace("\r\n", "\n", $body);
|
|
$body = explode("\n", $body);
|
|
|
|
$username = $payload['pull_request']['user']['login'];
|
|
$incltag = false;
|
|
$changelogbody = array();
|
|
$currentchangelogblock = array();
|
|
$foundcltag = false;
|
|
foreach ($body as $line) {
|
|
$line = trim($line);
|
|
if (substr($line,0,4) == ':cl:' || substr($line,0,1) == '??') {
|
|
$incltag = true;
|
|
$foundcltag = true;
|
|
$pos = strpos($line, " ");
|
|
if ($pos) {
|
|
$tmp = substr($line, $pos+1);
|
|
if (trim($tmp) != 'optional name here')
|
|
$username = $tmp;
|
|
}
|
|
continue;
|
|
} else if (substr($line,0,5) == '/:cl:' || substr($line,0,6) == '/ :cl:' || substr($line,0,5) == ':/cl:' || substr($line,0,5) == '/??' || substr($line,0,6) == '/ ??' ) {
|
|
$incltag = false;
|
|
$changelogbody = array_merge($changelogbody, $currentchangelogblock);
|
|
continue;
|
|
}
|
|
if (!$incltag)
|
|
continue;
|
|
|
|
$firstword = explode(' ', $line)[0];
|
|
$pos = strpos($line, " ");
|
|
$item = '';
|
|
if ($pos) {
|
|
$firstword = trim(substr($line, 0, $pos));
|
|
$item = trim(substr($line, $pos+1));
|
|
} else {
|
|
$firstword = $line;
|
|
}
|
|
|
|
if (!strlen($firstword)) {
|
|
$currentchangelogblock[count($currentchangelogblock)-1]['body'] .= "\n";
|
|
continue;
|
|
}
|
|
//not a prefix line.
|
|
//so we add it to the last changelog entry as a separate line
|
|
if (!strlen($firstword) || $firstword[strlen($firstword)-1] != ':') {
|
|
if (count($currentchangelogblock) <= 0)
|
|
continue;
|
|
$currentchangelogblock[count($currentchangelogblock)-1]['body'] .= "\n".$line;
|
|
continue;
|
|
}
|
|
$cltype = strtolower(substr($firstword, 0, -1));
|
|
switch ($cltype) {
|
|
case 'fix':
|
|
case 'fixes':
|
|
case 'bugfix':
|
|
if($item != 'fixed a few things') {
|
|
$tags[] = 'Fix';
|
|
$currentchangelogblock[] = array('type' => 'bugfix', 'body' => $item);
|
|
}
|
|
break;
|
|
case 'rsctweak':
|
|
case 'tweaks':
|
|
case 'tweak':
|
|
if($item != 'tweaked a few things') {
|
|
$tags[] = 'Tweak';
|
|
$currentchangelogblock[] = array('type' => 'tweak', 'body' => $item);
|
|
}
|
|
break;
|
|
case 'soundadd':
|
|
if($item != 'added a new sound thingy') {
|
|
$tags[] = 'Sound';
|
|
$currentchangelogblock[] = array('type' => 'soundadd', 'body' => $item);
|
|
}
|
|
break;
|
|
case 'sounddel':
|
|
if($item != 'removed an old sound thingy') {
|
|
$tags[] = 'Sound';
|
|
$tags[] = 'Removal';
|
|
$currentchangelogblock[] = array('type' => 'sounddel', 'body' => $item);
|
|
}
|
|
break;
|
|
case 'add':
|
|
case 'adds':
|
|
case 'rscadd':
|
|
if($item != 'Added new things' && $item != 'Added more things') {
|
|
$tags[] = 'Feature';
|
|
$currentchangelogblock[] = array('type' => 'rscadd', 'body' => $item);
|
|
}
|
|
break;
|
|
case 'del':
|
|
case 'dels':
|
|
case 'rscdel':
|
|
if($item != 'Removed old things') {
|
|
$tags[] = 'Removal';
|
|
$currentchangelogblock[] = array('type' => 'rscdel', 'body' => $item);
|
|
}
|
|
break;
|
|
case 'imageadd':
|
|
if($item != 'added some icons and images') {
|
|
$tags[] = 'Sprites';
|
|
$currentchangelogblock[] = array('type' => 'imageadd', 'body' => $item);
|
|
}
|
|
break;
|
|
case 'imagedel':
|
|
if($item != 'deleted some icons and images') {
|
|
$tags[] = 'Sprites';
|
|
$tags[] = 'Removal';
|
|
$currentchangelogblock[] = array('type' => 'imagedel', 'body' => $item);
|
|
}
|
|
break;
|
|
case 'typo':
|
|
case 'spellcheck':
|
|
if($item != 'fixed a few typos') {
|
|
$tags[] = 'Grammar and Formatting';
|
|
$currentchangelogblock[] = array('type' => 'spellcheck', 'body' => $item);
|
|
}
|
|
break;
|
|
case 'balance':
|
|
case 'rebalance':
|
|
if($item != 'rebalanced something'){
|
|
$tags[] = 'Balance/Rebalance';
|
|
$currentchangelogblock[] = array('type' => 'balance', 'body' => $item);
|
|
}
|
|
break;
|
|
case 'tgs':
|
|
$currentchangelogblock[] = array('type' => 'tgs', 'body' => $item);
|
|
break;
|
|
case 'code_imp':
|
|
case 'code':
|
|
if($item != 'changed some code'){
|
|
$tags[] = 'Code Improvement';
|
|
$currentchangelogblock[] = array('type' => 'code_imp', 'body' => $item);
|
|
}
|
|
break;
|
|
case 'refactor':
|
|
if($item != 'refactored some code'){
|
|
$tags[] = 'Refactor';
|
|
$currentchangelogblock[] = array('type' => 'refactor', 'body' => $item);
|
|
}
|
|
break;
|
|
case 'config':
|
|
if($item != 'changed some config setting'){
|
|
$tags[] = 'Config Update';
|
|
$currentchangelogblock[] = array('type' => 'config', 'body' => $item);
|
|
}
|
|
break;
|
|
case 'admin':
|
|
if($item != 'messed with admin stuff'){
|
|
$tags[] = 'Administration';
|
|
$currentchangelogblock[] = array('type' => 'admin', 'body' => $item);
|
|
}
|
|
break;
|
|
case 'server':
|
|
if($item != 'something server ops should know')
|
|
$currentchangelogblock[] = array('type' => 'server', 'body' => $item);
|
|
break;
|
|
default:
|
|
//we add it to the last changelog entry as a separate line
|
|
if (count($currentchangelogblock) > 0)
|
|
$currentchangelogblock[count($currentchangelogblock)-1]['body'] .= "\n".$line;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if(!count($changelogbody))
|
|
$no_changelog = true;
|
|
|
|
if ($no_changelog || !$compile)
|
|
return $tags;
|
|
|
|
$file = 'author: "'.trim(str_replace(array("\\", '"'), array("\\\\", "\\\""), $username)).'"'."\n";
|
|
$file .= "delete-after: True\n";
|
|
$file .= "changes: \n";
|
|
foreach ($changelogbody as $changelogitem) {
|
|
$type = $changelogitem['type'];
|
|
$body = trim(str_replace(array("\\", '"'), array("\\\\", "\\\""), $changelogitem['body']));
|
|
$file .= ' - '.$type.': "'.$body.'"';
|
|
$file .= "\n";
|
|
}
|
|
$content = array (
|
|
'branch' => $payload['pull_request']['base']['ref'],
|
|
'message' => 'Automatic changelog generation for PR #'.$payload['pull_request']['number'].' [ci skip]',
|
|
'content' => base64_encode($file)
|
|
);
|
|
|
|
$filename = '/html/changelogs/AutoChangeLog-pr-'.$payload['pull_request']['number'].'.yml';
|
|
echo github_apisend($payload['pull_request']['base']['repo']['url'].'/contents'.$filename, 'PUT', $content);
|
|
}
|
|
|
|
function game_server_send($addr, $port, $str) {
|
|
// All queries must begin with a question mark (ie "?players")
|
|
if($str{0} != '?') $str = ('?' . $str);
|
|
|
|
/* --- Prepare a packet to send to the server (based on a reverse-engineered packet structure) --- */
|
|
$query = "\x00\x83" . pack('n', strlen($str) + 6) . "\x00\x00\x00\x00\x00" . $str . "\x00";
|
|
|
|
/* --- Create a socket and connect it to the server --- */
|
|
$server = socket_create(AF_INET,SOCK_STREAM,SOL_TCP) or exit("ERROR");
|
|
socket_set_option($server, SOL_SOCKET, SO_SNDTIMEO, array('sec' => 2, 'usec' => 0)); //sets connect and send timeout to 2 seconds
|
|
if(!socket_connect($server,$addr,$port)) {
|
|
return "ERROR: Connection failed";
|
|
}
|
|
|
|
|
|
/* --- Send bytes to the server. Loop until all bytes have been sent --- */
|
|
$bytestosend = strlen($query);
|
|
$bytessent = 0;
|
|
while ($bytessent < $bytestosend) {
|
|
//echo $bytessent.'<br>';
|
|
$result = socket_write($server,substr($query,$bytessent),$bytestosend-$bytessent);
|
|
//echo 'Sent '.$result.' bytes<br>';
|
|
if ($result===FALSE)
|
|
return "ERROR: " . socket_strerror(socket_last_error());
|
|
$bytessent += $result;
|
|
}
|
|
|
|
/* --- Idle for a while until recieved bytes from game server --- */
|
|
$result = socket_read($server, 10000, PHP_BINARY_READ);
|
|
socket_close($server); // we don't need this anymore
|
|
|
|
if($result != "") {
|
|
if($result{0} == "\x00" || $result{1} == "\x83") { // make sure it's the right packet format
|
|
|
|
// Actually begin reading the output:
|
|
$sizebytes = unpack('n', $result{2} . $result{3}); // array size of the type identifier and content
|
|
$size = $sizebytes[1] - 1; // size of the string/floating-point (minus the size of the identifier byte)
|
|
|
|
if($result{4} == "\x2a") { // 4-byte big-endian floating-point
|
|
$unpackint = unpack('f', $result{5} . $result{6} . $result{7} . $result{8}); // 4 possible bytes: add them up together, unpack them as a floating-point
|
|
return $unpackint[1];
|
|
}
|
|
else if($result{4} == "\x06") { // ASCII string
|
|
$unpackstr = ""; // result string
|
|
$index = 5; // string index
|
|
|
|
while($size > 0) { // loop through the entire ASCII string
|
|
$size--;
|
|
$unpackstr .= $result{$index}; // add the string position to return string
|
|
$index++;
|
|
}
|
|
return $unpackstr;
|
|
}
|
|
}
|
|
}
|
|
return "";
|
|
}
|
|
?>
|