Last active
August 1, 2021 06:27
-
-
Save flashwave/3b9ad937db727d3dc9640dc70107e98d to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/* | |
* (21:42:47) malloc_CDLVII: i think that's one of the two rules of Flash Wave Development | |
* (21:42:57) malloc_CDLVII: 1. if it is written, it will be rewritten | |
* (21:43:04) malloc_CDLVII: 2. it will have automatic updates | |
*/ | |
header('Content-Type: text/plain; charset=utf-8'); | |
define('ACL_NONE', 0x00); | |
define('ACL_CHANGELOG', 0x01); | |
define('ACL_MIGRATE', 0x02); | |
define('ACL_BROADCAST', 0x04); | |
define('ACL_FULL', 0xFF); | |
ini_set('display_errors', 'on'); | |
error_reporting(-1); | |
$config = parse_ini_file(__DIR__ . '/../cfg/github-callback.ini', true, INI_SCANNER_TYPED); | |
if (!$config) | |
die('config'); | |
define('WH_CONFIG', $config); | |
define('WEBHOOK_SECRET_KEYS', $config['github']['access']); | |
define('BOAT_INTERCOM_HOST', $config['boat']['host']); | |
define('BOAT_INTERCOM_PORT', $config['boat']['port']); | |
define('BOAT_INTERCOM_SECRET', $config['boat']['secret']); | |
function get_changelog_user(string $email): ?string { | |
return WH_CONFIG['changelog'][$email] ?? null; | |
} | |
function get_flashii_config() | |
{ | |
return parse_ini_file('/www/flashii.net/config/config.ini', true, INI_SCANNER_TYPED); | |
} | |
define('CL_ADD', 1); | |
define('CL_REM', 2); | |
define('CL_UPD', 3); | |
define('CL_FIX', 4); | |
define('CL_IMP', 5); | |
define('CL_REV', 6); | |
function strip_prefix(string $line): string | |
{ | |
$findColon = mb_strpos($line, ':'); | |
return trim($findColon === false || $findColon >= 10 ? $line : mb_substr($line, $findColon + 1)); | |
} | |
function get_changelog_action(string &$line): int | |
{ | |
$original = trim($line); | |
$line = strip_prefix($line); | |
$firstSix = mb_strtolower(mb_substr($original, 0, 6)); | |
if ($firstSix === 'revert') | |
return CL_REV; | |
if ($firstSix === 'import') | |
return CL_IMP; | |
$firstThree = mb_strtolower(mb_substr($original, 0, 3)); | |
if ($firstThree === 'add') | |
return CL_ADD; | |
if ($firstThree === 'fix') | |
return CL_FIX; | |
$firstFour = mb_strtolower(mb_substr($original, 0, 4)); | |
$firstEight = mb_strtolower(mb_substr($original, 0, 8)); | |
if ($firstSix === 'delete' || $firstSix === 'remove' || $firstFour === 'nuke' || $firstEight === 'dropkick') | |
return CL_REM; | |
return CL_UPD; | |
} | |
function repo_info($repo, $ref) { | |
global $config; | |
$info = []; | |
if (array_key_exists('repo:' . $repo, $config)) | |
$info = array_merge_recursive($info, $config['repo:' . $repo]); | |
if (array_key_exists('repo:' . $repo . '/' . $ref, $config)) | |
$info = array_merge_recursive($info, $config['repo:' . $repo . '/' . $ref]); | |
return $info; | |
} | |
function write($line = '', $fail = false) { | |
wlog($line); | |
echo trim($line) . "\r\n"; | |
if ($fail) { | |
http_response_code(500); | |
exit; | |
} | |
} | |
function setup_wlog($event, $id) { | |
global $cblog; | |
if (!isset($cblog)) | |
$cblog = fopen('../logs/github-' . $event . '-' . time() . '.txt', 'wb+'); | |
} | |
function wlog($text = '') { | |
global $cblog; | |
if (!isset($cblog)) | |
return; | |
fwrite($cblog, trim($text) . "\r\n"); | |
} | |
function run_to_wlog($cmd, $input = null) { | |
$process = proc_open($cmd, [['pipe', 'r'], ['pipe', 'w'], ['pipe', 'w']], $pipes); | |
wlog("RUNNING `{$cmd}`"); | |
if (is_resource($process)) { | |
if ($input !== null) | |
fwrite($pipes[0], $input); | |
fclose($pipes[0]); | |
wlog(); | |
wlog('==> STDOUT'); | |
wlog(stream_get_contents($pipes[1])); | |
fclose($pipes[1]); | |
wlog(); | |
wlog('==> STDERR'); | |
wlog(stream_get_contents($pipes[2])); | |
fclose($pipes[2]); | |
wlog(); | |
wlog('==> EXIT ' . proc_close($process)); | |
} else wlog('!!> UNABLE TO RUN'); | |
wlog(); | |
} | |
function acl_check($perm) { | |
return defined('ACL') && (ACL & $perm) > 0; | |
} | |
define('BOAT_INTERCOM_AVAILABLE', defined('BOAT_INTERCOM_HOST') && defined('BOAT_INTERCOM_PORT') && defined('BOAT_INTERCOM_SECRET')); | |
function send_to_boat($message) { | |
if (!BOAT_INTERCOM_AVAILABLE) | |
return; | |
global $intercom_sock; | |
if (!$intercom_sock) | |
$intercom_sock = fsockopen(BOAT_INTERCOM_HOST, BOAT_INTERCOM_PORT, $errno, $errstr, 5); | |
if (!$intercom_sock) | |
return; | |
$message = chr(0xF) . hash_hmac('sha256', $message, BOAT_INTERCOM_SECRET) . $message . chr(0xF); | |
fwrite($intercom_sock, $message); | |
fflush($intercom_sock); | |
} | |
function send_to_boat_write($text) { | |
if (!BOAT_INTERCOM_AVAILABLE) | |
return; | |
global $intercom_sock_msg; | |
if (!isset($intercom_sock_msg) || empty($intercom_sock_msg)) | |
$intercom_sock_msg = ''; | |
$intercom_sock_msg .= $text; | |
} | |
function send_to_boat_flush() { | |
if (!BOAT_INTERCOM_AVAILABLE) | |
return; | |
global $intercom_sock_msg; | |
if (!isset($intercom_sock_msg) || empty($intercom_sock_msg)) | |
return; | |
send_to_boat($intercom_sock_msg); | |
unset($intercom_sock_msg); | |
} | |
if (version_compare(PHP_VERSION, '7.2', '<')) | |
write('misconfigured server', true); | |
function starts_with($string, $text) { | |
return substr($string, 0, strlen($text)) === $text; | |
} | |
if ($_SERVER['REQUEST_METHOD'] !== 'POST') | |
write('no', true); | |
define('IS_PROTECTED', true);// defined('WEBHOOK_SECRET_KEYS') && count(WEBHOOK_SECRET_KEYS) > 0); | |
if (!defined('WEBHOOK_SECRET_KEYS')) | |
define('WEBHOOK_SECRET_KEYS', []); | |
$isGitea = isset($_SERVER['HTTP_X_GITEA_DELIVERY']) && isset($_SERVER['HTTP_X_GITEA_EVENT']); | |
if ((!$isGitea && !starts_with($_SERVER['HTTP_USER_AGENT'], 'GitHub-Hookshot/')) | |
|| !isset($_SERVER['HTTP_X_GITHUB_EVENT'], $_SERVER['HTTP_X_GITHUB_DELIVERY']) | |
|| !(IS_PROTECTED ? isset($_SERVER[$isGitea ? 'HTTP_X_GITEA_SIGNATURE' : 'HTTP_X_HUB_SIGNATURE']) : true)) | |
write('missing headers', true); | |
if ($_SERVER['HTTP_X_GITHUB_EVENT'] === 'release') | |
setup_wlog($_SERVER['HTTP_X_GITHUB_EVENT'], $_SERVER['HTTP_X_GITHUB_DELIVERY']); | |
$raw_data = file_get_contents('php://input'); | |
if (IS_PROTECTED) { | |
[$algo, $hash] = $isGitea ? ['sha256', $_SERVER['HTTP_X_GITEA_SIGNATURE']] : explode('=', $_SERVER['HTTP_X_HUB_SIGNATURE']); | |
foreach (WEBHOOK_SECRET_KEYS as $secret => $acl) | |
if (hash_hmac($algo, $raw_data, $secret) === $hash) { | |
define('ACL', $acl); | |
break; | |
} | |
if (!defined('ACL')) | |
write('verification failed', true); | |
} else define('ACL', /*ACL_FULL*/ ACL_NONE); | |
if (ACL === ACL_NONE) | |
write('deactivated secret key', true); | |
$data = json_decode($_SERVER['CONTENT_TYPE'] === 'application/x-www-form-urlencoded' ? $_POST['payload'] : $raw_data); | |
if (!$data) | |
write('body is corrupt', true); | |
switch ($_SERVER['HTTP_X_GITHUB_EVENT']) { | |
case 'ping': | |
write('pong! ACL: ' . ACL); | |
send_to_boat("ping received from {$data->repository->full_name} with ACL: " . ACL); | |
break; | |
case 'create': | |
write('create event received!'); | |
write(); | |
fastcgi_finish_request(); | |
switch ($data->ref_type) { | |
case 'tag': | |
if (acl_check(ACL_BROADCAST)) | |
send_to_boat("[b][url={$data->repository->html_url}]{$data->repository->full_name}[/url][/b]: [url={$data->sender->html_url}]{$data->sender->login}[/url] created tag [url={$data->repository->html_url}/releases/tag/{$data->ref}]{$data->ref}[/url]"); | |
break; | |
case 'branch': | |
if (acl_check(ACL_BROADCAST)) | |
send_to_boat("[b][url={$data->repository->html_url}]{$data->repository->full_name}[/url][/b]: [url={$data->sender->html_url}]{$data->sender->login}[/url] created branch [url={$data->repository->html_url}/tree/{$data->ref}]{$data->ref}[/url]"); | |
break; | |
case 'repository': | |
// doubt we'll ever get here | |
// TODO: test organisation level webhooks? | |
break; | |
} | |
break; | |
case 'delete': | |
write('delete event received!'); | |
write(); | |
fastcgi_finish_request(); | |
switch ($data->ref_type) { | |
case 'tag': | |
if (acl_check(ACL_BROADCAST)) | |
send_to_boat("[b][url={$data->repository->html_url}]{$data->repository->full_name}[/url][/b]: [url={$data->sender->html_url}]{$data->sender->login}[/url] deleted tag {$data->ref}"); | |
break; | |
case 'branch': | |
if (acl_check(ACL_BROADCAST)) | |
send_to_boat("[b][url={$data->repository->html_url}]{$data->repository->full_name}[/url][/b]: [url={$data->sender->html_url}]{$data->sender->login}[/url] deleted branch {$data->ref}"); | |
break; | |
case 'repository': | |
if (acl_check(ACL_BROADCAST)) | |
send_to_boat("[b]{$data->repository->full_name}[/b]: [url={$data->sender->html_url}]{$data->sender->login}[/url] deleted the repository :crying:"); | |
break; | |
} | |
break; | |
case 'release': | |
if (!acl_check(ACL_MIGRATE)) | |
break; | |
$repo_info = repo_info($data->repository->full_name, ':release'); | |
$local_dirs = !empty($repo_info['prerelease_dir']) && is_array($repo_info['prerelease_dir']) ? $repo_info['prerelease_dir'] : []; | |
if (!$data->release->prerelease && !empty($repo_info['release_dir']) && is_array($repo_info['release_dir'])) { | |
$local_dirs = array_merge($local_dirs, $repo_info['release_dir']); | |
} | |
$local_dirs = array_unique($local_dirs, SORT_REGULAR); | |
if (count($local_dirs) < 1) | |
break; | |
wlog('Updating local repositories...'); | |
foreach ($local_dirs as $local_path) { | |
$self_path = getcwd(); | |
if (!chdir($local_path)) { | |
wlog('failed to switch to correct local path', true); | |
continue; | |
} | |
if (array_key_exists('before_pull', $repo_info)) { | |
wlog('Running `before_pull` commands...'); | |
foreach ($repo_info['before_pull'] as $command) | |
run_to_wlog($command); | |
} | |
if (array_key_exists('dont_run_purge', $repo_info) && $repo_info['dont_run_purge'] !== true) | |
run_to_wlog('git reset HEAD --hard'); | |
run_to_wlog('git fetch'); | |
run_to_wlog('git checkout tags/' . $data->release->tag_name); | |
if (array_key_exists('after_pull', $repo_info)) { | |
wlog('Running `after_pull` commands...'); | |
foreach ($repo_info['after_pull'] as $command) | |
run_to_wlog($command); | |
} | |
wlog(); | |
if (!chdir($self_path)) | |
wlog('Failed to return to home directory.', true); | |
wlog('Done!'); | |
} | |
break; | |
case 'push': | |
write('push event received!'); | |
write(); | |
fastcgi_finish_request(); | |
$commit_count = count($data->commits); | |
if ($commit_count < 1) | |
break; | |
/*if (acl_check(ACL_CHANGELOG) && $data->ref === 'refs/heads/master') { | |
$flashii_config = get_flashii_config(); | |
if (!empty($flashii_config['Database'])) { | |
$info = $flashii_config['Database']; | |
$dsn = 'mysql:'; | |
if ($info['unix_socket'] ?? false) { | |
$dsn .= 'unix_socket=' . $info['unix_socket'] . ';'; | |
} else { | |
$dsn .= 'host=' . ($info['host'] ?? '127.0.0.1') . ';'; | |
$dsn .= 'port=' . intval($info['port'] ?? 3306) . ';'; | |
} | |
$dsn .= 'charset=' . ($info['charset'] ?? 'utf8mb4') . ';'; | |
$dsn .= 'dbname=' . ($info['database'] ?? 'misuzu') . ';'; | |
$flashii = new PDO( | |
$dsn, | |
($info['username'] ?? null), | |
($info['password'] ?? null), | |
[ | |
PDO::ATTR_CASE => PDO::CASE_NATURAL, | |
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, | |
PDO::ATTR_ORACLE_NULLS => PDO::NULL_NATURAL, | |
PDO::ATTR_STRINGIFY_FETCHES => false, | |
PDO::ATTR_EMULATE_PREPARES => false, | |
PDO::MYSQL_ATTR_INIT_COMMAND => " | |
SET SESSION | |
sql_mode='STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION', | |
time_zone = '+00:00'; | |
" | |
]); | |
} | |
}*/ | |
if (acl_check(ACL_BROADCAST)) | |
send_to_boat_write("[b]{$data->repository->full_name}:" . substr($data->ref, strrpos($data->ref, '/') + 1) . "[/b] {$commit_count} new commit" . ($commit_count === 1 ? '' : 's') . "\r\n"); | |
if (!empty($flashii)) { | |
$flashiiChangelogAdd = $flashii->prepare(' | |
INSERT INTO `msz_changelog_changes` | |
( | |
`change_log`, `change_text`, `change_action`, | |
`user_id`, `change_created` | |
) | |
VALUES | |
(:log, :text, :action, :user, :created) | |
'); | |
$flashiiChangelogTag = $flashii->prepare(' | |
REPLACE INTO `msz_changelog_change_tags` | |
VALUES (:change_id, :tag_id) | |
'); | |
$changelogTags = repo_info($data->repository->full_name, $data->ref)['changelog_tags'] ?? []; | |
} | |
// update (public) changelog | |
foreach ($data->commits as $commit) { | |
$timestamp = strtotime($commit->timestamp); | |
$commit->message = trim($commit->message); | |
wlog("[{$commit->timestamp}] {$commit->author->email}: {$commit->message}"); | |
if (!empty($flashiiChangelogAdd) | |
&& mb_strpos($commit->message, $data->repository->full_name) === false | |
&& mb_substr($commit->message, 0, 2) !== '//' | |
&& mb_strpos(mb_strtolower($commit->message), "merge pull request") === false) { | |
$index = mb_strpos($commit->message, "\n"); | |
$line = $index === false ? $commit->message : mb_substr($commit->message, 0, $index); | |
$body = trim($index === false ? null : mb_substr($commit->message, $index + 1)); | |
$flashiiChangelogAdd->bindValue('user', get_changelog_user($commit->author->email)); | |
$flashiiChangelogAdd->bindValue('action', get_changelog_action($line)); | |
$flashiiChangelogAdd->bindValue('log', $line); | |
$flashiiChangelogAdd->bindValue('text', empty($body) ? null : $body); | |
$flashiiChangelogAdd->bindValue('created', date('Y-m-d H:i:s', $timestamp)); | |
$flashiiChangelogAdd->execute(); | |
$changelogId = $flashii->lastInsertId(); | |
if (!empty($changelogTags) && !empty($changelogId)) { | |
$flashiiChangelogTag->bindValue('change_id', $changelogId); | |
foreach ($changelogTags as $tag) { | |
$flashiiChangelogTag->bindValue('tag_id', $tag); | |
$flashiiChangelogTag->execute(); | |
} | |
} | |
unset($changelogId, $tag); | |
} | |
if (acl_check(ACL_BROADCAST)) | |
send_to_boat_write("[b][url={$commit->url}][code]" . substr($commit->id, 0, 7) . "[/code][/url][/b] {$commit->message} - [url=" . ($isGitea ? 'https://git.flash.moe/' : 'https://github.com/') . "{$commit->author->username}]{$commit->author->username}[/url]\r\n"); | |
} | |
send_to_boat_flush(); | |
if (acl_check(ACL_MIGRATE)) { | |
$repo_info = repo_info($data->repository->full_name, $data->ref); | |
if (array_key_exists('local_dir', $repo_info) | |
&& file_exists($repo_info['local_dir']) | |
&& is_dir($repo_info['local_dir'])) { | |
wlog('Updating local repository...'); | |
$local_path = $repo_info['local_dir']; | |
$self_path = getcwd(); | |
if (!chdir($local_path)) | |
wlog('failed to switch to correct local path', true); | |
if (array_key_exists('before_pull', $repo_info)) { | |
wlog('Running `before_pull` commands...'); | |
foreach ($repo_info['before_pull'] as $command) | |
run_to_wlog($command); | |
} | |
if (array_key_exists('dont_run_purge', $repo_info) && $repo_info['dont_run_purge'] !== true) | |
run_to_wlog('git reset HEAD --hard'); | |
run_to_wlog('git pull'); | |
if (array_key_exists('after_pull', $repo_info)) { | |
wlog('Running `after_pull` commands...'); | |
foreach ($repo_info['after_pull'] as $command) | |
run_to_wlog($command); | |
} | |
wlog(); | |
if (!chdir($self_path)) | |
wlog('Failed to return to home directory.', true); | |
wlog('Done!'); | |
} | |
} | |
break; | |
case 'status': | |
if (acl_check(ACL_BROADCAST)) { | |
$status_colour['failure'] = '#cb2431'; | |
//$status_colour['pending'] = '#b5ab86'; | |
$status_colour['success'] = '#28a745'; | |
$status_colour['error'] = '#586069'; | |
$status_emotes['failure'] = [':angry:', ':angrier:', ':angriest:', ':sad:', ':ouch:', ':dizzy:', ':sweat:', ':fat:', ':jew:']; | |
$status_emotes['success'] = [':happy:', ':lol:', ':yay:']; | |
$status_emotes['error'] = [':blank:']; | |
$status_name['continuous-integration/travis-ci/push'] = 'Unit Tests'; | |
$status_name['continuous-integration/styleci/push'] = 'Style Check'; | |
if (!array_key_exists($data->context, $status_name) || !array_key_exists($data->state, $status_colour)) { | |
//send_to_boat("/msg flash {$data->context}: {$data->state}"); | |
break; | |
} | |
send_to_boat("[b][url={$data->commit->html_url}]{$data->repository->full_name}[/url] [url={$data->target_url}]" . $status_name[$data->context] . '[/url][/b]: [b][i][color=' . $status_colour[$data->state] . ']' . ucfirst($data->state) . ' ' . $status_emotes[$data->state][array_rand($status_emotes[$data->state])] . '[/color][/i][/b]'); | |
} | |
break; | |
case 'watch': | |
if (!acl_check(ACL_BROADCAST)) | |
break; | |
switch ($data->action) { | |
case 'started': | |
send_to_boat("[url={$data->sender->html_url}]{$data->sender->login}[/url] starred [url={$data->repository->html_url}]{$data->repository->full_name}[/url] :love:"); | |
break; | |
} | |
break; | |
case 'fork': | |
if (!acl_check(ACL_BROADCAST)) | |
break; | |
send_to_boat("[url={$data->forkee->html_url}]{$data->forkee->owner->login}[/url] forked [url={$data->repository->html_url}]{$data->repository->full_name}[/url] :omg:"); | |
break; | |
case 'issues': | |
if (!acl_check(ACL_BROADCAST)) | |
break; | |
if (!in_array($data->action, ['opened', /*'edited',*/ 'closed', 'reopened'])) | |
break; | |
send_to_boat("[b][url={$data->repository->html_url}]{$data->repository->full_name}[/url] [url={$data->sender->html_url}]{$data->sender->login}[/url] {$data->action} issue [/b][url={$data->issue->html_url}][b]#{$data->issue->number}[/b]: {$data->issue->title}[/url]"); | |
break; | |
case 'pull_request': | |
if (!acl_check(ACL_BROADCAST)) | |
break; | |
if (!in_array($data->action, ['opened', /*'edited',*/ 'closed', 'reopened'])) | |
break; | |
send_to_boat("[b][url={$data->repository->html_url}]{$data->repository->full_name}[/url]: [url={$data->sender->html_url}]{$data->sender->login}[/url] {$data->action} pull request [/b][url={$data->pull_request->html_url}][b]#{$data->pull_request->number}[/b]: {$data->pull_request->title}[/url]"); | |
break; | |
default: | |
write('unhandled event: ' . $_SERVER['HTTP_X_GITHUB_EVENT']); | |
//send_to_boat("/msg flash X-GitHub-Event: {$_SERVER['HTTP_X_GITHUB_EVENT']}"); | |
break; | |
} | |
if (isset($cblog)) | |
fflush($cblog); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment