Skip to content

Instantly share code, notes, and snippets.

@esterTion
Last active August 9, 2023 20:11
Show Gist options
  • Star 51 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save esterTion/c673a5e2547cd54c202f129babaf601d to your computer and use it in GitHub Desktop.
Save esterTion/c673a5e2547cd54c202f129babaf601d to your computer and use it in GitHub Desktop.
<?php
// https://gist.github.com/esterTion/c673a5e2547cd54c202f129babaf601d
/*
This code is now maintained by yojohanshinwataikei solely
esterTion has retired from this project
*/
chdir(__DIR__);
require_once __DIR__ . '/../webpthumb/Workerman-master/Autoloader.php';
use Workerman\Worker;
use Workerman\Protocols\Websocket;
use Workerman\Lib\Timer;
function execQuery($db, $query) {
$returnVal = [];
if (!$db) {
//throw new Exception('Invalid db handle');
return false;
}
$result = $db->query($query);
if ($result === false) {
//throw new Exception('Failed executing query: '. $query);
return [];
}
$returnVal = $result->fetchAll(PDO::FETCH_ASSOC);
return $returnVal;
}
define("ARCAEA_APP_VER", "3.0.5");
define("ARCAEA_APP_VER_FULL", "3.0.5.0");
define('API_VER', '12');
define('SERVER_ENDPOINT', 'https://arcapi.lowiro.com/coffee/');
function getRandomToken() {
$tokens = json_decode(file_get_contents('tokens.json'), true);
return $tokens[
(time()+rand(0, 10000)) % count($tokens)
];
}
$context = array(
'ssl' => array(
'local_cert' => '/etc/stunnel/biliplus.crt',
'local_pk' => '/etc/stunnel/biliplus.pem',
'verify_peer' => false,
'SNI_enabled' => true
)
);
// Create a Websocket server
$ws_worker = new Worker("websocket://0.0.0.0:616", $context);
$ws_worker->transport = 'ssl';
// 2 processes
$ws_worker->count = 4;
// Emitted when new connection come
$ws_worker->onConnect = function ($connection) {
$connection->closed = false;
$connection->msgTimeout = Timer::add(5, function()use($connection){
$connection->websocketType = Websocket::BINARY_TYPE_BLOB;
$connection->send('timeout');
$connection->close();
}, null, false);
$connection->onWebSocketConnect = function ($connection) {
$connection->remote_ip = $connection->getRemoteIp();
};
};
// Emitted when data received
$ws_worker->onMessage = function($connection, $data)
{
Timer::del($connection->msgTimeout);
// fetch constants
file_put_contents(__DIR__.'/arc.log', date('[m/d H:i:s] ').$connection->remote_ip."\t".$data."\n", FILE_APPEND);
$cmd = explode(' ', $data);
if (empty($cmd)) {
$connection->websocketType = Websocket::BINARY_TYPE_BLOB;
$connection->send('invalid cmd');
return $connection->close();
}
if ($cmd[0] === 'constants') {
$songlist = json_decode(file_get_contents('songlist'), true);
$songTitleData = [];
foreach ($songlist['songs'] as &$song) {
$songTitleData[$song['id']] = $song['title_localized'];
}
$connection->websocketType = Websocket::BINARY_TYPE_ARRAYBUFFER;
$connection->send(brotli_compress(json_encode(['cmd'=>'songtitle', 'data'=>$songTitleData]), 5));
require '/data/home/web/task/mysql.php';
$mysqli->select_db('arc');
$select = $mysqli->query('SELECT id,difficulty,constant FROM chart_constants');
$songmap = [];
while ( ($row = $select->fetch_array(MYSQLI_ASSOC)) !== NULL ) {
if(!isset($songmap[$row['id']])) $songmap[$row['id']] = [];
$songmap[$row['id']][$row['difficulty']] = $row['constant'];
}
$mysqli->close();
$connection->send(brotli_compress(json_encode(['cmd'=>'constants', 'data'=>$songmap]), 5));
$connection->websocketType = Websocket::BINARY_TYPE_BLOB;
$connection->send('bye');
return $connection->close();
}
$known_code = new PDO('sqlite:arc-user.db');
$known_code->query('PRAGMA synchronous=3');
// player name lookup
if ($cmd[0] === 'lookup' && count($cmd) >= 2) {
if (!preg_match('/^[a-zA-Z0-9]+$/', $cmd[1])) {
$connection->websocketType = Websocket::BINARY_TYPE_BLOB;
$connection->send('invalid id');
return $connection->close();
}
$stmt = $known_code->prepare('SELECT * FROM user_info WHERE lower(name)=?');
if ($stmt->execute([strtolower($cmd[1])])) {
$result = $stmt->fetchAll(PDO::FETCH_ASSOC);
$connection->websocketType = Websocket::BINARY_TYPE_ARRAYBUFFER;
foreach($result as &$row) $row['data_time'] = date('Y/m/d', $row['data_time']);
$connection->send(brotli_compress(json_encode(['cmd'=>'lookup_result', 'data'=>$result]), 5));
}
$connection->websocketType = Websocket::BINARY_TYPE_BLOB;
$connection->send('bye');
return $connection->close();
}
// player score probe
if (!preg_match('/^\d{9}$/', $cmd[0])) {
$connection->websocketType = Websocket::BINARY_TYPE_BLOB;
$connection->send('invalid id');
return $connection->close();
}
$connection->send('queried');
$connection->websocketType = Websocket::BINARY_TYPE_ARRAYBUFFER;
$connection->constFrom = 0;
$connection->constTo = 12;
if (count($cmd) == 3) {
$connection->constFrom = $cmd[1] |0;
$connection->constTo = $cmd[2] |0;
}
$songlist = json_decode(file_get_contents('songlist'), true);
$songTitleData = [];
$songDateData = [];
foreach ($songlist['songs'] as &$song) {
$songTitleData[$song['id']] = $song['title_localized'];
$songDateData[$song['id']] = $song['date'];
}
$connection->send(brotli_compress(json_encode(['cmd'=>'songtitle', 'data'=>$songTitleData]), 5));
$userCode = $cmd[0];
if ($userCode === '000007357') {
$connection->send(brotli_compress(file_get_contents("test_user.json"), 5));
$i = 1;
while (file_exists("test_$i.json")) {
usleep(100000);
$connection->send(brotli_compress(file_get_contents("test_$i.json"), 5));
$i++;
}
$connection->websocketType = Websocket::BINARY_TYPE_BLOB;
$connection->send('bye');
return $connection->close();
}
if ($userCode === '000000001' || $userCode === '000000002') {
$connection->send(brotli_compress(json_encode(['cmd'=>'userinfo', 'data'=>[
'join_date'=>1489517588494,
'name' => ['', 'Hikari', 'Tairitsu'][(int)$userCode],
'rating' => 616,
'recent_score'=>[],
'user_code'=>$userCode,
'user_id'=>1000000 + $userCode
]]), 5));
$connection->websocketType = Websocket::BINARY_TYPE_BLOB;
$connection->send('bye');
return $connection->close();
}
$findId = execQuery($known_code, 'SELECT id FROM known_code WHERE code="'.$userCode.'"');
if (empty($findId) || empty($findId [0])) {
$userId = 0;
} else {
$userId = $findId[0]['id'];
}
file_put_contents(__DIR__.'/arc.log', date('[m/d H:i:s] ').$connection->remote_ip."\t".$userCode."\t".$userId."\n", FILE_APPEND);
require '/data/home/web/task/mysql.php';
$mysqli->select_db('arc');
$select = $mysqli->query('SELECT id,difficulty,constant FROM chart_constants');
$ptts = [];
$songmap = [];
while ( ($row = $select->fetch_array(MYSQLI_ASSOC)) !== NULL ) {
$ptts[] = [floatval($row['constant']), $row['id'], $row['difficulty']];
if(!isset($songmap[$row['id']])) $songmap[$row['id']] = [];
$songmap[$row['id']][$row['difficulty']] = $row['constant'];
}
$mysqli->close();
usort($ptts, function ($a, $b){return $b[0] > $a[0] ? 1 : -1;} );
$offset = count($ptts);
for ($i = 0; $i<count($ptts); $i++) {
if ($ptts[$i][0] < $connection->constTo) {
$offset = $i;
break;
}
}
addAndFetchScore($userCode, $userId, getRandomToken(), function ($cmd, $data) use(&$offset, $ptts, $songmap, &$connection, $userId, $userCode, &$known_code, $songDateData) {
switch ($cmd) {
case 'find_user': {
// 有code-id对应关系,直接查找
if ($userId) {
foreach ($data['friends'] as $friend) {
if ($friend['user_id'] == $userId) {
return ['success'=>true, 'info'=>$friend];
}
}
$known_code->prepare('DELETE FROM known_code WHERE code=?')->execute([$userCode]);
}
$insertStmt = $known_code->prepare('INSERT OR IGNORE INTO known_code (code,id) VALUES (?,?)');
// 只有一个好友,直接记录并返回
if (count($data['friends']) == 1) {
$insertStmt->execute([
$data['ucode'],
$data['friends'][0]['user_id']
]);
return ['success'=>true, 'info'=>$data['friends'][0]];
}
// 遍历好友列表,查找对应关系
$testStmt = $known_code->prepare('SELECT code FROM known_code WHERE id=?');
$noRecord = [];
foreach ($data['friends'] as $friend) {
$testStmt->execute([$friend['user_id']]);
$row = $testStmt->fetch();
if (empty($row)) {
$noRecord[] = $friend;
}
}
// 只有一个好友未记录,记录并返回
if (count($noRecord) == 1) {
$insertStmt->execute([
$data['ucode'],
$noRecord[0]['user_id']
]);
return ['success'=>true, 'info'=>$noRecord[0]];
}
// 未匹配时清空好友
return ['success'=>false];
}
case 'error': {
//echo "fail $data\n";
$connection->websocketType = Websocket::BINARY_TYPE_BLOB;
$connection->send('error,'.$data);
$connection->close();
$connection->closed = true;
return;
}
case 'userinfo': {
//print_r($data);
$known_code->beginTransaction();
if ($data['rating'] > 0) {
$execData = [
$data['join_date'],
$data['name'],
$data['rating'],
$userCode,
time(),
$data['user_id']
];
$known_code->prepare('INSERT OR IGNORE INTO user_info (join_date,name,rating,code,data_time,id) VALUES (?,?,?,?,?,?)')->execute($execData);
$known_code->prepare('UPDATE user_info SET join_date=?,name=?,rating=?,code=?,data_time=? WHERE id=?')->execute($execData);
$known_code->prepare('INSERT OR IGNORE INTO rating_record (id,date,rating) VALUES (?,?,?)')->execute([$data['user_id'], date('ymd'), $data['rating']]);
} else {
if ($connection->constFrom === 0) $connection->constFrom = 12;
$execData = [
$data['join_date'],
$data['name'],
$userCode,
$data['user_id']
];
$known_code->prepare('INSERT OR IGNORE INTO user_info (join_date,name,code,id) VALUES (?,?,?,?)')->execute($execData);
$known_code->prepare('UPDATE user_info SET join_date=?,name=?,code=? WHERE id=?')->execute($execData);
}
$known_code->commit();
$userptt = ($data['rating']/100);
$connection->userptt = $userptt;
if ($connection->constFrom === 0) $connection->constFrom = $userptt - 3;
$ratings = [];
//echo 'Fetched user info: user ptt '.$userptt."\n";
if (!empty($data['recent_score'])) foreach ($data['recent_score'] as &$item) {
if (!isset($item['rating'])) continue;
$item['constant'] = get_constant_from_score_and_rating($item['score'], $item['rating']);
$item['song_date'] = $songDateData[$item['song_id']];
if ($item['rating'] > 0) {
$ratings[] = '('.implode(',', [
'"'.$item['song_id'].'"',
$item['difficulty'],
get_constant_from_score_and_rating($item['score'], $item['rating']),
$item['score'],
$item['rating']
]).')';
}
}
$rating_records = $known_code->prepare('SELECT date,rating FROM rating_record WHERE id=?');
$rating_records->execute([$data['user_id']]);
$rating_records = $rating_records->fetchAll(PDO::FETCH_NUM);
$data['rating_records'] = $rating_records;
file_put_contents(__DIR__.'/arc.log', date('[m/d H:i:s] ').$connection->remote_ip."\t".$userCode.' --> '.number_format($data['rating']/100, 2).' '.$data['name']."\n", FILE_APPEND);
//file_put_contents("test_user.json", json_encode(['cmd'=>'userinfo', 'data'=>$data]));
$data['user_code'] = $userCode;
$connection->send(brotli_compress(json_encode(['cmd'=>'userinfo', 'data'=>$data]), 5));
if (!empty($ratings)) {
require '/data/home/web/task/mysql.php';
$mysqli->select_db('arc');
// note:mysql use insert ignore, but sqlite use insert or ignore
$mysqli->query('INSERT IGNORE INTO chart_constants (id,difficulty,constant,source_score,source_rating) VALUES '.implode(',', $ratings));
$mysqli->close();
}
if ($data['rating'] <= 0 && $connection->constFrom == 12) {
$connection->websocketType = Websocket::BINARY_TYPE_BLOB;
$connection->send('error,potential_hidden');
}
return;
}
case 'pending_fetch': {
$pending = [];
for ($i = 0; $i < 6 && $offset + $i < count($ptts); $i++) {
if ($ptts[$offset + $i][0] < $connection->constFrom) {
$i++;
break;
}
$pending[] = [
'id' => $ptts[$offset + $i][1],
'difficulty' => $ptts[$offset + $i][2]
];
}
$offset += $i;
//echo "fetch page request: $offset ".count($pending)."\n";
return $pending;
}
case 'fetched_data': {
if (empty($data)) return;
foreach ($data as &$item) {
$item['constant'] = $songmap[$item['song_id']][$item['difficulty']] ?: 0;
$item['rating'] = getRating($item['constant'], $item['score']);
$item['song_date'] = $songDateData[$item['song_id']];
}
//print_r($data);
//echo "fetched ".count($data)."\n";
//file_put_contents("test_".ceil($offset/15).".json", json_encode(['cmd'=>'scores', 'data' => $data]));
$connection->websocketType = Websocket::BINARY_TYPE_ARRAYBUFFER;
$connection->send(brotli_compress(json_encode(['cmd'=>'scores', 'data' => $data]), 5));
return;
}
case 'deleted': {
if ($connection->closed) return;
$connection->websocketType = Websocket::BINARY_TYPE_BLOB;
$connection->send('bye');
return $connection->close();
}
}
});
};
// Emitted when connection closed
/*$ws_worker->onClose = function ($connection) {
};*/
// Run worker
Worker::runAll();
function getRating(float $constant, int $score) {
if ($score > 10000000) return $constant + 2.0;
else if ($score > 9800000) return $constant + 1.0 + ($score - 9800000) / 200000;
else return max($constant + ($score - 9500000) / 300000, 0);
}
function get_constant_from_score_and_rating(int $score, float $rating) {
$return = 0;
if ($score > 10e6) {
$return = $rating - 2;
} else if ($score > 9.8e6) {
$return = $rating - 1.0 - ($score - 9.8e6) / 0.2e6;
} else if ($rating > 0) {
$return = $rating - ($score - 9.5e6) / 0.3e6;
}
return round($return * 100) / 100;
}
function addAndFetchScore($userCode, $userId, $authToken, $callback) {
// 添加
$curl = curl_init();
$headers = "";
curl_setopt_array($curl, [
CURLOPT_URL => SERVER_ENDPOINT.API_VER.'/friend/me/add',
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HEADER => FALSE,
CURLOPT_HTTPHEADER => [
'Content-Type: application/x-www-form-urlencoded; charset=utf-8',
'User-Agent: Arc-mobile/'.ARCAEA_APP_VER_FULL.' CFNetwork/758.3.15 Darwin/15.4.0',
'Accept: */*',
'Accept-Language: en-us',
'Authorization: Bearer '.$authToken,
'AppVersion: '.ARCAEA_APP_VER,
],
CURLOPT_ENCODING => 'gzip, deflate',
CURLOPT_TIMEOUT=>5,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => 'friend_code='.$userCode,
CURLOPT_HEADERFUNCTION => function($curl, $header) use (&$headers)
{
$headers=$headers."\n".$header;
return strlen($header);
}
]);
$addResult = curl_exec($curl);
$curl_errno=curl_errno($curl);
$status = curl_getinfo($curl, CURLINFO_HTTP_CODE);
if ($status === 401 || $status === 403) {
curl_close($curl);
$addResult = json_decode($addResult, true);
if ($addResult['error_code']===5){
call_user_func($callback, 'error', 'Please update arcaea');
}else if ($addResult['error_code']===603){
call_user_func($callback, 'error', 'account is locked');
file_put_contents(__DIR__.'/arc.log', date('[m/d H:i:s] ')."[locked]token:".$authToken."\n", FILE_APPEND);
}else{
call_user_func($callback, 'error', 'account cannot login');
file_put_contents(__DIR__.'/arc.log', date('[m/d H:i:s] ')."[cannot login]token:".$authToken."\n", FILE_APPEND);
}
file_put_contents(__DIR__.'/arc.log', date('[m/d H:i:s] ')."[add error]status:".$status."\nresult:".$addResult."\nheaders:".$headers."\ncurl_errno:".$curl_errno." ".curl_strerror($curl_errno)."\n", FILE_APPEND);
return;
}
if ($status !== 200) {
curl_close($curl);
file_put_contents(__DIR__.'/arc.log', date('[m/d H:i:s] ')."[add error]status:".$status."\nresult:".$addResult."\nheaders:".$headers."\ncurl_errno:".$curl_errno." ".curl_strerror($curl_errno)."\n", FILE_APPEND);
call_user_func($callback, 'error', 'add');
return;
}
usleep(5e4);
$addResult = json_decode($addResult, true);
$userInfo = call_user_func($callback, 'find_user', [
'ucode' => $userCode,
'uid' => $userId,
'friends' => $addResult['value']['friends']
]);
$deleteList = array_map(function($i){return $i['user_id'];}, $addResult['value']['friends']);
if (!$userInfo['success']) {
call_user_func($callback, 'error', 'add');
deleteFriends($deleteList, $authToken, $curl);
curl_close($curl);
return;
}
$userInfo = $userInfo['info'];
call_user_func($callback, 'userinfo', $userInfo);
// 获取
while (1) {
$pending = call_user_func($callback, 'pending_fetch', NULL);
if (empty($pending)) break;
$calls = [];
$responseMap = [];
$id = 0;
foreach ($pending as $item) {
$responseMap[$id] = $item;
$calls[] = [
'endpoint' => 'score/song/friend?song_id='.$item['id'].'&difficulty='.$item['difficulty'],
'id' => $id++
];
}
$calls = urlencode(json_encode($calls, JSON_UNESCAPED_SLASHES));
$headers = "";
curl_setopt_array($curl, [
CURLOPT_POSTFIELDS => '',
CURLOPT_POST=> false,
CURLOPT_HTTPHEADER => [
'Content-Type: application/x-www-form-urlencoded; charset=utf-8',
'User-Agent: Arc-mobile/'.ARCAEA_APP_VER_FULL.' CFNetwork/758.3.15 Darwin/15.4.0',
'Accept: */*',
'Accept-Language: en-us',
'Authorization: Bearer '.$authToken,
'AppVersion: '.ARCAEA_APP_VER,
],
CURLOPT_URL => SERVER_ENDPOINT.API_VER.'/compose/aggregate?calls='.$calls,
CURLOPT_HEADERFUNCTION => function($curl, $header) use (&$headers)
{
$headers=$headers."\n".$header;
return strlen($header);
}
]);
$fetchResult = curl_exec($curl);
$curl_errno=curl_errno($curl);
$status = curl_getinfo($curl, CURLINFO_HTTP_CODE);
if ($status !== 200) {
curl_close($curl);
file_put_contents(__DIR__.'/arc.log', date('[m/d H:i:s] ')."[fetch error]status:".$status."\nresult:".$fetchResult."\nheaders:".$headers."\ncurl_errno:".$curl_errno." ".curl_strerror($curl_errno)."\n", FILE_APPEND);
call_user_func($callback, 'error', 'fetch');
break;
}
$fetchResult = json_decode($fetchResult, true);
$returnVal = [];
foreach ($responseMap as $id => $item) {
foreach ($fetchResult['value'][$id]['value'] as $score) {
if ($score['user_id'] === $userInfo['user_id']) {
$returnVal[] = $score;
} // if
} // score
} // item
call_user_func($callback, 'fetched_data', $returnVal);
usleep(5e4);
}
// 删除
deleteFriends($deleteList, $authToken, $curl);
call_user_func($callback, 'deleted', NULL);
curl_close($curl);
}
function deleteFriends($list, $authToken, $curl) {
foreach ($list as $userId) {
curl_setopt_array($curl, [
CURLOPT_URL => SERVER_ENDPOINT.API_VER.'/friend/me/delete',
CURLOPT_HTTPHEADER => [
'Content-Type: application/x-www-form-urlencoded; charset=utf-8',
'User-Agent: Arc-mobile/'.ARCAEA_APP_VER_FULL.' CFNetwork/758.3.15 Darwin/15.4.0',
'Accept: */*',
'Accept-Language: en-us',
'Authorization: Bearer '.$authToken,
'AppVersion: '.ARCAEA_APP_VER,
],
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => 'friend_id='.$userId
]);
curl_exec($curl);
usleep(5e4);
}
}

