commit b884f2856954482876bfb4804f0ff15e41197550 Author: flashwave Date: Sun Jul 3 22:07:00 2022 +0000 Initial import diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..176a458 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f61408c --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/.debug +/config/config.ini diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..19f950d --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "lib/index"] + path = lib/index + url = https://github.com/flashwave/index.git diff --git a/lib/index b/lib/index new file mode 160000 index 0000000..8a5423f --- /dev/null +++ b/lib/index @@ -0,0 +1 @@ +Subproject commit 8a5423fea397e2f2adca0b9f46d1e5c21fd13c44 diff --git a/mince.php b/mince.php new file mode 100644 index 0000000..c2bea0b --- /dev/null +++ b/mince.php @@ -0,0 +1,46 @@ +Unable to connect to database'; + die($ex->getMessage()); +} + +Remote::setUrl($config['remote_url']); +Remote::setSecret($config['remote_secret']); + +if(empty($_COOKIE['mc_random'])) { + $sVerification = Utils::generatePassKey(32); + setcookie('mc_random', $sVerification, strtotime('1 day'), '/', $_SERVER['HTTP_HOST']); +} else + $sVerification = (string)filter_input(INPUT_COOKIE, 'mc_random'); + +$sVerification = hash('sha256', $sVerification); + +// replace this with id.flashii.net shit +$userInfo = ChatAuth::attempt($db, $config['chat_endpoint'], $config['chat_secret'], (string)filter_input(INPUT_COOKIE, 'msz_auth')); diff --git a/public/assets/bg_main.png b/public/assets/bg_main.png new file mode 100644 index 0000000..e4abed3 Binary files /dev/null and b/public/assets/bg_main.png differ diff --git a/public/assets/bg_top.png b/public/assets/bg_top.png new file mode 100644 index 0000000..22e39d0 Binary files /dev/null and b/public/assets/bg_top.png differ diff --git a/public/assets/dirt-x4.png b/public/assets/dirt-x4.png new file mode 100644 index 0000000..3d1e0f8 Binary files /dev/null and b/public/assets/dirt-x4.png differ diff --git a/public/assets/minecraft.eot b/public/assets/minecraft.eot new file mode 100644 index 0000000..db6b255 Binary files /dev/null and b/public/assets/minecraft.eot differ diff --git a/public/assets/minecraft.svg b/public/assets/minecraft.svg new file mode 100644 index 0000000..0b7a6c8 --- /dev/null +++ b/public/assets/minecraft.svg @@ -0,0 +1,347 @@ + + + + +Created by FontForge 20090622 at Sun Nov 12 10:48:06 2017 + By ffonts +Copyright \(c\) 2017 by Jacob Debono. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/assets/minecraft.woff b/public/assets/minecraft.woff new file mode 100644 index 0000000..d709833 Binary files /dev/null and b/public/assets/minecraft.woff differ diff --git a/public/assets/weblogo.png b/public/assets/weblogo.png new file mode 100644 index 0000000..5ef4735 Binary files /dev/null and b/public/assets/weblogo.png differ diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..522b384 Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/index.php b/public/index.php new file mode 100644 index 0000000..3ad7c11 --- /dev/null +++ b/public/index.php @@ -0,0 +1,244 @@ +lap('httpfx', 'HttpFx Created'); + +$loginUrl = $config['login_url']; + +$router->setDefaultErrorHandler(function($response, $request, $code, $text) use ($loginUrl, $userInfo) { + $body = HTML::getHeader($userInfo, $loginUrl, sprintf('%03d %s', $code, $text)); + $body .= '
'; + $body .= sprintf('

HTTP %03d

', $code); + $body .= sprintf('

%s

