mirror of
https://github.com/nihilvux/bancho.py.git
synced 2025-09-17 02:58:39 -07:00
Add files via upload
This commit is contained in:
486
migrations/base.sql
Normal file
486
migrations/base.sql
Normal file
@@ -0,0 +1,486 @@
|
|||||||
|
create table achievements
|
||||||
|
(
|
||||||
|
id int auto_increment
|
||||||
|
primary key,
|
||||||
|
file varchar(128) not null,
|
||||||
|
name varchar(128) charset utf8 not null,
|
||||||
|
`desc` varchar(256) charset utf8 not null,
|
||||||
|
cond varchar(64) not null,
|
||||||
|
constraint achievements_desc_uindex
|
||||||
|
unique (`desc`),
|
||||||
|
constraint achievements_file_uindex
|
||||||
|
unique (file),
|
||||||
|
constraint achievements_name_uindex
|
||||||
|
unique (name)
|
||||||
|
);
|
||||||
|
|
||||||
|
create table channels
|
||||||
|
(
|
||||||
|
id int auto_increment
|
||||||
|
primary key,
|
||||||
|
name varchar(32) not null,
|
||||||
|
topic varchar(256) not null,
|
||||||
|
read_priv int default 1 not null,
|
||||||
|
write_priv int default 2 not null,
|
||||||
|
auto_join tinyint(1) default 0 not null,
|
||||||
|
constraint channels_name_uindex
|
||||||
|
unique (name)
|
||||||
|
);
|
||||||
|
create index channels_auto_join_index
|
||||||
|
on channels (auto_join);
|
||||||
|
|
||||||
|
create table clans
|
||||||
|
(
|
||||||
|
id int auto_increment
|
||||||
|
primary key,
|
||||||
|
name varchar(16) charset utf8 not null,
|
||||||
|
tag varchar(6) charset utf8 not null,
|
||||||
|
owner int not null,
|
||||||
|
created_at datetime not null,
|
||||||
|
constraint clans_name_uindex
|
||||||
|
unique (name),
|
||||||
|
constraint clans_owner_uindex
|
||||||
|
unique (owner),
|
||||||
|
constraint clans_tag_uindex
|
||||||
|
unique (tag)
|
||||||
|
);
|
||||||
|
|
||||||
|
create table client_hashes
|
||||||
|
(
|
||||||
|
userid int not null,
|
||||||
|
osupath char(32) not null,
|
||||||
|
adapters char(32) not null,
|
||||||
|
uninstall_id char(32) not null,
|
||||||
|
disk_serial char(32) not null,
|
||||||
|
latest_time datetime not null,
|
||||||
|
occurrences int default 0 not null,
|
||||||
|
primary key (userid, osupath, adapters, uninstall_id, disk_serial)
|
||||||
|
);
|
||||||
|
|
||||||
|
create table comments
|
||||||
|
(
|
||||||
|
id int auto_increment
|
||||||
|
primary key,
|
||||||
|
target_id int not null comment 'replay, map, or set id',
|
||||||
|
target_type enum('replay', 'map', 'song') not null,
|
||||||
|
userid int not null,
|
||||||
|
time int not null,
|
||||||
|
comment varchar(80) charset utf8 not null,
|
||||||
|
colour char(6) null comment 'rgb hex string'
|
||||||
|
);
|
||||||
|
|
||||||
|
create table favourites
|
||||||
|
(
|
||||||
|
userid int not null,
|
||||||
|
setid int not null,
|
||||||
|
created_at int default 0 not null,
|
||||||
|
primary key (userid, setid)
|
||||||
|
);
|
||||||
|
|
||||||
|
create table ingame_logins
|
||||||
|
(
|
||||||
|
id int auto_increment
|
||||||
|
primary key,
|
||||||
|
userid int not null,
|
||||||
|
ip varchar(45) not null comment 'maxlen for ipv6',
|
||||||
|
osu_ver date not null,
|
||||||
|
osu_stream varchar(11) not null,
|
||||||
|
datetime datetime not null
|
||||||
|
);
|
||||||
|
|
||||||
|
create table relationships
|
||||||
|
(
|
||||||
|
user1 int not null,
|
||||||
|
user2 int not null,
|
||||||
|
type enum('friend', 'block') not null,
|
||||||
|
primary key (user1, user2)
|
||||||
|
);
|
||||||
|
|
||||||
|
create table logs
|
||||||
|
(
|
||||||
|
id int auto_increment
|
||||||
|
primary key,
|
||||||
|
`from` int not null comment 'both from and to are playerids',
|
||||||
|
`to` int not null,
|
||||||
|
`action` varchar(32) not null,
|
||||||
|
msg varchar(2048) charset utf8 null,
|
||||||
|
time datetime not null on update CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
create table mail
|
||||||
|
(
|
||||||
|
id int auto_increment
|
||||||
|
primary key,
|
||||||
|
from_id int not null,
|
||||||
|
to_id int not null,
|
||||||
|
msg varchar(2048) charset utf8 not null,
|
||||||
|
time int null,
|
||||||
|
`read` tinyint(1) default 0 not null
|
||||||
|
);
|
||||||
|
|
||||||
|
create table maps
|
||||||
|
(
|
||||||
|
server enum('osu!', 'private') default 'osu!' not null,
|
||||||
|
id int not null,
|
||||||
|
set_id int not null,
|
||||||
|
status int not null,
|
||||||
|
md5 char(32) not null,
|
||||||
|
artist varchar(128) charset utf8 not null,
|
||||||
|
title varchar(128) charset utf8 not null,
|
||||||
|
version varchar(128) charset utf8 not null,
|
||||||
|
creator varchar(19) charset utf8 not null,
|
||||||
|
filename varchar(256) charset utf8 not null,
|
||||||
|
last_update datetime not null,
|
||||||
|
total_length int not null,
|
||||||
|
max_combo int not null,
|
||||||
|
frozen tinyint(1) default 0 not null,
|
||||||
|
plays int default 0 not null,
|
||||||
|
passes int default 0 not null,
|
||||||
|
mode tinyint(1) default 0 not null,
|
||||||
|
bpm float(12,2) default 0.00 not null,
|
||||||
|
cs float(4,2) default 0.00 not null,
|
||||||
|
ar float(4,2) default 0.00 not null,
|
||||||
|
od float(4,2) default 0.00 not null,
|
||||||
|
hp float(4,2) default 0.00 not null,
|
||||||
|
diff float(6,3) default 0.000 not null,
|
||||||
|
primary key (server, id),
|
||||||
|
constraint maps_id_uindex
|
||||||
|
unique (id),
|
||||||
|
constraint maps_md5_uindex
|
||||||
|
unique (md5)
|
||||||
|
);
|
||||||
|
create index maps_set_id_index
|
||||||
|
on maps (set_id);
|
||||||
|
create index maps_status_index
|
||||||
|
on maps (status);
|
||||||
|
create index maps_filename_index
|
||||||
|
on maps (filename);
|
||||||
|
create index maps_plays_index
|
||||||
|
on maps (plays);
|
||||||
|
create index maps_mode_index
|
||||||
|
on maps (mode);
|
||||||
|
create index maps_frozen_index
|
||||||
|
on maps (frozen);
|
||||||
|
|
||||||
|
create table mapsets
|
||||||
|
(
|
||||||
|
server enum('osu!', 'private') default 'osu!' not null,
|
||||||
|
id int not null,
|
||||||
|
last_osuapi_check datetime default CURRENT_TIMESTAMP not null,
|
||||||
|
primary key (server, id),
|
||||||
|
constraint nmapsets_id_uindex
|
||||||
|
unique (id)
|
||||||
|
);
|
||||||
|
|
||||||
|
create table map_requests
|
||||||
|
(
|
||||||
|
id int auto_increment
|
||||||
|
primary key,
|
||||||
|
map_id int not null,
|
||||||
|
player_id int not null,
|
||||||
|
datetime datetime not null,
|
||||||
|
active tinyint(1) not null
|
||||||
|
);
|
||||||
|
|
||||||
|
create table performance_reports
|
||||||
|
(
|
||||||
|
scoreid bigint(20) unsigned not null,
|
||||||
|
mod_mode enum('vanilla', 'relax', 'autopilot') default 'vanilla' not null,
|
||||||
|
os varchar(64) not null,
|
||||||
|
fullscreen tinyint(1) not null,
|
||||||
|
fps_cap varchar(16) not null,
|
||||||
|
compatibility tinyint(1) not null,
|
||||||
|
version varchar(16) not null,
|
||||||
|
start_time int not null,
|
||||||
|
end_time int not null,
|
||||||
|
frame_count int not null,
|
||||||
|
spike_frames int not null,
|
||||||
|
aim_rate int not null,
|
||||||
|
completion tinyint(1) not null,
|
||||||
|
identifier varchar(128) null comment 'really don''t know much about this yet',
|
||||||
|
average_frametime int not null,
|
||||||
|
primary key (scoreid, mod_mode)
|
||||||
|
);
|
||||||
|
|
||||||
|
create table ratings
|
||||||
|
(
|
||||||
|
userid int not null,
|
||||||
|
map_md5 char(32) not null,
|
||||||
|
rating tinyint(2) not null,
|
||||||
|
primary key (userid, map_md5)
|
||||||
|
);
|
||||||
|
|
||||||
|
create table scores
|
||||||
|
(
|
||||||
|
id bigint unsigned auto_increment
|
||||||
|
primary key,
|
||||||
|
map_md5 char(32) not null,
|
||||||
|
score int not null,
|
||||||
|
pp float(7,3) not null,
|
||||||
|
acc float(6,3) not null,
|
||||||
|
max_combo int not null,
|
||||||
|
mods int not null,
|
||||||
|
n300 int not null,
|
||||||
|
n100 int not null,
|
||||||
|
n50 int not null,
|
||||||
|
nmiss int not null,
|
||||||
|
ngeki int not null,
|
||||||
|
nkatu int not null,
|
||||||
|
grade varchar(2) default 'N' not null,
|
||||||
|
status tinyint not null,
|
||||||
|
mode tinyint not null,
|
||||||
|
play_time datetime not null,
|
||||||
|
time_elapsed int not null,
|
||||||
|
client_flags int not null,
|
||||||
|
userid int not null,
|
||||||
|
perfect tinyint(1) not null,
|
||||||
|
online_checksum char(32) not null
|
||||||
|
);
|
||||||
|
create index scores_map_md5_index
|
||||||
|
on scores (map_md5);
|
||||||
|
create index scores_score_index
|
||||||
|
on scores (score);
|
||||||
|
create index scores_pp_index
|
||||||
|
on scores (pp);
|
||||||
|
create index scores_mods_index
|
||||||
|
on scores (mods);
|
||||||
|
create index scores_status_index
|
||||||
|
on scores (status);
|
||||||
|
create index scores_mode_index
|
||||||
|
on scores (mode);
|
||||||
|
create index scores_play_time_index
|
||||||
|
on scores (play_time);
|
||||||
|
create index scores_userid_index
|
||||||
|
on scores (userid);
|
||||||
|
create index scores_online_checksum_index
|
||||||
|
on scores (online_checksum);
|
||||||
|
create index scores_fetch_leaderboard_generic_index
|
||||||
|
on scores (map_md5, status, mode);
|
||||||
|
|
||||||
|
create table startups
|
||||||
|
(
|
||||||
|
id int auto_increment
|
||||||
|
primary key,
|
||||||
|
ver_major tinyint not null,
|
||||||
|
ver_minor tinyint not null,
|
||||||
|
ver_micro tinyint not null,
|
||||||
|
datetime datetime not null
|
||||||
|
);
|
||||||
|
|
||||||
|
create table stats
|
||||||
|
(
|
||||||
|
id int auto_increment,
|
||||||
|
mode tinyint(1) not null,
|
||||||
|
tscore bigint unsigned default 0 not null,
|
||||||
|
rscore bigint unsigned default 0 not null,
|
||||||
|
pp int unsigned default 0 not null,
|
||||||
|
plays int unsigned default 0 not null,
|
||||||
|
playtime int unsigned default 0 not null,
|
||||||
|
acc float(6,3) default 0.000 not null,
|
||||||
|
max_combo int unsigned default 0 not null,
|
||||||
|
total_hits int unsigned default 0 not null,
|
||||||
|
replay_views int unsigned default 0 not null,
|
||||||
|
xh_count int unsigned default 0 not null,
|
||||||
|
x_count int unsigned default 0 not null,
|
||||||
|
sh_count int unsigned default 0 not null,
|
||||||
|
s_count int unsigned default 0 not null,
|
||||||
|
a_count int unsigned default 0 not null,
|
||||||
|
primary key (id, mode)
|
||||||
|
);
|
||||||
|
create index stats_mode_index
|
||||||
|
on stats (mode);
|
||||||
|
create index stats_pp_index
|
||||||
|
on stats (pp);
|
||||||
|
create index stats_tscore_index
|
||||||
|
on stats (tscore);
|
||||||
|
create index stats_rscore_index
|
||||||
|
on stats (rscore);
|
||||||
|
|
||||||
|
create table tourney_pool_maps
|
||||||
|
(
|
||||||
|
map_id int not null,
|
||||||
|
pool_id int not null,
|
||||||
|
mods int not null,
|
||||||
|
slot tinyint not null,
|
||||||
|
primary key (map_id, pool_id)
|
||||||
|
);
|
||||||
|
create index tourney_pool_maps_mods_slot_index
|
||||||
|
on tourney_pool_maps (mods, slot);
|
||||||
|
create index tourney_pool_maps_tourney_pools_id_fk
|
||||||
|
on tourney_pool_maps (pool_id);
|
||||||
|
|
||||||
|
create table tourney_pools
|
||||||
|
(
|
||||||
|
id int auto_increment
|
||||||
|
primary key,
|
||||||
|
name varchar(16) not null,
|
||||||
|
created_at datetime not null,
|
||||||
|
created_by int not null
|
||||||
|
);
|
||||||
|
|
||||||
|
create index tourney_pools_users_id_fk
|
||||||
|
on tourney_pools (created_by);
|
||||||
|
|
||||||
|
create table user_achievements
|
||||||
|
(
|
||||||
|
userid int not null,
|
||||||
|
achid int not null,
|
||||||
|
primary key (userid, achid)
|
||||||
|
);
|
||||||
|
create index user_achievements_achid_index
|
||||||
|
on user_achievements (achid);
|
||||||
|
create index user_achievements_userid_index
|
||||||
|
on user_achievements (userid);
|
||||||
|
|
||||||
|
create table users
|
||||||
|
(
|
||||||
|
id int auto_increment
|
||||||
|
primary key,
|
||||||
|
name varchar(32) charset utf8 not null,
|
||||||
|
safe_name varchar(32) charset utf8 not null,
|
||||||
|
email varchar(254) not null,
|
||||||
|
priv int default 1 not null,
|
||||||
|
pw_bcrypt char(60) not null,
|
||||||
|
country char(2) default 'xx' not null,
|
||||||
|
silence_end int default 0 not null,
|
||||||
|
donor_end int default 0 not null,
|
||||||
|
creation_time int default 0 not null,
|
||||||
|
latest_activity int default 0 not null,
|
||||||
|
clan_id int default 0 not null,
|
||||||
|
clan_priv tinyint(1) default 0 not null,
|
||||||
|
preferred_mode int default 0 not null,
|
||||||
|
play_style int default 0 not null,
|
||||||
|
custom_badge_name varchar(16) charset utf8 null,
|
||||||
|
custom_badge_icon varchar(64) null,
|
||||||
|
userpage_content varchar(2048) charset utf8 null,
|
||||||
|
api_key char(36) null,
|
||||||
|
constraint users_api_key_uindex
|
||||||
|
unique (api_key),
|
||||||
|
constraint users_email_uindex
|
||||||
|
unique (email),
|
||||||
|
constraint users_name_uindex
|
||||||
|
unique (name),
|
||||||
|
constraint users_safe_name_uindex
|
||||||
|
unique (safe_name)
|
||||||
|
);
|
||||||
|
create index users_priv_index
|
||||||
|
on users (priv);
|
||||||
|
create index users_clan_id_index
|
||||||
|
on users (clan_id);
|
||||||
|
create index users_clan_priv_index
|
||||||
|
on users (clan_priv);
|
||||||
|
create index users_country_index
|
||||||
|
on users (country);
|
||||||
|
|
||||||
|
insert into users (id, name, safe_name, priv, country, silence_end, email, pw_bcrypt, creation_time, latest_activity)
|
||||||
|
values (1, 'BanchoBot', 'banchobot', 1, 'ca', 0, 'bot@akatsuki.pw',
|
||||||
|
'_______________________my_cool_bcrypt_______________________', UNIX_TIMESTAMP(), UNIX_TIMESTAMP());
|
||||||
|
|
||||||
|
INSERT INTO stats (id, mode) VALUES (1, 0); # vn!std
|
||||||
|
INSERT INTO stats (id, mode) VALUES (1, 1); # vn!taiko
|
||||||
|
INSERT INTO stats (id, mode) VALUES (1, 2); # vn!catch
|
||||||
|
INSERT INTO stats (id, mode) VALUES (1, 3); # vn!mania
|
||||||
|
INSERT INTO stats (id, mode) VALUES (1, 4); # rx!std
|
||||||
|
INSERT INTO stats (id, mode) VALUES (1, 5); # rx!taiko
|
||||||
|
INSERT INTO stats (id, mode) VALUES (1, 6); # rx!catch
|
||||||
|
INSERT INTO stats (id, mode) VALUES (1, 8); # ap!std
|
||||||
|
|
||||||
|
|
||||||
|
# userid 2 is reserved for ppy in osu!, and the
|
||||||
|
# client will not allow users to pm this id.
|
||||||
|
# If you want this, simply remove these two lines.
|
||||||
|
alter table users auto_increment = 3;
|
||||||
|
alter table stats auto_increment = 3;
|
||||||
|
|
||||||
|
insert into channels (name, topic, read_priv, write_priv, auto_join)
|
||||||
|
values ('#osu', 'General discussion.', 1, 2, true),
|
||||||
|
('#announce', 'Exemplary performance and public announcements.', 1, 24576, true),
|
||||||
|
('#lobby', 'Multiplayer lobby discussion room.', 1, 2, false),
|
||||||
|
('#supporter', 'General discussion for supporters.', 48, 48, false),
|
||||||
|
('#staff', 'General discussion for staff members.', 28672, 28672, true),
|
||||||
|
('#admin', 'General discussion for administrators.', 24576, 24576, true),
|
||||||
|
('#dev', 'General discussion for developers.', 16384, 16384, true);
|
||||||
|
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (1, 'osu-skill-pass-1', 'Rising Star', 'Can''t go forward without the first steps.', '(score.mods & 1 == 0) and 1 <= score.sr < 2 and mode_vn == 0');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (2, 'osu-skill-pass-2', 'Constellation Prize', 'Definitely not a consolation prize. Now things start getting hard!', '(score.mods & 1 == 0) and 2 <= score.sr < 3 and mode_vn == 0');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (3, 'osu-skill-pass-3', 'Building Confidence', 'Oh, you''ve SO got this.', '(score.mods & 1 == 0) and 3 <= score.sr < 4 and mode_vn == 0');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (4, 'osu-skill-pass-4', 'Insanity Approaches', 'You''re not twitching, you''re just ready.', '(score.mods & 1 == 0) and 4 <= score.sr < 5 and mode_vn == 0');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (5, 'osu-skill-pass-5', 'These Clarion Skies', 'Everything seems so clear now.', '(score.mods & 1 == 0) and 5 <= score.sr < 6 and mode_vn == 0');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (6, 'osu-skill-pass-6', 'Above and Beyond', 'A cut above the rest.', '(score.mods & 1 == 0) and 6 <= score.sr < 7 and mode_vn == 0');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (7, 'osu-skill-pass-7', 'Supremacy', 'All marvel before your prowess.', '(score.mods & 1 == 0) and 7 <= score.sr < 8 and mode_vn == 0');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (8, 'osu-skill-pass-8', 'Absolution', 'My god, you''re full of stars!', '(score.mods & 1 == 0) and 8 <= score.sr < 9 and mode_vn == 0');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (9, 'osu-skill-pass-9', 'Event Horizon', 'No force dares to pull you under.', '(score.mods & 1 == 0) and 9 <= score.sr < 10 and mode_vn == 0');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (10, 'osu-skill-pass-10', 'Phantasm', 'Fevered is your passion, extraordinary is your skill.', '(score.mods & 1 == 0) and 10 <= score.sr < 11 and mode_vn == 0');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (11, 'osu-skill-fc-1', 'Totality', 'All the notes. Every single one.', 'score.perfect and 1 <= score.sr < 2 and mode_vn == 0');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (12, 'osu-skill-fc-2', 'Business As Usual', 'Two to go, please.', 'score.perfect and 2 <= score.sr < 3 and mode_vn == 0');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (13, 'osu-skill-fc-3', 'Building Steam', 'Hey, this isn''t so bad.', 'score.perfect and 3 <= score.sr < 4 and mode_vn == 0');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (14, 'osu-skill-fc-4', 'Moving Forward', 'Bet you feel good about that.', 'score.perfect and 4 <= score.sr < 5 and mode_vn == 0');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (15, 'osu-skill-fc-5', 'Paradigm Shift', 'Surprisingly difficult.', 'score.perfect and 5 <= score.sr < 6 and mode_vn == 0');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (16, 'osu-skill-fc-6', 'Anguish Quelled', 'Don''t choke.', 'score.perfect and 6 <= score.sr < 7 and mode_vn == 0');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (17, 'osu-skill-fc-7', 'Never Give Up', 'Excellence is its own reward.', 'score.perfect and 7 <= score.sr < 8 and mode_vn == 0');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (18, 'osu-skill-fc-8', 'Aberration', 'They said it couldn''t be done. They were wrong.', 'score.perfect and 8 <= score.sr < 9 and mode_vn == 0');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (19, 'osu-skill-fc-9', 'Chosen', 'Reign among the Prometheans, where you belong.', 'score.perfect and 9 <= score.sr < 10 and mode_vn == 0');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (20, 'osu-skill-fc-10', 'Unfathomable', 'You have no equal.', 'score.perfect and 10 <= score.sr < 11 and mode_vn == 0');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (21, 'osu-combo-500', '500 Combo', '500 big ones! You''re moving up in the world!', '500 <= score.max_combo < 750 and mode_vn == 0');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (22, 'osu-combo-750', '750 Combo', '750 notes back to back? Woah.', '750 <= score.max_combo < 1000 and mode_vn == 0');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (23, 'osu-combo-1000', '1000 Combo', 'A thousand reasons why you rock at this game.', '1000 <= score.max_combo < 2000 and mode_vn == 0');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (24, 'osu-combo-2000', '2000 Combo', 'Nothing can stop you now.', '2000 <= score.max_combo and mode_vn == 0');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (25, 'taiko-skill-pass-1', 'My First Don', 'Marching to the beat of your own drum. Literally.', '(score.mods & 1 == 0) and 1 <= score.sr < 2 and mode_vn == 1');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (26, 'taiko-skill-pass-2', 'Katsu Katsu Katsu', 'Hora! Izuko!', '(score.mods & 1 == 0) and 2 <= score.sr < 3 and mode_vn == 1');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (27, 'taiko-skill-pass-3', 'Not Even Trying', 'Muzukashii? Not even.', '(score.mods & 1 == 0) and 3 <= score.sr < 4 and mode_vn == 1');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (28, 'taiko-skill-pass-4', 'Face Your Demons', 'The first trials are now behind you, but are you a match for the Oni?', '(score.mods & 1 == 0) and 4 <= score.sr < 5 and mode_vn == 1');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (29, 'taiko-skill-pass-5', 'The Demon Within', 'No rest for the wicked.', '(score.mods & 1 == 0) and 5 <= score.sr < 6 and mode_vn == 1');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (30, 'taiko-skill-pass-6', 'Drumbreaker', 'Too strong.', '(score.mods & 1 == 0) and 6 <= score.sr < 7 and mode_vn == 1');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (31, 'taiko-skill-pass-7', 'The Godfather', 'You are the Don of Dons.', '(score.mods & 1 == 0) and 7 <= score.sr < 8 and mode_vn == 1');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (32, 'taiko-skill-pass-8', 'Rhythm Incarnate', 'Feel the beat. Become the beat.', '(score.mods & 1 == 0) and 8 <= score.sr < 9 and mode_vn == 1');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (33, 'taiko-skill-fc-1', 'Keeping Time', 'Don, then katsu. Don, then katsu..', 'score.perfect and 1 <= score.sr < 2 and mode_vn == 1');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (34, 'taiko-skill-fc-2', 'To Your Own Beat', 'Straight and steady.', 'score.perfect and 2 <= score.sr < 3 and mode_vn == 1');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (35, 'taiko-skill-fc-3', 'Big Drums', 'Bigger scores to match.', 'score.perfect and 3 <= score.sr < 4 and mode_vn == 1');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (36, 'taiko-skill-fc-4', 'Adversity Overcome', 'Difficult? Not for you.', 'score.perfect and 4 <= score.sr < 5 and mode_vn == 1');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (37, 'taiko-skill-fc-5', 'Demonslayer', 'An Oni felled forevermore.', 'score.perfect and 5 <= score.sr < 6 and mode_vn == 1');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (38, 'taiko-skill-fc-6', 'Rhythm''s Call', 'Heralding true skill.', 'score.perfect and 6 <= score.sr < 7 and mode_vn == 1');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (39, 'taiko-skill-fc-7', 'Time Everlasting', 'Not a single beat escapes you.', 'score.perfect and 7 <= score.sr < 8 and mode_vn == 1');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (40, 'taiko-skill-fc-8', 'The Drummer''s Throne', 'Percussive brilliance befitting royalty alone.', 'score.perfect and 8 <= score.sr < 9 and mode_vn == 1');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (41, 'fruits-skill-pass-1', 'A Slice Of Life', 'Hey, this fruit catching business isn''t bad.', '(score.mods & 1 == 0) and 1 <= score.sr < 2 and mode_vn == 2');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (42, 'fruits-skill-pass-2', 'Dashing Ever Forward', 'Fast is how you do it.', '(score.mods & 1 == 0) and 2 <= score.sr < 3 and mode_vn == 2');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (43, 'fruits-skill-pass-3', 'Zesty Disposition', 'No scurvy for you, not with that much fruit.', '(score.mods & 1 == 0) and 3 <= score.sr < 4 and mode_vn == 2');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (44, 'fruits-skill-pass-4', 'Hyperdash ON!', 'Time and distance is no obstacle to you.', '(score.mods & 1 == 0) and 4 <= score.sr < 5 and mode_vn == 2');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (45, 'fruits-skill-pass-5', 'It''s Raining Fruit', 'And you can catch them all.', '(score.mods & 1 == 0) and 5 <= score.sr < 6 and mode_vn == 2');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (46, 'fruits-skill-pass-6', 'Fruit Ninja', 'Legendary techniques.', '(score.mods & 1 == 0) and 6 <= score.sr < 7 and mode_vn == 2');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (47, 'fruits-skill-pass-7', 'Dreamcatcher', 'No fruit, only dreams now.', '(score.mods & 1 == 0) and 7 <= score.sr < 8 and mode_vn == 2');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (48, 'fruits-skill-pass-8', 'Lord of the Catch', 'Your kingdom kneels before you.', '(score.mods & 1 == 0) and 8 <= score.sr < 9 and mode_vn == 2');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (49, 'fruits-skill-fc-1', 'Sweet And Sour', 'Apples and oranges, literally.', 'score.perfect and 1 <= score.sr < 2 and mode_vn == 2');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (50, 'fruits-skill-fc-2', 'Reaching The Core', 'The seeds of future success.', 'score.perfect and 2 <= score.sr < 3 and mode_vn == 2');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (51, 'fruits-skill-fc-3', 'Clean Platter', 'Clean only of failure. It is completely full, otherwise.', 'score.perfect and 3 <= score.sr < 4 and mode_vn == 2');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (52, 'fruits-skill-fc-4', 'Between The Rain', 'No umbrella needed.', 'score.perfect and 4 <= score.sr < 5 and mode_vn == 2');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (53, 'fruits-skill-fc-5', 'Addicted', 'That was an overdose?', 'score.perfect and 5 <= score.sr < 6 and mode_vn == 2');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (54, 'fruits-skill-fc-6', 'Quickening', 'A dash above normal limits.', 'score.perfect and 6 <= score.sr < 7 and mode_vn == 2');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (55, 'fruits-skill-fc-7', 'Supersonic', 'Faster than is reasonably necessary.', 'score.perfect and 7 <= score.sr < 8 and mode_vn == 2');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (56, 'fruits-skill-fc-8', 'Dashing Scarlet', 'Speed beyond mortal reckoning.', 'score.perfect and 8 <= score.sr < 9 and mode_vn == 2');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (57, 'mania-skill-pass-1', 'First Steps', 'It isn''t 9-to-5, but 1-to-9. Keys, that is.', '(score.mods & 1 == 0) and 1 <= score.sr < 2 and mode_vn == 3');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (58, 'mania-skill-pass-2', 'No Normal Player', 'Not anymore, at least.', '(score.mods & 1 == 0) and 2 <= score.sr < 3 and mode_vn == 3');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (59, 'mania-skill-pass-3', 'Impulse Drive', 'Not quite hyperspeed, but getting close.', '(score.mods & 1 == 0) and 3 <= score.sr < 4 and mode_vn == 3');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (60, 'mania-skill-pass-4', 'Hyperspeed', 'Woah.', '(score.mods & 1 == 0) and 4 <= score.sr < 5 and mode_vn == 3');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (61, 'mania-skill-pass-5', 'Ever Onwards', 'Another challenge is just around the corner.', '(score.mods & 1 == 0) and 5 <= score.sr < 6 and mode_vn == 3');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (62, 'mania-skill-pass-6', 'Another Surpassed', 'Is there no limit to your skills?', '(score.mods & 1 == 0) and 6 <= score.sr < 7 and mode_vn == 3');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (63, 'mania-skill-pass-7', 'Extra Credit', 'See me after class.', '(score.mods & 1 == 0) and 7 <= score.sr < 8 and mode_vn == 3');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (64, 'mania-skill-pass-8', 'Maniac', 'There''s just no stopping you.', '(score.mods & 1 == 0) and 8 <= score.sr < 9 and mode_vn == 3');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (65, 'mania-skill-fc-1', 'Keystruck', 'The beginning of a new story', 'score.perfect and 1 <= score.sr < 2 and mode_vn == 3');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (66, 'mania-skill-fc-2', 'Keying In', 'Finding your groove.', 'score.perfect and 2 <= score.sr < 3 and mode_vn == 3');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (67, 'mania-skill-fc-3', 'Hyperflow', 'You can *feel* the rhythm.', 'score.perfect and 3 <= score.sr < 4 and mode_vn == 3');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (68, 'mania-skill-fc-4', 'Breakthrough', 'Many skills mastered, rolled into one.', 'score.perfect and 4 <= score.sr < 5 and mode_vn == 3');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (69, 'mania-skill-fc-5', 'Everything Extra', 'Giving your all is giving everything you have.', 'score.perfect and 5 <= score.sr < 6 and mode_vn == 3');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (70, 'mania-skill-fc-6', 'Level Breaker', 'Finesse beyond reason', 'score.perfect and 6 <= score.sr < 7 and mode_vn == 3');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (71, 'mania-skill-fc-7', 'Step Up', 'A precipice rarely seen.', 'score.perfect and 7 <= score.sr < 8 and mode_vn == 3');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (72, 'mania-skill-fc-8', 'Behind The Veil', 'Supernatural!', 'score.perfect and 8 <= score.sr < 9 and mode_vn == 3');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (73, 'all-intro-suddendeath', 'Finality', 'High stakes, no regrets.', 'score.mods == 32');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (74, 'all-intro-hidden', 'Blindsight', 'I can see just perfectly', 'score.mods & 8');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (75, 'all-intro-perfect', 'Perfectionist', 'Accept nothing but the best.', 'score.mods & 16384');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (76, 'all-intro-hardrock', 'Rock Around The Clock', "You can\'t stop the rock.", 'score.mods & 16');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (77, 'all-intro-doubletime', 'Time And A Half', "Having a right ol\' time. One and a half of them, almost.", 'score.mods & 64');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (78, 'all-intro-flashlight', 'Are You Afraid Of The Dark?', "Harder than it looks, probably because it\'s hard to look.", 'score.mods & 1024');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (79, 'all-intro-easy', 'Dial It Right Back', 'Sometimes you just want to take it easy.', 'score.mods & 2');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (80, 'all-intro-nofail', 'Risk Averse', 'Safety nets are fun!', 'score.mods & 1');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (81, 'all-intro-nightcore', 'Sweet Rave Party', 'Founded in the fine tradition of changing things that were just fine as they were.', 'score.mods & 512');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (82, 'all-intro-halftime', 'Slowboat', 'You got there. Eventually.', 'score.mods & 256');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (83, 'all-intro-spunout', 'Burned Out', 'One cannot always spin to win.', 'score.mods & 4096');
|
477
migrations/migrations.sql
Normal file
477
migrations/migrations.sql
Normal file
@@ -0,0 +1,477 @@
|
|||||||
|
# This file contains any sql updates, along with the
|
||||||
|
# version they are required from. Touching this without
|
||||||
|
# at least reading utils/updater.py is certainly a bad idea :)
|
||||||
|
|
||||||
|
# v3.0.6
|
||||||
|
alter table users change name_safe safe_name varchar(32) not null;
|
||||||
|
alter table users drop key users_name_safe_uindex;
|
||||||
|
alter table users add constraint users_safe_name_uindex unique (safe_name);
|
||||||
|
alter table users change pw_hash pw_bcrypt char(60) not null;
|
||||||
|
insert into channels (name, topic, read_priv, write_priv, auto_join) values
|
||||||
|
('#supporter', 'General discussion for p2w gamers.', 48, 48, false),
|
||||||
|
('#staff', 'General discussion for the cool kids.', 28672, 28672, true),
|
||||||
|
('#admin', 'General discussion for the cool.', 24576, 24576, true),
|
||||||
|
('#dev', 'General discussion for the.', 16384, 16384, true);
|
||||||
|
|
||||||
|
# v3.0.8
|
||||||
|
alter table users modify safe_name varchar(32) charset utf8 not null;
|
||||||
|
alter table users modify name varchar(32) charset utf8 not null;
|
||||||
|
alter table mail modify msg varchar(2048) charset utf8 not null;
|
||||||
|
alter table logs modify msg varchar(2048) charset utf8 not null;
|
||||||
|
drop table if exists comments;
|
||||||
|
create table comments
|
||||||
|
(
|
||||||
|
id int auto_increment
|
||||||
|
primary key,
|
||||||
|
target_id int not null comment 'replay, map, or set id',
|
||||||
|
target_type enum('replay', 'map', 'song') not null,
|
||||||
|
userid int not null,
|
||||||
|
time int not null,
|
||||||
|
comment varchar(80) charset utf8 not null,
|
||||||
|
colour char(6) null comment 'rgb hex string'
|
||||||
|
);
|
||||||
|
|
||||||
|
# v3.0.9
|
||||||
|
alter table stats modify tscore_vn_std int unsigned default 0 not null;
|
||||||
|
alter table stats modify tscore_vn_taiko int unsigned default 0 not null;
|
||||||
|
alter table stats modify tscore_vn_catch int unsigned default 0 not null;
|
||||||
|
alter table stats modify tscore_vn_mania int unsigned default 0 not null;
|
||||||
|
alter table stats modify tscore_rx_std int unsigned default 0 not null;
|
||||||
|
alter table stats modify tscore_rx_taiko int unsigned default 0 not null;
|
||||||
|
alter table stats modify tscore_rx_catch int unsigned default 0 not null;
|
||||||
|
alter table stats modify tscore_ap_std int unsigned default 0 not null;
|
||||||
|
alter table stats modify rscore_vn_std int unsigned default 0 not null;
|
||||||
|
alter table stats modify rscore_vn_taiko int unsigned default 0 not null;
|
||||||
|
alter table stats modify rscore_vn_catch int unsigned default 0 not null;
|
||||||
|
alter table stats modify rscore_vn_mania int unsigned default 0 not null;
|
||||||
|
alter table stats modify rscore_rx_std int unsigned default 0 not null;
|
||||||
|
alter table stats modify rscore_rx_taiko int unsigned default 0 not null;
|
||||||
|
alter table stats modify rscore_rx_catch int unsigned default 0 not null;
|
||||||
|
alter table stats modify rscore_ap_std int unsigned default 0 not null;
|
||||||
|
alter table stats modify pp_vn_std smallint unsigned default 0 not null;
|
||||||
|
alter table stats modify pp_vn_taiko smallint unsigned default 0 not null;
|
||||||
|
alter table stats modify pp_vn_catch smallint unsigned default 0 not null;
|
||||||
|
alter table stats modify pp_vn_mania smallint unsigned default 0 not null;
|
||||||
|
alter table stats modify pp_rx_std smallint unsigned default 0 not null;
|
||||||
|
alter table stats modify pp_rx_taiko smallint unsigned default 0 not null;
|
||||||
|
alter table stats modify pp_rx_catch smallint unsigned default 0 not null;
|
||||||
|
alter table stats modify pp_ap_std smallint unsigned default 0 not null;
|
||||||
|
alter table stats modify plays_vn_std int unsigned default 0 not null;
|
||||||
|
alter table stats modify plays_vn_taiko int unsigned default 0 not null;
|
||||||
|
alter table stats modify plays_vn_catch int unsigned default 0 not null;
|
||||||
|
alter table stats modify plays_vn_mania int unsigned default 0 not null;
|
||||||
|
alter table stats modify plays_rx_std int unsigned default 0 not null;
|
||||||
|
alter table stats modify plays_rx_taiko int unsigned default 0 not null;
|
||||||
|
alter table stats modify plays_rx_catch int unsigned default 0 not null;
|
||||||
|
alter table stats modify plays_ap_std int unsigned default 0 not null;
|
||||||
|
alter table stats modify playtime_vn_std int unsigned default 0 not null;
|
||||||
|
alter table stats modify playtime_vn_taiko int unsigned default 0 not null;
|
||||||
|
alter table stats modify playtime_vn_catch int unsigned default 0 not null;
|
||||||
|
alter table stats modify playtime_vn_mania int unsigned default 0 not null;
|
||||||
|
alter table stats modify playtime_rx_std int unsigned default 0 not null;
|
||||||
|
alter table stats modify playtime_rx_taiko int unsigned default 0 not null;
|
||||||
|
alter table stats modify playtime_rx_catch int unsigned default 0 not null;
|
||||||
|
alter table stats modify playtime_ap_std int unsigned default 0 not null;
|
||||||
|
alter table stats modify maxcombo_vn_std int unsigned default 0 not null;
|
||||||
|
alter table stats modify maxcombo_vn_taiko int unsigned default 0 not null;
|
||||||
|
alter table stats modify maxcombo_vn_catch int unsigned default 0 not null;
|
||||||
|
alter table stats modify maxcombo_vn_mania int unsigned default 0 not null;
|
||||||
|
alter table stats modify maxcombo_rx_std int unsigned default 0 not null;
|
||||||
|
alter table stats modify maxcombo_rx_taiko int unsigned default 0 not null;
|
||||||
|
alter table stats modify maxcombo_rx_catch int unsigned default 0 not null;
|
||||||
|
alter table stats modify maxcombo_ap_std int unsigned default 0 not null;
|
||||||
|
|
||||||
|
# v3.0.10
|
||||||
|
update channels set write_priv = 24576 where name = '#announce';
|
||||||
|
|
||||||
|
# v3.1.0
|
||||||
|
alter table maps modify bpm float(12,2) default 0.00 not null;
|
||||||
|
alter table stats modify tscore_vn_std bigint unsigned default 0 not null;
|
||||||
|
alter table stats modify tscore_vn_taiko bigint unsigned default 0 not null;
|
||||||
|
alter table stats modify tscore_vn_catch bigint unsigned default 0 not null;
|
||||||
|
alter table stats modify tscore_vn_mania bigint unsigned default 0 not null;
|
||||||
|
alter table stats modify tscore_rx_std bigint unsigned default 0 not null;
|
||||||
|
alter table stats modify tscore_rx_taiko bigint unsigned default 0 not null;
|
||||||
|
alter table stats modify tscore_rx_catch bigint unsigned default 0 not null;
|
||||||
|
alter table stats modify tscore_ap_std bigint unsigned default 0 not null;
|
||||||
|
alter table stats modify rscore_vn_std bigint unsigned default 0 not null;
|
||||||
|
alter table stats modify rscore_vn_taiko bigint unsigned default 0 not null;
|
||||||
|
alter table stats modify rscore_vn_catch bigint unsigned default 0 not null;
|
||||||
|
alter table stats modify rscore_vn_mania bigint unsigned default 0 not null;
|
||||||
|
alter table stats modify rscore_rx_std bigint unsigned default 0 not null;
|
||||||
|
alter table stats modify rscore_rx_taiko bigint unsigned default 0 not null;
|
||||||
|
alter table stats modify rscore_rx_catch bigint unsigned default 0 not null;
|
||||||
|
alter table stats modify rscore_ap_std bigint unsigned default 0 not null;
|
||||||
|
alter table stats modify pp_vn_std int unsigned default 0 not null;
|
||||||
|
alter table stats modify pp_vn_taiko int unsigned default 0 not null;
|
||||||
|
alter table stats modify pp_vn_catch int unsigned default 0 not null;
|
||||||
|
alter table stats modify pp_vn_mania int unsigned default 0 not null;
|
||||||
|
alter table stats modify pp_rx_std int unsigned default 0 not null;
|
||||||
|
alter table stats modify pp_rx_taiko int unsigned default 0 not null;
|
||||||
|
alter table stats modify pp_rx_catch int unsigned default 0 not null;
|
||||||
|
alter table stats modify pp_ap_std int unsigned default 0 not null;
|
||||||
|
|
||||||
|
# v3.1.2
|
||||||
|
create table clans
|
||||||
|
(
|
||||||
|
id int auto_increment
|
||||||
|
primary key,
|
||||||
|
name varchar(16) not null,
|
||||||
|
tag varchar(6) not null,
|
||||||
|
owner int not null,
|
||||||
|
created_at datetime not null,
|
||||||
|
constraint clans_name_uindex
|
||||||
|
unique (name),
|
||||||
|
constraint clans_owner_uindex
|
||||||
|
unique (owner),
|
||||||
|
constraint clans_tag_uindex
|
||||||
|
unique (tag)
|
||||||
|
);
|
||||||
|
alter table users add clan_id int default 0 not null;
|
||||||
|
alter table users add clan_rank tinyint(1) default 0 not null;
|
||||||
|
create table achievements
|
||||||
|
(
|
||||||
|
id int auto_increment
|
||||||
|
primary key,
|
||||||
|
file varchar(128) not null,
|
||||||
|
name varchar(128) not null,
|
||||||
|
`desc` varchar(256) not null,
|
||||||
|
cond varchar(64) not null,
|
||||||
|
mode tinyint(1) not null,
|
||||||
|
constraint achievements_desc_uindex
|
||||||
|
unique (`desc`),
|
||||||
|
constraint achievements_file_uindex
|
||||||
|
unique (file),
|
||||||
|
constraint achievements_name_uindex
|
||||||
|
unique (name)
|
||||||
|
);
|
||||||
|
create table user_achievements
|
||||||
|
(
|
||||||
|
userid int not null,
|
||||||
|
achid int not null,
|
||||||
|
primary key (userid, achid)
|
||||||
|
);
|
||||||
|
insert into achievements (`id`, `file`, `name`, `desc`, `cond`, `mode`) values (1, 'osu-skill-pass-1', 'Rising Star', 'Can''t go forward without the first steps.', '(score.mods & 259 == 0) and 2 >= score.sr > 1', 0);
|
||||||
|
insert into achievements (`id`, `file`, `name`, `desc`, `cond`, `mode`) values (2, 'osu-skill-pass-2', 'Constellation Prize', 'Definitely not a consolation prize. Now things start getting hard!', '(score.mods & 259 == 0) and 3 >= score.sr > 2', 0);
|
||||||
|
insert into achievements (`id`, `file`, `name`, `desc`, `cond`, `mode`) values (3, 'osu-skill-pass-3', 'Building Confidence', 'Oh, you''ve SO got this.', '(score.mods & 259 == 0) and 4 >= score.sr > 3', 0);
|
||||||
|
insert into achievements (`id`, `file`, `name`, `desc`, `cond`, `mode`) values (4, 'osu-skill-pass-4', 'Insanity Approaches', 'You''re not twitching, you''re just ready.', '(score.mods & 259 == 0) and 5 >= score.sr > 4', 0);
|
||||||
|
insert into achievements (`id`, `file`, `name`, `desc`, `cond`, `mode`) values (5, 'osu-skill-pass-5', 'These Clarion Skies', 'Everything seems so clear now.', '(score.mods & 259 == 0) and 6 >= score.sr > 5', 0);
|
||||||
|
insert into achievements (`id`, `file`, `name`, `desc`, `cond`, `mode`) values (6, 'osu-skill-pass-6', 'Above and Beyond', 'A cut above the rest.', '(score.mods & 259 == 0) and 7 >= score.sr > 6', 0);
|
||||||
|
insert into achievements (`id`, `file`, `name`, `desc`, `cond`, `mode`) values (7, 'osu-skill-pass-7', 'Supremacy', 'All marvel before your prowess.', '(score.mods & 259 == 0) and 8 >= score.sr > 7', 0);
|
||||||
|
insert into achievements (`id`, `file`, `name`, `desc`, `cond`, `mode`) values (8, 'osu-skill-pass-8', 'Absolution', 'My god, you''re full of stars!', '(score.mods & 259 == 0) and 9 >= score.sr > 8', 0);
|
||||||
|
insert into achievements (`id`, `file`, `name`, `desc`, `cond`, `mode`) values (9, 'osu-skill-pass-9', 'Event Horizon', 'No force dares to pull you under.', '(score.mods & 259 == 0) and 10 >= score.sr > 9', 0);
|
||||||
|
insert into achievements (`id`, `file`, `name`, `desc`, `cond`, `mode`) values (10, 'osu-skill-pass-10', 'Phantasm', 'Fevered is your passion, extraordinary is your skill.', '(score.mods & 259 == 0) and 11 >= score.sr > 10', 0);
|
||||||
|
insert into achievements (`id`, `file`, `name`, `desc`, `cond`, `mode`) values (11, 'osu-skill-fc-1', 'Totality', 'All the notes. Every single one.', 'score.perfect and 2 >= score.sr > 1', 0);
|
||||||
|
insert into achievements (`id`, `file`, `name`, `desc`, `cond`, `mode`) values (12, 'osu-skill-fc-2', 'Business As Usual', 'Two to go, please.', 'score.perfect and 3 >= score.sr > 2', 0);
|
||||||
|
insert into achievements (`id`, `file`, `name`, `desc`, `cond`, `mode`) values (13, 'osu-skill-fc-3', 'Building Steam', 'Hey, this isn''t so bad.', 'score.perfect and 4 >= score.sr > 3', 0);
|
||||||
|
insert into achievements (`id`, `file`, `name`, `desc`, `cond`, `mode`) values (14, 'osu-skill-fc-4', 'Moving Forward', 'Bet you feel good about that.', 'score.perfect and 5 >= score.sr > 4', 0);
|
||||||
|
insert into achievements (`id`, `file`, `name`, `desc`, `cond`, `mode`) values (15, 'osu-skill-fc-5', 'Paradigm Shift', 'Surprisingly difficult.', 'score.perfect and 6 >= score.sr > 5', 0);
|
||||||
|
insert into achievements (`id`, `file`, `name`, `desc`, `cond`, `mode`) values (16, 'osu-skill-fc-6', 'Anguish Quelled', 'Don''t choke.', 'score.perfect and 7 >= score.sr > 6', 0);
|
||||||
|
insert into achievements (`id`, `file`, `name`, `desc`, `cond`, `mode`) values (17, 'osu-skill-fc-7', 'Never Give Up', 'Excellence is its own reward.', 'score.perfect and 8 >= score.sr > 7', 0);
|
||||||
|
insert into achievements (`id`, `file`, `name`, `desc`, `cond`, `mode`) values (18, 'osu-skill-fc-8', 'Aberration', 'They said it couldn''t be done. They were wrong.', 'score.perfect and 9 >= score.sr > 8', 0);
|
||||||
|
insert into achievements (`id`, `file`, `name`, `desc`, `cond`, `mode`) values (19, 'osu-skill-fc-9', 'Chosen', 'Reign among the Prometheans, where you belong.', 'score.perfect and 10 >= score.sr > 9', 0);
|
||||||
|
insert into achievements (`id`, `file`, `name`, `desc`, `cond`, `mode`) values (20, 'osu-skill-fc-10', 'Unfathomable', 'You have no equal.', 'score.perfect and 11 >= score.sr > 10', 0);
|
||||||
|
insert into achievements (`id`, `file`, `name`, `desc`, `cond`, `mode`) values (21, 'osu-combo-500', '500 Combo', '500 big ones! You''re moving up in the world!', '750 >= score.max_combo > 500', 0);
|
||||||
|
insert into achievements (`id`, `file`, `name`, `desc`, `cond`, `mode`) values (22, 'osu-combo-750', '750 Combo', '750 notes back to back? Woah.', '1000 >= score.max_combo > 750', 0);
|
||||||
|
insert into achievements (`id`, `file`, `name`, `desc`, `cond`, `mode`) values (23, 'osu-combo-1000', '1000 Combo', 'A thousand reasons why you rock at this game.', '2000 >= score.max_combo > 1000', 0);
|
||||||
|
insert into achievements (`id`, `file`, `name`, `desc`, `cond`, `mode`) values (24, 'osu-combo-2000', '2000 Combo', 'Nothing can stop you now.', 'score.max_combo >= 2000', 0);
|
||||||
|
insert into achievements (`id`, `file`, `name`, `desc`, `cond`, `mode`) values (25, 'taiko-skill-pass-1', 'My First Don', 'Marching to the beat of your own drum. Literally.', '(score.mods & 259 == 0) and 2 >= score.sr > 1', 1);
|
||||||
|
insert into achievements (`id`, `file`, `name`, `desc`, `cond`, `mode`) values (26, 'taiko-skill-pass-2', 'Katsu Katsu Katsu', 'Hora! Izuko!', '(score.mods & 259 == 0) and 3 >= score.sr > 2', 1);
|
||||||
|
insert into achievements (`id`, `file`, `name`, `desc`, `cond`, `mode`) values (27, 'taiko-skill-pass-3', 'Not Even Trying', 'Muzukashii? Not even.', '(score.mods & 259 == 0) and 4 >= score.sr > 3', 1);
|
||||||
|
insert into achievements (`id`, `file`, `name`, `desc`, `cond`, `mode`) values (28, 'taiko-skill-pass-4', 'Face Your Demons', 'The first trials are now behind you, but are you a match for the Oni?', '(score.mods & 259 == 0) and 5 >= score.sr > 4', 1);
|
||||||
|
insert into achievements (`id`, `file`, `name`, `desc`, `cond`, `mode`) values (29, 'taiko-skill-pass-5', 'The Demon Within', 'No rest for the wicked.', '(score.mods & 259 == 0) and 6 >= score.sr > 5', 1);
|
||||||
|
insert into achievements (`id`, `file`, `name`, `desc`, `cond`, `mode`) values (30, 'taiko-skill-pass-6', 'Drumbreaker', 'Too strong.', '(score.mods & 259 == 0) and 7 >= score.sr > 6', 1);
|
||||||
|
insert into achievements (`id`, `file`, `name`, `desc`, `cond`, `mode`) values (31, 'taiko-skill-pass-7', 'The Godfather', 'You are the Don of Dons.', '(score.mods & 259 == 0) and 8 >= score.sr > 7', 1);
|
||||||
|
insert into achievements (`id`, `file`, `name`, `desc`, `cond`, `mode`) values (32, 'taiko-skill-pass-8', 'Rhythm Incarnate', 'Feel the beat. Become the beat.', '(score.mods & 259 == 0) and 9 >= score.sr > 8', 1);
|
||||||
|
insert into achievements (`id`, `file`, `name`, `desc`, `cond`, `mode`) values (33, 'taiko-skill-fc-1', 'Keeping Time', 'Don, then katsu. Don, then katsu..', 'score.perfect and 2 >= score.sr > 1', 1);
|
||||||
|
insert into achievements (`id`, `file`, `name`, `desc`, `cond`, `mode`) values (34, 'taiko-skill-fc-2', 'To Your Own Beat', 'Straight and steady.', 'score.perfect and 3 >= score.sr > 2', 1);
|
||||||
|
insert into achievements (`id`, `file`, `name`, `desc`, `cond`, `mode`) values (35, 'taiko-skill-fc-3', 'Big Drums', 'Bigger scores to match.', 'score.perfect and 4 >= score.sr > 3', 1);
|
||||||
|
insert into achievements (`id`, `file`, `name`, `desc`, `cond`, `mode`) values (36, 'taiko-skill-fc-4', 'Adversity Overcome', 'Difficult? Not for you.', 'score.perfect and 5 >= score.sr > 4', 1);
|
||||||
|
insert into achievements (`id`, `file`, `name`, `desc`, `cond`, `mode`) values (37, 'taiko-skill-fc-5', 'Demonslayer', 'An Oni felled forevermore.', 'score.perfect and 6 >= score.sr > 5', 1);
|
||||||
|
insert into achievements (`id`, `file`, `name`, `desc`, `cond`, `mode`) values (38, 'taiko-skill-fc-6', 'Rhythm''s Call', 'Heralding true skill.', 'score.perfect and 7 >= score.sr > 6', 1);
|
||||||
|
insert into achievements (`id`, `file`, `name`, `desc`, `cond`, `mode`) values (39, 'taiko-skill-fc-7', 'Time Everlasting', 'Not a single beat escapes you.', 'score.perfect and 8 >= score.sr > 7', 1);
|
||||||
|
insert into achievements (`id`, `file`, `name`, `desc`, `cond`, `mode`) values (40, 'taiko-skill-fc-8', 'The Drummer''s Throne', 'Percussive brilliance befitting royalty alone.', 'score.perfect and 9 >= score.sr > 8', 1);
|
||||||
|
insert into achievements (`id`, `file`, `name`, `desc`, `cond`, `mode`) values (41, 'fruits-skill-pass-1', 'A Slice Of Life', 'Hey, this fruit catching business isn''t bad.', '(score.mods & 259 == 0) and 2 >= score.sr > 1', 2);
|
||||||
|
insert into achievements (`id`, `file`, `name`, `desc`, `cond`, `mode`) values (42, 'fruits-skill-pass-2', 'Dashing Ever Forward', 'Fast is how you do it.', '(score.mods & 259 == 0) and 3 >= score.sr > 2', 2);
|
||||||
|
insert into achievements (`id`, `file`, `name`, `desc`, `cond`, `mode`) values (43, 'fruits-skill-pass-3', 'Zesty Disposition', 'No scurvy for you, not with that much fruit.', '(score.mods & 259 == 0) and 4 >= score.sr > 3', 2);
|
||||||
|
insert into achievements (`id`, `file`, `name`, `desc`, `cond`, `mode`) values (44, 'fruits-skill-pass-4', 'Hyperdash ON!', 'Time and distance is no obstacle to you.', '(score.mods & 259 == 0) and 5 >= score.sr > 4', 2);
|
||||||
|
insert into achievements (`id`, `file`, `name`, `desc`, `cond`, `mode`) values (45, 'fruits-skill-pass-5', 'It''s Raining Fruit', 'And you can catch them all.', '(score.mods & 259 == 0) and 6 >= score.sr > 5', 2);
|
||||||
|
insert into achievements (`id`, `file`, `name`, `desc`, `cond`, `mode`) values (46, 'fruits-skill-pass-6', 'Fruit Ninja', 'Legendary techniques.', '(score.mods & 259 == 0) and 7 >= score.sr > 6', 2);
|
||||||
|
insert into achievements (`id`, `file`, `name`, `desc`, `cond`, `mode`) values (47, 'fruits-skill-pass-7', 'Dreamcatcher', 'No fruit, only dreams now.', '(score.mods & 259 == 0) and 8 >= score.sr > 7', 2);
|
||||||
|
insert into achievements (`id`, `file`, `name`, `desc`, `cond`, `mode`) values (48, 'fruits-skill-pass-8', 'Lord of the Catch', 'Your kingdom kneels before you.', '(score.mods & 259 == 0) and 9 >= score.sr > 8', 2);
|
||||||
|
insert into achievements (`id`, `file`, `name`, `desc`, `cond`, `mode`) values (49, 'fruits-skill-fc-1', 'Sweet And Sour', 'Apples and oranges, literally.', 'score.perfect and 2 >= score.sr > 1', 2);
|
||||||
|
insert into achievements (`id`, `file`, `name`, `desc`, `cond`, `mode`) values (50, 'fruits-skill-fc-2', 'Reaching The Core', 'The seeds of future success.', 'score.perfect and 3 >= score.sr > 2', 2);
|
||||||
|
insert into achievements (`id`, `file`, `name`, `desc`, `cond`, `mode`) values (51, 'fruits-skill-fc-3', 'Clean Platter', 'Clean only of failure. It is completely full, otherwise.', 'score.perfect and 4 >= score.sr > 3', 2);
|
||||||
|
insert into achievements (`id`, `file`, `name`, `desc`, `cond`, `mode`) values (52, 'fruits-skill-fc-4', 'Between The Rain', 'No umbrella needed.', 'score.perfect and 5 >= score.sr > 4', 2);
|
||||||
|
insert into achievements (`id`, `file`, `name`, `desc`, `cond`, `mode`) values (53, 'fruits-skill-fc-5', 'Addicted', 'That was an overdose?', 'score.perfect and 6 >= score.sr > 5', 2);
|
||||||
|
insert into achievements (`id`, `file`, `name`, `desc`, `cond`, `mode`) values (54, 'fruits-skill-fc-6', 'Quickening', 'A dash above normal limits.', 'score.perfect and 7 >= score.sr > 6', 2);
|
||||||
|
insert into achievements (`id`, `file`, `name`, `desc`, `cond`, `mode`) values (55, 'fruits-skill-fc-7', 'Supersonic', 'Faster than is reasonably necessary.', 'score.perfect and 8 >= score.sr > 7', 2);
|
||||||
|
insert into achievements (`id`, `file`, `name`, `desc`, `cond`, `mode`) values (56, 'fruits-skill-fc-8', 'Dashing Scarlet', 'Speed beyond mortal reckoning.', 'score.perfect and 9 >= score.sr > 8', 2);
|
||||||
|
insert into achievements (`id`, `file`, `name`, `desc`, `cond`, `mode`) values (57, 'mania-skill-pass-1', 'First Steps', 'It isn''t 9-to-5, but 1-to-9. Keys, that is.', '(score.mods & 259 == 0) and 2 >= score.sr > 1', 3);
|
||||||
|
insert into achievements (`id`, `file`, `name`, `desc`, `cond`, `mode`) values (58, 'mania-skill-pass-2', 'No Normal Player', 'Not anymore, at least.', '(score.mods & 259 == 0) and 3 >= score.sr > 2', 3);
|
||||||
|
insert into achievements (`id`, `file`, `name`, `desc`, `cond`, `mode`) values (59, 'mania-skill-pass-3', 'Impulse Drive', 'Not quite hyperspeed, but getting close.', '(score.mods & 259 == 0) and 4 >= score.sr > 3', 3);
|
||||||
|
insert into achievements (`id`, `file`, `name`, `desc`, `cond`, `mode`) values (60, 'mania-skill-pass-4', 'Hyperspeed', 'Woah.', '(score.mods & 259 == 0) and 5 >= score.sr > 4', 3);
|
||||||
|
insert into achievements (`id`, `file`, `name`, `desc`, `cond`, `mode`) values (61, 'mania-skill-pass-5', 'Ever Onwards', 'Another challenge is just around the corner.', '(score.mods & 259 == 0) and 6 >= score.sr > 5', 3);
|
||||||
|
insert into achievements (`id`, `file`, `name`, `desc`, `cond`, `mode`) values (62, 'mania-skill-pass-6', 'Another Surpassed', 'Is there no limit to your skills?', '(score.mods & 259 == 0) and 7 >= score.sr > 6', 3);
|
||||||
|
insert into achievements (`id`, `file`, `name`, `desc`, `cond`, `mode`) values (63, 'mania-skill-pass-7', 'Extra Credit', 'See me after class.', '(score.mods & 259 == 0) and 8 >= score.sr > 7', 3);
|
||||||
|
insert into achievements (`id`, `file`, `name`, `desc`, `cond`, `mode`) values (64, 'mania-skill-pass-8', 'Maniac', 'There''s just no stopping you.', '(score.mods & 259 == 0) and 9 >= score.sr > 8', 3);
|
||||||
|
insert into achievements (`id`, `file`, `name`, `desc`, `cond`, `mode`) values (65, 'mania-skill-fc-1', 'Keystruck', 'The beginning of a new story', 'score.perfect and (score.mods & 259 == 0) and 2 >= score.sr > 1', 3);
|
||||||
|
insert into achievements (`id`, `file`, `name`, `desc`, `cond`, `mode`) values (66, 'mania-skill-fc-2', 'Keying In', 'Finding your groove.', 'score.perfect and 3 >= score.sr > 2', 3);
|
||||||
|
insert into achievements (`id`, `file`, `name`, `desc`, `cond`, `mode`) values (67, 'mania-skill-fc-3', 'Hyperflow', 'You can *feel* the rhythm.', 'score.perfect and 4 >= score.sr > 3', 3);
|
||||||
|
insert into achievements (`id`, `file`, `name`, `desc`, `cond`, `mode`) values (68, 'mania-skill-fc-4', 'Breakthrough', 'Many skills mastered, rolled into one.', 'score.perfect and 5 >= score.sr > 4', 3);
|
||||||
|
insert into achievements (`id`, `file`, `name`, `desc`, `cond`, `mode`) values (69, 'mania-skill-fc-5', 'Everything Extra', 'Giving your all is giving everything you have.', 'score.perfect and 6 >= score.sr > 5', 3);
|
||||||
|
insert into achievements (`id`, `file`, `name`, `desc`, `cond`, `mode`) values (70, 'mania-skill-fc-6', 'Level Breaker', 'Finesse beyond reason', 'score.perfect and 7 >= score.sr > 6', 3);
|
||||||
|
insert into achievements (`id`, `file`, `name`, `desc`, `cond`, `mode`) values (71, 'mania-skill-fc-7', 'Step Up', 'A precipice rarely seen.', 'score.perfect and 8 >= score.sr > 7', 3);
|
||||||
|
insert into achievements (`id`, `file`, `name`, `desc`, `cond`, `mode`) values (72, 'mania-skill-fc-8', 'Behind The Veil', 'Supernatural!', 'score.perfect and 9 >= score.sr > 8', 3);
|
||||||
|
|
||||||
|
# v3.1.3
|
||||||
|
alter table clans modify name varchar(16) charset utf8 not null;
|
||||||
|
alter table clans modify tag varchar(6) charset utf8 not null;
|
||||||
|
alter table achievements modify name varchar(128) charset utf8 not null;
|
||||||
|
alter table achievements modify `desc` varchar(256) charset utf8 not null;
|
||||||
|
alter table maps modify artist varchar(128) charset utf8 not null;
|
||||||
|
alter table maps modify title varchar(128) charset utf8 not null;
|
||||||
|
alter table maps modify version varchar(128) charset utf8 not null;
|
||||||
|
alter table maps modify creator varchar(19) charset utf8 not null comment 'not 100%% certain on len';
|
||||||
|
alter table tourney_pools drop foreign key tourney_pools_users_id_fk;
|
||||||
|
alter table tourney_pool_maps drop foreign key tourney_pool_maps_tourney_pools_id_fk;
|
||||||
|
alter table stats drop foreign key stats_users_id_fk;
|
||||||
|
alter table ratings drop foreign key ratings_maps_md5_fk;
|
||||||
|
alter table ratings drop foreign key ratings_users_id_fk;
|
||||||
|
alter table logs modify `from` int not null comment 'both from and to are playerids';
|
||||||
|
|
||||||
|
# v3.1.9
|
||||||
|
alter table scores_rx modify id bigint(20) unsigned auto_increment;
|
||||||
|
update scores_rx set id = id + (6148914691236517205 - 1);
|
||||||
|
select @max_rx := MAX(id) + 1 from scores_rx;
|
||||||
|
set @s = CONCAT('alter table scores_rx auto_increment = ', @max_rx);
|
||||||
|
prepare stmt from @s;
|
||||||
|
execute stmt;
|
||||||
|
deallocate PREPARE stmt;
|
||||||
|
alter table scores_ap modify id bigint(20) unsigned auto_increment;
|
||||||
|
update scores_ap set id = id + (12297829382473034410 - 1);
|
||||||
|
select @max_ap := MAX(id) + 1 from scores_ap;
|
||||||
|
set @s = CONCAT('alter table scores_ap auto_increment = ', @max_ap);
|
||||||
|
prepare stmt from @s;
|
||||||
|
execute stmt;
|
||||||
|
deallocate PREPARE stmt;
|
||||||
|
alter table performance_reports modify scoreid bigint(20) unsigned auto_increment;
|
||||||
|
|
||||||
|
# v3.2.0
|
||||||
|
create table map_requests
|
||||||
|
(
|
||||||
|
id int auto_increment
|
||||||
|
primary key,
|
||||||
|
map_id int not null,
|
||||||
|
player_id int not null,
|
||||||
|
datetime datetime not null,
|
||||||
|
active tinyint(1) not null
|
||||||
|
);
|
||||||
|
|
||||||
|
# v3.2.1
|
||||||
|
update scores_rx set id = id - 3074457345618258603;
|
||||||
|
update scores_ap set id = id - 6148914691236517206;
|
||||||
|
|
||||||
|
# v3.2.2
|
||||||
|
alter table maps add max_combo int not null after total_length;
|
||||||
|
alter table users change clan_rank clan_priv tinyint(1) default 0 not null;
|
||||||
|
|
||||||
|
# v3.2.3
|
||||||
|
alter table users add api_key char(36) default NULL null;
|
||||||
|
create unique index users_api_key_uindex on users (api_key);
|
||||||
|
|
||||||
|
# v3.2.4
|
||||||
|
update achievements set file = replace(file, 'ctb', 'fruits') where mode = 2;
|
||||||
|
|
||||||
|
# v3.2.5
|
||||||
|
update achievements set cond = '(score.mods & 1 == 0) and 1 <= score.sr < 2' where file in ('osu-skill-pass-1', 'taiko-skill-pass-1', 'fruits-skill-pass-1', 'mania-skill-pass-1');
|
||||||
|
update achievements set cond = '(score.mods & 1 == 0) and 2 <= score.sr < 3' where file in ('osu-skill-pass-2', 'taiko-skill-pass-2', 'fruits-skill-pass-2', 'mania-skill-pass-2');
|
||||||
|
update achievements set cond = '(score.mods & 1 == 0) and 3 <= score.sr < 4' where file in ('osu-skill-pass-3', 'taiko-skill-pass-3', 'fruits-skill-pass-3', 'mania-skill-pass-3');
|
||||||
|
update achievements set cond = '(score.mods & 1 == 0) and 4 <= score.sr < 5' where file in ('osu-skill-pass-4', 'taiko-skill-pass-4', 'fruits-skill-pass-4', 'mania-skill-pass-4');
|
||||||
|
update achievements set cond = '(score.mods & 1 == 0) and 5 <= score.sr < 6' where file in ('osu-skill-pass-5', 'taiko-skill-pass-5', 'fruits-skill-pass-5', 'mania-skill-pass-5');
|
||||||
|
update achievements set cond = '(score.mods & 1 == 0) and 6 <= score.sr < 7' where file in ('osu-skill-pass-6', 'taiko-skill-pass-6', 'fruits-skill-pass-6', 'mania-skill-pass-6');
|
||||||
|
update achievements set cond = '(score.mods & 1 == 0) and 7 <= score.sr < 8' where file in ('osu-skill-pass-7', 'taiko-skill-pass-7', 'fruits-skill-pass-7', 'mania-skill-pass-7');
|
||||||
|
update achievements set cond = '(score.mods & 1 == 0) and 8 <= score.sr < 9' where file in ('osu-skill-pass-8', 'taiko-skill-pass-8', 'fruits-skill-pass-8', 'mania-skill-pass-8');
|
||||||
|
update achievements set cond = '(score.mods & 1 == 0) and 9 <= score.sr < 10' where file = 'osu-skill-pass-9';
|
||||||
|
update achievements set cond = '(score.mods & 1 == 0) and 10 <= score.sr < 11' where file = 'osu-skill-pass-10';
|
||||||
|
|
||||||
|
update achievements set cond = 'score.perfect and 1 <= score.sr < 2' where file in ('osu-skill-fc-1', 'taiko-skill-fc-1', 'fruits-skill-fc-1', 'mania-skill-fc-1');
|
||||||
|
update achievements set cond = 'score.perfect and 2 <= score.sr < 3' where file in ('osu-skill-fc-2', 'taiko-skill-fc-2', 'fruits-skill-fc-2', 'mania-skill-fc-2');
|
||||||
|
update achievements set cond = 'score.perfect and 3 <= score.sr < 4' where file in ('osu-skill-fc-3', 'taiko-skill-fc-3', 'fruits-skill-fc-3', 'mania-skill-fc-3');
|
||||||
|
update achievements set cond = 'score.perfect and 4 <= score.sr < 5' where file in ('osu-skill-fc-4', 'taiko-skill-fc-4', 'fruits-skill-fc-4', 'mania-skill-fc-4');
|
||||||
|
update achievements set cond = 'score.perfect and 5 <= score.sr < 6' where file in ('osu-skill-fc-5', 'taiko-skill-fc-5', 'fruits-skill-fc-5', 'mania-skill-fc-5');
|
||||||
|
update achievements set cond = 'score.perfect and 6 <= score.sr < 7' where file in ('osu-skill-fc-6', 'taiko-skill-fc-6', 'fruits-skill-fc-6', 'mania-skill-fc-6');
|
||||||
|
update achievements set cond = 'score.perfect and 7 <= score.sr < 8' where file in ('osu-skill-fc-7', 'taiko-skill-fc-7', 'fruits-skill-fc-7', 'mania-skill-fc-7');
|
||||||
|
update achievements set cond = 'score.perfect and 8 <= score.sr < 9' where file in ('osu-skill-fc-8', 'taiko-skill-fc-8', 'fruits-skill-fc-8', 'mania-skill-fc-8');
|
||||||
|
update achievements set cond = 'score.perfect and 9 <= score.sr < 10' where file = 'osu-skill-fc-9';
|
||||||
|
update achievements set cond = 'score.perfect and 10 <= score.sr < 11' where file = 'osu-skill-fc-10';
|
||||||
|
|
||||||
|
update achievements set cond = '500 <= score.max_combo < 750' where file = 'osu-combo-500';
|
||||||
|
update achievements set cond = '750 <= score.max_combo < 1000' where file = 'osu-combo-750';
|
||||||
|
update achievements set cond = '1000 <= score.max_combo < 2000' where file = 'osu-combo-1000';
|
||||||
|
update achievements set cond = '2000 <= score.max_combo' where file = 'osu-combo-2000';
|
||||||
|
|
||||||
|
# v3.2.6
|
||||||
|
alter table stats change maxcombo_vn_std max_combo_vn_std int unsigned default 0 not null;
|
||||||
|
alter table stats change maxcombo_vn_taiko max_combo_vn_taiko int unsigned default 0 not null;
|
||||||
|
alter table stats change maxcombo_vn_catch max_combo_vn_catch int unsigned default 0 not null;
|
||||||
|
alter table stats change maxcombo_vn_mania max_combo_vn_mania int unsigned default 0 not null;
|
||||||
|
alter table stats change maxcombo_rx_std max_combo_rx_std int unsigned default 0 not null;
|
||||||
|
alter table stats change maxcombo_rx_taiko max_combo_rx_taiko int unsigned default 0 not null;
|
||||||
|
alter table stats change maxcombo_rx_catch max_combo_rx_catch int unsigned default 0 not null;
|
||||||
|
alter table stats change maxcombo_ap_std max_combo_ap_std int unsigned default 0 not null;
|
||||||
|
|
||||||
|
# v3.2.7
|
||||||
|
drop table if exists user_hashes;
|
||||||
|
|
||||||
|
# v3.3.0
|
||||||
|
rename table friendships to relationships;
|
||||||
|
alter table relationships add type enum('friend', 'block') not null;
|
||||||
|
|
||||||
|
# v3.3.1
|
||||||
|
create table ingame_logins
|
||||||
|
(
|
||||||
|
id int auto_increment
|
||||||
|
primary key,
|
||||||
|
userid int not null,
|
||||||
|
ip varchar(45) not null comment 'maxlen for ipv6',
|
||||||
|
osu_ver date not null,
|
||||||
|
osu_stream varchar(11) not null,
|
||||||
|
datetime datetime not null
|
||||||
|
);
|
||||||
|
|
||||||
|
# v3.3.7
|
||||||
|
update achievements set cond = CONCAT(cond, ' and mode_vn == 0') where mode = 0;
|
||||||
|
update achievements set cond = CONCAT(cond, ' and mode_vn == 1') where mode = 1;
|
||||||
|
update achievements set cond = CONCAT(cond, ' and mode_vn == 2') where mode = 2;
|
||||||
|
update achievements set cond = CONCAT(cond, ' and mode_vn == 3') where mode = 3;
|
||||||
|
alter table achievements drop column mode;
|
||||||
|
|
||||||
|
# v3.3.8
|
||||||
|
create table mapsets
|
||||||
|
(
|
||||||
|
server enum('osu!', 'gulag') default 'osu!' not null,
|
||||||
|
id int not null,
|
||||||
|
last_osuapi_check datetime default CURRENT_TIMESTAMP not null,
|
||||||
|
primary key (server, id),
|
||||||
|
constraint nmapsets_id_uindex
|
||||||
|
unique (id)
|
||||||
|
);
|
||||||
|
|
||||||
|
# v3.4.1
|
||||||
|
alter table maps add filename varchar(256) charset utf8 not null after creator;
|
||||||
|
|
||||||
|
# v3.5.2
|
||||||
|
alter table scores_vn add online_checksum char(32) not null;
|
||||||
|
alter table scores_rx add online_checksum char(32) not null;
|
||||||
|
alter table scores_ap add online_checksum char(32) not null;
|
||||||
|
|
||||||
|
# v4.1.1
|
||||||
|
alter table stats add total_hits int unsigned default 0 not null after max_combo;
|
||||||
|
|
||||||
|
# v4.1.2
|
||||||
|
alter table stats add replay_views int unsigned default 0 not null after total_hits;
|
||||||
|
|
||||||
|
# v4.1.3
|
||||||
|
alter table users add preferred_mode int default 0 not null after latest_activity;
|
||||||
|
alter table users add play_style int default 0 not null after preferred_mode;
|
||||||
|
alter table users add custom_badge_name varchar(16) charset utf8 null after play_style;
|
||||||
|
alter table users add custom_badge_icon varchar(64) null after custom_badge_name;
|
||||||
|
alter table users add userpage_content varchar(2048) charset utf8 null after custom_badge_icon;
|
||||||
|
|
||||||
|
# v4.2.0
|
||||||
|
# please refer to tools/migrate_v420 for further v4.2.0 migrations
|
||||||
|
update stats set mode = 8 where mode = 7;
|
||||||
|
|
||||||
|
# v4.3.1
|
||||||
|
alter table maps change server server enum('osu!', 'private') default 'osu!' not null;
|
||||||
|
alter table mapsets change server server enum('osu!', 'private') default 'osu!' not null;
|
||||||
|
|
||||||
|
# v4.4.2
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (73, 'all-intro-suddendeath', 'Finality', 'High stakes, no regrets.', 'score.mods == 32');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (74, 'all-intro-hidden', 'Blindsight', 'I can see just perfectly', 'score.mods & 8');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (75, 'all-intro-perfect', 'Perfectionist', 'Accept nothing but the best.', 'score.mods & 16384');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (76, 'all-intro-hardrock', 'Rock Around The Clock', "You can\'t stop the rock.", 'score.mods & 16');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (77, 'all-intro-doubletime', 'Time And A Half', "Having a right ol\' time. One and a half of them, almost.", 'score.mods & 64');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (78, 'all-intro-flashlight', 'Are You Afraid Of The Dark?', "Harder than it looks, probably because it\'s hard to look.", 'score.mods & 1024');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (79, 'all-intro-easy', 'Dial It Right Back', 'Sometimes you just want to take it easy.', 'score.mods & 2');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (80, 'all-intro-nofail', 'Risk Averse', 'Safety nets are fun!', 'score.mods & 1');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (81, 'all-intro-nightcore', 'Sweet Rave Party', 'Founded in the fine tradition of changing things that were just fine as they were.', 'score.mods & 512');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (82, 'all-intro-halftime', 'Slowboat', 'You got there. Eventually.', 'score.mods & 256');
|
||||||
|
insert into achievements (id, file, name, `desc`, cond) values (83, 'all-intro-spunout', 'Burned Out', 'One cannot always spin to win.', 'score.mods & 4096');
|
||||||
|
|
||||||
|
# v4.4.3
|
||||||
|
alter table favourites add created_at int default 0 not null;
|
||||||
|
|
||||||
|
# v4.7.1
|
||||||
|
lock tables maps write;
|
||||||
|
alter table maps drop primary key;
|
||||||
|
alter table maps add primary key (id);
|
||||||
|
alter table maps modify column server enum('osu!', 'private') not null default 'osu!' after id;
|
||||||
|
unlock tables;
|
||||||
|
|
||||||
|
# v5.0.1
|
||||||
|
create index channels_auto_join_index
|
||||||
|
on channels (auto_join);
|
||||||
|
|
||||||
|
create index maps_set_id_index
|
||||||
|
on maps (set_id);
|
||||||
|
create index maps_status_index
|
||||||
|
on maps (status);
|
||||||
|
create index maps_filename_index
|
||||||
|
on maps (filename);
|
||||||
|
create index maps_plays_index
|
||||||
|
on maps (plays);
|
||||||
|
create index maps_mode_index
|
||||||
|
on maps (mode);
|
||||||
|
create index maps_frozen_index
|
||||||
|
on maps (frozen);
|
||||||
|
|
||||||
|
create index scores_map_md5_index
|
||||||
|
on scores (map_md5);
|
||||||
|
create index scores_score_index
|
||||||
|
on scores (score);
|
||||||
|
create index scores_pp_index
|
||||||
|
on scores (pp);
|
||||||
|
create index scores_mods_index
|
||||||
|
on scores (mods);
|
||||||
|
create index scores_status_index
|
||||||
|
on scores (status);
|
||||||
|
create index scores_mode_index
|
||||||
|
on scores (mode);
|
||||||
|
create index scores_play_time_index
|
||||||
|
on scores (play_time);
|
||||||
|
create index scores_userid_index
|
||||||
|
on scores (userid);
|
||||||
|
create index scores_online_checksum_index
|
||||||
|
on scores (online_checksum);
|
||||||
|
|
||||||
|
create index stats_mode_index
|
||||||
|
on stats (mode);
|
||||||
|
create index stats_pp_index
|
||||||
|
on stats (pp);
|
||||||
|
create index stats_tscore_index
|
||||||
|
on stats (tscore);
|
||||||
|
create index stats_rscore_index
|
||||||
|
on stats (rscore);
|
||||||
|
|
||||||
|
create index tourney_pool_maps_mods_slot_index
|
||||||
|
on tourney_pool_maps (mods, slot);
|
||||||
|
|
||||||
|
create index user_achievements_achid_index
|
||||||
|
on user_achievements (achid);
|
||||||
|
create index user_achievements_userid_index
|
||||||
|
on user_achievements (userid);
|
||||||
|
|
||||||
|
create index users_priv_index
|
||||||
|
on users (priv);
|
||||||
|
create index users_clan_id_index
|
||||||
|
on users (clan_id);
|
||||||
|
create index users_clan_priv_index
|
||||||
|
on users (clan_priv);
|
||||||
|
create index users_country_index
|
||||||
|
on users (country);
|
||||||
|
|
||||||
|
# v5.2.2
|
||||||
|
create index scores_fetch_leaderboard_generic_index
|
||||||
|
on scores (map_md5, status, mode);
|
16
scripts/install-nginx-config.sh
Normal file
16
scripts/install-nginx-config.sh
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
echo "Sourcing environment from .env file"
|
||||||
|
set -a
|
||||||
|
source .env
|
||||||
|
set +a
|
||||||
|
|
||||||
|
echo "Installing nginx configuration"
|
||||||
|
envsubst '${APP_PORT},${DOMAIN},${SSL_CERT_PATH},${SSL_KEY_PATH},${DATA_DIRECTORY}' < ext/nginx.conf.example > /etc/nginx/sites-available/bancho.conf
|
||||||
|
ln -f -s /etc/nginx/sites-available/bancho.conf /etc/nginx/sites-enabled/bancho.conf
|
||||||
|
|
||||||
|
echo "Restarting nginx"
|
||||||
|
nginx -s reload
|
||||||
|
|
||||||
|
echo "Nginx configuration installed"
|
61
scripts/run-tests.sh
Normal file
61
scripts/run-tests.sh
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -eo pipefail
|
||||||
|
|
||||||
|
export DB_HOST=mysql-test
|
||||||
|
export REDIS_HOST=redis-test
|
||||||
|
|
||||||
|
initDB() {
|
||||||
|
echo "Initializing database..."
|
||||||
|
if [[ "$DB_USE_SSL" == "true" ]]; then
|
||||||
|
EXTRA_PARAMS="--ssl"
|
||||||
|
else
|
||||||
|
EXTRA_PARAMS=""
|
||||||
|
fi
|
||||||
|
|
||||||
|
DB_QUERIES=(
|
||||||
|
"DROP DATABASE IF EXISTS $DB_NAME"
|
||||||
|
"CREATE DATABASE $DB_NAME"
|
||||||
|
)
|
||||||
|
|
||||||
|
for query in "${DB_QUERIES[@]}"
|
||||||
|
do
|
||||||
|
mysql \
|
||||||
|
--host=$DB_HOST \
|
||||||
|
--port=$DB_PORT \
|
||||||
|
--user=root \
|
||||||
|
--database=mysql \
|
||||||
|
--password=$DB_PASS \
|
||||||
|
$EXTRA_PARAMS \
|
||||||
|
--execute="$query"
|
||||||
|
done
|
||||||
|
|
||||||
|
redis-cli -h $REDIS_HOST -p $REDIS_PORT FLUSHALL
|
||||||
|
}
|
||||||
|
|
||||||
|
execDBStatement() {
|
||||||
|
if [[ "$DB_USE_SSL" == "true" ]]; then
|
||||||
|
EXTRA_PARAMS="--ssl"
|
||||||
|
else
|
||||||
|
EXTRA_PARAMS=""
|
||||||
|
fi
|
||||||
|
|
||||||
|
mysql \
|
||||||
|
--host=$DB_HOST \
|
||||||
|
--port=$DB_PORT \
|
||||||
|
--user=root \
|
||||||
|
--database=$DB_NAME \
|
||||||
|
--password=$DB_PASS \
|
||||||
|
$EXTRA_PARAMS \
|
||||||
|
--execute="$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
initDB
|
||||||
|
|
||||||
|
execDBStatement "source /srv/root/migrations/base.sql"
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
echo "Running tests..."
|
||||||
|
coverage run -m pytest -vv -s tests/
|
||||||
|
coverage report --show-missing --fail-under=45
|
||||||
|
coverage html
|
10
scripts/start_server.sh
Normal file
10
scripts/start_server.sh
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euxo pipefail
|
||||||
|
|
||||||
|
# Checking MySQL TCP connection
|
||||||
|
scripts/wait-for-it.sh --timeout=60 $DB_HOST:$DB_PORT
|
||||||
|
|
||||||
|
# Checking Redis connection
|
||||||
|
scripts/wait-for-it.sh --timeout=60 $REDIS_HOST:$REDIS_PORT
|
||||||
|
|
||||||
|
python main.py
|
182
scripts/wait-for-it.sh
Normal file
182
scripts/wait-for-it.sh
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Use this script to test if a given TCP host/port are available
|
||||||
|
|
||||||
|
WAITFORIT_cmdname=${0##*/}
|
||||||
|
|
||||||
|
echoerr() { if [[ $WAITFORIT_QUIET -ne 1 ]]; then echo "$@" 1>&2; fi }
|
||||||
|
|
||||||
|
usage()
|
||||||
|
{
|
||||||
|
cat << USAGE >&2
|
||||||
|
Usage:
|
||||||
|
$WAITFORIT_cmdname host:port [-s] [-t timeout] [-- command args]
|
||||||
|
-h HOST | --host=HOST Host or IP under test
|
||||||
|
-p PORT | --port=PORT TCP port under test
|
||||||
|
Alternatively, you specify the host and port as host:port
|
||||||
|
-s | --strict Only execute subcommand if the test succeeds
|
||||||
|
-q | --quiet Don't output any status messages
|
||||||
|
-t TIMEOUT | --timeout=TIMEOUT
|
||||||
|
Timeout in seconds, zero for no timeout
|
||||||
|
-- COMMAND ARGS Execute command with args after the test finishes
|
||||||
|
USAGE
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
wait_for()
|
||||||
|
{
|
||||||
|
if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then
|
||||||
|
echoerr "$WAITFORIT_cmdname: waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT"
|
||||||
|
else
|
||||||
|
echoerr "$WAITFORIT_cmdname: waiting for $WAITFORIT_HOST:$WAITFORIT_PORT without a timeout"
|
||||||
|
fi
|
||||||
|
WAITFORIT_start_ts=$(date +%s)
|
||||||
|
while :
|
||||||
|
do
|
||||||
|
if [[ $WAITFORIT_ISBUSY -eq 1 ]]; then
|
||||||
|
nc -z $WAITFORIT_HOST $WAITFORIT_PORT
|
||||||
|
WAITFORIT_result=$?
|
||||||
|
else
|
||||||
|
(echo -n > /dev/tcp/$WAITFORIT_HOST/$WAITFORIT_PORT) >/dev/null 2>&1
|
||||||
|
WAITFORIT_result=$?
|
||||||
|
fi
|
||||||
|
if [[ $WAITFORIT_result -eq 0 ]]; then
|
||||||
|
WAITFORIT_end_ts=$(date +%s)
|
||||||
|
echoerr "$WAITFORIT_cmdname: $WAITFORIT_HOST:$WAITFORIT_PORT is available after $((WAITFORIT_end_ts - WAITFORIT_start_ts)) seconds"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
return $WAITFORIT_result
|
||||||
|
}
|
||||||
|
|
||||||
|
wait_for_wrapper()
|
||||||
|
{
|
||||||
|
# In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692
|
||||||
|
if [[ $WAITFORIT_QUIET -eq 1 ]]; then
|
||||||
|
timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --quiet --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT &
|
||||||
|
else
|
||||||
|
timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT &
|
||||||
|
fi
|
||||||
|
WAITFORIT_PID=$!
|
||||||
|
trap "kill -INT -$WAITFORIT_PID" INT
|
||||||
|
wait $WAITFORIT_PID
|
||||||
|
WAITFORIT_RESULT=$?
|
||||||
|
if [[ $WAITFORIT_RESULT -ne 0 ]]; then
|
||||||
|
echoerr "$WAITFORIT_cmdname: timeout occurred after waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT"
|
||||||
|
fi
|
||||||
|
return $WAITFORIT_RESULT
|
||||||
|
}
|
||||||
|
|
||||||
|
# process arguments
|
||||||
|
while [[ $# -gt 0 ]]
|
||||||
|
do
|
||||||
|
case "$1" in
|
||||||
|
*:* )
|
||||||
|
WAITFORIT_hostport=(${1//:/ })
|
||||||
|
WAITFORIT_HOST=${WAITFORIT_hostport[0]}
|
||||||
|
WAITFORIT_PORT=${WAITFORIT_hostport[1]}
|
||||||
|
shift 1
|
||||||
|
;;
|
||||||
|
--child)
|
||||||
|
WAITFORIT_CHILD=1
|
||||||
|
shift 1
|
||||||
|
;;
|
||||||
|
-q | --quiet)
|
||||||
|
WAITFORIT_QUIET=1
|
||||||
|
shift 1
|
||||||
|
;;
|
||||||
|
-s | --strict)
|
||||||
|
WAITFORIT_STRICT=1
|
||||||
|
shift 1
|
||||||
|
;;
|
||||||
|
-h)
|
||||||
|
WAITFORIT_HOST="$2"
|
||||||
|
if [[ $WAITFORIT_HOST == "" ]]; then break; fi
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--host=*)
|
||||||
|
WAITFORIT_HOST="${1#*=}"
|
||||||
|
shift 1
|
||||||
|
;;
|
||||||
|
-p)
|
||||||
|
WAITFORIT_PORT="$2"
|
||||||
|
if [[ $WAITFORIT_PORT == "" ]]; then break; fi
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--port=*)
|
||||||
|
WAITFORIT_PORT="${1#*=}"
|
||||||
|
shift 1
|
||||||
|
;;
|
||||||
|
-t)
|
||||||
|
WAITFORIT_TIMEOUT="$2"
|
||||||
|
if [[ $WAITFORIT_TIMEOUT == "" ]]; then break; fi
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--timeout=*)
|
||||||
|
WAITFORIT_TIMEOUT="${1#*=}"
|
||||||
|
shift 1
|
||||||
|
;;
|
||||||
|
--)
|
||||||
|
shift
|
||||||
|
WAITFORIT_CLI=("$@")
|
||||||
|
break
|
||||||
|
;;
|
||||||
|
--help)
|
||||||
|
usage
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echoerr "Unknown argument: $1"
|
||||||
|
usage
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ "$WAITFORIT_HOST" == "" || "$WAITFORIT_PORT" == "" ]]; then
|
||||||
|
echoerr "Error: you need to provide a host and port to test."
|
||||||
|
usage
|
||||||
|
fi
|
||||||
|
|
||||||
|
WAITFORIT_TIMEOUT=${WAITFORIT_TIMEOUT:-15}
|
||||||
|
WAITFORIT_STRICT=${WAITFORIT_STRICT:-0}
|
||||||
|
WAITFORIT_CHILD=${WAITFORIT_CHILD:-0}
|
||||||
|
WAITFORIT_QUIET=${WAITFORIT_QUIET:-0}
|
||||||
|
|
||||||
|
# Check to see if timeout is from busybox?
|
||||||
|
WAITFORIT_TIMEOUT_PATH=$(type -p timeout)
|
||||||
|
WAITFORIT_TIMEOUT_PATH=$(realpath $WAITFORIT_TIMEOUT_PATH 2>/dev/null || readlink -f $WAITFORIT_TIMEOUT_PATH)
|
||||||
|
|
||||||
|
WAITFORIT_BUSYTIMEFLAG=""
|
||||||
|
if [[ $WAITFORIT_TIMEOUT_PATH =~ "busybox" ]]; then
|
||||||
|
WAITFORIT_ISBUSY=1
|
||||||
|
# Check if busybox timeout uses -t flag
|
||||||
|
# (recent Alpine versions don't support -t anymore)
|
||||||
|
if timeout &>/dev/stdout | grep -q -e '-t '; then
|
||||||
|
WAITFORIT_BUSYTIMEFLAG="-t"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
WAITFORIT_ISBUSY=0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $WAITFORIT_CHILD -gt 0 ]]; then
|
||||||
|
wait_for
|
||||||
|
WAITFORIT_RESULT=$?
|
||||||
|
exit $WAITFORIT_RESULT
|
||||||
|
else
|
||||||
|
if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then
|
||||||
|
wait_for_wrapper
|
||||||
|
WAITFORIT_RESULT=$?
|
||||||
|
else
|
||||||
|
wait_for
|
||||||
|
WAITFORIT_RESULT=$?
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $WAITFORIT_CLI != "" ]]; then
|
||||||
|
if [[ $WAITFORIT_RESULT -ne 0 && $WAITFORIT_STRICT -eq 1 ]]; then
|
||||||
|
echoerr "$WAITFORIT_cmdname: strict mode, refusing to execute subprocess"
|
||||||
|
exit $WAITFORIT_RESULT
|
||||||
|
fi
|
||||||
|
exec "${WAITFORIT_CLI[@]}"
|
||||||
|
else
|
||||||
|
exit $WAITFORIT_RESULT
|
||||||
|
fi
|
0
testing/__init__.py
Normal file
0
testing/__init__.py
Normal file
0
testing/sample_data/__init__.py
Normal file
0
testing/sample_data/__init__.py
Normal file
157
testing/sample_data/sample_beatmap_data.py
Normal file
157
testing/sample_data/sample_beatmap_data.py
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
def vivid_osu_file_sample_response() -> bytes:
|
||||||
|
with open("testing/sample_data/vivid_osu_file.osu", "rb") as f:
|
||||||
|
return f.read()
|
||||||
|
|
||||||
|
|
||||||
|
def vivid_getbeatmaps_sample_response() -> list[dict[str, Any]]:
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"beatmapset_id": 141,
|
||||||
|
"beatmap_id": 313,
|
||||||
|
"approved": 1,
|
||||||
|
"total_length": 188,
|
||||||
|
"hit_length": 159,
|
||||||
|
"version": "Easy",
|
||||||
|
"file_md5": "72bdc73c3f17013c5d0ba8443c9045b2",
|
||||||
|
"diff_size": 3,
|
||||||
|
"diff_overall": 3,
|
||||||
|
"diff_approach": 3,
|
||||||
|
"diff_drain": 3,
|
||||||
|
"mode": 0,
|
||||||
|
"approved_date": "2007-11-01T06:09:15Z",
|
||||||
|
"last_update": "2014-05-18T08:16:40Z",
|
||||||
|
"artist": "FAIRY FORE",
|
||||||
|
"artist_unicode": "FAIRY FORE",
|
||||||
|
"title": "Vivid",
|
||||||
|
"title_unicode": "Vivid",
|
||||||
|
"creator": "Hitoshirenu Shourai",
|
||||||
|
"creator_id": 602,
|
||||||
|
"bpm": 168,
|
||||||
|
"source": "",
|
||||||
|
"tags": "",
|
||||||
|
"genre_id": 0,
|
||||||
|
"language_id": 0,
|
||||||
|
"favourite_count": 336,
|
||||||
|
"storyboard": 0,
|
||||||
|
"video": 0,
|
||||||
|
"download_unavailable": 0,
|
||||||
|
"playcount": 53584,
|
||||||
|
"passcount": 15160,
|
||||||
|
"packs": ["S5", "T79"],
|
||||||
|
"max_combo": 294,
|
||||||
|
"difficultyrating": 1.54,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"beatmapset_id": 141,
|
||||||
|
"beatmap_id": 314,
|
||||||
|
"approved": 1,
|
||||||
|
"total_length": 185,
|
||||||
|
"hit_length": 182,
|
||||||
|
"version": "Hard",
|
||||||
|
"file_md5": "dd1749b4422a1dab9a2945a6bfccc5ef",
|
||||||
|
"diff_size": 5,
|
||||||
|
"diff_overall": 5,
|
||||||
|
"diff_approach": 5,
|
||||||
|
"diff_drain": 5,
|
||||||
|
"mode": 0,
|
||||||
|
"approved_date": "2007-11-01T06:09:15Z",
|
||||||
|
"last_update": "2014-05-18T16:45:16Z",
|
||||||
|
"artist": "FAIRY FORE",
|
||||||
|
"artist_unicode": "FAIRY FORE",
|
||||||
|
"title": "Vivid",
|
||||||
|
"title_unicode": "Vivid",
|
||||||
|
"creator": "Hitoshirenu Shourai",
|
||||||
|
"creator_id": 602,
|
||||||
|
"bpm": 168,
|
||||||
|
"source": "",
|
||||||
|
"tags": "",
|
||||||
|
"genre_id": 0,
|
||||||
|
"language_id": 0,
|
||||||
|
"favourite_count": 336,
|
||||||
|
"storyboard": 0,
|
||||||
|
"video": 0,
|
||||||
|
"download_unavailable": 0,
|
||||||
|
"playcount": 79331,
|
||||||
|
"passcount": 8523,
|
||||||
|
"packs": ["S5", "T79"],
|
||||||
|
"max_combo": 723,
|
||||||
|
"difficultyrating": 3.75,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"beatmapset_id": 141,
|
||||||
|
"beatmap_id": 315,
|
||||||
|
"approved": 1,
|
||||||
|
"total_length": 14,
|
||||||
|
"hit_length": 14,
|
||||||
|
"version": "Insane",
|
||||||
|
"file_md5": "1cf5b2c2edfafd055536d2cefcb89c0e",
|
||||||
|
"diff_size": 6,
|
||||||
|
"diff_overall": 7,
|
||||||
|
"diff_approach": 7,
|
||||||
|
"diff_drain": 2,
|
||||||
|
"mode": 0,
|
||||||
|
"approved_date": "2007-11-01T06:09:15Z",
|
||||||
|
"last_update": "2014-05-18T15:41:48Z",
|
||||||
|
"artist": "FAIRY FORE",
|
||||||
|
"artist_unicode": "FAIRY FORE",
|
||||||
|
"title": "Vivid",
|
||||||
|
"title_unicode": "Vivid",
|
||||||
|
"creator": "Hitoshirenu Shourai",
|
||||||
|
"creator_id": 602,
|
||||||
|
"bpm": 168,
|
||||||
|
"source": "",
|
||||||
|
"tags": "",
|
||||||
|
"genre_id": 0,
|
||||||
|
"language_id": 0,
|
||||||
|
"favourite_count": 336,
|
||||||
|
"storyboard": 0,
|
||||||
|
"video": 0,
|
||||||
|
"download_unavailable": 0,
|
||||||
|
"playcount": 1632137,
|
||||||
|
"passcount": 987366,
|
||||||
|
"packs": ["S5", "T79"],
|
||||||
|
"max_combo": 114,
|
||||||
|
"difficultyrating": 5.23,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"beatmapset_id": 141,
|
||||||
|
"beatmap_id": 316,
|
||||||
|
"approved": 1,
|
||||||
|
"total_length": 188,
|
||||||
|
"hit_length": 159,
|
||||||
|
"version": "Normal",
|
||||||
|
"file_md5": "0236aeb3bb5f110d7eacf4045092efac",
|
||||||
|
"diff_size": 5,
|
||||||
|
"diff_overall": 5,
|
||||||
|
"diff_approach": 5,
|
||||||
|
"diff_drain": 5,
|
||||||
|
"mode": 0,
|
||||||
|
"approved_date": "2007-11-01T06:09:15Z",
|
||||||
|
"last_update": "2014-05-18T16:26:49Z",
|
||||||
|
"artist": "FAIRY FORE",
|
||||||
|
"artist_unicode": "FAIRY FORE",
|
||||||
|
"title": "Vivid",
|
||||||
|
"title_unicode": "Vivid",
|
||||||
|
"creator": "Hitoshirenu Shourai",
|
||||||
|
"creator_id": 602,
|
||||||
|
"bpm": 168,
|
||||||
|
"source": "",
|
||||||
|
"tags": "",
|
||||||
|
"genre_id": 0,
|
||||||
|
"language_id": 0,
|
||||||
|
"favourite_count": 336,
|
||||||
|
"storyboard": 0,
|
||||||
|
"video": 0,
|
||||||
|
"download_unavailable": 0,
|
||||||
|
"playcount": 49671,
|
||||||
|
"passcount": 13422,
|
||||||
|
"packs": ["S5", "T79"],
|
||||||
|
"max_combo": 478,
|
||||||
|
"difficultyrating": 2.28,
|
||||||
|
},
|
||||||
|
]
|
139
testing/sample_data/vivid_osu_file.osu
Normal file
139
testing/sample_data/vivid_osu_file.osu
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
osu file format v3
|
||||||
|
|
||||||
|
[General]
|
||||||
|
AudioFilename: 01_-_vivid.mp3
|
||||||
|
AudioLeadIn: 2000
|
||||||
|
AudioHash: f9e55f878282eba37a8da909fcc40994
|
||||||
|
PreviewTime: -1
|
||||||
|
SampleSet: Normal
|
||||||
|
EditorBookmarks: 3317,5460,8942,14389,16085,17514,19300,22692,25907,28942,31800,34300,37335
|
||||||
|
|
||||||
|
[Metadata]
|
||||||
|
Title:Vivid
|
||||||
|
Artist:FAIRY FORE
|
||||||
|
Creator:Hitoshirenu Shourai
|
||||||
|
Version:Insane
|
||||||
|
|
||||||
|
[Difficulty]
|
||||||
|
HPDrainRate:2
|
||||||
|
CircleSize:6
|
||||||
|
OverallDifficulty:7
|
||||||
|
SliderMultiplier: 1
|
||||||
|
SliderTickRate: 2
|
||||||
|
|
||||||
|
[Events]
|
||||||
|
0,0,"Chocobos.jpg"
|
||||||
|
3,0,0,0,255
|
||||||
|
|
||||||
|
[TimingPoints]
|
||||||
|
520,357.142857142857
|
||||||
|
|
||||||
|
[HitObjects]
|
||||||
|
256,192,520,1,0,
|
||||||
|
224,192,609,1,0,
|
||||||
|
192,192,698,1,0,
|
||||||
|
208,160,787,1,0,
|
||||||
|
224,144,877,5,0,
|
||||||
|
256,144,966,1,0,
|
||||||
|
288,144,1055,1,0,
|
||||||
|
320,160,1144,1,0,
|
||||||
|
320,192,1234,5,0,
|
||||||
|
304,220,1323,1,0,
|
||||||
|
288,240,1412,1,0,
|
||||||
|
256,240,1502,1,0,
|
||||||
|
224,240,1591,5,0,
|
||||||
|
192,240,1680,1,0,
|
||||||
|
160,240,1769,1,0,
|
||||||
|
128,224,1859,1,0,
|
||||||
|
128,192,1948,5,0,
|
||||||
|
144,168,2037,1,0,
|
||||||
|
160,144,2127,1,0,
|
||||||
|
177,120,2216,1,0,
|
||||||
|
192,96,2305,5,0,
|
||||||
|
224,96,2394,1,0,
|
||||||
|
256,96,2484,1,0,
|
||||||
|
288,96,2573,1,0,
|
||||||
|
320,96,2662,5,0,
|
||||||
|
339,122,2752,1,0,
|
||||||
|
352,144,2841,1,0,
|
||||||
|
373,173,2930,1,0,
|
||||||
|
384,192,3019,5,0,
|
||||||
|
373,220,3109,1,0,
|
||||||
|
360,248,3198,1,0,
|
||||||
|
337,272,3287,1,0,
|
||||||
|
320,288,3377,5,0,
|
||||||
|
288,288,3466,1,0,
|
||||||
|
256,288,3555,1,0,
|
||||||
|
224,288,3644,1,0,
|
||||||
|
192,288,3734,5,0,
|
||||||
|
136,288,3912,1,0,
|
||||||
|
96,256,4091,5,0,
|
||||||
|
72,216,4180,1,0,
|
||||||
|
72,176,4269,1,0,
|
||||||
|
80,144,4359,1,0,
|
||||||
|
96,120,4448,5,0,
|
||||||
|
128,88,4627,1,0,
|
||||||
|
160,48,4805,5,0,
|
||||||
|
192,48,4895,1,0,
|
||||||
|
224,48,4984,1,0,
|
||||||
|
256,48,5073,1,0,
|
||||||
|
288,48,5162,5,0,
|
||||||
|
352,48,5341,1,0,
|
||||||
|
416,128,5698,6,0,B|416:128|416:192,1,49
|
||||||
|
416,272,6234,5,0,
|
||||||
|
416,312,6323,1,0,
|
||||||
|
416,352,6412,1,0,
|
||||||
|
376,352,6502,1,0,
|
||||||
|
336,352,6591,5,0,
|
||||||
|
336,272,6770,1,0,
|
||||||
|
256,272,6948,5,0,
|
||||||
|
256,312,7037,1,0,
|
||||||
|
256,352,7127,1,0,
|
||||||
|
216,352,7216,1,0,
|
||||||
|
176,352,7305,5,0,
|
||||||
|
176,272,7484,1,0,
|
||||||
|
176,192,7662,5,0,
|
||||||
|
136,192,7752,1,0,
|
||||||
|
96,192,7841,1,0,
|
||||||
|
64,160,7930,1,0,
|
||||||
|
32,128,8020,5,0,
|
||||||
|
32,48,8198,1,0,
|
||||||
|
112,112,8555,6,0,B|112:112|112:48,1,49
|
||||||
|
224,32,9091,5,0,
|
||||||
|
224,64,9180,1,0,
|
||||||
|
224,96,9270,1,0,
|
||||||
|
256,96,9359,1,0,
|
||||||
|
288,96,9448,5,0,
|
||||||
|
288,160,9627,1,0,
|
||||||
|
224,160,9805,5,0,
|
||||||
|
224,192,9895,1,0,
|
||||||
|
224,224,9984,1,0,
|
||||||
|
256,224,10073,1,0,
|
||||||
|
288,224,10162,5,0,
|
||||||
|
288,288,10341,1,0,
|
||||||
|
224,288,10520,5,0,
|
||||||
|
224,320,10609,1,0,
|
||||||
|
224,352,10698,1,0,
|
||||||
|
256,352,10787,1,0,
|
||||||
|
288,352,10877,5,0,
|
||||||
|
352,352,11055,1,0,
|
||||||
|
352,288,11234,5,0,
|
||||||
|
352,256,11323,1,0,
|
||||||
|
352,224,11412,1,0,
|
||||||
|
384,224,11502,1,0,
|
||||||
|
416,224,11591,5,0,
|
||||||
|
416,288,11769,1,0,
|
||||||
|
480,256,11948,5,0,
|
||||||
|
480,224,12037,1,0,
|
||||||
|
480,192,12127,1,0,
|
||||||
|
480,160,12216,1,0,
|
||||||
|
416,160,12305,5,0,
|
||||||
|
352,160,12484,1,0,
|
||||||
|
352,96,12662,5,0,
|
||||||
|
384,96,12752,1,0,
|
||||||
|
416,96,12841,1,0,
|
||||||
|
448,96,12930,1,0,
|
||||||
|
416,32,13019,5,0,
|
||||||
|
352,32,13198,1,0,
|
||||||
|
288,32,13377,6,0,B|288:32|176:32,1,98
|
||||||
|
192,96,13912,2,0,B|192:96|304:96,1,98
|
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
57
tests/conftest.py
Normal file
57
tests/conftest.py
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import AsyncIterator
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
import pytest
|
||||||
|
import respx
|
||||||
|
from asgi_lifespan import LifespanManager
|
||||||
|
from asgi_lifespan._types import ASGIApp
|
||||||
|
from fastapi import status
|
||||||
|
|
||||||
|
from app.api.init_api import asgi_app
|
||||||
|
|
||||||
|
# TODO: fixtures for postgres database connection(s) for itests
|
||||||
|
|
||||||
|
# TODO: I believe if we switch to fastapi.TestClient, we
|
||||||
|
# will no longer need to use the asgi-lifespan dependency.
|
||||||
|
# (We do not need an asynchronous http client for our tests)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def mock_out_initial_image_downloads(respx_mock: respx.MockRouter) -> None:
|
||||||
|
# mock out default avatar download
|
||||||
|
respx_mock.get("https://i.cmyui.xyz/U24XBZw-4wjVME-JaEz3.png").mock(
|
||||||
|
return_value=httpx.Response(
|
||||||
|
status_code=status.HTTP_200_OK,
|
||||||
|
headers={"Content-Type": "image/png"},
|
||||||
|
content=b"i am a png file",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
# mock out achievement image downloads
|
||||||
|
respx_mock.get(url__regex=r"https://assets.ppy.sh/medals/client/.+").mock(
|
||||||
|
return_value=httpx.Response(
|
||||||
|
status_code=status.HTTP_200_OK,
|
||||||
|
headers={"Content-Type": "image/png"},
|
||||||
|
content=b"i am a png file",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
async def app() -> AsyncIterator[ASGIApp]:
|
||||||
|
async with LifespanManager(
|
||||||
|
asgi_app,
|
||||||
|
startup_timeout=None,
|
||||||
|
shutdown_timeout=None,
|
||||||
|
) as manager:
|
||||||
|
yield manager.app
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
async def http_client(app: ASGIApp) -> AsyncIterator[httpx.AsyncClient]:
|
||||||
|
async with httpx.AsyncClient(app=app, base_url="http://test") as client:
|
||||||
|
yield client
|
||||||
|
|
||||||
|
|
||||||
|
pytest_plugins = []
|
0
tests/integration/__init__.py
Normal file
0
tests/integration/__init__.py
Normal file
243
tests/integration/domains/osu_test.py
Normal file
243
tests/integration/domains/osu_test.py
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import secrets
|
||||||
|
from datetime import datetime
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
import respx
|
||||||
|
from fastapi import status
|
||||||
|
from httpx import AsyncClient
|
||||||
|
|
||||||
|
from app import encryption
|
||||||
|
from testing.sample_data import sample_beatmap_data
|
||||||
|
|
||||||
|
|
||||||
|
async def test_score_submission(
|
||||||
|
http_client: AsyncClient,
|
||||||
|
respx_mock: respx.MockRouter,
|
||||||
|
) -> None:
|
||||||
|
# ARRANGE
|
||||||
|
|
||||||
|
username = f"test-{secrets.token_hex(4)}"
|
||||||
|
email_address = f"cmyui-{secrets.token_hex(4)}@akatsuki.pw"
|
||||||
|
passwd_plaintext = "myPassword321$"
|
||||||
|
passwd_md5 = hashlib.md5(passwd_plaintext.encode()).hexdigest()
|
||||||
|
|
||||||
|
respx_mock.get("http://ip-api.com/line/").mock(
|
||||||
|
return_value=httpx.Response(
|
||||||
|
status_code=status.HTTP_200_OK,
|
||||||
|
content=b"\n".join((b"success", b"CA", b"43.6485", b"-79.4054")),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
response = await http_client.post(
|
||||||
|
url="/users",
|
||||||
|
headers={
|
||||||
|
"Host": "osu.cmyui.xyz",
|
||||||
|
"X-Forwarded-For": "127.0.0.1",
|
||||||
|
"X-Real-IP": "127.0.0.1",
|
||||||
|
},
|
||||||
|
data={
|
||||||
|
"user[username]": username,
|
||||||
|
"user[password]": passwd_plaintext,
|
||||||
|
"user[user_email]": email_address,
|
||||||
|
"check": "0",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert response.status_code == status.HTTP_200_OK
|
||||||
|
|
||||||
|
osu_version = "20230814"
|
||||||
|
utc_offset = -5
|
||||||
|
display_city = 1
|
||||||
|
pm_private = 1
|
||||||
|
|
||||||
|
osu_path_md5 = hashlib.md5(b"lol123").hexdigest()
|
||||||
|
adapters_str = ".".join(("1", "2", "3")) + "."
|
||||||
|
adapters_md5 = hashlib.md5(b"lol123").hexdigest()
|
||||||
|
uninstall_md5 = hashlib.md5(b"lol123").hexdigest() # or uniqueid 1
|
||||||
|
disk_signature_md5 = hashlib.md5(b"lol123").hexdigest() # or uniqueid 2
|
||||||
|
|
||||||
|
client_hashes = (
|
||||||
|
":".join(
|
||||||
|
(
|
||||||
|
osu_path_md5,
|
||||||
|
adapters_str,
|
||||||
|
adapters_md5,
|
||||||
|
# double md5 unique ids on login; single time on score submission
|
||||||
|
hashlib.md5(uninstall_md5.encode()).hexdigest(),
|
||||||
|
hashlib.md5(disk_signature_md5.encode()).hexdigest(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
+ ":"
|
||||||
|
)
|
||||||
|
|
||||||
|
login_data = (
|
||||||
|
"\n".join(
|
||||||
|
(
|
||||||
|
username,
|
||||||
|
passwd_md5,
|
||||||
|
"|".join(
|
||||||
|
(
|
||||||
|
"b" + osu_version,
|
||||||
|
str(utc_offset),
|
||||||
|
str(display_city),
|
||||||
|
client_hashes,
|
||||||
|
str(pm_private),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
).encode()
|
||||||
|
+ b"\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
response = await http_client.post(
|
||||||
|
url="/",
|
||||||
|
headers={
|
||||||
|
"Host": "c.cmyui.xyz",
|
||||||
|
"User-Agent": "osu!",
|
||||||
|
"CF-Connecting-IP": "127.0.0.1",
|
||||||
|
},
|
||||||
|
content=login_data,
|
||||||
|
)
|
||||||
|
assert response.status_code == status.HTTP_200_OK
|
||||||
|
|
||||||
|
# cho token must be valid uuid
|
||||||
|
try:
|
||||||
|
UUID(response.headers["cho-token"])
|
||||||
|
except ValueError:
|
||||||
|
raise AssertionError(
|
||||||
|
"cho-token is not a valid uuid",
|
||||||
|
response.headers["cho-token"],
|
||||||
|
)
|
||||||
|
|
||||||
|
has_supporter = True
|
||||||
|
|
||||||
|
beatmap_md5 = "1cf5b2c2edfafd055536d2cefcb89c0e"
|
||||||
|
n300 = 83
|
||||||
|
n100 = 14
|
||||||
|
n50 = 5
|
||||||
|
ngeki = 23
|
||||||
|
nkatu = 6
|
||||||
|
nmiss = 6
|
||||||
|
score = 26810
|
||||||
|
max_combo = 52
|
||||||
|
perfect = False
|
||||||
|
grade = "C"
|
||||||
|
mods = 136
|
||||||
|
passed = True
|
||||||
|
game_mode = 0
|
||||||
|
client_time = datetime.now()
|
||||||
|
|
||||||
|
storyboard_md5 = hashlib.md5(b"lol123").hexdigest()
|
||||||
|
|
||||||
|
score_online_checksum = hashlib.md5(
|
||||||
|
"chickenmcnuggets{0}o15{1}{2}smustard{3}{4}uu{5}{6}{7}{8}{9}{10}{11}Q{12}{13}{15}{14:%y%m%d%H%M%S}{16}{17}".format(
|
||||||
|
n100 + n300,
|
||||||
|
n50,
|
||||||
|
ngeki,
|
||||||
|
nkatu,
|
||||||
|
nmiss,
|
||||||
|
beatmap_md5,
|
||||||
|
max_combo,
|
||||||
|
perfect,
|
||||||
|
username,
|
||||||
|
score,
|
||||||
|
grade,
|
||||||
|
mods,
|
||||||
|
passed,
|
||||||
|
game_mode,
|
||||||
|
client_time,
|
||||||
|
osu_version,
|
||||||
|
client_hashes,
|
||||||
|
storyboard_md5,
|
||||||
|
# yyMMddHHmmss
|
||||||
|
).encode(),
|
||||||
|
).hexdigest()
|
||||||
|
|
||||||
|
score_data = [
|
||||||
|
beatmap_md5,
|
||||||
|
username + (" " if has_supporter else ""),
|
||||||
|
score_online_checksum,
|
||||||
|
str(n300),
|
||||||
|
str(n100),
|
||||||
|
str(n50),
|
||||||
|
str(ngeki),
|
||||||
|
str(nkatu),
|
||||||
|
str(nmiss),
|
||||||
|
str(score),
|
||||||
|
str(max_combo),
|
||||||
|
str(perfect),
|
||||||
|
str(grade),
|
||||||
|
str(mods),
|
||||||
|
str(passed),
|
||||||
|
str(game_mode),
|
||||||
|
client_time.strftime("%y%m%d%H%M%S"),
|
||||||
|
str(osu_version),
|
||||||
|
"26685362", # TODO what is this?
|
||||||
|
]
|
||||||
|
|
||||||
|
iv_b64 = b"N2Q1YWZiNzYzNWFiYWZjZWMyMWMwM2QwMDEzOGRiNDk="
|
||||||
|
visual_settings_b64 = b"YHD/rr/lajZIr+ZC6UbFYvCOwTOaEF3qhJCFaZUlQA8="
|
||||||
|
|
||||||
|
score_data_b64, client_hash_b64 = encryption.encrypt_score_aes_data(
|
||||||
|
score_data,
|
||||||
|
client_hashes,
|
||||||
|
iv_b64=iv_b64,
|
||||||
|
osu_version=osu_version,
|
||||||
|
)
|
||||||
|
score_time = 13358
|
||||||
|
fail_time = 0
|
||||||
|
exited_out = False
|
||||||
|
|
||||||
|
# mock out 3rd party calls to get beatmap data
|
||||||
|
for url in (
|
||||||
|
"https://osu.direct/api/get_beatmaps?h=1cf5b2c2edfafd055536d2cefcb89c0e",
|
||||||
|
"https://osu.direct/api/get_beatmaps?s=141",
|
||||||
|
):
|
||||||
|
respx_mock.get(url).mock(
|
||||||
|
return_value=httpx.Response(
|
||||||
|
status_code=status.HTTP_200_OK,
|
||||||
|
json=sample_beatmap_data.vivid_getbeatmaps_sample_response(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
respx_mock.get("https://old.ppy.sh/osu/315").mock(
|
||||||
|
return_value=httpx.Response(
|
||||||
|
status_code=status.HTTP_200_OK,
|
||||||
|
content=sample_beatmap_data.vivid_osu_file_sample_response(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# ACT
|
||||||
|
response = await http_client.post(
|
||||||
|
url="/web/osu-submit-modular-selector.php",
|
||||||
|
headers={"Host": "osu.cmyui.xyz", "token": "auth-token"},
|
||||||
|
data={
|
||||||
|
"x": exited_out,
|
||||||
|
"ft": fail_time,
|
||||||
|
"fs": visual_settings_b64,
|
||||||
|
"bmk": beatmap_md5, # (`updated_beatmap_hash` in code)
|
||||||
|
"sbk": storyboard_md5,
|
||||||
|
"iv": iv_b64,
|
||||||
|
"c1": f"{uninstall_md5}|{disk_signature_md5}",
|
||||||
|
"st": score_time,
|
||||||
|
"pass": passwd_md5,
|
||||||
|
"osuver": osu_version,
|
||||||
|
"s": client_hash_b64,
|
||||||
|
# score param
|
||||||
|
"score": score_data_b64,
|
||||||
|
},
|
||||||
|
files={
|
||||||
|
# simulate replay data
|
||||||
|
"score": b"12345"
|
||||||
|
* 100,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# ASSERT
|
||||||
|
assert response.status_code == status.HTTP_200_OK
|
||||||
|
assert (
|
||||||
|
response.read()
|
||||||
|
== b"beatmapId:315|beatmapSetId:141|beatmapPlaycount:1|beatmapPasscount:1|approvedDate:2014-05-18 15:41:48|\n|chartId:beatmap|chartUrl:https://osu.cmyui.xyz/s/141|chartName:Beatmap Ranking|rankBefore:|rankAfter:1|rankedScoreBefore:|rankedScoreAfter:26810|totalScoreBefore:|totalScoreAfter:26810|maxComboBefore:|maxComboAfter:52|accuracyBefore:|accuracyAfter:81.94|ppBefore:|ppAfter:10.448|onlineScoreId:1|\n|chartId:overall|chartUrl:https://cmyui.xyz/u/3|chartName:Overall Ranking|rankBefore:|rankAfter:1|rankedScoreBefore:|rankedScoreAfter:26810|totalScoreBefore:|totalScoreAfter:26810|maxComboBefore:|maxComboAfter:52|accuracyBefore:|accuracyAfter:81.94|ppBefore:|ppAfter:11|achievements-new:osu-skill-pass-4+Insanity Approaches+You're not twitching, you're just ready./all-intro-hidden+Blindsight+I can see just perfectly"
|
||||||
|
)
|
0
tests/unit/__init__.py
Normal file
0
tests/unit/__init__.py
Normal file
669
tests/unit/packets_test.py
Normal file
669
tests/unit/packets_test.py
Normal file
@@ -0,0 +1,669 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
import app.packets
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("test_input", "expected"),
|
||||||
|
[
|
||||||
|
(0, b"\x05\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00"),
|
||||||
|
(2_147_483_647, b"\x05\x00\x00\x04\x00\x00\x00\xff\xff\xff\x7f"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_write_user_id(test_input, expected):
|
||||||
|
assert app.packets.login_reply(test_input) == expected
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("test_input", "expected"),
|
||||||
|
[
|
||||||
|
(
|
||||||
|
{
|
||||||
|
"sender": "cmyui",
|
||||||
|
"msg": "woah woah crazy!!",
|
||||||
|
"recipient": "jacobian",
|
||||||
|
"sender_id": 32,
|
||||||
|
},
|
||||||
|
b"\x07\x00\x00(\x00\x00\x00\x0b\x05cmyui\x0b\x11woah woah crazy!!\x0b\x08jacobian \x00\x00\x00",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
{
|
||||||
|
"sender": "",
|
||||||
|
"msg": "",
|
||||||
|
"recipient": "",
|
||||||
|
"sender_id": 0,
|
||||||
|
},
|
||||||
|
b"\x07\x00\x00\x07\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_write_send_message(test_input, expected):
|
||||||
|
assert app.packets.send_message(**test_input) == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_write_pong():
|
||||||
|
assert app.packets.pong() == b"\x08\x00\x00\x00\x00\x00\x00"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("test_input", "expected"),
|
||||||
|
[
|
||||||
|
(
|
||||||
|
{"old": "cmyui", "new": "abcgamer321"},
|
||||||
|
b"\t\x00\x00\x16\x00\x00\x00\x0b\x14cmyui>>>>abcgamer321",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
{"old": "", "new": ""},
|
||||||
|
b"\t\x00\x00\x06\x00\x00\x00\x0b\x04>>>>",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_write_change_username(test_input, expected):
|
||||||
|
assert app.packets.change_username(**test_input) == expected
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("test_input", "expected"),
|
||||||
|
[
|
||||||
|
(
|
||||||
|
{
|
||||||
|
"user_id": 1001,
|
||||||
|
"action": 2, # playing
|
||||||
|
"info_text": "gaming", # TODO: get a realistic one
|
||||||
|
"map_md5": "60b725f10c9c85c70d97880dfe8191b3",
|
||||||
|
"mods": 64,
|
||||||
|
"mode": 0,
|
||||||
|
"map_id": 1723723,
|
||||||
|
"ranked_score": 1_238_917_112,
|
||||||
|
"accuracy": 92.32,
|
||||||
|
"plays": 3821,
|
||||||
|
"total_score": 3_812_428_392,
|
||||||
|
"global_rank": 42,
|
||||||
|
"pp": 8291,
|
||||||
|
},
|
||||||
|
b"\x0b\x00\x00V\x00\x00\x00\xe9\x03\x00\x00\x02\x0b\x06gaming\x0b 60b725f10c9c85c70d97880dfe8191b3@\x00\x00\x00\x00KM\x1a\x00\xf8_\xd8I\x00\x00\x00\x00\xd6Vl?\xed\x0e\x00\x00h\n=\xe3\x00\x00\x00\x00*\x00\x00\x00c ",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
{
|
||||||
|
"user_id": 0,
|
||||||
|
"action": 0,
|
||||||
|
"info_text": "",
|
||||||
|
"map_md5": "", # TODO: can this even be empty
|
||||||
|
"mods": 0,
|
||||||
|
"mode": 0,
|
||||||
|
"map_id": 0,
|
||||||
|
"ranked_score": 0,
|
||||||
|
"accuracy": 0.0,
|
||||||
|
"plays": 0,
|
||||||
|
"total_score": 0,
|
||||||
|
"global_rank": 0,
|
||||||
|
"pp": 0,
|
||||||
|
},
|
||||||
|
b"\x0b\x00\x00.\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_write_user_stats(test_input, expected):
|
||||||
|
assert app.packets._user_stats(**test_input) == expected
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("test_input", "expected"),
|
||||||
|
[
|
||||||
|
(0, b"\x0c\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00\x00"),
|
||||||
|
(2_147_483_647, b"\x0c\x00\x00\x05\x00\x00\x00\xff\xff\xff\x7f\x00"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_write_logout(test_input, expected):
|
||||||
|
assert app.packets.logout(test_input) == expected
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("test_input", "expected"),
|
||||||
|
[
|
||||||
|
(0, b"\x0d\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00"),
|
||||||
|
(2_147_483_647, b"\x0d\x00\x00\x04\x00\x00\x00\xff\xff\xff\x7f"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_write_spectator_joined(test_input, expected):
|
||||||
|
assert app.packets.spectator_joined(test_input) == expected
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("test_input", "expected"),
|
||||||
|
[
|
||||||
|
(0, b"\x0e\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00"),
|
||||||
|
(2_147_483_647, b"\x0e\x00\x00\x04\x00\x00\x00\xff\xff\xff\x7f"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_write_spectator_left(test_input, expected):
|
||||||
|
assert app.packets.spectator_left(test_input) == expected
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.xfail(reason="need to implement proper writing")
|
||||||
|
@pytest.mark.parametrize(("test_input", "expected"), [({}, b"")])
|
||||||
|
def test_write_spectate_frames(test_input, expected):
|
||||||
|
assert app.packets.spectate_frames(test_input) == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_write_version_update():
|
||||||
|
assert app.packets.version_update() == b"\x13\x00\x00\x00\x00\x00\x00"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("test_input", "expected"),
|
||||||
|
[
|
||||||
|
(0, b"\x16\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00"),
|
||||||
|
(2_147_483_647, b"\x16\x00\x00\x04\x00\x00\x00\xff\xff\xff\x7f"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_write_spectator_cant_spectate(test_input, expected):
|
||||||
|
assert app.packets.spectator_cant_spectate(test_input) == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_write_get_attention():
|
||||||
|
assert app.packets.get_attention() == b"\x17\x00\x00\x00\x00\x00\x00"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("test_input", "expected"),
|
||||||
|
[
|
||||||
|
("waowww", b"\x18\x00\x00\x08\x00\x00\x00\x0b\x06waowww"),
|
||||||
|
("", b"\x18\x00\x00\x01\x00\x00\x00\x00"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_write_notification(test_input, expected):
|
||||||
|
assert app.packets.notification(test_input) == expected
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.xfail(reason="need to remove bancho.py match object")
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("test_input", "expected"),
|
||||||
|
[
|
||||||
|
({"m": None, "send_pw": False}, b""),
|
||||||
|
({"m": None, "send_pw": True}, b""),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_write_update_match(test_input, expected):
|
||||||
|
assert app.packets.update_match(test_input) == expected
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.xfail(reason="need to remove bancho.py match object")
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("test_input", "expected"),
|
||||||
|
[
|
||||||
|
({}, b""),
|
||||||
|
({}, b""),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_write_new_match(test_input, expected):
|
||||||
|
assert app.packets.new_match(test_input) == expected
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("test_input", "expected"),
|
||||||
|
[
|
||||||
|
(0, b"\x1c\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00"),
|
||||||
|
(2_147_483_647, b"\x1c\x00\x00\x04\x00\x00\x00\xff\xff\xff\x7f"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_write_dispose_match(test_input, expected):
|
||||||
|
assert app.packets.dispose_match(test_input) == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_write_toggle_block_non_friend_pm():
|
||||||
|
assert app.packets.toggle_block_non_friend_dm() == b'"\x00\x00\x00\x00\x00\x00'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.xfail(reason="need to remove bancho.py match object")
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("test_input", "expected"),
|
||||||
|
[
|
||||||
|
({}, b""),
|
||||||
|
({}, b""),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_write_match_join_success(test_input, expected):
|
||||||
|
assert app.packets.match_join_success(test_input) == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_write_match_join_fail():
|
||||||
|
assert app.packets.match_join_fail() == b"%\x00\x00\x00\x00\x00\x00"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("test_input", "expected"),
|
||||||
|
[
|
||||||
|
(0, b"*\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00"),
|
||||||
|
(2_147_483_647, b"*\x00\x00\x04\x00\x00\x00\xff\xff\xff\x7f"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_write_fellow_spectator_joined(test_input, expected):
|
||||||
|
assert app.packets.fellow_spectator_joined(test_input) == expected
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("test_input", "expected"),
|
||||||
|
[
|
||||||
|
(0, b"+\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00"),
|
||||||
|
(2_147_483_647, b"+\x00\x00\x04\x00\x00\x00\xff\xff\xff\x7f"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_write_fellow_spectator_left(test_input, expected):
|
||||||
|
assert app.packets.fellow_spectator_left(test_input) == expected
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.xfail(reason="need to remove bancho.py match object")
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("test_input", "expected"),
|
||||||
|
[
|
||||||
|
({}, b""),
|
||||||
|
({}, b""),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_write_match_start(test_input, expected):
|
||||||
|
assert app.packets.match_start(test_input) == expected
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("test_input", "expected"),
|
||||||
|
[
|
||||||
|
(
|
||||||
|
app.packets.ScoreFrame(
|
||||||
|
time=38242, # TODO: check if realistic
|
||||||
|
id=28, # TODO: check if realistic
|
||||||
|
num300=320,
|
||||||
|
num100=48,
|
||||||
|
num50=2,
|
||||||
|
num_geki=32,
|
||||||
|
num_katu=8,
|
||||||
|
num_miss=3,
|
||||||
|
total_score=492_392,
|
||||||
|
current_combo=39,
|
||||||
|
max_combo=122,
|
||||||
|
perfect=False,
|
||||||
|
current_hp=245, # TODO: check if realistic
|
||||||
|
tag_byte=0,
|
||||||
|
score_v2=False,
|
||||||
|
# NOTE: this stuff isn't written
|
||||||
|
# combo_portion=0.0,
|
||||||
|
# bonus_portion=0.0,
|
||||||
|
),
|
||||||
|
b"0\x00\x00\x1d\x00\x00\x00b\x95\x00\x00\x1c@\x010\x00\x02\x00 \x00\x08\x00\x03\x00h\x83\x07\x00z\x00'\x00\x00\xf5\x00\x00",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
app.packets.ScoreFrame(
|
||||||
|
time=0,
|
||||||
|
id=0,
|
||||||
|
num300=0,
|
||||||
|
num100=0,
|
||||||
|
num50=0,
|
||||||
|
num_geki=0,
|
||||||
|
num_katu=0,
|
||||||
|
num_miss=0,
|
||||||
|
total_score=0,
|
||||||
|
current_combo=0,
|
||||||
|
max_combo=0,
|
||||||
|
perfect=False,
|
||||||
|
current_hp=0,
|
||||||
|
tag_byte=0,
|
||||||
|
score_v2=False,
|
||||||
|
combo_portion=0.0,
|
||||||
|
bonus_portion=0.0,
|
||||||
|
),
|
||||||
|
b"0\x00\x00\x1d\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_write_match_score_update(test_input, expected):
|
||||||
|
assert app.packets.match_score_update(test_input) == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_write_match_transfer_host():
|
||||||
|
assert app.packets.match_transfer_host() == b"2\x00\x00\x00\x00\x00\x00"
|
||||||
|
|
||||||
|
|
||||||
|
def test_write_match_all_players_loaded():
|
||||||
|
assert app.packets.match_all_players_loaded() == b"5\x00\x00\x00\x00\x00\x00"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("test_input", "expected"),
|
||||||
|
[
|
||||||
|
(0, b"9\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00"),
|
||||||
|
(2_147_483_647, b"9\x00\x00\x04\x00\x00\x00\xff\xff\xff\x7f"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_write_match_player_failed(test_input, expected):
|
||||||
|
assert app.packets.match_player_failed(test_input) == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_write_match_complete():
|
||||||
|
assert app.packets.match_complete() == b":\x00\x00\x00\x00\x00\x00"
|
||||||
|
|
||||||
|
|
||||||
|
def test_write_match_skip():
|
||||||
|
assert app.packets.match_skip() == b"=\x00\x00\x00\x00\x00\x00"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("test_input", "expected"),
|
||||||
|
[
|
||||||
|
("#osu", b"@\x00\x00\x06\x00\x00\x00\x0b\x04#osu"),
|
||||||
|
("", b"@\x00\x00\x01\x00\x00\x00\x00"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_write_channel_join(test_input, expected):
|
||||||
|
assert app.packets.channel_join(test_input) == expected
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("test_input", "expected"),
|
||||||
|
[
|
||||||
|
(
|
||||||
|
("#osu", "le topique", 123),
|
||||||
|
b"A\x00\x00\x14\x00\x00\x00\x0b\x04#osu\x0b\nle topique{\x00",
|
||||||
|
),
|
||||||
|
(("", "", 0), b"A\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_write_channel_info(test_input, expected):
|
||||||
|
assert app.packets.channel_info(*test_input) == expected
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("test_input", "expected"),
|
||||||
|
[
|
||||||
|
("#osu", b"B\x00\x00\x06\x00\x00\x00\x0b\x04#osu"),
|
||||||
|
("", b"B\x00\x00\x01\x00\x00\x00\x00"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_write_channel_kick(test_input, expected):
|
||||||
|
assert app.packets.channel_kick(test_input) == expected
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("test_input", "expected"),
|
||||||
|
[
|
||||||
|
(
|
||||||
|
("#osu", "le topique", 123),
|
||||||
|
b"C\x00\x00\x14\x00\x00\x00\x0b\x04#osu\x0b\nle topique{\x00",
|
||||||
|
),
|
||||||
|
(("", "", 0), b"C\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_write_channel_auto_join(test_input, expected):
|
||||||
|
assert app.packets.channel_auto_join(*test_input) == expected
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: test_write_beatmap_info_reply? it's disabled in
|
||||||
|
# app.packets but perhaps for completion i can keep it in
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("test_input", "expected"),
|
||||||
|
[
|
||||||
|
(0, b"G\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00"),
|
||||||
|
(2_147_483_647, b"G\x00\x00\x04\x00\x00\x00\xff\xff\xff\x7f"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_write_bancho_privileges(test_input, expected):
|
||||||
|
assert app.packets.bancho_privileges(test_input) == expected
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("test_input", "expected"),
|
||||||
|
[
|
||||||
|
(
|
||||||
|
[1, 4, 1001],
|
||||||
|
b"H\x00\x00\x0e\x00\x00\x00\x03\x00\x01\x00\x00\x00\x04\x00\x00\x00\xe9\x03\x00\x00",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
[],
|
||||||
|
b"H\x00\x00\x02\x00\x00\x00\x00\x00",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_write_friends_list(test_input, expected):
|
||||||
|
assert app.packets.friends_list(test_input) == expected
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("test_input", "expected"),
|
||||||
|
[
|
||||||
|
(0, b"K\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00"),
|
||||||
|
(2_147_483_647, b"K\x00\x00\x04\x00\x00\x00\xff\xff\xff\x7f"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_write_protocol_version(test_input, expected):
|
||||||
|
assert app.packets.protocol_version(test_input) == expected
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("test_input", "expected"),
|
||||||
|
[
|
||||||
|
(
|
||||||
|
("https://icon-url.ca/a.png", "https://onclick-url.ca/a.png"),
|
||||||
|
b"L\x00\x008\x00\x00\x00\x0b6https://icon-url.ca/a.png|https://onclick-url.ca/a.png",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
("", ""),
|
||||||
|
b"L\x00\x00\x03\x00\x00\x00\x0b\x01|",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_write_main_menu_icon(test_input, expected):
|
||||||
|
assert app.packets.main_menu_icon(*test_input) == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_write_monitor():
|
||||||
|
assert app.packets.monitor() == b"P\x00\x00\x00\x00\x00\x00"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("test_input", "expected"),
|
||||||
|
[
|
||||||
|
(0, b"Q\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00"),
|
||||||
|
(2_147_483_647, b"Q\x00\x00\x04\x00\x00\x00\xff\xff\xff\x7f"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_write_match_player_skipped(test_input, expected):
|
||||||
|
assert app.packets.match_player_skipped(test_input) == expected
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("test_input", "expected"),
|
||||||
|
[
|
||||||
|
(
|
||||||
|
{
|
||||||
|
"user_id": 1001,
|
||||||
|
"name": "cmyui",
|
||||||
|
"utc_offset": -5,
|
||||||
|
"country_code": 38,
|
||||||
|
"bancho_privileges": 31, # owner|dev|supporter|mod|player
|
||||||
|
"mode": 0,
|
||||||
|
"longitude": 43.768,
|
||||||
|
"latitude": -79.522,
|
||||||
|
"global_rank": 42,
|
||||||
|
},
|
||||||
|
b"S\x00\x00\x1a\x00\x00\x00\xe9\x03\x00\x00\x0b\x05cmyui\x13&\x1fo\x12/BD\x0b\x9f\xc2*\x00\x00\x00",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
{
|
||||||
|
"user_id": 0,
|
||||||
|
"name": "",
|
||||||
|
"utc_offset": 0,
|
||||||
|
"country_code": 0,
|
||||||
|
"bancho_privileges": 0,
|
||||||
|
"mode": 0,
|
||||||
|
"longitude": 0.0,
|
||||||
|
"latitude": 0.0,
|
||||||
|
"global_rank": 0,
|
||||||
|
},
|
||||||
|
b"S\x00\x00\x14\x00\x00\x00\x00\x00\x00\x00\x00\x18\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_write_user_presence(test_input, expected):
|
||||||
|
assert app.packets._user_presence(**test_input) == expected
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("test_input", "expected"),
|
||||||
|
[
|
||||||
|
(0, b"V\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00"),
|
||||||
|
(2_147_483_647, b"V\x00\x00\x04\x00\x00\x00\xff\xff\xff\x7f"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_write_restart_server(test_input, expected):
|
||||||
|
assert app.packets.restart_server(test_input) == expected
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.xfail(reason="need to remove bancho.py match object")
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("test_input", "expected"),
|
||||||
|
[
|
||||||
|
({"p": None, "t_name": "cover"}, b""),
|
||||||
|
({"p": None, "t_name": "cover"}, b""),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_write_match_invite(test_input, expected):
|
||||||
|
assert app.packets.match_invite(**test_input) == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_channel_info_end():
|
||||||
|
assert app.packets.channel_info_end() == b"Y\x00\x00\x00\x00\x00\x00"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("test_input", "expected"),
|
||||||
|
[
|
||||||
|
("newpassword", b"[\x00\x00\x0d\x00\x00\x00\x0b\x0bnewpassword"),
|
||||||
|
("", b"[\x00\x00\x01\x00\x00\x00\x00"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_write_match_change_password(test_input, expected):
|
||||||
|
assert app.packets.match_change_password(test_input) == expected
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("test_input", "expected"),
|
||||||
|
[
|
||||||
|
(0, b"\\\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00"),
|
||||||
|
(2_147_483_647, b"\\\x00\x00\x04\x00\x00\x00\xff\xff\xff\x7f"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_write_silence_end(test_input, expected):
|
||||||
|
assert app.packets.silence_end(test_input) == expected
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("test_input", "expected"),
|
||||||
|
[
|
||||||
|
(0, b"^\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00"),
|
||||||
|
(2_147_483_647, b"^\x00\x00\x04\x00\x00\x00\xff\xff\xff\x7f"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_write_user_silenced(test_input, expected):
|
||||||
|
assert app.packets.user_silenced(test_input) == expected
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("test_input", "expected"),
|
||||||
|
[
|
||||||
|
(0, b"_\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00"),
|
||||||
|
(2_147_483_647, b"_\x00\x00\x04\x00\x00\x00\xff\xff\xff\x7f"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_write_user_presence_single(test_input, expected):
|
||||||
|
assert app.packets.user_presence_single(test_input) == expected
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("test_input", "expected"),
|
||||||
|
[
|
||||||
|
(
|
||||||
|
[1, 4, 1001],
|
||||||
|
b"`\x00\x00\x0e\x00\x00\x00\x03\x00\x01\x00\x00\x00\x04\x00\x00\x00\xe9\x03\x00\x00",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
[],
|
||||||
|
b"`\x00\x00\x02\x00\x00\x00\x00\x00",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_write_user_presence_bundle(test_input, expected):
|
||||||
|
assert app.packets.user_presence_bundle(test_input) == expected
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("test_input", "expected"),
|
||||||
|
[
|
||||||
|
("cover", b"d\x00\x00\r\x00\x00\x00\x00\x00\x0b\x05cover\x00\x00\x00\x00"),
|
||||||
|
("", b"d\x00\x00\x07\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_write_user_dm_blocked(test_input, expected):
|
||||||
|
assert app.packets.user_dm_blocked(test_input) == expected
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("test_input", "expected"),
|
||||||
|
[
|
||||||
|
("cover", b"e\x00\x00\r\x00\x00\x00\x00\x00\x0b\x05cover\x00\x00\x00\x00"),
|
||||||
|
("", b"e\x00\x00\x07\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_write_target_silenced(test_input, expected):
|
||||||
|
assert app.packets.target_silenced(test_input) == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_write_version_update_forced():
|
||||||
|
assert app.packets.version_update_forced() == b"f\x00\x00\x00\x00\x00\x00"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("test_input", "expected"),
|
||||||
|
[
|
||||||
|
(0, b"g\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00"),
|
||||||
|
(2_147_483_647, b"g\x00\x00\x04\x00\x00\x00\xff\xff\xff\x7f"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_write_switch_server(test_input, expected):
|
||||||
|
assert app.packets.switch_server(test_input) == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_write_account_restricted():
|
||||||
|
assert app.packets.account_restricted() == b"h\x00\x00\x00\x00\x00\x00"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("test_input", "expected"),
|
||||||
|
[
|
||||||
|
("yoyoo rip rtx", b"i\x00\x00\x0f\x00\x00\x00\x0b\ryoyoo rip rtx"),
|
||||||
|
("", b"i\x00\x00\x01\x00\x00\x00\x00"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_write_rtx(test_input, expected):
|
||||||
|
assert app.packets.rtx(test_input) == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_write_match_abort():
|
||||||
|
assert app.packets.match_abort() == b"j\x00\x00\x00\x00\x00\x00"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("test_input", "expected"),
|
||||||
|
[
|
||||||
|
(
|
||||||
|
"61.91.139.24",
|
||||||
|
b"k\x00\x00\x0e\x00\x00\x00\x0b\x0c61.91.139.24",
|
||||||
|
),
|
||||||
|
("", b"k\x00\x00\x01\x00\x00\x00\x00"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_write_switch_tournament_server(test_input, expected):
|
||||||
|
assert app.packets.switch_tournament_server(test_input) == expected
|
52
tools/enable_geoip_module.sh
Normal file
52
tools/enable_geoip_module.sh
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# ensure admin privileges
|
||||||
|
if (( $EUID != 0 )); then
|
||||||
|
printf "This script must be run with administrative privileges."
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
|
||||||
|
root_dir=$(pwd)
|
||||||
|
nginx_version=$(nginx -v 2>&1 | awk -F' ' '{print $3}' | grep -o '[0-9.]*$')
|
||||||
|
|
||||||
|
# download the nginx source and the geoip2 module in a temp folder
|
||||||
|
mkdir "temp" && cd "temp"
|
||||||
|
wget http://nginx.org/download/nginx-$nginx_version.tar.gz
|
||||||
|
tar zxvf nginx-$nginx_version.tar.gz
|
||||||
|
wget -O ngx_http_geoip2_module.tar.gz https://github.com/leev/ngx_http_geoip2_module/archive/master.tar.gz
|
||||||
|
tar zxvf ngx_http_geoip2_module.tar.gz
|
||||||
|
|
||||||
|
# install essentials apps to compile software and add ppas
|
||||||
|
apt update && apt install -y \
|
||||||
|
software-properties-common \
|
||||||
|
build-essential
|
||||||
|
|
||||||
|
# install maxmind's ppa and the libraries required to build nginx
|
||||||
|
add-apt-repository ppa:maxmind/ppa -y
|
||||||
|
apt install -y \
|
||||||
|
libmaxminddb0 \
|
||||||
|
libmaxminddb-dev \
|
||||||
|
mmdb-bin \
|
||||||
|
geoipupdate \
|
||||||
|
libpcre3 \
|
||||||
|
libpcre3-dev \
|
||||||
|
zlib1g \
|
||||||
|
zlib1g-dev \
|
||||||
|
libssl-dev
|
||||||
|
|
||||||
|
# build nginx with the geoip2 module
|
||||||
|
cd nginx-$nginx_version
|
||||||
|
./configure --add-dynamic-module=../ngx_http_geoip2_module-master $(nginx -V) --with-compat
|
||||||
|
make
|
||||||
|
|
||||||
|
# install the new dynamic module in nginx
|
||||||
|
mkdir -p /etc/nginx/modules-available /etc/nginx/modules-enabled
|
||||||
|
cp objs/ngx_http_geoip2_module.so /usr/lib/nginx/modules
|
||||||
|
echo "load_module modules/ngx_http_geoip2_module.so;" > /etc/nginx/modules-available/mod-http-geoip2.conf
|
||||||
|
rm -f /etc/nginx/modules-enabled/60-mod-http-geoip2.conf
|
||||||
|
ln -s /etc/nginx/modules-available/mod-http-geoip2.conf /etc/nginx/modules-enabled/60-mod-http-geoip2.conf
|
||||||
|
|
||||||
|
cd "$root_dir" && rm -r temp
|
||||||
|
|
||||||
|
printf "The GeoIP2 module has been installed and enabled."
|
21
tools/generate_cf_dns_records.sh
Normal file
21
tools/generate_cf_dns_records.sh
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
read -p "What's your server domain? " domain
|
||||||
|
domain=${domain:-example.com}
|
||||||
|
|
||||||
|
read -p "What's your server IP? " ip
|
||||||
|
ip=${ip:-0.0.0.0}
|
||||||
|
|
||||||
|
printf '%s\n' \
|
||||||
|
"a 1 IN A $ip"\
|
||||||
|
"api 1 IN A $ip"\
|
||||||
|
"assets 1 IN A $ip"\
|
||||||
|
"b 1 IN A $ip"\
|
||||||
|
"c 1 IN A $ip"\
|
||||||
|
"c4 1 IN A $ip"\
|
||||||
|
"ce 1 IN A $ip"\
|
||||||
|
"$domain 1 IN A $ip"\
|
||||||
|
"i 1 IN A $ip"\
|
||||||
|
"osu 1 IN A $ip"\
|
||||||
|
"s 1 IN A $ip" >> "cf_records.txt"
|
||||||
|
|
||||||
|
printf "Your Cloudflare DNS records have been generated."
|
13
tools/local_cert_generator.sh
Normal file
13
tools/local_cert_generator.sh
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
read -p "What's the name of your server? " name
|
||||||
|
name=${name=Bancho}
|
||||||
|
read -p "What's the base domain of your server? " domain
|
||||||
|
domain=*.${domain=ppy.sh}
|
||||||
|
read -p "What country is this based in? (ISO country code) " location
|
||||||
|
location=${location=CA}
|
||||||
|
|
||||||
|
args="/CN=$domain/O=$name/C=$location"
|
||||||
|
openssl req -subj $args -new -newkey rsa:4096 -sha256 -days 36500 -nodes -x509 -keyout key.pem -out cert.pem
|
||||||
|
openssl x509 -outform der -in cert.pem -out cert.crt
|
||||||
|
|
||||||
|
printf "Your certificates have been generated."
|
85
tools/migrate_logs.py
Normal file
85
tools/migrate_logs.py
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
#!/usr/bin/env python3.11
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import databases
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.abspath(os.pardir))
|
||||||
|
os.chdir(os.path.abspath(os.pardir))
|
||||||
|
|
||||||
|
import app.settings
|
||||||
|
|
||||||
|
LOG_REGEX = re.compile(
|
||||||
|
r"<(.*)\((.*)\)> (?P<action>unrestricted|restricted|unsilenced|silenced|added note) ?(\((.*)\))? ?(\: (?P<note>.*))? ?(?:for (?P<reason>.*))?",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def main() -> int:
|
||||||
|
async with databases.Database(app.settings.DB_DSN) as db:
|
||||||
|
async with (
|
||||||
|
db.connection() as select_conn,
|
||||||
|
db.connection() as update_conn,
|
||||||
|
):
|
||||||
|
# add/adjust new columns, keeping them null until we are finished
|
||||||
|
print("Creating new columns")
|
||||||
|
|
||||||
|
await update_conn.execute(
|
||||||
|
"ALTER TABLE `logs` ADD COLUMN `action` VARCHAR(32) null after `to`",
|
||||||
|
)
|
||||||
|
await update_conn.execute(
|
||||||
|
"ALTER TABLE `logs` MODIFY `msg` VARCHAR(2048) null",
|
||||||
|
) # now used as reason
|
||||||
|
|
||||||
|
# get all logs & change
|
||||||
|
print("Getting all old logs")
|
||||||
|
for row in await select_conn.fetch_all(f"SELECT * FROM logs"):
|
||||||
|
note = row["msg"]
|
||||||
|
|
||||||
|
note_match = LOG_REGEX.match(row["msg"])
|
||||||
|
if not note_match:
|
||||||
|
continue
|
||||||
|
|
||||||
|
reason = note_match["reason"]
|
||||||
|
note = note_match["note"]
|
||||||
|
|
||||||
|
msg = None
|
||||||
|
if reason:
|
||||||
|
msg = reason
|
||||||
|
elif note:
|
||||||
|
msg = note
|
||||||
|
|
||||||
|
if note:
|
||||||
|
action = "note"
|
||||||
|
else:
|
||||||
|
action = (
|
||||||
|
note_match["action"][:-2]
|
||||||
|
if "silence" not in note_match["reason"]
|
||||||
|
else note_match["action"][:-1]
|
||||||
|
)
|
||||||
|
|
||||||
|
await update_conn.execute(
|
||||||
|
"UPDATE logs SET action = :action, msg = :msg, time = :time WHERE id = :id",
|
||||||
|
{
|
||||||
|
"action": action,
|
||||||
|
"msg": msg,
|
||||||
|
"id": row["id"],
|
||||||
|
"time": row["time"],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# change action column to not null
|
||||||
|
await update_conn.execute(
|
||||||
|
"ALTER TABLE `logs` MODIFY `action` VARCHAR(32) not null",
|
||||||
|
)
|
||||||
|
|
||||||
|
print("Finished migrating logs!")
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(asyncio.run(main()))
|
8
tools/migrate_v420/go.mod
Normal file
8
tools/migrate_v420/go.mod
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
module scoreMigrator
|
||||||
|
|
||||||
|
go 1.17
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/go-sql-driver/mysql v1.6.0
|
||||||
|
github.com/jmoiron/sqlx v1.3.4
|
||||||
|
)
|
344
tools/migrate_v420/main.go
Normal file
344
tools/migrate_v420/main.go
Normal file
@@ -0,0 +1,344 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "github.com/go-sql-driver/mysql"
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
|
||||||
|
"database/sql"
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
/// MIGRATION TOOL INSTRUCTIONS ///
|
||||||
|
// first install golang from https://go.dev/doc/install
|
||||||
|
|
||||||
|
// next, install the dependencies for running this tool
|
||||||
|
// $ go get
|
||||||
|
|
||||||
|
// next, configure these parameters
|
||||||
|
var SQLUsername string = "cmyui"
|
||||||
|
var SQLPassword string = "lol123"
|
||||||
|
var SQLDatabase string = "gulag_old"
|
||||||
|
var SQLHost string = "127.0.0.1"
|
||||||
|
var SQLPort string = "3306"
|
||||||
|
var GulagPath string = "/home/cmyui/programming/gulag" // NOTE: no trailing slash!
|
||||||
|
|
||||||
|
// then, build & run the binary. this will create the new
|
||||||
|
// scores table, move all scores to the new tables, and
|
||||||
|
// move all existing replays to their new locations.
|
||||||
|
// NOTE: at the end, you will be prompted to delete the old
|
||||||
|
// scores tables. you should only do this once you are
|
||||||
|
// certain the migration ran without any issues.
|
||||||
|
// NOTE: you may want to back up your gulag/.data/osr folder
|
||||||
|
// which contains the server's replays, just in case
|
||||||
|
// there are any issues.
|
||||||
|
// $ go run .
|
||||||
|
|
||||||
|
var DB *sqlx.DB
|
||||||
|
|
||||||
|
type Score struct {
|
||||||
|
ID int64
|
||||||
|
MapMD5 string `db:"map_md5"`
|
||||||
|
Score int
|
||||||
|
PP float32
|
||||||
|
Acc float32
|
||||||
|
MaxCombo int `db:"max_combo"`
|
||||||
|
Mods int
|
||||||
|
N300 int
|
||||||
|
N100 int
|
||||||
|
N50 int
|
||||||
|
Nmiss int
|
||||||
|
Ngeki int
|
||||||
|
Nkatu int
|
||||||
|
Grade string
|
||||||
|
Status int
|
||||||
|
Mode int
|
||||||
|
PlayTime int64 `db:"play_time"`
|
||||||
|
TimeElapsed int `db:"time_elapsed"`
|
||||||
|
ClientFlags int `db:"client_flags"`
|
||||||
|
UserID int64 `db:"userid"`
|
||||||
|
Perfect int
|
||||||
|
OnlineChecksum sql.NullString `db:"online_checksum"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var create_scores = `
|
||||||
|
create table scores (
|
||||||
|
id bigint unsigned auto_increment
|
||||||
|
primary key,
|
||||||
|
map_md5 char(32) not null,
|
||||||
|
score int not null,
|
||||||
|
pp float(7,3) not null,
|
||||||
|
acc float(6,3) not null,
|
||||||
|
max_combo int not null,
|
||||||
|
mods int not null,
|
||||||
|
n300 int not null,
|
||||||
|
n100 int not null,
|
||||||
|
n50 int not null,
|
||||||
|
nmiss int not null,
|
||||||
|
ngeki int not null,
|
||||||
|
nkatu int not null,
|
||||||
|
grade varchar(2) default 'N' not null,
|
||||||
|
status tinyint not null,
|
||||||
|
mode tinyint not null,
|
||||||
|
play_time datetime not null,
|
||||||
|
time_elapsed int not null,
|
||||||
|
client_flags int not null,
|
||||||
|
userid int not null,
|
||||||
|
perfect tinyint(1) not null,
|
||||||
|
online_checksum char(32) not null default ''
|
||||||
|
);
|
||||||
|
`
|
||||||
|
|
||||||
|
var insert_score = `
|
||||||
|
INSERT INTO scores VALUES (
|
||||||
|
NULL,
|
||||||
|
:map_md5,
|
||||||
|
:score,
|
||||||
|
:pp,
|
||||||
|
:acc,
|
||||||
|
:max_combo,
|
||||||
|
:mods,
|
||||||
|
:n300,
|
||||||
|
:n100,
|
||||||
|
:n50,
|
||||||
|
:nmiss,
|
||||||
|
:ngeki,
|
||||||
|
:nkatu,
|
||||||
|
:grade,
|
||||||
|
:status,
|
||||||
|
:mode,
|
||||||
|
FROM_UNIXTIME(:play_time),
|
||||||
|
:time_elapsed,
|
||||||
|
:client_flags,
|
||||||
|
:userid,
|
||||||
|
:perfect,
|
||||||
|
:online_checksum
|
||||||
|
)`
|
||||||
|
|
||||||
|
var replaysMoved int32
|
||||||
|
|
||||||
|
func recalculate_chunk(chunk []Score, table string, increase int) {
|
||||||
|
tx := DB.MustBegin()
|
||||||
|
batch := 1
|
||||||
|
|
||||||
|
for _, score := range chunk {
|
||||||
|
score.Mode += increase
|
||||||
|
|
||||||
|
if batch == 0 {
|
||||||
|
tx = DB.MustBegin()
|
||||||
|
}
|
||||||
|
batch++
|
||||||
|
|
||||||
|
if !score.OnlineChecksum.Valid {
|
||||||
|
score.OnlineChecksum.String = ""
|
||||||
|
score.OnlineChecksum.Valid = true
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := tx.NamedExec(insert_score, &score)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
new_id, err := res.LastInsertId()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if score.Status != 0 {
|
||||||
|
// this is a submitted score, move the replay file as well
|
||||||
|
oldReplayPath := fmt.Sprintf("/tmp/gulag_replays/%d.osr", score.ID)
|
||||||
|
if _, err := os.Stat(oldReplayPath); os.IsNotExist(err) {
|
||||||
|
fmt.Printf("Warning: replay file for old ID %d could not be found\n", score.ID)
|
||||||
|
} else {
|
||||||
|
newReplayPath := fmt.Sprintf("%s/.data/osr/%d.osr", GulagPath, new_id)
|
||||||
|
os.Rename(oldReplayPath, newReplayPath)
|
||||||
|
atomic.AddInt32(&replaysMoved, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if batch == 3000 {
|
||||||
|
batch = 0
|
||||||
|
tx.Commit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if batch != 0 {
|
||||||
|
tx.Commit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func SplitToChunks(slice interface{}, chunkSize int) interface{} {
|
||||||
|
sliceType := reflect.TypeOf(slice)
|
||||||
|
sliceVal := reflect.ValueOf(slice)
|
||||||
|
length := sliceVal.Len()
|
||||||
|
if sliceType.Kind() != reflect.Slice {
|
||||||
|
panic("parameter must be []T")
|
||||||
|
}
|
||||||
|
n := 0
|
||||||
|
if length%chunkSize > 0 {
|
||||||
|
n = 1
|
||||||
|
}
|
||||||
|
SST := reflect.MakeSlice(reflect.SliceOf(sliceType), 0, length/chunkSize+n)
|
||||||
|
st, ed := 0, 0
|
||||||
|
for st < length {
|
||||||
|
ed = st + chunkSize
|
||||||
|
if ed > length {
|
||||||
|
ed = length
|
||||||
|
}
|
||||||
|
SST = reflect.Append(SST, sliceVal.Slice(st, ed))
|
||||||
|
st = ed
|
||||||
|
}
|
||||||
|
return SST.Interface()
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// start migration timer
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
|
// ensure gulag path exists
|
||||||
|
if _, err := os.Stat(GulagPath); os.IsNotExist(err) {
|
||||||
|
panic("Gulag path is invalid")
|
||||||
|
}
|
||||||
|
|
||||||
|
// connect to the database
|
||||||
|
dbDSN := fmt.Sprintf("%s:%s@(%s:%s)/%s", SQLUsername, SQLPassword, SQLHost, SQLPort, SQLDatabase)
|
||||||
|
DB = sqlx.MustConnect("mysql", dbDSN)
|
||||||
|
|
||||||
|
// move replays to temp directory
|
||||||
|
err := os.Rename(fmt.Sprintf("%s/.data/osr", GulagPath), "/tmp/gulag_replays")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// create new replay directory in gulag/.data
|
||||||
|
err = os.Mkdir(fmt.Sprintf("%s/.data/osr", GulagPath), 0755)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
|
// create new scores table
|
||||||
|
DB.MustExec(create_scores)
|
||||||
|
|
||||||
|
// migrate vn_scores table
|
||||||
|
vn_scores := []Score{}
|
||||||
|
vn_rows, err := DB.Queryx(`
|
||||||
|
SELECT id, map_md5, score, pp, acc, max_combo, mods, n300, n100,
|
||||||
|
n50, nmiss, ngeki, nkatu, grade, status, mode, UNIX_TIMESTAMP(play_time) AS play_time,
|
||||||
|
time_elapsed, client_flags, userid, perfect, online_checksum FROM scores_vn`)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for vn_rows.Next() {
|
||||||
|
score := Score{}
|
||||||
|
err := vn_rows.StructScan(&score)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
vn_scores = append(vn_scores, score)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, vn_chunk := range SplitToChunks(vn_scores, 10000).([][]Score) {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(chunk []Score) {
|
||||||
|
defer wg.Done()
|
||||||
|
recalculate_chunk(chunk, "scores_vn", 0)
|
||||||
|
}(vn_chunk)
|
||||||
|
}
|
||||||
|
|
||||||
|
// migrate rx_scores table
|
||||||
|
rx_scores := []Score{}
|
||||||
|
rx_rows, err := DB.Queryx(`
|
||||||
|
SELECT id, map_md5, score, pp, acc, max_combo, mods, n300, n100,
|
||||||
|
n50, nmiss, ngeki, nkatu, grade, status, mode, UNIX_TIMESTAMP(play_time) AS play_time,
|
||||||
|
time_elapsed, client_flags, userid, perfect, online_checksum FROM scores_rx`)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for rx_rows.Next() {
|
||||||
|
score := Score{}
|
||||||
|
err := rx_rows.StructScan(&score)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rx_scores = append(rx_scores, score)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, rx_chunk := range SplitToChunks(rx_scores, 10000).([][]Score) {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(chunk []Score) {
|
||||||
|
defer wg.Done()
|
||||||
|
recalculate_chunk(chunk, "scores_rx", 4)
|
||||||
|
}(rx_chunk)
|
||||||
|
}
|
||||||
|
|
||||||
|
// migrate ap_scores table
|
||||||
|
ap_scores := []Score{}
|
||||||
|
ap_rows, err := DB.Queryx(`
|
||||||
|
SELECT id, map_md5, score, pp, acc, max_combo, mods, n300, n100,
|
||||||
|
n50, nmiss, ngeki, nkatu, grade, status, mode, UNIX_TIMESTAMP(play_time) AS play_time,
|
||||||
|
time_elapsed, client_flags, userid, perfect, online_checksum FROM scores_ap`)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for ap_rows.Next() {
|
||||||
|
score := Score{}
|
||||||
|
err := ap_rows.StructScan(&score)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ap_scores = append(ap_scores, score)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ap_chunk := range SplitToChunks(ap_scores, 10000).([][]Score) {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(chunk []Score) {
|
||||||
|
defer wg.Done()
|
||||||
|
recalculate_chunk(chunk, "scores_ap", 8)
|
||||||
|
}(ap_chunk)
|
||||||
|
}
|
||||||
|
|
||||||
|
// wait for all migrations to complete
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
// attempt to remove the temp replays directory
|
||||||
|
err = os.Remove("/tmp/gulag_replays")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("There are some replays files for which scores could not be found in the database. They have been left at /tmp/gulag_replays.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// print elapsed time spent migrating
|
||||||
|
elapsed := time.Since(start)
|
||||||
|
fmt.Printf("Score migrator took %s\n", elapsed)
|
||||||
|
fmt.Printf("Moved %d replays\n", replaysMoved)
|
||||||
|
|
||||||
|
// prompt user to delete the old scores tables if they're certain everything is successful
|
||||||
|
fmt.Printf("Do you wish to drop the old tables? [only do this if you're certain migrations have been successful] (y/n)\n>> ")
|
||||||
|
var res string
|
||||||
|
fmt.Scanln(&res)
|
||||||
|
res = strings.ToLower(res)
|
||||||
|
|
||||||
|
if res == "y" {
|
||||||
|
fmt.Println("Dropping old tables")
|
||||||
|
DB.MustExec("drop table scores_vn")
|
||||||
|
DB.MustExec("drop table scores_rx")
|
||||||
|
DB.MustExec("drop table scores_ap")
|
||||||
|
} else {
|
||||||
|
fmt.Println("Not dropping old tables")
|
||||||
|
}
|
||||||
|
}
|
167
tools/proxy.py
Normal file
167
tools/proxy.py
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
# like budget wireshark for osu! server stuff
|
||||||
|
# usage: enable http://localhost:8080 proxy in windows,
|
||||||
|
# (https://i.cmyui.xyz/DNnqifKHyBSA9X8NEHg.png)
|
||||||
|
# and run this with `mitmdump -qs tools/proxy.py`
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
domain = "cmyui.xyz" # XXX: put your domain here
|
||||||
|
|
||||||
|
import re
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
from enum import IntEnum
|
||||||
|
from enum import unique
|
||||||
|
|
||||||
|
from mitmproxy import http
|
||||||
|
|
||||||
|
|
||||||
|
@unique
|
||||||
|
class ServerPackets(IntEnum):
|
||||||
|
USER_ID = 5
|
||||||
|
SEND_MESSAGE = 7
|
||||||
|
PONG = 8
|
||||||
|
HANDLE_IRC_CHANGE_USERNAME = 9 # unused
|
||||||
|
HANDLE_IRC_QUIT = 10
|
||||||
|
USER_STATS = 11
|
||||||
|
USER_LOGOUT = 12
|
||||||
|
SPECTATOR_JOINED = 13
|
||||||
|
SPECTATOR_LEFT = 14
|
||||||
|
SPECTATE_FRAMES = 15
|
||||||
|
VERSION_UPDATE = 19
|
||||||
|
SPECTATOR_CANT_SPECTATE = 22
|
||||||
|
GET_ATTENTION = 23
|
||||||
|
NOTIFICATION = 24
|
||||||
|
UPDATE_MATCH = 26
|
||||||
|
NEW_MATCH = 27
|
||||||
|
DISPOSE_MATCH = 28
|
||||||
|
TOGGLE_BLOCK_NON_FRIEND_DMS = 34
|
||||||
|
MATCH_JOIN_SUCCESS = 36
|
||||||
|
MATCH_JOIN_FAIL = 37
|
||||||
|
FELLOW_SPECTATOR_JOINED = 42
|
||||||
|
FELLOW_SPECTATOR_LEFT = 43
|
||||||
|
ALL_PLAYERS_LOADED = 45
|
||||||
|
MATCH_START = 46
|
||||||
|
MATCH_SCORE_UPDATE = 48
|
||||||
|
MATCH_TRANSFER_HOST = 50
|
||||||
|
MATCH_ALL_PLAYERS_LOADED = 53
|
||||||
|
MATCH_PLAYER_FAILED = 57
|
||||||
|
MATCH_COMPLETE = 58
|
||||||
|
MATCH_SKIP = 61
|
||||||
|
UNAUTHORIZED = 62 # unused
|
||||||
|
CHANNEL_JOIN_SUCCESS = 64
|
||||||
|
CHANNEL_INFO = 65
|
||||||
|
CHANNEL_KICK = 66
|
||||||
|
CHANNEL_AUTO_JOIN = 67
|
||||||
|
BEATMAP_INFO_REPLY = 69
|
||||||
|
PRIVILEGES = 71
|
||||||
|
FRIENDS_LIST = 72
|
||||||
|
PROTOCOL_VERSION = 75
|
||||||
|
MAIN_MENU_ICON = 76
|
||||||
|
MONITOR = 80 # unused
|
||||||
|
MATCH_PLAYER_SKIPPED = 81
|
||||||
|
USER_PRESENCE = 83
|
||||||
|
RESTART = 86
|
||||||
|
MATCH_INVITE = 88
|
||||||
|
CHANNEL_INFO_END = 89
|
||||||
|
MATCH_CHANGE_PASSWORD = 91
|
||||||
|
SILENCE_END = 92
|
||||||
|
USER_SILENCED = 94
|
||||||
|
USER_PRESENCE_SINGLE = 95
|
||||||
|
USER_PRESENCE_BUNDLE = 96
|
||||||
|
USER_DM_BLOCKED = 100
|
||||||
|
TARGET_IS_SILENCED = 101
|
||||||
|
VERSION_UPDATE_FORCED = 102
|
||||||
|
SWITCH_SERVER = 103
|
||||||
|
ACCOUNT_RESTRICTED = 104
|
||||||
|
RTX = 105 # unused
|
||||||
|
MATCH_ABORT = 106
|
||||||
|
SWITCH_TOURNAMENT_SERVER = 107
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"<{self.name} ({self.value})>"
|
||||||
|
|
||||||
|
|
||||||
|
BYTE_ORDER_SUFFIXES = ["B", "KB", "MB", "GB"]
|
||||||
|
|
||||||
|
|
||||||
|
def fmt_bytes(n: int | float) -> str:
|
||||||
|
suffix = None
|
||||||
|
for suffix in BYTE_ORDER_SUFFIXES:
|
||||||
|
if n < 1024:
|
||||||
|
break
|
||||||
|
n /= 1024
|
||||||
|
return f"{n:,.2f}{suffix}"
|
||||||
|
|
||||||
|
|
||||||
|
DOMAIN_RGX = re.compile(
|
||||||
|
rf"^(?P<subdomain>osu|c[e4]?|a|s|b|assets)\.(?:ppy\.sh|{re.escape(domain)})$",
|
||||||
|
)
|
||||||
|
|
||||||
|
PACKET_HEADER_FMT = struct.Struct("<HxI") # header gives us packet id & data length
|
||||||
|
|
||||||
|
print(f"\x1b[0;92mListening (ppy.sh & {domain})\x1b[0m\n")
|
||||||
|
|
||||||
|
|
||||||
|
def response(flow: http.HTTPFlow) -> None:
|
||||||
|
r_match = DOMAIN_RGX.match(flow.request.host)
|
||||||
|
if not r_match:
|
||||||
|
return # unrelated request
|
||||||
|
|
||||||
|
body = flow.response.content
|
||||||
|
if not body:
|
||||||
|
return # empty resp
|
||||||
|
|
||||||
|
sys.stdout.write(f"\x1b[0;93m[{flow.request.method}] {flow.request.url}\x1b[0m\n")
|
||||||
|
body_view = memoryview(body)
|
||||||
|
body_len = len(body)
|
||||||
|
|
||||||
|
if r_match["subdomain"] in ("c", "ce", "c4", "c5", "c6"):
|
||||||
|
if flow.request.method == "POST":
|
||||||
|
packet_num = 1
|
||||||
|
while body_view:
|
||||||
|
# read header
|
||||||
|
_pid, plen = PACKET_HEADER_FMT.unpack_from(body_view)
|
||||||
|
pid = ServerPackets(_pid)
|
||||||
|
body_view = body_view[7:]
|
||||||
|
|
||||||
|
# read data
|
||||||
|
pdata = str(body_view[:plen].tobytes())[2:-1] # remove b''
|
||||||
|
body_view = body_view[plen:]
|
||||||
|
|
||||||
|
sys.stdout.write(f"[{packet_num}] \x1b[0;95m{pid!r}\x1b[0m {pdata}\n")
|
||||||
|
|
||||||
|
packet_num += 1
|
||||||
|
|
||||||
|
if packet_num % 5: # don't build up too much in ram
|
||||||
|
sys.stdout.flush()
|
||||||
|
sys.stdout.write("\n")
|
||||||
|
else: # format varies per request
|
||||||
|
if ( # todo check host
|
||||||
|
(
|
||||||
|
# jfif, jpe, jpeg, jpg graphics file
|
||||||
|
body_view[:4] == b"\xff\xd8\xff\xe0"
|
||||||
|
and body_view[6:11] == b"JFIF\x00"
|
||||||
|
)
|
||||||
|
or (
|
||||||
|
# exif digital jpg
|
||||||
|
body_view[:4] == b"\xff\xd8\xff\xe1"
|
||||||
|
and body_view[6:11] == b"Exif\x00"
|
||||||
|
)
|
||||||
|
or (
|
||||||
|
# spiff still picture jpg
|
||||||
|
body_view[:4] == b"\xff\xd8\xff\xe8"
|
||||||
|
and body_view[6:12] == b"SPIFF\x00"
|
||||||
|
)
|
||||||
|
):
|
||||||
|
sys.stdout.write(f"[{fmt_bytes(body_len)} jpeg file]\n\n")
|
||||||
|
elif (
|
||||||
|
body_view[:8] == b"\x89PNG\r\n\x1a\n"
|
||||||
|
and body_view[-8:] == b"\x49END\xae\x42\x60\x82"
|
||||||
|
):
|
||||||
|
sys.stdout.write(f"[{fmt_bytes(body_len)} png file]\n\n")
|
||||||
|
elif body_view[:6] in (b"GIF87a", b"GIF89a") and body_view[-2:] == b"\x00\x3b":
|
||||||
|
sys.stdout.write(f"[{fmt_bytes(body_len)} gif file]\n\n")
|
||||||
|
else:
|
||||||
|
sys.stdout.write(f"{str(body)[2:-1]}\n\n") # remove b''
|
||||||
|
|
||||||
|
sys.stdout.flush()
|
279
tools/recalc.py
Normal file
279
tools/recalc.py
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
#!/usr/bin/env python3.11
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import asyncio
|
||||||
|
import math
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from collections.abc import Awaitable
|
||||||
|
from collections.abc import Iterator
|
||||||
|
from collections.abc import Sequence
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from dataclasses import field
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
from typing import TypeVar
|
||||||
|
|
||||||
|
import databases
|
||||||
|
from akatsuki_pp_py import Beatmap
|
||||||
|
from akatsuki_pp_py import Calculator
|
||||||
|
from redis import asyncio as aioredis
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.abspath(os.pardir))
|
||||||
|
os.chdir(os.path.abspath(os.pardir))
|
||||||
|
|
||||||
|
try:
|
||||||
|
import app.settings
|
||||||
|
import app.state.services
|
||||||
|
from app.constants.gamemodes import GameMode
|
||||||
|
from app.constants.mods import Mods
|
||||||
|
from app.constants.privileges import Privileges
|
||||||
|
from app.objects.beatmap import ensure_osu_file_is_available
|
||||||
|
except ModuleNotFoundError:
|
||||||
|
print("\x1b[;91mMust run from tools/ directory\x1b[m")
|
||||||
|
raise
|
||||||
|
|
||||||
|
T = TypeVar("T")
|
||||||
|
|
||||||
|
|
||||||
|
DEBUG = False
|
||||||
|
BEATMAPS_PATH = Path.cwd() / ".data/osu"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Context:
|
||||||
|
database: databases.Database
|
||||||
|
redis: aioredis.Redis
|
||||||
|
beatmaps: dict[int, Beatmap] = field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
|
def divide_chunks(values: list[T], n: int) -> Iterator[list[T]]:
|
||||||
|
for i in range(0, len(values), n):
|
||||||
|
yield values[i : i + n]
|
||||||
|
|
||||||
|
|
||||||
|
async def recalculate_score(
|
||||||
|
score: dict[str, Any],
|
||||||
|
beatmap_path: Path,
|
||||||
|
ctx: Context,
|
||||||
|
) -> None:
|
||||||
|
beatmap = ctx.beatmaps.get(score["map_id"])
|
||||||
|
if beatmap is None:
|
||||||
|
beatmap = Beatmap(path=str(beatmap_path))
|
||||||
|
ctx.beatmaps[score["map_id"]] = beatmap
|
||||||
|
|
||||||
|
calculator = Calculator(
|
||||||
|
mode=GameMode(score["mode"]).as_vanilla,
|
||||||
|
mods=score["mods"],
|
||||||
|
combo=score["max_combo"],
|
||||||
|
n_geki=score["ngeki"], # Mania 320s
|
||||||
|
n300=score["n300"],
|
||||||
|
n_katu=score["nkatu"], # Mania 200s, Catch tiny droplets
|
||||||
|
n100=score["n100"],
|
||||||
|
n50=score["n50"],
|
||||||
|
n_misses=score["nmiss"],
|
||||||
|
)
|
||||||
|
attrs = calculator.performance(beatmap)
|
||||||
|
|
||||||
|
new_pp: float = attrs.pp
|
||||||
|
if math.isnan(new_pp) or math.isinf(new_pp):
|
||||||
|
new_pp = 0.0
|
||||||
|
|
||||||
|
new_pp = min(new_pp, 9999.999)
|
||||||
|
|
||||||
|
await ctx.database.execute(
|
||||||
|
"UPDATE scores SET pp = :new_pp WHERE id = :id",
|
||||||
|
{"new_pp": new_pp, "id": score["id"]},
|
||||||
|
)
|
||||||
|
|
||||||
|
if DEBUG:
|
||||||
|
print(
|
||||||
|
f"Recalculated score ID {score['id']} ({score['pp']:.3f}pp -> {new_pp:.3f}pp)",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def process_score_chunk(
|
||||||
|
chunk: list[dict[str, Any]],
|
||||||
|
ctx: Context,
|
||||||
|
) -> None:
|
||||||
|
tasks: list[Awaitable[None]] = []
|
||||||
|
for score in chunk:
|
||||||
|
osu_file_available = await ensure_osu_file_is_available(
|
||||||
|
score["map_id"],
|
||||||
|
expected_md5=score["map_md5"],
|
||||||
|
)
|
||||||
|
if osu_file_available:
|
||||||
|
tasks.append(
|
||||||
|
recalculate_score(
|
||||||
|
score,
|
||||||
|
BEATMAPS_PATH / f"{score['map_id']}.osu",
|
||||||
|
ctx,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
await asyncio.gather(*tasks)
|
||||||
|
|
||||||
|
|
||||||
|
async def recalculate_user(
|
||||||
|
id: int,
|
||||||
|
game_mode: GameMode,
|
||||||
|
ctx: Context,
|
||||||
|
) -> None:
|
||||||
|
best_scores = await ctx.database.fetch_all(
|
||||||
|
"SELECT s.pp, s.acc FROM scores s "
|
||||||
|
"INNER JOIN maps m ON s.map_md5 = m.md5 "
|
||||||
|
"WHERE s.userid = :user_id AND s.mode = :mode "
|
||||||
|
"AND s.status = 2 AND m.status IN (2, 3) " # ranked, approved
|
||||||
|
"ORDER BY s.pp DESC",
|
||||||
|
{"user_id": id, "mode": game_mode},
|
||||||
|
)
|
||||||
|
|
||||||
|
total_scores = len(best_scores)
|
||||||
|
if not total_scores:
|
||||||
|
return
|
||||||
|
|
||||||
|
# calculate new total weighted accuracy
|
||||||
|
weighted_acc = sum(row["acc"] * 0.95**i for i, row in enumerate(best_scores))
|
||||||
|
bonus_acc = 100.0 / (20 * (1 - 0.95**total_scores))
|
||||||
|
acc = (weighted_acc * bonus_acc) / 100
|
||||||
|
|
||||||
|
# calculate new total weighted pp
|
||||||
|
weighted_pp = sum(row["pp"] * 0.95**i for i, row in enumerate(best_scores))
|
||||||
|
bonus_pp = 416.6667 * (1 - 0.9994**total_scores)
|
||||||
|
pp = round(weighted_pp + bonus_pp)
|
||||||
|
|
||||||
|
await ctx.database.execute(
|
||||||
|
"UPDATE stats SET pp = :pp, acc = :acc WHERE id = :id AND mode = :mode",
|
||||||
|
{"pp": pp, "acc": acc, "id": id, "mode": game_mode},
|
||||||
|
)
|
||||||
|
|
||||||
|
user_info = await ctx.database.fetch_one(
|
||||||
|
"SELECT country, priv FROM users WHERE id = :id",
|
||||||
|
{"id": id},
|
||||||
|
)
|
||||||
|
if user_info is None:
|
||||||
|
raise Exception(f"Unknown user ID {id}?")
|
||||||
|
|
||||||
|
if user_info["priv"] & Privileges.UNRESTRICTED:
|
||||||
|
await ctx.redis.zadd(
|
||||||
|
f"bancho:leaderboard:{game_mode.value}",
|
||||||
|
{str(id): pp},
|
||||||
|
)
|
||||||
|
|
||||||
|
await ctx.redis.zadd(
|
||||||
|
f"bancho:leaderboard:{game_mode.value}:{user_info['country']}",
|
||||||
|
{str(id): pp},
|
||||||
|
)
|
||||||
|
|
||||||
|
if DEBUG:
|
||||||
|
print(f"Recalculated user ID {id} ({pp:.3f}pp, {acc:.3f}%)")
|
||||||
|
|
||||||
|
|
||||||
|
async def process_user_chunk(
|
||||||
|
chunk: list[int],
|
||||||
|
game_mode: GameMode,
|
||||||
|
ctx: Context,
|
||||||
|
) -> None:
|
||||||
|
tasks: list[Awaitable[None]] = []
|
||||||
|
for id in chunk:
|
||||||
|
tasks.append(recalculate_user(id, game_mode, ctx))
|
||||||
|
|
||||||
|
await asyncio.gather(*tasks)
|
||||||
|
|
||||||
|
|
||||||
|
async def recalculate_mode_users(mode: GameMode, ctx: Context) -> None:
|
||||||
|
user_ids = [
|
||||||
|
row["id"] for row in await ctx.database.fetch_all("SELECT id FROM users")
|
||||||
|
]
|
||||||
|
|
||||||
|
for id_chunk in divide_chunks(user_ids, 100):
|
||||||
|
await process_user_chunk(id_chunk, mode, ctx)
|
||||||
|
|
||||||
|
|
||||||
|
async def recalculate_mode_scores(mode: GameMode, ctx: Context) -> None:
|
||||||
|
scores = [
|
||||||
|
dict(row)
|
||||||
|
for row in await ctx.database.fetch_all(
|
||||||
|
"""\
|
||||||
|
SELECT scores.id, scores.mode, scores.mods, scores.map_md5,
|
||||||
|
scores.pp, scores.acc, scores.max_combo,
|
||||||
|
scores.ngeki, scores.n300, scores.nkatu, scores.n100, scores.n50, scores.nmiss,
|
||||||
|
maps.id as `map_id`
|
||||||
|
FROM scores
|
||||||
|
INNER JOIN maps ON scores.map_md5 = maps.md5
|
||||||
|
WHERE scores.status = 2
|
||||||
|
AND scores.mode = :mode
|
||||||
|
ORDER BY scores.pp DESC
|
||||||
|
""",
|
||||||
|
{"mode": mode},
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
for score_chunk in divide_chunks(scores, 100):
|
||||||
|
await process_score_chunk(score_chunk, ctx)
|
||||||
|
|
||||||
|
|
||||||
|
async def main(argv: Sequence[str] | None = None) -> int:
|
||||||
|
argv = argv if argv is not None else sys.argv[1:]
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Recalculate performance for scores and/or stats",
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"-d",
|
||||||
|
"--debug",
|
||||||
|
help="Enable debug logging",
|
||||||
|
action="store_true",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--no-scores",
|
||||||
|
help="Disable recalculating scores",
|
||||||
|
action="store_true",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--no-stats",
|
||||||
|
help="Disable recalculating user stats",
|
||||||
|
action="store_true",
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"-m",
|
||||||
|
"--mode",
|
||||||
|
nargs=argparse.ONE_OR_MORE,
|
||||||
|
required=False,
|
||||||
|
default=["0", "1", "2", "3", "4", "5", "6", "8"],
|
||||||
|
# would love to do things like "vn!std", but "!" will break interpretation
|
||||||
|
choices=["0", "1", "2", "3", "4", "5", "6", "8"],
|
||||||
|
)
|
||||||
|
args = parser.parse_args(argv)
|
||||||
|
|
||||||
|
global DEBUG
|
||||||
|
DEBUG = args.debug
|
||||||
|
|
||||||
|
db = databases.Database(app.settings.DB_DSN)
|
||||||
|
await db.connect()
|
||||||
|
|
||||||
|
redis = await aioredis.from_url(app.settings.REDIS_DSN)
|
||||||
|
|
||||||
|
ctx = Context(db, redis)
|
||||||
|
|
||||||
|
for mode in args.mode:
|
||||||
|
mode = GameMode(int(mode))
|
||||||
|
|
||||||
|
if not args.no_scores:
|
||||||
|
await recalculate_mode_scores(mode, ctx)
|
||||||
|
|
||||||
|
if not args.no_stats:
|
||||||
|
await recalculate_mode_users(mode, ctx)
|
||||||
|
|
||||||
|
await app.state.services.http_client.aclose()
|
||||||
|
await db.disconnect()
|
||||||
|
await redis.aclose()
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(asyncio.run(main()))
|
Reference in New Issue
Block a user