WebSocket endpoint: wss://arc.estertion.win:616

WebSocket commands:

  • <UserCode> [constant from] [constant to] (e.g. 000007357 10 12)
    • UserCode must be 9 digits
    • constant from & constant to in range from 1 to 12
  • lookup <UserName> (e.g. lookup tairitsu)
    • UserName valid characters: a-z 0-9
    • case insensitive
  • constants
    • fetch current constants in database

WebSocket response:

  • pure string: contains following commands
    • queried
    • invalid id
    • bye
    • error <error_name>
  • array buffer: brotli compressed json, has property cmd and data
    • songtitle: title objects from songlist
    • userinfo: user info from friend list response
    • scores: fetched song scores
    • lookup_result: results for user lookup
    • constants: constants in database
@Jim-Luo
Copy link

Jim-Luo commented Sep 5, 2020

我把楼主的ws用python稍微封装了一下,不会的可以试试这个https://github.com/littlebutt/Arcapi

@Mqpzi
Copy link

Mqpzi commented Sep 21, 2020

同求可直接get或post的api接口()php真的不会用orz

@swssxy
Copy link

swssxy commented Mar 31, 2021

php真的看不懂,我打开人就直接傻了
同求可以直接get或post的api

Copy link

ghost commented Feb 9, 2022

请问为什么我发送_'384317410 8 12'_请求返回的一直是曲目列表和曲师列表?不知道怎么处理
我是在wss://arc-src.estertion.win:616 请求的
可以在chara_x@126.com联系我

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment