Skip to content

Instantly share code, notes, and snippets.

@flashwave
Last active August 1, 2021 06:27
Show Gist options
  • Save flashwave/3b9ad937db727d3dc9640dc70107e98d to your computer and use it in GitHub Desktop.
Save flashwave/3b9ad937db727d3dc9640dc70107e98d to your computer and use it in GitHub Desktop.
<?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