Add files via upload

This commit is contained in:
purr
2025-04-04 21:32:15 +09:00
committed by GitHub
parent 966e7691a3
commit ebc53bfece
24 changed files with 3466 additions and 0 deletions

486
migrations/base.sql Normal file
View 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
View 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);

View 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
View 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
View 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
View 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
View File

View File

View 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,
},
]

View 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
View File

57
tests/conftest.py Normal file
View 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 = []

View File

View 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
View File

669
tests/unit/packets_test.py Normal file
View 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

View 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."

View 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."

View 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
View 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()))

View 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
View 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
View 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
View 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()))