', $text); + $body .= '
'; + $body .= HTML::getFooter(); + $response->setContent($body); +}); + +$router->use('/', function($response) use ($timing) { + $response->setPoweredBy('Mince+Index'); + $response->setServerTiming($timing); +}); + +$router->get('/index.php', function($response) { + $response->redirect('/', true); +}); +$router->get('/map.php', function($response) { + $response->redirect('/maps/survival', true); +}); + +$router->get('/', function($response, $request) use ($db, $loginUrl, $userInfo, $sVerification) { + $name = (string)$request->getParam('name'); + $error = (string)$request->getParam('error'); + + if(!empty($error) && ctype_lower($error)) { + $errors = [ + 'request' => ['Invalid request type.', 'Try to reload the page and try again.'], + 'verify' => ['Request verification failed.', 'Try to reload the page and try again.'], + 'itainthappenin' => ['Haha', 'No'], + 'short' => ['Invalid username', 'The provided name is too short.'], + 'long' => ['Invalid username', 'The provided name is too long.'], + 'invalid' => ['Invalid username', 'The provided name contains invalid characters.'], + 'conflict' => ['Username conflict', 'This username is already whitelisted with someone, contact flashwave if this is unexpected.'], + 'connect' => ['Failed to connect to the server', 'The server is probably offline, pope flashwave if this is not expected.'], + 'not-listed' => ['You have not been whitelisted yet', 'Add yourself to the whitelist before trying to remove yourself from it.'], + ]; + + if(array_key_exists($error, $errors)) { + $mErrorTitle = $errors[$error][0]; + $mErrorComment = $errors[$error][1]; + } else { + $mErrorTitle = 'Unexpected response from server'; + $mErrorComment = $error; + } + } + + $body = HTML::getHeader($userInfo, $loginUrl); + + if(!empty($mErrorTitle)) { + $body .= '
'; + $body .= sprintf('

%s

', $mErrorTitle); + $body .= sprintf('

%s

', $mErrorComment ?? 'No further details provided.'); + $body .= '
'; + } + + if($userInfo->success) { + if($userInfo->mc_whitelisted < 1) { + $body .= '
'; + $body .= '

Add to Whitelist

'; + $body .= '

This will give you access to the server.

'; + $body .= '
'; + $body .= sprintf(' ', $sVerification); + $body .= ' '; + $body .= ' '; + $body .= '
'; + $body .= '
'; + } + + $body .= '
'; + $body .= '

Server address

'; + $body .= '

Down currently because I don\'t have the resources to run multiple games at once yet.

'; + //$body .= '
mc.flashii.net
'; + //$body .= '

An SRV record is used, so no port needs to be specified.

'; + $body .= '

We recommend you use MultiMC/PolyMC for easy version and mod management and Adoptium Java 17 to actually be able to launch the game. Other launchers work completely fine though.

'; + $body .= '
'; + + $body .= '
'; + $body .= '

Bedrock versions

'; + $body .= '

Through the black magic bestowed upon us by GeyserMC it\'s possible to play on the server through any of the updated Bedrock versions of Minecraft.

'; + $body .= '

This should allow you to play on the server from a phone, a tablet or a console, provided you also have an account for the original version of the game.

'; + $body .= '

You will need to link your Minecraft and Bedrock accounts, you can do this by connecting to link.geysermc.org in both versions of the game and following the on-screen instructions.

'; + $body .= '

Unfortunately, the Bedrock versions of the game don\'t support SRV records, so you\'ll have to join with a different address:

'; + //$body .= '
mcb.flashii.net:19132
'; + $body .= '

Also down, lol!

'; + $body .= '
'; + + if($userInfo->mc_whitelisted > 0) { + $body .= '
'; + $body .= '

Remove from Whitelist

'; + $body .= '

This will revoke your access to the server.

'; + $body .= sprintf('

You are currently whitelisted as %s on %s.

', $userInfo->mc_username, date('Y-m-d H:i:s T', $userInfo->mc_whitelisted)); + $body .= '
'; + $body .= sprintf(' ', $sVerification); + $body .= ' '; + $body .= '
'; + $body .= '
'; + } + + $body .= '
'; + $body .= '

All Of Fabric 5 (1.3.0)

'; + $body .= '
aof.flashii.net
'; + $body .= '

An SRV record is used, so no port needs to be specified.

'; + $body .= '

There\'s a pack for MultiMC/PolyMC somewhere and you\'ll probably need Adoptium Java 17 to actually be able to launch the game. Other launchers work completely fine though, probably.

'; + $body .= '

This server doesn\'t currently use a whitelist, so you don\'t have to worry about that.

'; + $body .= '
'; + } else { + $body .= '
'; + $body .= '

You must be logged in to use this website!

'; + $body .= '

This website allows you to whitelist yourself on our Minecraft server, for which you need to be logged in.

'; + $body .= '

So it doesn\'t make sense to display the details either.

'; + $body .= '
'; + } + + $body .= '
'; + $body .= '

Rules

'; + $body .= '

1. Observe Global Rules.

'; + $body .= '

2. Don\'t be an asshole.

'; + $body .= '

3. Don\'t flood.

'; + $body .= '
'; + + $body .= HTML::getFooter(); + + return $body; +}); + +$router->get('/maps', function($response) { + $response->redirect('/maps/survival'); +}); +$router->get('/maps/survival', function($response) use ($loginUrl, $userInfo) { + $body = HTML::getHeader($userInfo, $loginUrl); + + $body .= '
'; + $body .= ' '; + $body .= '
'; + + $body .= HTML::getFooter(); + + return $body; +}); + +$router->use('/whitelist', function($response, $request) use ($sVerification) { + if(!$request->isFormContent()) { + $response->redirect('/?error=request'); + return true; + } + + $body = $request->getContent(); + + if(!$body->hasParam('boob') || !hash_equals($sVerification, (string)$body->getParam('boob'))) { + $response->redirect('/?error=verify'); + return true; + } +}); + +$router->post('/whitelist/add', function($response, $request) use ($db, $userInfo) { + if($userInfo->user_id == 45) { + $response->redirect('/?error=itainthappenin'); + return true; + } + + $body = $request->getContent(); + $name = (string)$body->getParam('name'); + $resp = Whitelist::add($db, $userInfo, $name); + + if($resp === '') + $response->redirect('/'); + else { + if($resp === 'invalid') + $name = ''; + $response->redirect("/?error={$resp}&name={$name}"); + } +}); + +$router->post('/whitelist/remove', function($response) use ($db, $userInfo) { + $resp = Whitelist::remove($db, $userInfo); + + if($resp === '') + $response->redirect('/'); + else + $response->redirect("/?error={$resp}"); +}); + +$router->get('/status', function($response) { + $response->redirect('/status/survival'); +}); + +$router->get('/status/survival', function() { + return 'todo: make something here'; +}); + +$router->get('/status/survival.json', function() { + return ServerQuery::create('mc.flashii.net')->stats(); +}); + +$router->get('/status/survival.png', function($response) { + $stats = ServerQuery::create('mc.flashii.net')->stats(); + + $image = new \Imagick; + $image->newImage(100, 100, 'black', 'png'); + + $draw = new \ImagickDraw; + $draw->setFillColor('white'); + + $image->annotateImage($draw, 10, 10, 0, $stats->motd); + + $response->setContentType('image/png'); + $response->setContent((string)$image); + + $image->destroy(); +}); + +$router->get('/errors/:code', function($res, $req, $code) { + $code = intval($code); + if($code < 100 || $code >= 600) + $code = 400; + return $code; +}); + +$timing->lap('routes'); + +$router->dispatch(); diff --git a/public/mince.css b/public/mince.css new file mode 100644 index 0000000..7132715 --- /dev/null +++ b/public/mince.css @@ -0,0 +1,157 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; + position: relative; + outline-style: none; +} + +a, a:active, a:visited { + color: #aaf; + text-decoration: none; +} + +html, +body, +.wrapper { + width: 100%; + height: 100%; +} + +body { + background: #111 url('/assets/bg_main.png'); + color: #e0d0d0; + font: 13px/1.4 'Helvetica Neue', sans-serif; +} + +.wrapper {} + +.header { + background: #211710 url('/assets/bg_top.png'); + height: 48px; + overflow: hidden; +} +.header-inner { + max-width: 1000px; + width: 100%; + margin: 0 auto; + display: flex; + align-items: center; +} +.header-fat { + flex: 1 1 auto; +} +.header-logo { + flex: 0 0 auto; +} +.header-logo img { + vertical-align: bottom; +} +.header-user { + flex: 0 0 auto; + font-size: 16px; + font-weight: 700; + text-decoration: none; +} + +.footer { + color: #444; + height: 26px; + width: 100%; + text-align: center; + padding-top: 14px; +} + +.content { + max-width: 1000px; + width: 100%; + margin: 0 auto; + margin-top: 1em; + padding: 20px 0 40px; +} + +.details p { + margin: .2em 0; +} +.details pre { + font-size: 1.5em; +} + +.error { + margin-bottom: 20px; + color: #f33; +} + +.whitelist { + margin-bottom: 20px; +} +.whitelist input[type="submit"] { + font-size: 1.5em; + margin: 5px; + border-radius: 4px; + padding: 5px 10px; + border: 1px solid #2a2; + background-color: #252; + color: #cfc; + transition: background-color .2s; +} +.whitelist input[type="submit"]:hover, +.whitelist input[type="submit"]:focus { + background-color: #272; +} +.whitelist input[type="submit"]:active { + background-color: #232; +} + +.unwhitelist { + margin-top: 20px; +} +.unwhitelist input[type="submit"] { + font-size: 1.5em; + margin: 5px; + border-radius: 4px; + padding: 5px 10px; + border: 1px solid #a22; + background-color: #522; + color: #fcc; + transition: background-color .2s; +} +.unwhitelist input[type="submit"]:hover, +.unwhitelist input[type="submit"]:focus { + background-color: #722; +} +.unwhitelist input[type="submit"]:active { + background-color: #322; +} + +label { + margin-top: 1em; + display: block; +} +label .label-header { + text-transform: uppercase; + font-size: 10px; + color: #AAA; +} +label .label-input input { + padding: 2px; + display: block; + font-size: 18px; + margin-bottom: 2px; + max-width: 300px; + width: 100%; +} + +.rules { + margin-top: 20px; +} + +.bedrock { + margin-top: 20px; +} +.bedrock pre { + font-size: 1.5em; +} +.bedrock p { + margin: .2em 0; +} diff --git a/src/ChatAuth.php b/src/ChatAuth.php new file mode 100644 index 0000000..3e21dbb --- /dev/null +++ b/src/ChatAuth.php @@ -0,0 +1,76 @@ + 0) { + $loginRequest = [ + 'user_id' => $unpacked['user'], + 'token' => 'SESS:' . $cookie, + 'ip' => $_SERVER['REMOTE_ADDR'], + ]; + $loginSignature = hash_hmac('sha256', implode('#', $loginRequest), $secret); + + $login = curl_init($endPoint); + curl_setopt_array($login, [ + CURLOPT_AUTOREFERER => false, + CURLOPT_FAILONERROR => false, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_HEADER => false, + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => json_encode($loginRequest), + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TCP_FASTOPEN => true, + CURLOPT_CONNECTTIMEOUT => 2, + CURLOPT_MAXREDIRS => 2, + CURLOPT_PROTOCOLS => CURLPROTO_HTTPS, + CURLOPT_TIMEOUT => 5, + CURLOPT_USERAGENT => 'mc.flashii.net', + CURLOPT_HTTPHEADER => [ + 'Content-Type: application/json', + 'X-SharpChat-Signature: ' . $loginSignature, + ], + ]); + $userInfo = json_decode(curl_exec($login)); + curl_close($login); + } + } + + if(empty($userInfo->success)) { + $userInfo = new stdClass; + $userInfo->success = false; + $userInfo->user_id = 0; + $userInfo->username = 'Anonymous'; + $userInfo->colour_raw = 0x40000000; + $userInfo->rank = 0; + $userInfo->hierarchy = 0; + $userInfo->is_silenced = '1970-01-01T00:00:00+00:00'; + $userInfo->perms = 0; + $userInfo->mc_username = null; + $userInfo->mc_whitelisted = 0; + } else { + $getWhitelist = $db->prepare('SELECT `minecraft_username`, UNIX_TIMESTAMP(`whitelist_added`) AS `whitelist_added` FROM `whitelist_2022` WHERE `flashii_id` = ?'); + $getWhitelist->addParameter(1, $userInfo->user_id); + $getWhitelist->execute(); + $whitelist = $getWhitelist->getResult(); + + if($whitelist->next()) { + $userInfo->mc_username = $whitelist->getString(0); + $userInfo->mc_whitelisted = $whitelist->getInteger(1); + } else { + $userInfo->mc_username = null; + $userInfo->mc_whitelisted = 0; + } + } + + return $userInfo; + } +} diff --git a/src/HTML.php b/src/HTML.php new file mode 100644 index 0000000..d1da8e7 --- /dev/null +++ b/src/HTML.php @@ -0,0 +1,48 @@ +success) { + $userBar = 'Logged in as ' . $userInfo->username; + } else { + $userBar = 'Log in'; + } + + return << + + + + {$title} + + + +
+ +
+HTML; + } + + public static function getFooter(): string { + return << +
+ Flashwave 2022 | Site design "borrowed" from pre-Microsoft Mojang | "Minecraft" is a trademark of Mojang +
+
+ + +HTML; + } +} diff --git a/src/Remote.php b/src/Remote.php new file mode 100644 index 0000000..cd83d28 --- /dev/null +++ b/src/Remote.php @@ -0,0 +1,45 @@ + false, + CURLOPT_FAILONERROR => false, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_HEADER => false, + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => [ + 'm' => $mode, + 's' => $sign, + 'n' => $name, + ], + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TCP_FASTOPEN => true, + CURLOPT_CONNECTTIMEOUT => 2, + CURLOPT_MAXREDIRS => 2, + CURLOPT_PROTOCOLS => CURLPROTO_HTTP | CURLPROTO_HTTPS, + CURLOPT_TIMEOUT => 5, + CURLOPT_USERAGENT => 'mc.flashii.net', + ]); + $response = curl_exec($request); + curl_close($request); + + return $response; + } +} diff --git a/src/ServerQuery.php b/src/ServerQuery.php new file mode 100644 index 0000000..8af8c2b --- /dev/null +++ b/src/ServerQuery.php @@ -0,0 +1,166 @@ +addr = (string)$addr; + $this->port = $port; + $this->sessionId = random_int(0, 0x7FFFFFFF) & 0x0F0F0F0F; + $this->socket = socket_create($addr->isV6() ? AF_INET6 : AF_INET, SOCK_DGRAM, SOL_UDP); + $this->handshake(); + } + + public function __destruct() { + socket_close($this->socket); + } + + public function handshake(): void { + $response = $this->send(9); + $length = strlen($response); + $token = ''; + + for($i = 0; $i < $length; ++$i) { + $char = $response[$i]; + if($char === "\0") + break; + $token .= $char; + } + + $this->challengeToken = intval($token); + } + + public function stats(): object { + $response = $this->send(0, pack('N', $this->challengeToken)); + + $offset = 0; + $data = new stdClass; + + $data->motd = self::readString($response, $offset); + $data->gametype = self::readString($response, $offset); + $data->map = self::readString($response, $offset); + $data->numplayers = self::readString($response, $offset); + $data->maxplayers = self::readString($response, $offset); + $data->hostport = unpack('v', substr($response, $offset, 2))[1]; + $offset += 2; + $data->hostip = self::readString($response, $offset); + + return $data; + } + + private static function readString(string $source, int &$offset): string { + $length = strlen($source); + $string = ''; + + for(; $offset < $length; ++$offset) { + $char = $source[$offset]; + if($char === "\0") + break; + $string .= $char; + } + + ++$offset; + + return $string; + } + + private function send(int $type, string $payload = ''): string { + $payload = "\xFE\xFD" . pack('CN', $type, $this->sessionId) . $payload; + socket_sendto($this->socket, $payload, strlen($payload), 0, $this->addr, $this->port); + socket_recv($this->socket, $response, 1024, MSG_WAITALL); + + $data = unpack('Ctype/Nsession', $response); + + if($data['type'] !== $type) + throw new RuntimeException('Type does not match.'); + if($data['session'] !== $this->sessionId) + throw new RuntimeException('Session id does not match.'); + + return substr($response, 5); + } + + public static function create(AString|string $endPoint): ServerQuery { + $endPoint = AString::cast($endPoint); + $firstChar = $endPoint[0]; + + if($firstChar === '[') { // IPv6 + if($endPoint->contains(']:')) + $endPoint = IPEndPoint::parse($endPoint); + else + $endPoint = new IPEndPoint(IPAddress::parse($endPoint->trim('[]')), self::PORT); + + return new ServerQuery($endPoint->getAddress(), $endPoint->getPort()); + } elseif(is_numeric($firstChar)) { // IPv4 + if($endPoint->contains(':')) + $endPoint = IPEndPoint::parse($endPoint); + else + $endPoint = new IPEndPoint(IPAddress::parse($endPoint), self::PORT); + + return new ServerQuery($endPoint->getAddress(), $endPoint->getPort()); + } else { // DNS + if($endPoint->contains(':')) + $endPoint = DnsEndPoint::parse($endPoint); + else { + $endPoint = new DnsEndPoint($endPoint, self::PORT); + + $records = dns_get_record('_minecraft._tcp.' . (string)$endPoint->getHost(), DNS_SRV); + + if(!empty($records)) { + usort($records, function($a, $b) { + $priority = $a['pri'] <=> $b['pri']; + if($priority !== 0) + return $priority; + $weight = $a['weight'] <=> $b['weight']; + if($weight !== 0) + return $priority; + return 0; + }); + + foreach($records as $record) { + try { + return ServerQuery::create($record['target'] . ':' . $record['port']); + } catch(Exception $ex) {} + } + } + } + + $records = dns_get_record((string)$endPoint->getHost(), DNS_A); + + if(!empty($records)) { + foreach($records as $record) { + try { + return new ServerQuery(IPAddress::parse($record['ip']), $endPoint->getPort()); + } catch(Exception $ex) {} + } + } + + $records = dns_get_record((string)$endPoint->getHost(), DNS_AAAA); + + if(!empty($records)) { + foreach($records as $record) { + try { + return new ServerQuery(IPAddress::parse($record['ipv6']), $endPoint->getPort()); + } catch(Exception $ex) {} + } + } + } + + throw new RuntimeException('Failed to connect.'); + } +} diff --git a/src/Utils.php b/src/Utils.php new file mode 100644 index 0000000..807f6e4 --- /dev/null +++ b/src/Utils.php @@ -0,0 +1,14 @@ + 16) + return 'long'; + if(!preg_match('#^([a-zA-Z0-9_]{3,16})$#', $userName)) + return 'invalid'; + + $dupeCheck = $db->prepare('SELECT COUNT(`flashii_id`) > 0 FROM `whitelist_2022` WHERE `minecraft_username` = ?'); + $dupeCheck->addParameter(1, $userName, DbType::STRING); + $dupeCheck->execute(); + $dupeResult = $dupeCheck->getResult(); + + if(!$dupeResult->next()) + return 'dupefail'; + if($dupeResult->getInteger(0)) + return 'conflict'; + + if(!empty($userInfo->mc_whitelisted) || !empty($userInfo->mc_username)) { + $resp = self::remove($db, $userInfo); + if($resp !== '') + return $resp; + } + + $resp = Remote::call('wl:add', $userName); + + if($resp !== 'success') + return $resp; + + $insert = $db->prepare('INSERT INTO `whitelist_2022` (`flashii_id`, `minecraft_username`) VALUES (?, ?)'); + $insert->addParameter(1, $userInfo->user_id); + $insert->addParameter(2, $userName, DbType::STRING); + $insert->execute(); + + $userInfo->mc_username = $userName; + $userInfo->mc_whitelisted = time(); + + return ''; + } + + public static function remove(IDbConnection $db, object $userInfo): string { + if(empty($userInfo->mc_whitelisted) || empty($userInfo->mc_username)) + return 'not-listed'; + + $resp = Remote::call('wl:remove', $userInfo->mc_username); + + if($resp !== 'success') + return $resp; + + $delete = $db->prepare('DELETE FROM `whitelist_2022` WHERE `flashii_id` = ?'); + $delete->addParameter(1, $userInfo->user_id); + $delete->execute(); + + $userInfo->mc_username = null; + $userInfo->mc_whitelisted = 0; + + return ''; + } +}