diff --git a/tools/github_webhook_processor.php b/tools/github_webhook_processor.php new file mode 100644 index 000000000000..26a08418d693 --- /dev/null +++ b/tools/github_webhook_processor.php @@ -0,0 +1,345 @@ +getLine()}: " . htmlSpecialChars($e->getMessage()); + die(); +}); +$rawPost = NULL; +if ($hookSecret !== NULL) { + 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.'); + } +} +if (!isset($_SERVER['HTTP_CONTENT_TYPE'])) { + throw new \Exception("Missing HTTP 'Content-Type' header."); +} elseif (!isset($_SERVER['HTTP_X_GITHUB_EVENT'])) { + throw new \Exception("Missing HTTP 'X-Github-Event' header."); +} +switch ($_SERVER['HTTP_CONTENT_TYPE']) { + 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: $_SERVER[HTTP_CONTENT_TYPE]"); +} +# 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; + 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 handle_pr($payload) { + $action = 'opened'; + switch ($payload["action"]) { + case 'opened': + case 'reopened': + $action = $payload['action']; + break; + case 'closed': + if (!$payload['pull_request']['merged']) { + $action = 'closed'; + } + else { + $action = 'merged'; + checkchangelog($payload, true); + } + break; + default: + return; + } + + if (strtolower(substr($payload['pull_request']['title'], 0, 3)) == '[s]') { + echo "PR Announcement Halted; Secret tag detected.\n"; + return; + } + + $msg = 'Pull Request '.$action.' by '.htmlSpecialChars($payload['sender']['login']).': '.htmlSpecialChars('#'.$payload['pull_request']['number'].' '.$payload['pull_request']['user']['login'].' - '.$payload['pull_request']['title']).''; + sendtoallservers('?announce='.urlencode($msg)); + +} + +function checkchangelog($payload, $merge = false) { + global $apiKey; + if (!$merge) + return; + 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']; + $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:') { + $incltag = true; + $foundcltag = true; + $pos = strpos($line, " "); + if ($pos) + $username = substr($line, $pos+1); + continue; + } else if (substr($line,0,5) == '/:cl:' || substr($line,0,6) == '/ :cl:' || substr($line,0,5) == ':/cl:') { + $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': + $currentchangelogblock[] = array('type' => 'bugfix', 'body' => $item); + break; + case 'wip': + $currentchangelogblock[] = array('type' => 'wip', 'body' => $item); + break; + case 'rsctweak': + case 'tweaks': + case 'tweak': + $currentchangelogblock[] = array('type' => 'tweak', 'body' => $item); + break; + case 'soundadd': + $currentchangelogblock[] = array('type' => 'soundadd', 'body' => $item); + break; + case 'sounddel': + $currentchangelogblock[] = array('type' => 'sounddel', 'body' => $item); + break; + case 'add': + case 'adds': + case 'rscadd': + $currentchangelogblock[] = array('type' => 'rscadd', 'body' => $item); + break; + case 'del': + case 'dels': + case 'rscdel': + $currentchangelogblock[] = array('type' => 'rscdel', 'body' => $item); + break; + case 'imageadd': + $currentchangelogblock[] = array('type' => 'imageadd', 'body' => $item); + break; + case 'imagedel': + $currentchangelogblock[] = array('type' => 'imagedel', 'body' => $item); + break; + case 'typo': + case 'spellcheck': + $currentchangelogblock[] = array('type' => 'spellcheck', 'body' => $item); + break; + case 'experimental': + case 'experiment': + $currentchangelogblock[] = array('type' => 'experiment', 'body' => $item); + break; + case 'tgs': + $currentchangelogblock[] = array('type' => 'tgs', '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)) + return; + + $file = 'author: '.$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 ( + 'message' => 'Automatic changelog generation for PR #'.$payload['pull_request']['number'], + 'content' => base64_encode($file) + ); + $scontext = array('http' => array( + 'method' => 'PUT', + 'header' => + "Content-type: application/json\r\n". + 'Authorization: token ' . $apiKey, + 'content' => json_encode($content), + 'ignore_errors' => true, + 'user_agent' => 'tgstation13.org-Github-Automation-Tools' + )); + $filename = '/html/changelogs/AutoChangeLog-pr-'.$payload['pull_request']['number'].'.yml'; + echo file_get_contents($payload['pull_request']['base']['repo']['url'].'/contents'.$filename, false, stream_context_create($scontext)); +} + +function sendtoallservers($str) { + global $servers; + foreach ($servers as $serverid => $server) { + if (isset($server['comskey'])) + $rtn = export($server['address'], $server['port'], $str.'&key='.$server['comskey']); + else + $rtn = export($server['address'], $server['port'], $str); + + echo "Server Number $serverid replied: $rtn\n"; + } +} + + + +function export($addr, $port, $str) { + global $error; + // 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)) { + $error = true; + return "ERROR"; + } + + + /* --- Send bytes to the server. Loop until all bytes have been sent --- */ + $bytestosend = strlen($query); + $bytessent = 0; + while ($bytessent < $bytestosend) { + //echo $bytessent.'
'; + $result = socket_write($server,substr($query,$bytessent),$bytestosend-$bytessent); + //echo 'Sent '.$result.' bytes
'; + if ($result===FALSE) die(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; + } + } + } + //if we get to this point, something went wrong; + $error = true; + return "ERROR"; +} +?>