<?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', '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);

	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_dismiss_changelog_review($payload){
	global $require_changelogs;
	global $no_changelog;

	if(!$require_changelogs)
		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);
			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,
		'Quality of Life' => 1,
		'Priority: High' => 15,
		'Priority: CRITICAL' => 20,
		'Unit Tests' => 6,
		'Logging' => 1,
		'Feedback' => 2,
		'Performance' => 12,
		'Atomic' => 2,
		'Feature' => -10,
		'Balance/Rebalance' => -8,
		'Sound' => 1,
		'Sprites' => 1,
		'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);
		}
	}

	return $maxNegative + $maxPositive;
}

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)) {
			if (count($currentchangelogblock) <= 0)
				continue;
			$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 'qol':
				if($item != 'made something easier to use') {
					$tags[] = 'Quality of Life';
					$currentchangelogblock[] = array('type' => 'qol', '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 mechanics or gameplay changes' && $item != 'Added more things') {
					$tags[] = 'Mechanic';
					$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 'expansion':
				if($item != 'Expands content of an existing feature'){
					$tags[] = 'Content Expansion';
					$currentchangelogblock[] = array('type' => 'expansion', '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;
			case 'tweak':
				if($item != 'tweaked a few things')
					$tags[] = 'Tweak';
					$currentchangelogblock[] = array('type' => 'tweak', '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 "";
}
?>
