├── .cargo └── config.toml ├── .env.example ├── .gitattributes ├── .github ├── FUNDING.yml └── workflows │ ├── check.yml │ ├── deploy.yml │ └── roadmap.yml ├── .gitignore ├── .sqlx ├── query-02fe3a854468f7bd55076081084d0c89a5f5745b0ee8fe20eba752d6fd5d92e6.json ├── query-0530845213db6938c1dbef3c70a4789cc06c18d876fd28bebad7e5019dd990e7.json ├── query-057d3f9a920c8ef1d8e2bd97f0a7dd429a599506df9b65768c5e358e4b36f460.json ├── query-0e0db1c5f4acec83f98eeeff3b05529c44947fc2789388902b46055e8436b1a6.json ├── query-0e433f1a5f1609bac4b8d161fa57557e6514d3036432ff81a1e022b23b76013d.json ├── query-14dc3fec9ff696d82c4a4e4d3b73807c62d95550b4dade6650487b28d0427e4d.json ├── query-1d359cbace668cd27222e76337e61f7fb6b5bd78d61d0b134563fba190341435.json ├── query-1f0002253b4c59c13821419714e32e9ec53a8a9e687e9b801140d2e21aa60dfa.json ├── query-1fbb1962e614748c30f6fc40f8baee79dd6a38bdf2c91d9f700f5a08062c5c19.json ├── query-21cc0cb0c439b63edaaeb51f7727e492e2bd822045bc58ec4ec8f9882556bbd8.json ├── query-222389f3bfc98a2e36d29b44792b23d2bae7007aff9a70b3161c4c72ef85c34e.json ├── query-257bfc063d14483df622995734492914045486a967fe30e321828491a1dab84a.json ├── query-26b20d4e1aae8f234e4a5295d5c892ba739303b90c57992685af8a5d565885bf.json ├── query-27fc81ceab1a9a0b3526082feb24eb6c0e7c78282aa6f493cf3c4d96ae30ec78.json ├── query-2969365f091f2408b8e9fbce514024420d26fe358dbf08afc58074515d3c1e78.json ├── query-30719710015a08503f4b1e7178b659e86ecbe928d3929da13dda045137b69d38.json ├── query-32a5e31c8b23211b9656ea4490bddb274af9dc1afc5198a4a37f43f9bb061464.json ├── query-369d3652737db22b47d3db2004a25fb210cc4857a56710f9ec5365582ab9cfe2.json ├── query-380fc3f7a232b0f145ae03ec0ec838a57bfe42db5b2bcbd5a636bbbffd2e0b5e.json ├── query-3a26d9aab70278d6d23e9b224a416c9010b50ab450cdb2cba7689ecd25d73d6f.json ├── query-40aaa1881361b5793d0a7ff1aef3c840c18bef6ea7d839161e7297a740d11e00.json ├── query-46f62972a9ab0535c92b020be111bb5d4aed8c8af3f2156afb2a61c2aeae6355.json ├── query-4d9d86454d838f1fd2bfc30c019680978373896c7e01f09e939a63de314137e0.json ├── query-4dd4fce1ba302fc2ddb754e2641d433a666678ea629969637e021ae54366885a.json ├── query-56c783134577013d4ca9ef1328dba43223c3b745c1a4a5ce9148cb9d94fe9d00.json ├── query-5c9012746a35ed9aca1d3b21f3e3f9fb93f61f8327458fec9716696faeb5d144.json ├── query-5f5d3b5e331806027c5fe6c8b85dccf7af5ae8d2e8f78dc51c37cee0a381460c.json ├── query-611d29835f9095dcf111fb1243775a51f3121fab98b6b6bc0a6f381376ed5fd1.json ├── query-61af8b78e54ed6962ccaecb83ddab3b83b4ad116c3b1f90b06d82ebdecc03ba2.json ├── query-62fab7c476b991a91b952f778442a7897cbbac8c2e6fd30d140fee58b6434f9b.json ├── query-68356a75b1e66719f90cf17ddeed1a170c119343d0d4c515308f6409997f2557.json ├── query-74ed6117f3b940209a8c16e41f94f6909c24d51bfbddc02746a9ca4400abc374.json ├── query-7734c6b98a367b9b6fab653edcd83f8b60179e932e2ef78ea25095e56d756256.json ├── query-789ed5aef078e6abf48755e46be402db4b6a5d1a430673ddb84db58c29e9c2ba.json ├── query-7a9f634eec21891847a18fc53c1f168ebcbec56e9c3f3d2198f24ef609414c8f.json ├── query-82332eb70e5e28b22e5ed7f3e118931d94fab08797e7f8675ddff4e858b3b27a.json ├── query-8547a49cb48c682ed2ad23dd0f338da6d1e50ae3a682276806b46dfe5b5f3833.json ├── query-85a26a8fa694a2393f3fb55229f8dc0386fb1271568ad2ce168bd49d80f6f58b.json ├── query-8b88f6e2659ffeb87114f3c9e099c9ba96a6ce79c842fffe9a665fc1f5334822.json ├── query-8f560c7c1634f00ce00878148ce3612e1a87cb8a81057dc6edbfa9a9ef49c56c.json ├── query-935eb7bfdf09762ae2c943ab087237049870721fb6522785fe25b280652246c2.json ├── query-96598cd5497eb9b19abfb1703e8be43c32a91bee2067d7c40ce0900ae6d0f195.json ├── query-97170f08a3a6c67c60616849ec0476cffcf82061c60987a4dd6d8e542c024adb.json ├── query-97fc6f5b147aa68eeb73d4ee5768d1ffcc3bacf38b714c231efdfafa5607a126.json ├── query-9956ebc2544c0db0b81478c8ec7be5e3020e2db5bd6d0fd03469e63b89a52f6c.json ├── query-a0fba34dd8755e7ab1118367a8c1b6a2500d6b5ad3ea80a61abf1ec0f652aece.json ├── query-aae569635dca674787a3c8952802667a0a4f3ad0604338caa708d146cea2bbb9.json ├── query-adfe9de02db6d8af635558f7c243b4c7c13093b460943160577157b01cb2c5d4.json ├── query-b376479448f2ff14b7c6e41babdc571aee46db423c84b3315c0e0c50c9ddb51f.json ├── query-c3d5dac053a03b0fc47aa553a73274f1c704a981645d03150a981e70b7d9910a.json ├── query-c6be23c6930c006c0543b9a3468e0b52154a2c1bcc54995b1aca7e191feff106.json ├── query-c869e094800f4fcdceb1f3105e472cf1f2af9c230d3acb0ba4c073021c3d7d13.json ├── query-c8ceed90ce73b8faf91857d8ecb7505d3bbec38088486440bdf484a6ef64b017.json ├── query-cf16c21ccc39e70343cf7f2a17a67991142059b67d3e4c36c315e669c27d07d4.json ├── query-dffdc3cb9035cbf7d8d2dfbd1dc7cf503d4af5b60fc0b3f3fa057202cbd56483.json ├── query-e4c13068406ed0461fd8816fa8ff2d1f9ead9779e6c35e389c89af2cd13a62af.json ├── query-e7983db7937eb39ea9066a717b89c18d5d469aba9c61536e9bc598f52a0398f1.json ├── query-e8420a97191f72ebf8978f38d3d71ecbd0901081ac009738b58a91418bc708bb.json ├── query-eb2d8df67e3fb80c7798600f895597a274302c0574e70723472a2959f64f5731.json ├── query-f6e31bee48c671466ccf9972401fe4b26ed75580e7c4cb870952d2853ecb69fc.json ├── query-fabcfd346174afa82e766f233674abf031d026bb55c632b459f24517eaa12dc1.json ├── query-fcfc547353fe778233d3a6407bc36fb78992719918d6d3198f1bcf070f72fd00.json ├── query-fd8e58738ccbd3545818ec95ae5529e90ee260b10d4b7c26191d843184b52fc0.json ├── query-feac8cf9d9ba3bc8cb4875c59ec30587a1fec73bc05a929ffa67d32b495f414a.json ├── query-ff290f1421104f7ffb98653a0bcd84cdf0b6c1aa3791775c4fbb1d97c65fe60f.json └── query-ffd820fbdf987b68780011586413f1b8c3eb2f0b93083d839c8de935eb1ab28b.json ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── bathbot-cache ├── Cargo.toml └── src │ ├── cache │ ├── cold_resume.rs │ ├── delete.rs │ ├── fetch.rs │ ├── mod.rs │ └── store.rs │ ├── key │ ├── mod.rs │ ├── set.rs │ ├── single.rs │ └── to_key.rs │ ├── lib.rs │ ├── model │ ├── archive.rs │ ├── connection.rs │ ├── mod.rs │ └── stats.rs │ └── util │ ├── aligned_vec.rs │ ├── mod.rs │ ├── serialize.rs │ └── zipped.rs ├── bathbot-cards ├── Cargo.toml ├── assets │ ├── backgrounds │ │ ├── advanced.png │ │ ├── apprentice.png │ │ ├── expert.png │ │ ├── god.png │ │ ├── legendary.png │ │ ├── master.png │ │ ├── newbie.png │ │ ├── novice.png │ │ ├── outstanding.png │ │ ├── professional.png │ │ ├── rookie.png │ │ └── seasoned.png │ ├── branding │ │ ├── icon.png │ │ └── text.svg │ ├── fonts │ │ ├── LICENSE.txt │ │ ├── Roboto-Black.ttf │ │ ├── Roboto-BlackItalic.ttf │ │ ├── Roboto-Bold.ttf │ │ ├── Roboto-BoldItalic.ttf │ │ ├── Roboto-Italic.ttf │ │ ├── Roboto-Light.ttf │ │ ├── Roboto-LightItalic.ttf │ │ ├── Roboto-Medium.ttf │ │ ├── Roboto-MediumItalic.ttf │ │ ├── Roboto-Regular.ttf │ │ ├── Roboto-Thin.ttf │ │ └── Roboto-ThinItalic.ttf │ └── gamemodes │ │ ├── Catch.svg │ │ ├── Mania.svg │ │ ├── Standard.svg │ │ └── Taiko.svg └── src │ ├── builder │ ├── card │ │ ├── footer.rs │ │ ├── header.rs │ │ ├── info.rs │ │ └── mod.rs │ ├── font.rs │ ├── mod.rs │ └── paint.rs │ ├── card.rs │ ├── error.rs │ ├── font.rs │ ├── lib.rs │ ├── skills │ ├── description.rs │ ├── mod.rs │ ├── prefix.rs │ ├── suffix.rs │ └── title.rs │ └── svg.rs ├── bathbot-client ├── Cargo.toml └── src │ ├── client.rs │ ├── discord.rs │ ├── error.rs │ ├── github.rs │ ├── lib.rs │ ├── metrics.rs │ ├── miss_analyzer.rs │ ├── multipart.rs │ ├── osekai.rs │ ├── osu.rs │ ├── osustats.rs │ ├── osutrack.rs │ ├── relax.rs │ ├── respektive.rs │ ├── site.rs │ ├── snipe │ ├── huismetbenen.rs │ ├── kittenroleplay.rs │ └── mod.rs │ └── twitch.rs ├── bathbot-macros ├── Cargo.toml └── src │ ├── bucket.rs │ ├── embed_data │ └── mod.rs │ ├── flags.rs │ ├── has_mods │ └── mod.rs │ ├── has_name │ └── mod.rs │ ├── lib.rs │ ├── message │ ├── attrs.rs │ ├── command.rs │ └── mod.rs │ ├── pagination │ └── mod.rs │ ├── prefix │ ├── attrs.rs │ ├── command.rs │ └── mod.rs │ ├── slash │ ├── attrs.rs │ └── mod.rs │ └── util.rs ├── bathbot-model ├── Cargo.toml └── src │ ├── command_fields.rs │ ├── country_code.rs │ ├── deser.rs │ ├── either.rs │ ├── embed_builder │ ├── mod.rs │ ├── settings.rs │ └── value.rs │ ├── games.rs │ ├── github.rs │ ├── huismetbenen.rs │ ├── kittenroleplay.rs │ ├── lib.rs │ ├── osekai.rs │ ├── osu.rs │ ├── osu_stats.rs │ ├── osutrack.rs │ ├── personal_best.rs │ ├── ranking_entries.rs │ ├── relax.rs │ ├── respektive.rs │ ├── rkyv_util │ ├── as_non_zero.rs │ ├── bitflags.rs │ ├── deref_as_box.rs │ ├── deref_as_string.rs │ ├── map_boxed_slice.rs │ ├── map_unwrap_or_default.rs │ ├── mod.rs │ ├── niche_deref_as_box.rs │ ├── str_as_string.rs │ ├── time.rs │ └── unwrap_or_default.rs │ ├── rosu_v2 │ ├── grade.rs │ ├── mod.rs │ ├── mode.rs │ ├── ranking.rs │ └── user.rs │ ├── score_slim.rs │ ├── twilight │ ├── channel │ │ ├── mod.rs │ │ └── permission_overwrite.rs │ ├── guild │ │ ├── member.rs │ │ ├── mod.rs │ │ └── role.rs │ ├── id │ │ ├── map.rs │ │ └── mod.rs │ ├── mod.rs │ ├── session.rs │ ├── user │ │ ├── current_user.rs │ │ └── mod.rs │ └── util │ │ ├── image_hash.rs │ │ └── mod.rs │ ├── twitch.rs │ └── user_stats.rs ├── bathbot-psql ├── Cargo.toml ├── migrations │ ├── 20221102221131_base.down.sql │ ├── 20221102221131_base.up.sql │ ├── 20230201231043_inc-version-len.down.sql │ ├── 20230201231043_inc-version-len.up.sql │ ├── 20230212055648_remove_map_max_combo.down.sql │ ├── 20230212055648_remove_map_max_combo.up.sql │ ├── 20230504191905_remove_countries.down.sql │ ├── 20230504191905_remove_countries.up.sql │ ├── 20230531141825_add_bookmarks_table.down.sql │ ├── 20230531141825_add_bookmarks_table.up.sql │ ├── 20230613122212_add_render_support.down.sql │ ├── 20230613122212_add_render_support.up.sql │ ├── 20230708224944_adjust_user_config_skin_color.down.sql │ ├── 20230708224944_adjust_user_config_skin_color.up.sql │ ├── 20230906135717_add_render_fields.down.sql │ ├── 20230906135717_add_render_fields.up.sql │ ├── 20231020095331_materialized_scores_view.down.sql │ ├── 20231020095331_materialized_scores_view.up.sql │ ├── 20240228202351_attribute_adjustments.down.sql │ ├── 20240228202351_attribute_adjustments.up.sql │ ├── 20240407231623_lazer_stable_toggle.down.sql │ ├── 20240407231623_lazer_stable_toggle.up.sql │ ├── 20240724163823_score_data.down.sql │ ├── 20240724163823_score_data.up.sql │ ├── 20240727204755_score_format.down.sql │ ├── 20240727204755_score_format.up.sql │ ├── 20241031101334_pp_update.down.sql │ ├── 20241031101334_pp_update.up.sql │ ├── 20241210012458_remove_scores.down.sql │ ├── 20241210012458_remove_scores.up.sql │ ├── 20250114084143_tracking_rework.down.sql │ ├── 20250114084143_tracking_rework.up.sql │ ├── 20250120223657_tracking_with_datetime.down.sql │ ├── 20250120223657_tracking_with_datetime.up.sql │ ├── 20250318160023_osu_file_data.down.sql │ └── 20250318160023_osu_file_data.up.sql └── src │ ├── database.rs │ ├── impls │ ├── bookmarks.rs │ ├── configs │ │ ├── guild.rs │ │ ├── mod.rs │ │ └── user.rs │ ├── games │ │ ├── bg.rs │ │ ├── hl.rs │ │ └── mod.rs │ ├── mod.rs │ ├── osu │ │ ├── map.rs │ │ ├── mapset.rs │ │ ├── mod.rs │ │ ├── name.rs │ │ ├── rank_pp.rs │ │ ├── render.rs │ │ ├── score.rs │ │ ├── tracked_users.rs │ │ └── user.rs │ └── tracked_streams.rs │ ├── lib.rs │ ├── model │ ├── configs │ │ ├── authorities.rs │ │ ├── guild.rs │ │ ├── hide_solutions.rs │ │ ├── list_size.rs │ │ ├── mod.rs │ │ ├── retries.rs │ │ ├── score_data.rs │ │ ├── skin.rs │ │ └── user.rs │ ├── games │ │ ├── bg.rs │ │ ├── hl.rs │ │ └── mod.rs │ ├── mod.rs │ ├── osu │ │ ├── bookmark.rs │ │ ├── map.rs │ │ ├── mapset.rs │ │ ├── mod.rs │ │ ├── tracked_user.rs │ │ └── user.rs │ └── render.rs │ └── util.rs ├── bathbot-server ├── Cargo.toml └── src │ ├── lib.rs │ ├── middleware │ ├── metrics.rs │ └── mod.rs │ ├── routes │ ├── auth │ │ ├── error.rs │ │ ├── mod.rs │ │ ├── osu.rs │ │ └── twitch.rs │ ├── guild_count.rs │ ├── metrics.rs │ ├── mod.rs │ └── osudirect.rs │ ├── server.rs │ ├── standby.rs │ └── state.rs ├── bathbot-util ├── Cargo.toml └── src │ ├── buckets.rs │ ├── builder │ ├── author.rs │ ├── embed.rs │ ├── footer.rs │ ├── message.rs │ ├── mod.rs │ └── modal.rs │ ├── constants.rs │ ├── cow.rs │ ├── datetime.rs │ ├── exp_backoff.rs │ ├── ext │ ├── authored.rs │ ├── mod.rs │ └── score.rs │ ├── hasher.rs │ ├── html.rs │ ├── lib.rs │ ├── macros.rs │ ├── matcher.rs │ ├── matrix.rs │ ├── metrics.rs │ ├── mods_fmt.rs │ ├── msg_origin.rs │ ├── numbers.rs │ ├── osu.rs │ ├── query │ ├── filter.rs │ ├── impls │ │ ├── bookmark.rs │ │ ├── mod.rs │ │ ├── regular.rs │ │ └── top.rs │ ├── mod.rs │ ├── operator.rs │ ├── optional.rs │ └── searchable.rs │ ├── string_cmp.rs │ └── tourney_badges.rs ├── bathbot ├── Cargo.toml └── src │ ├── active │ ├── builder.rs │ ├── impls │ │ ├── badges.rs │ │ ├── bg_game │ │ │ ├── game.rs │ │ │ ├── game_wrapper.rs │ │ │ ├── hints.rs │ │ │ ├── img_reveal.rs │ │ │ ├── mapset.rs │ │ │ ├── mod.rs │ │ │ └── util.rs │ │ ├── bookmarks.rs │ │ ├── changelog.rs │ │ ├── compare │ │ │ ├── mod.rs │ │ │ ├── most_played.rs │ │ │ ├── scores.rs │ │ │ └── top.rs │ │ ├── embed_builder.rs │ │ ├── help │ │ │ ├── interaction.rs │ │ │ ├── mod.rs │ │ │ └── prefix.rs │ │ ├── higherlower │ │ │ ├── mod.rs │ │ │ ├── score_pp.rs │ │ │ └── state.rs │ │ ├── leaderboard.rs │ │ ├── map.rs │ │ ├── map_search.rs │ │ ├── match_compare.rs │ │ ├── match_costs.rs │ │ ├── medals │ │ │ ├── common.rs │ │ │ ├── list.rs │ │ │ ├── missing.rs │ │ │ ├── mod.rs │ │ │ └── recent.rs │ │ ├── mod.rs │ │ ├── most_played.rs │ │ ├── nochoke.rs │ │ ├── osekai │ │ │ ├── medal_count.rs │ │ │ ├── mod.rs │ │ │ └── rarity.rs │ │ ├── osustats │ │ │ ├── best.rs │ │ │ ├── mod.rs │ │ │ ├── players.rs │ │ │ └── scores.rs │ │ ├── profile │ │ │ ├── availability.rs │ │ │ ├── mod.rs │ │ │ ├── top100_mappers.rs │ │ │ ├── top100_mods.rs │ │ │ └── top100_stats.rs │ │ ├── ranking.rs │ │ ├── ranking_countries.rs │ │ ├── recent_list.rs │ │ ├── relax │ │ │ ├── mod.rs │ │ │ └── top.rs │ │ ├── render │ │ │ ├── cached.rs │ │ │ ├── import.rs │ │ │ ├── mod.rs │ │ │ └── settings.rs │ │ ├── simulate │ │ │ ├── attrs.rs │ │ │ ├── data.rs │ │ │ ├── mod.rs │ │ │ ├── state.rs │ │ │ └── top_old.rs │ │ ├── single_score.rs │ │ ├── skins.rs │ │ ├── slash_commands.rs │ │ ├── snipe │ │ │ ├── country_list.rs │ │ │ ├── difference.rs │ │ │ ├── mod.rs │ │ │ └── player_list.rs │ │ ├── top.rs │ │ ├── top_if.rs │ │ └── track_list.rs │ ├── mod.rs │ ├── origin.rs │ ├── pagination.rs │ └── response.rs │ ├── commands │ ├── fun │ │ ├── bg_game │ │ │ ├── bigger.rs │ │ │ ├── hint.rs │ │ │ ├── mod.rs │ │ │ ├── rankings.rs │ │ │ ├── skip.rs │ │ │ └── stop.rs │ │ ├── higherlower_game.rs │ │ ├── minesweeper.rs │ │ └── mod.rs │ ├── help │ │ ├── interaction.rs │ │ ├── message.rs │ │ └── mod.rs │ ├── mod.rs │ ├── osu │ │ ├── attributes.rs │ │ ├── avatar.rs │ │ ├── badges │ │ │ ├── mod.rs │ │ │ ├── query.rs │ │ │ └── user.rs │ │ ├── bookmarks │ │ │ ├── message.rs │ │ │ ├── mod.rs │ │ │ └── slash.rs │ │ ├── bws.rs │ │ ├── cards.rs │ │ ├── claim_name.rs │ │ ├── compare │ │ │ ├── common.rs │ │ │ ├── mod.rs │ │ │ ├── most_played.rs │ │ │ ├── profile.rs │ │ │ └── score.rs │ │ ├── daily_challenge │ │ │ ├── mod.rs │ │ │ └── user.rs │ │ ├── fix.rs │ │ ├── graphs │ │ │ ├── bpm.rs │ │ │ ├── map_strains.rs │ │ │ ├── medals.rs │ │ │ ├── mod.rs │ │ │ ├── osutrack │ │ │ │ ├── accuracy.rs │ │ │ │ ├── grades.rs │ │ │ │ ├── hit_ratios.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── playcount.rs │ │ │ │ ├── pp_rank.rs │ │ │ │ └── score.rs │ │ │ ├── playcount_replays.rs │ │ │ ├── rank.rs │ │ │ ├── score_rank.rs │ │ │ ├── snipe_count.rs │ │ │ ├── sniped.rs │ │ │ ├── top_date.rs │ │ │ ├── top_index.rs │ │ │ └── top_time.rs │ │ ├── leaderboard.rs │ │ ├── link.rs │ │ ├── map.rs │ │ ├── map_search.rs │ │ ├── mapper.rs │ │ ├── match_compare.rs │ │ ├── match_costs.rs │ │ ├── match_live.rs │ │ ├── medals │ │ │ ├── common.rs │ │ │ ├── icons_image.rs │ │ │ ├── list.rs │ │ │ ├── medal.rs │ │ │ ├── missing.rs │ │ │ ├── mod.rs │ │ │ ├── recent.rs │ │ │ └── stats.rs │ │ ├── mod.rs │ │ ├── most_played.rs │ │ ├── nochoke.rs │ │ ├── osekai │ │ │ ├── medal_count.rs │ │ │ ├── mod.rs │ │ │ ├── rarity.rs │ │ │ └── user_value.rs │ │ ├── osustats │ │ │ ├── best.rs │ │ │ ├── counts.rs │ │ │ ├── globals.rs │ │ │ ├── list.rs │ │ │ └── mod.rs │ │ ├── pinned.rs │ │ ├── pp.rs │ │ ├── profile.rs │ │ ├── rank │ │ │ ├── mod.rs │ │ │ ├── pp.rs │ │ │ └── score.rs │ │ ├── ranking │ │ │ ├── countries.rs │ │ │ ├── mod.rs │ │ │ └── players.rs │ │ ├── ratios.rs │ │ ├── recent │ │ │ ├── fix.rs │ │ │ ├── leaderboard.rs │ │ │ ├── list.rs │ │ │ ├── mod.rs │ │ │ └── score.rs │ │ ├── relax │ │ │ ├── mod.rs │ │ │ ├── profile.rs │ │ │ └── top.rs │ │ ├── render.rs │ │ ├── serverleaderboard.rs │ │ ├── simulate │ │ │ ├── args.rs │ │ │ ├── mod.rs │ │ │ └── parsed_map.rs │ │ ├── snipe │ │ │ ├── country_snipe_list.rs │ │ │ ├── country_snipe_stats.rs │ │ │ ├── mod.rs │ │ │ ├── player_snipe_list.rs │ │ │ ├── player_snipe_stats.rs │ │ │ ├── sniped.rs │ │ │ └── sniped_difference.rs │ │ ├── top │ │ │ ├── if_.rs │ │ │ ├── mod.rs │ │ │ └── old.rs │ │ └── whatif.rs │ ├── owner │ │ ├── add_bg.rs │ │ ├── cache.rs │ │ ├── mod.rs │ │ ├── request_members.rs │ │ ├── reshard.rs │ │ └── tracking_stats.rs │ ├── songs │ │ ├── bombsaway.rs │ │ ├── catchit.rs │ │ ├── chicago.rs │ │ ├── ding.rs │ │ ├── fireandflames.rs │ │ ├── fireflies.rs │ │ ├── flamingo.rs │ │ ├── glorydays.rs │ │ ├── harumachi.rs │ │ ├── hitorigoto.rs │ │ ├── lionheart.rs │ │ ├── mod.rs │ │ ├── mylove.rs │ │ ├── padoru.rs │ │ ├── pretender.rs │ │ ├── rockefeller.rs │ │ ├── saygoodbye.rs │ │ ├── startagain.rs │ │ ├── tijdmachine.rs │ │ ├── time_traveler.rs │ │ ├── wordsneversaid.rs │ │ └── zenzenzense.rs │ ├── tracking │ │ ├── mod.rs │ │ ├── track.rs │ │ ├── track_list.rs │ │ ├── untrack.rs │ │ └── untrack_all.rs │ ├── twitch │ │ ├── addstream.rs │ │ ├── mod.rs │ │ ├── removestream.rs │ │ └── tracked.rs │ └── utility │ │ ├── authorities.rs │ │ ├── changelog.rs │ │ ├── commands.rs │ │ ├── config.rs │ │ ├── embed_builder.rs │ │ ├── invite.rs │ │ ├── mod.rs │ │ ├── ping.rs │ │ ├── prefix.rs │ │ ├── roll.rs │ │ ├── server_config.rs │ │ └── skin.rs │ ├── core │ ├── commands │ │ ├── checks.rs │ │ ├── flags.rs │ │ ├── interaction │ │ │ ├── command.rs │ │ │ └── mod.rs │ │ ├── mod.rs │ │ ├── origin.rs │ │ └── prefix │ │ │ ├── args.rs │ │ │ ├── command.rs │ │ │ └── mod.rs │ ├── config.rs │ ├── context │ │ ├── discord.rs │ │ ├── games.rs │ │ ├── manager.rs │ │ ├── matchlive.rs │ │ ├── messages.rs │ │ ├── mod.rs │ │ ├── osutrack.rs │ │ ├── set_commands.rs │ │ ├── shutdown.rs │ │ └── twitch.rs │ ├── events │ │ ├── interaction │ │ │ ├── autocomplete.rs │ │ │ ├── command.rs │ │ │ └── mod.rs │ │ ├── message │ │ │ ├── mod.rs │ │ │ └── parse.rs │ │ └── mod.rs │ ├── logging.rs │ ├── metrics.rs │ └── mod.rs │ ├── embeds │ ├── mod.rs │ ├── osu │ │ ├── attributes.rs │ │ ├── claim_name.rs │ │ ├── country_snipe_stats.rs │ │ ├── fix_score.rs │ │ ├── match_live.rs │ │ ├── medal_stats.rs │ │ ├── mod.rs │ │ ├── osustats_counts.rs │ │ ├── player_snipe_stats.rs │ │ ├── pp_missing.rs │ │ ├── profile_compare.rs │ │ ├── ratio.rs │ │ ├── sniped.rs │ │ └── whatif.rs │ └── utility │ │ ├── config.rs │ │ ├── mod.rs │ │ └── server_config.rs │ ├── main.rs │ ├── manager │ ├── bookmarks.rs │ ├── games.rs │ ├── github.rs │ ├── guild_config.rs │ ├── huismetbenen_country.rs │ ├── mod.rs │ ├── osu_map.rs │ ├── osu_scores.rs │ ├── osu_user.rs │ ├── pp.rs │ ├── rank_pp_approx.rs │ ├── redis │ │ ├── mod.rs │ │ └── osu.rs │ ├── replay.rs │ ├── twitch.rs │ └── user_config.rs │ ├── matchlive │ ├── mod.rs │ └── types.rs │ ├── tracking │ ├── mod.rs │ ├── ordr │ │ └── mod.rs │ ├── osu │ │ ├── entry.rs │ │ ├── mod.rs │ │ ├── params.rs │ │ ├── process_score.rs │ │ ├── require_top.rs │ │ └── stats.rs │ ├── scores_ws.rs │ └── twitch │ │ ├── mod.rs │ │ ├── online_streams.rs │ │ └── twitch_loop.rs │ └── util │ ├── check_permissions.rs │ ├── emote.rs │ ├── ext │ ├── cached_user.rs │ ├── channel.rs │ ├── component.rs │ ├── interaction_command.rs │ ├── message.rs │ ├── mod.rs │ └── modal.rs │ ├── interaction.rs │ ├── mod.rs │ ├── monthly.rs │ ├── osu.rs │ └── searchable.rs ├── docker-compose.yml ├── media ├── bb-icon.svg ├── bb-text-coloured-hori.svg └── emotes │ ├── A_.png │ ├── B_.png │ ├── C_.png │ ├── D_.png │ ├── F_.png │ ├── SH.png │ ├── S_.png │ ├── XH.png │ ├── X_.png │ ├── bpm.png │ ├── count_objects.png │ ├── count_sliders.png │ ├── count_spinners.png │ ├── end.png │ ├── expand.png │ ├── minimize.png │ ├── miss.png │ ├── my_position.png │ ├── osu.png │ ├── osu_ctb.png │ ├── osu_mania.png │ ├── osu_std.png │ ├── osu_taiko.png │ ├── single_step.png │ ├── single_step_back.png │ ├── start.png │ ├── tracking.png │ └── twitch.png └── rustfmt.toml /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | rustflags = ["--cfg", "tokio_unstable"] # tokio-console debugging -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # enforce LF newlines 2 | * text=auto -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | ko_fi: bathbot -------------------------------------------------------------------------------- /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | name: Check project 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | 8 | jobs: 9 | clippy: 10 | runs-on: ubuntu-24.04 11 | 12 | strategy: 13 | matrix: 14 | include: 15 | - kind: default-features 16 | features: default 17 | - kind: full-features 18 | features: full 19 | 20 | steps: 21 | - name: Checkout project 22 | uses: actions/checkout@v3 23 | 24 | - name: Install stable toolchain 25 | uses: dtolnay/rust-toolchain@stable 26 | with: 27 | components: clippy 28 | 29 | # See failed run 30 | - name: "Install fontconfig" 31 | run: sudo apt-get -y install libfontconfig1-dev jq 32 | 33 | - name: Cache dependencies 34 | uses: Swatinem/rust-cache@v2 35 | 36 | - name: Run clippy 37 | env: 38 | RUSTFLAGS: -D warnings 39 | run: cargo clippy --workspace --features ${{ matrix.features }} --all-targets --no-deps 40 | 41 | rustfmt: 42 | name: Format 43 | runs-on: ubuntu-24.04 44 | 45 | steps: 46 | - name: Checkout sources 47 | uses: actions/checkout@v3 48 | 49 | - name: Install nightly toolchain 50 | uses: dtolnay/rust-toolchain@stable 51 | with: 52 | components: rustfmt 53 | toolchain: nightly 54 | 55 | - name: Check code formatting 56 | run: cargo fmt --all -- --check -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Build and Deploy 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v[0-9]+.[0-9]+.[0-9]+*' 7 | workflow_dispatch: 8 | 9 | env: 10 | CARGO_INCREMENTAL: 0 11 | CARGO_NET_RETRY: 10 12 | RUSTFLAGS: -Cdebuginfo=1 -Dwarnings 13 | 14 | jobs: 15 | build: 16 | runs-on: ubuntu-24.04 17 | 18 | steps: 19 | - name: Checkout project 20 | uses: actions/checkout@v3 21 | 22 | - name: Install rust toolchain 23 | uses: actions-rs/toolchain@v1 24 | with: 25 | toolchain: stable 26 | profile: minimal 27 | override: true 28 | 29 | # See failed run 30 | - name: "Install fontconfig" 31 | run: sudo apt-get -y install libfontconfig1-dev jq 32 | 33 | - name: Cache dependencies 34 | uses: Swatinem/rust-cache@v1 35 | 36 | - name: Build 37 | run: cargo build --release --features full 38 | 39 | - name: Deploy 40 | uses: appleboy/scp-action@v0.1.7 41 | with: 42 | host: ${{ secrets.SCP_HOST }} 43 | username: ${{ secrets.SCP_USERNAME }} 44 | password: ${{ secrets.SCP_PASSWORD }} 45 | key: ${{ secrets.SCP_KEY }} 46 | source: target/release/bathbot-twilight 47 | target: ${{ secrets.SCP_TARGET }} 48 | strip_components: 2 -------------------------------------------------------------------------------- /.github/workflows/roadmap.yml: -------------------------------------------------------------------------------- 1 | name: Create roadmap items 2 | 3 | on: 4 | issues: 5 | types: 6 | - opened 7 | 8 | jobs: 9 | add-to-project: 10 | name: Add issue to project 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/add-to-project@v0.4.0 14 | with: 15 | project-url: https://github.com/users/MaxOhn/projects/3 16 | github-token: ${{ secrets.CI_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /*/target 2 | /*/.env 3 | /logs 4 | /*/.vscode 5 | /docker-volume 6 | /target 7 | /backgrounds 8 | /maps 9 | /*/output.* 10 | .env 11 | /.vscode 12 | /.idea 13 | resume_score_id.txt -------------------------------------------------------------------------------- /.sqlx/query-057d3f9a920c8ef1d8e2bd97f0a7dd429a599506df9b65768c5e358e4b36f460.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\nDELETE FROM \n tracked_twitch_streams \nWHERE \n channel_id = $1", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Left": [ 8 | "Int8" 9 | ] 10 | }, 11 | "nullable": [] 12 | }, 13 | "hash": "057d3f9a920c8ef1d8e2bd97f0a7dd429a599506df9b65768c5e358e4b36f460" 14 | } 15 | -------------------------------------------------------------------------------- /.sqlx/query-0e0db1c5f4acec83f98eeeff3b05529c44947fc2789388902b46055e8436b1a6.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\nWITH stats AS (\n SELECT \n global_rank, \n pp, \n last_update \n FROM \n osu_user_mode_stats \n WHERE \n gamemode = $1 \n AND global_rank > 0 \n AND NOW() - last_update < interval '2 days'\n) \nSELECT \n * \nFROM \n (\n (\n SELECT \n global_rank, \n pp, \n last_update, \n 0 :: INT2 AS pos \n FROM \n stats \n WHERE \n pp >= $2 \n ORDER BY \n pp ASC \n LIMIT \n 5\n ) \n UNION ALL \n (\n SELECT \n global_rank, \n pp, \n last_update, \n 1 :: INT2 AS pos \n FROM \n stats \n WHERE \n pp <= $2 \n ORDER BY \n pp DESC \n LIMIT \n 5\n )\n ) AS neighbors", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "global_rank", 9 | "type_info": "Int4" 10 | }, 11 | { 12 | "ordinal": 1, 13 | "name": "pp", 14 | "type_info": "Float4" 15 | }, 16 | { 17 | "ordinal": 2, 18 | "name": "last_update", 19 | "type_info": "Timestamptz" 20 | }, 21 | { 22 | "ordinal": 3, 23 | "name": "pos", 24 | "type_info": "Int2" 25 | } 26 | ], 27 | "parameters": { 28 | "Left": [ 29 | "Int2", 30 | "Float4" 31 | ] 32 | }, 33 | "nullable": [ 34 | null, 35 | null, 36 | null, 37 | null 38 | ] 39 | }, 40 | "hash": "0e0db1c5f4acec83f98eeeff3b05529c44947fc2789388902b46055e8436b1a6" 41 | } 42 | -------------------------------------------------------------------------------- /.sqlx/query-0e433f1a5f1609bac4b8d161fa57557e6514d3036432ff81a1e022b23b76013d.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\nSELECT \n user_id, \n username \nFROM \n osu_user_names \nWHERE \n user_id = ANY($1)", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "user_id", 9 | "type_info": "Int4" 10 | }, 11 | { 12 | "ordinal": 1, 13 | "name": "username", 14 | "type_info": "Varchar" 15 | } 16 | ], 17 | "parameters": { 18 | "Left": [ 19 | "Int4Array" 20 | ] 21 | }, 22 | "nullable": [ 23 | false, 24 | false 25 | ] 26 | }, 27 | "hash": "0e433f1a5f1609bac4b8d161fa57557e6514d3036432ff81a1e022b23b76013d" 28 | } 29 | -------------------------------------------------------------------------------- /.sqlx/query-14dc3fec9ff696d82c4a4e4d3b73807c62d95550b4dade6650487b28d0427e4d.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\nINSERT INTO render_video_urls (score_id, video_url) \nVALUES \n ($1, $2) ON CONFLICT (score_id) DO \nUPDATE \nSET \n video_url = $2", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Left": [ 8 | "Int8", 9 | "Varchar" 10 | ] 11 | }, 12 | "nullable": [] 13 | }, 14 | "hash": "14dc3fec9ff696d82c4a4e4d3b73807c62d95550b4dade6650487b28d0427e4d" 15 | } 16 | -------------------------------------------------------------------------------- /.sqlx/query-1d359cbace668cd27222e76337e61f7fb6b5bd78d61d0b134563fba190341435.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\nSELECT \n user_id \nFROM \n osu_user_names \nWHERE \n username ILIKE $1 OR username ILIKE $2", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "user_id", 9 | "type_info": "Int4" 10 | } 11 | ], 12 | "parameters": { 13 | "Left": [ 14 | "Text", 15 | "Text" 16 | ] 17 | }, 18 | "nullable": [ 19 | false 20 | ] 21 | }, 22 | "hash": "1d359cbace668cd27222e76337e61f7fb6b5bd78d61d0b134563fba190341435" 23 | } 24 | -------------------------------------------------------------------------------- /.sqlx/query-1f0002253b4c59c13821419714e32e9ec53a8a9e687e9b801140d2e21aa60dfa.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\nINSERT INTO\n osu_users_100th_pp(user_id, gamemode, pp, last_updated)\nVALUES\n ($1, $2, $3, $4)\nON CONFLICT\n (user_id, gamemode)\nDO\n UPDATE\nSET\n pp = $3,\n last_updated = $4", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Left": [ 8 | "Int4", 9 | "Int2", 10 | "Float4", 11 | "Timestamptz" 12 | ] 13 | }, 14 | "nullable": [] 15 | }, 16 | "hash": "1f0002253b4c59c13821419714e32e9ec53a8a9e687e9b801140d2e21aa60dfa" 17 | } 18 | -------------------------------------------------------------------------------- /.sqlx/query-1fbb1962e614748c30f6fc40f8baee79dd6a38bdf2c91d9f700f5a08062c5c19.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\nSELECT \n map_id, \n map_version AS version\nFROM \n (\n SELECT \n map_id, \n mapset_id, \n map_version \n FROM \n osu_maps\n ) AS maps \n JOIN (\n SELECT \n mapset_id \n FROM \n osu_maps \n WHERE \n map_id = $1\n ) AS mapset ON maps.mapset_id = mapset.mapset_id", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "map_id", 9 | "type_info": "Int4" 10 | }, 11 | { 12 | "ordinal": 1, 13 | "name": "version", 14 | "type_info": "Varchar" 15 | } 16 | ], 17 | "parameters": { 18 | "Left": [ 19 | "Int4" 20 | ] 21 | }, 22 | "nullable": [ 23 | false, 24 | false 25 | ] 26 | }, 27 | "hash": "1fbb1962e614748c30f6fc40f8baee79dd6a38bdf2c91d9f700f5a08062c5c19" 28 | } 29 | -------------------------------------------------------------------------------- /.sqlx/query-21cc0cb0c439b63edaaeb51f7727e492e2bd822045bc58ec4ec8f9882556bbd8.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\nINSERT INTO bggame_scores (discord_id, score) \nSELECT\n *\nFROM\n UNNEST($1::INT8[], $2::INT4[]) ON CONFLICT (discord_id) DO \nUPDATE \nSET \n score = bggame_scores.score + excluded.score", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Left": [ 8 | "Int8Array", 9 | "Int4Array" 10 | ] 11 | }, 12 | "nullable": [] 13 | }, 14 | "hash": "21cc0cb0c439b63edaaeb51f7727e492e2bd822045bc58ec4ec8f9882556bbd8" 15 | } 16 | -------------------------------------------------------------------------------- /.sqlx/query-222389f3bfc98a2e36d29b44792b23d2bae7007aff9a70b3161c4c72ef85c34e.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\nINSERT INTO user_map_bookmarks (user_id, map_id) \nVALUES \n ($1, $2) ON CONFLICT (user_id, map_id) DO NOTHING", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Left": [ 8 | "Int8", 9 | "Int4" 10 | ] 11 | }, 12 | "nullable": [] 13 | }, 14 | "hash": "222389f3bfc98a2e36d29b44792b23d2bae7007aff9a70b3161c4c72ef85c34e" 15 | } 16 | -------------------------------------------------------------------------------- /.sqlx/query-26b20d4e1aae8f234e4a5295d5c892ba739303b90c57992685af8a5d565885bf.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\nSELECT \n twitch_id \nFROM \n user_configs \nWHERE \n osu_id = $1\n AND twitch_id IS NOT NULL", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "twitch_id", 9 | "type_info": "Int8" 10 | } 11 | ], 12 | "parameters": { 13 | "Left": [ 14 | "Int4" 15 | ] 16 | }, 17 | "nullable": [ 18 | true 19 | ] 20 | }, 21 | "hash": "26b20d4e1aae8f234e4a5295d5c892ba739303b90c57992685af8a5d565885bf" 22 | } 23 | -------------------------------------------------------------------------------- /.sqlx/query-27fc81ceab1a9a0b3526082feb24eb6c0e7c78282aa6f493cf3c4d96ae30ec78.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\nINSERT INTO map_tags (\n mapset_id, image_filename, gamemode\n) \nVALUES \n ($1, $2, $3) ON CONFLICT (mapset_id) DO \nUPDATE \nSET \n image_filename = $2", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Left": [ 8 | "Int4", 9 | "Varchar", 10 | "Int2" 11 | ] 12 | }, 13 | "nullable": [] 14 | }, 15 | "hash": "27fc81ceab1a9a0b3526082feb24eb6c0e7c78282aa6f493cf3c4d96ae30ec78" 16 | } 17 | -------------------------------------------------------------------------------- /.sqlx/query-2969365f091f2408b8e9fbce514024420d26fe358dbf08afc58074515d3c1e78.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\nINSERT INTO higherlower_scores (\n discord_id, game_version, highscore\n) \nVALUES \n ($1, $2, $3) ON CONFLICT (discord_id, game_version) DO \nUPDATE \nSET \n highscore = $3 \nWHERE \n higherlower_scores.highscore < $3 RETURNING highscore", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "highscore", 9 | "type_info": "Int4" 10 | } 11 | ], 12 | "parameters": { 13 | "Left": [ 14 | "Int8", 15 | "Int2", 16 | "Int4" 17 | ] 18 | }, 19 | "nullable": [ 20 | false 21 | ] 22 | }, 23 | "hash": "2969365f091f2408b8e9fbce514024420d26fe358dbf08afc58074515d3c1e78" 24 | } 25 | -------------------------------------------------------------------------------- /.sqlx/query-30719710015a08503f4b1e7178b659e86ecbe928d3929da13dda045137b69d38.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\nSELECT \n username \nFROM \n osu_user_names \nWHERE \n user_id = $1", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "username", 9 | "type_info": "Varchar" 10 | } 11 | ], 12 | "parameters": { 13 | "Left": [ 14 | "Int4" 15 | ] 16 | }, 17 | "nullable": [ 18 | false 19 | ] 20 | }, 21 | "hash": "30719710015a08503f4b1e7178b659e86ecbe928d3929da13dda045137b69d38" 22 | } 23 | -------------------------------------------------------------------------------- /.sqlx/query-32a5e31c8b23211b9656ea4490bddb274af9dc1afc5198a4a37f43f9bb061464.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\nSELECT \n skin_url \nFROM \n (\n SELECT \n skin_url, \n osu_id \n FROM \n user_configs \n WHERE \n skin_url IS NOT NULL \n AND osu_id IS NOT NULL\n ) AS configs \n JOIN (\n SELECT \n user_id \n FROM \n osu_user_names \n WHERE \n username ILIKE $1\n ) AS names ON configs.osu_id = names.user_id", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "skin_url", 9 | "type_info": "Varchar" 10 | } 11 | ], 12 | "parameters": { 13 | "Left": [ 14 | "Text" 15 | ] 16 | }, 17 | "nullable": [ 18 | true 19 | ] 20 | }, 21 | "hash": "32a5e31c8b23211b9656ea4490bddb274af9dc1afc5198a4a37f43f9bb061464" 22 | } 23 | -------------------------------------------------------------------------------- /.sqlx/query-369d3652737db22b47d3db2004a25fb210cc4857a56710f9ec5365582ab9cfe2.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\nSELECT \n twitch_id \nFROM \n (\n SELECT \n twitch_id, \n osu_id \n FROM \n user_configs \n WHERE \n twitch_id IS NOT NULL \n AND osu_id IS NOT NULL\n ) AS configs \n JOIN (\n SELECT \n user_id \n FROM \n osu_user_names \n WHERE \n username ILIKE $1\n ) AS names ON configs.osu_id = names.user_id", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "twitch_id", 9 | "type_info": "Int8" 10 | } 11 | ], 12 | "parameters": { 13 | "Left": [ 14 | "Text" 15 | ] 16 | }, 17 | "nullable": [ 18 | true 19 | ] 20 | }, 21 | "hash": "369d3652737db22b47d3db2004a25fb210cc4857a56710f9ec5365582ab9cfe2" 22 | } 23 | -------------------------------------------------------------------------------- /.sqlx/query-380fc3f7a232b0f145ae03ec0ec838a57bfe42db5b2bcbd5a636bbbffd2e0b5e.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\nSELECT \n discord_id \nFROM \n user_configs \nWHERE \n osu_id = $1", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "discord_id", 9 | "type_info": "Int8" 10 | } 11 | ], 12 | "parameters": { 13 | "Left": [ 14 | "Int4" 15 | ] 16 | }, 17 | "nullable": [ 18 | false 19 | ] 20 | }, 21 | "hash": "380fc3f7a232b0f145ae03ec0ec838a57bfe42db5b2bcbd5a636bbbffd2e0b5e" 22 | } 23 | -------------------------------------------------------------------------------- /.sqlx/query-3a26d9aab70278d6d23e9b224a416c9010b50ab450cdb2cba7689ecd25d73d6f.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\nSELECT \n video_url \nFROM \n render_video_urls \nWHERE \n score_id = $1", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "video_url", 9 | "type_info": "Varchar" 10 | } 11 | ], 12 | "parameters": { 13 | "Left": [ 14 | "Int8" 15 | ] 16 | }, 17 | "nullable": [ 18 | false 19 | ] 20 | }, 21 | "hash": "3a26d9aab70278d6d23e9b224a416c9010b50ab450cdb2cba7689ecd25d73d6f" 22 | } 23 | -------------------------------------------------------------------------------- /.sqlx/query-40aaa1881361b5793d0a7ff1aef3c840c18bef6ea7d839161e7297a740d11e00.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\nINSERT INTO user_configs (\n discord_id, osu_id, gamemode, twitch_id, \n retries, score_embed, list_size, \n timezone_seconds, render_button, score_data\n) \nVALUES \n ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) ON CONFLICT (discord_id) DO \nUPDATE \nSET \n osu_id = $2, \n gamemode = $3, \n twitch_id = $4, \n retries = $5, \n score_embed = $6, \n list_size = $7, \n timezone_seconds = $8, \n render_button = $9, \n score_data = $10", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Left": [ 8 | "Int8", 9 | "Int4", 10 | "Int2", 11 | "Int8", 12 | "Int2", 13 | "Jsonb", 14 | "Int2", 15 | "Int4", 16 | "Bool", 17 | "Int2" 18 | ] 19 | }, 20 | "nullable": [] 21 | }, 22 | "hash": "40aaa1881361b5793d0a7ff1aef3c840c18bef6ea7d839161e7297a740d11e00" 23 | } 24 | -------------------------------------------------------------------------------- /.sqlx/query-46f62972a9ab0535c92b020be111bb5d4aed8c8af3f2156afb2a61c2aeae6355.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\nSELECT \n username \nFROM \n osu_user_names \nWHERE \n user_id = (\n SELECT \n osu_id \n FROM \n user_configs \n WHERE \n discord_id = $1\n )", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "username", 9 | "type_info": "Varchar" 10 | } 11 | ], 12 | "parameters": { 13 | "Left": [ 14 | "Int8" 15 | ] 16 | }, 17 | "nullable": [ 18 | false 19 | ] 20 | }, 21 | "hash": "46f62972a9ab0535c92b020be111bb5d4aed8c8af3f2156afb2a61c2aeae6355" 22 | } 23 | -------------------------------------------------------------------------------- /.sqlx/query-4d9d86454d838f1fd2bfc30c019680978373896c7e01f09e939a63de314137e0.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\nSELECT \n artist, \n title \nFROM \n osu_mapsets \nWHERE \n mapset_id = $1", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "artist", 9 | "type_info": "Varchar" 10 | }, 11 | { 12 | "ordinal": 1, 13 | "name": "title", 14 | "type_info": "Varchar" 15 | } 16 | ], 17 | "parameters": { 18 | "Left": [ 19 | "Int4" 20 | ] 21 | }, 22 | "nullable": [ 23 | false, 24 | false 25 | ] 26 | }, 27 | "hash": "4d9d86454d838f1fd2bfc30c019680978373896c7e01f09e939a63de314137e0" 28 | } 29 | -------------------------------------------------------------------------------- /.sqlx/query-4dd4fce1ba302fc2ddb754e2641d433a666678ea629969637e021ae54366885a.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\nINSERT INTO tracked_osu_users (\n user_id, gamemode, channel_id, min_index, max_index,\n min_pp, max_pp, min_combo_percent, max_combo_percent\n)\nVALUES\n ($1, $2, $3, $4, $5, $6, $7, $8, $9)\nON CONFLICT\n (user_id, gamemode, channel_id)\nDO\n UPDATE\nSET\n min_index = $4,\n max_index = $5,\n min_pp = $6,\n max_pp = $7,\n min_combo_percent = $8,\n max_combo_percent = $9", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Left": [ 8 | "Int4", 9 | "Int2", 10 | "Int8", 11 | "Int2", 12 | "Int2", 13 | "Float4", 14 | "Float4", 15 | "Float4", 16 | "Float4" 17 | ] 18 | }, 19 | "nullable": [] 20 | }, 21 | "hash": "4dd4fce1ba302fc2ddb754e2641d433a666678ea629969637e021ae54366885a" 22 | } 23 | -------------------------------------------------------------------------------- /.sqlx/query-56c783134577013d4ca9ef1328dba43223c3b745c1a4a5ce9148cb9d94fe9d00.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\nSELECT \n DISTINCT ON (version) map_id, \n map_version AS version \nFROM \n osu_maps \nWHERE \n mapset_id = $1 \nORDER BY \n version, \n last_update DESC", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "map_id", 9 | "type_info": "Int4" 10 | }, 11 | { 12 | "ordinal": 1, 13 | "name": "version", 14 | "type_info": "Varchar" 15 | } 16 | ], 17 | "parameters": { 18 | "Left": [ 19 | "Int4" 20 | ] 21 | }, 22 | "nullable": [ 23 | false, 24 | false 25 | ] 26 | }, 27 | "hash": "56c783134577013d4ca9ef1328dba43223c3b745c1a4a5ce9148cb9d94fe9d00" 28 | } 29 | -------------------------------------------------------------------------------- /.sqlx/query-5c9012746a35ed9aca1d3b21f3e3f9fb93f61f8327458fec9716696faeb5d144.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\nSELECT \n osu_id \nFROM \n user_configs \nWHERE \n discord_id = $1", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "osu_id", 9 | "type_info": "Int4" 10 | } 11 | ], 12 | "parameters": { 13 | "Left": [ 14 | "Int8" 15 | ] 16 | }, 17 | "nullable": [ 18 | true 19 | ] 20 | }, 21 | "hash": "5c9012746a35ed9aca1d3b21f3e3f9fb93f61f8327458fec9716696faeb5d144" 22 | } 23 | -------------------------------------------------------------------------------- /.sqlx/query-5f5d3b5e331806027c5fe6c8b85dccf7af5ae8d2e8f78dc51c37cee0a381460c.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\nINSERT INTO osu_mapsets (\n mapset_id, user_id, artist, title, \n creator, source, tags, video, storyboard, \n bpm, rank_status, ranked_date, genre_id, \n language_id, thumbnail, cover\n) \nVALUES \n (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, \n $11, $12, $13, $14, $15, $16\n ) ON CONFLICT (mapset_id) DO \nUPDATE \nSET \n user_id = $2, \n artist = $3, \n title = $4, \n creator = $5, \n source = $6, \n tags = $7, \n video = $8, \n storyboard = $9, \n bpm = $10, \n rank_status = $11, \n ranked_date = $12, \n genre_id = $13, \n language_id = $14, \n thumbnail = $15, \n cover = $16, \n last_update = NOW()", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Left": [ 8 | "Int4", 9 | "Int4", 10 | "Varchar", 11 | "Varchar", 12 | "Varchar", 13 | "Varchar", 14 | "Varchar", 15 | "Bool", 16 | "Bool", 17 | "Float4", 18 | "Int2", 19 | "Timestamptz", 20 | "Int2", 21 | "Int2", 22 | "Varchar", 23 | "Varchar" 24 | ] 25 | }, 26 | "nullable": [] 27 | }, 28 | "hash": "5f5d3b5e331806027c5fe6c8b85dccf7af5ae8d2e8f78dc51c37cee0a381460c" 29 | } 30 | -------------------------------------------------------------------------------- /.sqlx/query-611d29835f9095dcf111fb1243775a51f3121fab98b6b6bc0a6f381376ed5fd1.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\nSELECT \n highscore \nFROM \n higherlower_scores \nWHERE \n discord_id = $1 \n AND game_version = $2", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "highscore", 9 | "type_info": "Int4" 10 | } 11 | ], 12 | "parameters": { 13 | "Left": [ 14 | "Int8", 15 | "Int2" 16 | ] 17 | }, 18 | "nullable": [ 19 | false 20 | ] 21 | }, 22 | "hash": "611d29835f9095dcf111fb1243775a51f3121fab98b6b6bc0a6f381376ed5fd1" 23 | } 24 | -------------------------------------------------------------------------------- /.sqlx/query-61af8b78e54ed6962ccaecb83ddab3b83b4ad116c3b1f90b06d82ebdecc03ba2.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\nSELECT \nuser_id, \nusername \nfrom \nosu_user_names \nWHERE \nusername ILIKE ANY($1)", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "user_id", 9 | "type_info": "Int4" 10 | }, 11 | { 12 | "ordinal": 1, 13 | "name": "username", 14 | "type_info": "Varchar" 15 | } 16 | ], 17 | "parameters": { 18 | "Left": [ 19 | "TextArray" 20 | ] 21 | }, 22 | "nullable": [ 23 | false, 24 | false 25 | ] 26 | }, 27 | "hash": "61af8b78e54ed6962ccaecb83ddab3b83b4ad116c3b1f90b06d82ebdecc03ba2" 28 | } 29 | -------------------------------------------------------------------------------- /.sqlx/query-62fab7c476b991a91b952f778442a7897cbbac8c2e6fd30d140fee58b6434f9b.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\nINSERT INTO osu_replays (score_id, replay) \nVALUES \n ($1, $2) ON CONFLICT (score_id) DO NOTHING", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Left": [ 8 | "Int8", 9 | "Bytea" 10 | ] 11 | }, 12 | "nullable": [] 13 | }, 14 | "hash": "62fab7c476b991a91b952f778442a7897cbbac8c2e6fd30d140fee58b6434f9b" 15 | } 16 | -------------------------------------------------------------------------------- /.sqlx/query-68356a75b1e66719f90cf17ddeed1a170c119343d0d4c515308f6409997f2557.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\nINSERT INTO tracked_twitch_streams (channel_id, user_id) \nVALUES \n ($1, $2) ON CONFLICT (channel_id, user_id) DO NOTHING", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Left": [ 8 | "Int8", 9 | "Int8" 10 | ] 11 | }, 12 | "nullable": [] 13 | }, 14 | "hash": "68356a75b1e66719f90cf17ddeed1a170c119343d0d4c515308f6409997f2557" 15 | } 16 | -------------------------------------------------------------------------------- /.sqlx/query-74ed6117f3b940209a8c16e41f94f6909c24d51bfbddc02746a9ca4400abc374.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\nINSERT INTO osu_user_mode_stats (\n user_id, gamemode, accuracy, pp, country_rank, \n global_rank, count_ss, count_ssh, \n count_s, count_sh, count_a, user_level, \n max_combo, playcount, playtime, ranked_score, \n replays_watched, total_hits, total_score, \n scores_first\n) \nVALUES \n (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, \n $11, $12, $13, $14, $15, $16, $17, $18, \n $19, $20\n ) ON CONFLICT (user_id, gamemode) DO \nUPDATE \nSET \n accuracy = $3, \n pp = $4, \n country_rank = $5, \n global_rank = $6, \n count_ss = $7, \n count_ssh = $8, \n count_s = $9, \n count_sh = $10, \n count_a = $11, \n user_level = $12, \n max_combo = $13, \n playcount = $14, \n playtime = $15, \n ranked_score = $16, \n replays_watched = $17, \n total_hits = $18, \n total_score = $19, \n scores_first = $20,\n last_update = NOW()", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Left": [ 8 | "Int4", 9 | "Int2", 10 | "Float4", 11 | "Float4", 12 | "Int4", 13 | "Int4", 14 | "Int4", 15 | "Int4", 16 | "Int4", 17 | "Int4", 18 | "Int4", 19 | "Float4", 20 | "Int4", 21 | "Int4", 22 | "Int4", 23 | "Int8", 24 | "Int4", 25 | "Int8", 26 | "Int8", 27 | "Int4" 28 | ] 29 | }, 30 | "nullable": [] 31 | }, 32 | "hash": "74ed6117f3b940209a8c16e41f94f6909c24d51bfbddc02746a9ca4400abc374" 33 | } 34 | -------------------------------------------------------------------------------- /.sqlx/query-7734c6b98a367b9b6fab653edcd83f8b60179e932e2ef78ea25095e56d756256.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\nDELETE FROM \n tracked_osu_users\nWHERE\n user_id = $1\n AND ($2::INT2 is NULL OR gamemode = $2)\n AND channel_id = $3", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Left": [ 8 | "Int4", 9 | "Int2", 10 | "Int8" 11 | ] 12 | }, 13 | "nullable": [] 14 | }, 15 | "hash": "7734c6b98a367b9b6fab653edcd83f8b60179e932e2ef78ea25095e56d756256" 16 | } 17 | -------------------------------------------------------------------------------- /.sqlx/query-789ed5aef078e6abf48755e46be402db4b6a5d1a430673ddb84db58c29e9c2ba.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\nDELETE FROM \n osu_user_mode_stats \nWHERE \n user_id = $1", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Left": [ 8 | "Int4" 9 | ] 10 | }, 11 | "nullable": [] 12 | }, 13 | "hash": "789ed5aef078e6abf48755e46be402db4b6a5d1a430673ddb84db58c29e9c2ba" 14 | } 15 | -------------------------------------------------------------------------------- /.sqlx/query-7a9f634eec21891847a18fc53c1f168ebcbec56e9c3f3d2198f24ef609414c8f.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\nUPDATE user_configs\nSET\n score_embed = $2\nWHERE\n discord_id = $1", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Left": [ 8 | "Int8", 9 | "Jsonb" 10 | ] 11 | }, 12 | "nullable": [] 13 | }, 14 | "hash": "7a9f634eec21891847a18fc53c1f168ebcbec56e9c3f3d2198f24ef609414c8f" 15 | } 16 | -------------------------------------------------------------------------------- /.sqlx/query-82332eb70e5e28b22e5ed7f3e118931d94fab08797e7f8675ddff4e858b3b27a.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\nSELECT \n discord_id, \n score \nFROM \n bggame_scores", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "discord_id", 9 | "type_info": "Int8" 10 | }, 11 | { 12 | "ordinal": 1, 13 | "name": "score", 14 | "type_info": "Int4" 15 | } 16 | ], 17 | "parameters": { 18 | "Left": [] 19 | }, 20 | "nullable": [ 21 | false, 22 | false 23 | ] 24 | }, 25 | "hash": "82332eb70e5e28b22e5ed7f3e118931d94fab08797e7f8675ddff4e858b3b27a" 26 | } 27 | -------------------------------------------------------------------------------- /.sqlx/query-8547a49cb48c682ed2ad23dd0f338da6d1e50ae3a682276806b46dfe5b5f3833.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n SELECT\n content\n FROM\n osu_map_file_content\n WHERE\n map_id = $1", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "content", 9 | "type_info": "Bytea" 10 | } 11 | ], 12 | "parameters": { 13 | "Left": [ 14 | "Int4" 15 | ] 16 | }, 17 | "nullable": [ 18 | false 19 | ] 20 | }, 21 | "hash": "8547a49cb48c682ed2ad23dd0f338da6d1e50ae3a682276806b46dfe5b5f3833" 22 | } 23 | -------------------------------------------------------------------------------- /.sqlx/query-85a26a8fa694a2393f3fb55229f8dc0386fb1271568ad2ce168bd49d80f6f58b.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\nINSERT INTO osu_user_stats (\n user_id, country_code, join_date, \n comment_count, kudosu_total, kudosu_available, \n forum_post_count, badges, played_maps, \n followers, graveyard_mapset_count, \n loved_mapset_count, mapping_followers, \n previous_usernames_count, ranked_mapset_count, \n medals\n) \nVALUES \n (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, \n $11, $12, $13, $14, $15, $16\n ) ON CONFLICT (user_id) DO \nUPDATE \nSET \n country_code = $2, \n comment_count = $4, \n kudosu_total = $5, \n kudosu_available = $6, \n forum_post_count = $7, \n badges = $8, \n played_maps = $9, \n followers = $10, \n graveyard_mapset_count = $11, \n loved_mapset_count = $12, \n mapping_followers = $13, \n previous_usernames_count = $14, \n ranked_mapset_count = $15, \n medals = $16,\n last_update = NOW()", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Left": [ 8 | "Int4", 9 | "Varchar", 10 | "Timestamptz", 11 | "Int4", 12 | "Int4", 13 | "Int4", 14 | "Int4", 15 | "Int4", 16 | "Int4", 17 | "Int4", 18 | "Int4", 19 | "Int4", 20 | "Int4", 21 | "Int4", 22 | "Int4", 23 | "Int4" 24 | ] 25 | }, 26 | "nullable": [] 27 | }, 28 | "hash": "85a26a8fa694a2393f3fb55229f8dc0386fb1271568ad2ce168bd49d80f6f58b" 29 | } 30 | -------------------------------------------------------------------------------- /.sqlx/query-8b88f6e2659ffeb87114f3c9e099c9ba96a6ce79c842fffe9a665fc1f5334822.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\nSELECT \n skin_url \nFROM \n user_configs \nWHERE \n discord_id = $1", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "skin_url", 9 | "type_info": "Varchar" 10 | } 11 | ], 12 | "parameters": { 13 | "Left": [ 14 | "Int8" 15 | ] 16 | }, 17 | "nullable": [ 18 | true 19 | ] 20 | }, 21 | "hash": "8b88f6e2659ffeb87114f3c9e099c9ba96a6ce79c842fffe9a665fc1f5334822" 22 | } 23 | -------------------------------------------------------------------------------- /.sqlx/query-8f560c7c1634f00ce00878148ce3612e1a87cb8a81057dc6edbfa9a9ef49c56c.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\nDELETE FROM \n osu_user_stats \nWHERE \n user_id = $1", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Left": [ 8 | "Int4" 9 | ] 10 | }, 11 | "nullable": [] 12 | }, 13 | "hash": "8f560c7c1634f00ce00878148ce3612e1a87cb8a81057dc6edbfa9a9ef49c56c" 14 | } 15 | -------------------------------------------------------------------------------- /.sqlx/query-935eb7bfdf09762ae2c943ab087237049870721fb6522785fe25b280652246c2.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\nINSERT INTO guild_configs (\n guild_id, authorities, prefixes, allow_songs, \n retries, list_size, \n render_button, allow_custom_skins, \n hide_medal_solution, score_data\n) \nVALUES \n ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)\nON CONFLICT\n (guild_id)\nDO \n UPDATE \nSET \n authorities = $2, \n prefixes = $3, \n allow_songs = $4, \n retries = $5, \n list_size = $6, \n render_button = $7, \n allow_custom_skins = $8, \n hide_medal_solution = $9, \n score_data = $10", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Left": [ 8 | "Int8", 9 | "Bytea", 10 | "Jsonb", 11 | "Bool", 12 | "Int2", 13 | "Int2", 14 | "Bool", 15 | "Bool", 16 | "Int2", 17 | "Int2" 18 | ] 19 | }, 20 | "nullable": [] 21 | }, 22 | "hash": "935eb7bfdf09762ae2c943ab087237049870721fb6522785fe25b280652246c2" 23 | } 24 | -------------------------------------------------------------------------------- /.sqlx/query-96598cd5497eb9b19abfb1703e8be43c32a91bee2067d7c40ce0900ae6d0f195.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\nDELETE FROM\n osu_maps\nWHERE\n mapset_id = $1\nRETURNING\n map_id, checksum", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "map_id", 9 | "type_info": "Int4" 10 | }, 11 | { 12 | "ordinal": 1, 13 | "name": "checksum", 14 | "type_info": "Varchar" 15 | } 16 | ], 17 | "parameters": { 18 | "Left": [ 19 | "Int4" 20 | ] 21 | }, 22 | "nullable": [ 23 | false, 24 | false 25 | ] 26 | }, 27 | "hash": "96598cd5497eb9b19abfb1703e8be43c32a91bee2067d7c40ce0900ae6d0f195" 28 | } 29 | -------------------------------------------------------------------------------- /.sqlx/query-97170f08a3a6c67c60616849ec0476cffcf82061c60987a4dd6d8e542c024adb.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\nDELETE FROM \n osu_map_file_content \nWHERE \n map_id = $1", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Left": [ 8 | "Int4" 9 | ] 10 | }, 11 | "nullable": [] 12 | }, 13 | "hash": "97170f08a3a6c67c60616849ec0476cffcf82061c60987a4dd6d8e542c024adb" 14 | } 15 | -------------------------------------------------------------------------------- /.sqlx/query-97fc6f5b147aa68eeb73d4ee5768d1ffcc3bacf38b714c231efdfafa5607a126.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\nDELETE FROM \n user_map_bookmarks \nWHERE \n user_id = $1 \n AND map_id = $2", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Left": [ 8 | "Int8", 9 | "Int4" 10 | ] 11 | }, 12 | "nullable": [] 13 | }, 14 | "hash": "97fc6f5b147aa68eeb73d4ee5768d1ffcc3bacf38b714c231efdfafa5607a126" 15 | } 16 | -------------------------------------------------------------------------------- /.sqlx/query-a0fba34dd8755e7ab1118367a8c1b6a2500d6b5ad3ea80a61abf1ec0f652aece.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\nWITH stats AS (\n SELECT \n global_rank, \n pp, \n last_update \n FROM \n osu_user_mode_stats \n WHERE \n gamemode = $1 \n AND global_rank > 0 \n AND NOW() - last_update < interval '2 days'\n) \nSELECT \n * \nFROM \n (\n (\n SELECT \n global_rank, \n pp, \n last_update, \n 0 :: INT2 AS pos \n FROM \n stats \n WHERE \n global_rank <= $2 \n ORDER BY \n pp ASC \n LIMIT \n 5\n ) \n UNION ALL \n (\n SELECT \n global_rank, \n pp, \n last_update, \n 1 :: INT2 AS pos \n FROM \n stats \n WHERE \n global_rank >= $2 \n ORDER BY \n pp DESC \n LIMIT \n 5\n )\n ) AS neighbors", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "global_rank", 9 | "type_info": "Int4" 10 | }, 11 | { 12 | "ordinal": 1, 13 | "name": "pp", 14 | "type_info": "Float4" 15 | }, 16 | { 17 | "ordinal": 2, 18 | "name": "last_update", 19 | "type_info": "Timestamptz" 20 | }, 21 | { 22 | "ordinal": 3, 23 | "name": "pos", 24 | "type_info": "Int2" 25 | } 26 | ], 27 | "parameters": { 28 | "Left": [ 29 | "Int2", 30 | "Int4" 31 | ] 32 | }, 33 | "nullable": [ 34 | null, 35 | null, 36 | null, 37 | null 38 | ] 39 | }, 40 | "hash": "a0fba34dd8755e7ab1118367a8c1b6a2500d6b5ad3ea80a61abf1ec0f652aece" 41 | } 42 | -------------------------------------------------------------------------------- /.sqlx/query-aae569635dca674787a3c8952802667a0a4f3ad0604338caa708d146cea2bbb9.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\nSELECT \n osu.user_id, \n username, \n skin_url \nFROM \n (\n SELECT DISTINCT ON (osu_id) \n skin_url, \n osu_id \n FROM \n user_configs \n WHERE \n skin_url IS NOT NULL \n AND osu_id IS NOT NULL\n ) AS configs \n JOIN osu_user_names AS osu ON configs.osu_id = osu.user_id \n JOIN (\n SELECT \n user_id, \n MIN(global_rank) AS global_rank \n FROM \n osu_user_mode_stats \n WHERE \n global_rank > 0 \n GROUP BY \n user_id\n ) AS stats ON osu.user_id = stats.user_id \nORDER BY \n global_rank", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "user_id", 9 | "type_info": "Int4" 10 | }, 11 | { 12 | "ordinal": 1, 13 | "name": "username", 14 | "type_info": "Varchar" 15 | }, 16 | { 17 | "ordinal": 2, 18 | "name": "skin_url", 19 | "type_info": "Varchar" 20 | } 21 | ], 22 | "parameters": { 23 | "Left": [] 24 | }, 25 | "nullable": [ 26 | false, 27 | false, 28 | true 29 | ] 30 | }, 31 | "hash": "aae569635dca674787a3c8952802667a0a4f3ad0604338caa708d146cea2bbb9" 32 | } 33 | -------------------------------------------------------------------------------- /.sqlx/query-adfe9de02db6d8af635558f7c243b4c7c13093b460943160577157b01cb2c5d4.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\nSELECT \n gamemode \nFROM \n user_configs \nWHERE \n discord_id = $1", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "gamemode", 9 | "type_info": "Int2" 10 | } 11 | ], 12 | "parameters": { 13 | "Left": [ 14 | "Int8" 15 | ] 16 | }, 17 | "nullable": [ 18 | true 19 | ] 20 | }, 21 | "hash": "adfe9de02db6d8af635558f7c243b4c7c13093b460943160577157b01cb2c5d4" 22 | } 23 | -------------------------------------------------------------------------------- /.sqlx/query-b376479448f2ff14b7c6e41babdc571aee46db423c84b3315c0e0c50c9ddb51f.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\nSELECT\n user_id,\n gamemode,\n min_index,\n max_index,\n min_pp,\n max_pp,\n min_combo_percent,\n max_combo_percent\nFROM\n tracked_osu_users\nWHERE\n channel_id = $1", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "user_id", 9 | "type_info": "Int4" 10 | }, 11 | { 12 | "ordinal": 1, 13 | "name": "gamemode", 14 | "type_info": "Int2" 15 | }, 16 | { 17 | "ordinal": 2, 18 | "name": "min_index", 19 | "type_info": "Int2" 20 | }, 21 | { 22 | "ordinal": 3, 23 | "name": "max_index", 24 | "type_info": "Int2" 25 | }, 26 | { 27 | "ordinal": 4, 28 | "name": "min_pp", 29 | "type_info": "Float4" 30 | }, 31 | { 32 | "ordinal": 5, 33 | "name": "max_pp", 34 | "type_info": "Float4" 35 | }, 36 | { 37 | "ordinal": 6, 38 | "name": "min_combo_percent", 39 | "type_info": "Float4" 40 | }, 41 | { 42 | "ordinal": 7, 43 | "name": "max_combo_percent", 44 | "type_info": "Float4" 45 | } 46 | ], 47 | "parameters": { 48 | "Left": [ 49 | "Int8" 50 | ] 51 | }, 52 | "nullable": [ 53 | false, 54 | false, 55 | true, 56 | true, 57 | true, 58 | true, 59 | true, 60 | true 61 | ] 62 | }, 63 | "hash": "b376479448f2ff14b7c6e41babdc571aee46db423c84b3315c0e0c50c9ddb51f" 64 | } 65 | -------------------------------------------------------------------------------- /.sqlx/query-c3d5dac053a03b0fc47aa553a73274f1c704a981645d03150a981e70b7d9910a.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\nDELETE FROM \n tracked_osu_users\nWHERE \n channel_id = $1\n AND ($2::INT2 IS NULL OR gamemode = $2)", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Left": [ 8 | "Int8", 9 | "Int2" 10 | ] 11 | }, 12 | "nullable": [] 13 | }, 14 | "hash": "c3d5dac053a03b0fc47aa553a73274f1c704a981645d03150a981e70b7d9910a" 15 | } 16 | -------------------------------------------------------------------------------- /.sqlx/query-c6be23c6930c006c0543b9a3468e0b52154a2c1bcc54995b1aca7e191feff106.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\nINSERT INTO osu_map_file_content (map_id, content) \nVALUES \n ($1, $2) ON CONFLICT (map_id) DO \nUPDATE \nSET \n content = $2", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Left": [ 8 | "Int4", 9 | "Bytea" 10 | ] 11 | }, 12 | "nullable": [] 13 | }, 14 | "hash": "c6be23c6930c006c0543b9a3468e0b52154a2c1bcc54995b1aca7e191feff106" 15 | } 16 | -------------------------------------------------------------------------------- /.sqlx/query-c869e094800f4fcdceb1f3105e472cf1f2af9c230d3acb0ba4c073021c3d7d13.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\nSELECT \n skin_url \nFROM \n user_configs \nWHERE \n osu_id = $1 \n AND skin_url IS NOT NULL", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "skin_url", 9 | "type_info": "Varchar" 10 | } 11 | ], 12 | "parameters": { 13 | "Left": [ 14 | "Int4" 15 | ] 16 | }, 17 | "nullable": [ 18 | true 19 | ] 20 | }, 21 | "hash": "c869e094800f4fcdceb1f3105e472cf1f2af9c230d3acb0ba4c073021c3d7d13" 22 | } 23 | -------------------------------------------------------------------------------- /.sqlx/query-dffdc3cb9035cbf7d8d2dfbd1dc7cf503d4af5b60fc0b3f3fa057202cbd56483.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\nDELETE FROM \n osu_user_names \nWHERE \n user_id = $1", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Left": [ 8 | "Int4" 9 | ] 10 | }, 11 | "nullable": [] 12 | }, 13 | "hash": "dffdc3cb9035cbf7d8d2dfbd1dc7cf503d4af5b60fc0b3f3fa057202cbd56483" 14 | } 15 | -------------------------------------------------------------------------------- /.sqlx/query-e8420a97191f72ebf8978f38d3d71ecbd0901081ac009738b58a91418bc708bb.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\nUPDATE\n osu_mapsets\nSET\n user_id = bulk.user_id,\n artist = bulk.artist,\n title = bulk.title,\n creator = bulk.creator,\n source = bulk.source,\n video = bulk.video,\n rank_status = bulk.rank_status,\n thumbnail = bulk.thumbnail,\n cover = bulk.cover,\n last_update = NOW()\nFROM\n UNNEST(\n $1::INT4[], $2::VARCHAR[], $3::VARCHAR[], $4::VARCHAR[],\n $5::VARCHAR[], $6::BOOL[], $7::INT2[], $8::VARCHAR[],\n $9::VARCHAR[], $10::INT4[]\n ) AS bulk(\n user_id, artist, title, creator, source, video,\n rank_status, thumbnail, cover, mapset_id\n )\nWHERE\n osu_mapsets.mapset_id = bulk.mapset_id", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Left": [ 8 | "Int4Array", 9 | "VarcharArray", 10 | "VarcharArray", 11 | "VarcharArray", 12 | "VarcharArray", 13 | "BoolArray", 14 | "Int2Array", 15 | "VarcharArray", 16 | "VarcharArray", 17 | "Int4Array" 18 | ] 19 | }, 20 | "nullable": [] 21 | }, 22 | "hash": "e8420a97191f72ebf8978f38d3d71ecbd0901081ac009738b58a91418bc708bb" 23 | } 24 | -------------------------------------------------------------------------------- /.sqlx/query-eb2d8df67e3fb80c7798600f895597a274302c0574e70723472a2959f64f5731.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\nINSERT INTO osu_user_names (user_id, username) \nVALUES \n ($1, $2) ON CONFLICT (user_id) DO \nUPDATE \nSET \n username = $2", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Left": [ 8 | "Int4", 9 | "Varchar" 10 | ] 11 | }, 12 | "nullable": [] 13 | }, 14 | "hash": "eb2d8df67e3fb80c7798600f895597a274302c0574e70723472a2959f64f5731" 15 | } 16 | -------------------------------------------------------------------------------- /.sqlx/query-f6e31bee48c671466ccf9972401fe4b26ed75580e7c4cb870952d2853ecb69fc.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\nUPDATE \n user_configs \nSET \n skin_url = $2 \nWHERE \n discord_id = $1", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Left": [ 8 | "Int8", 9 | "Varchar" 10 | ] 11 | }, 12 | "nullable": [] 13 | }, 14 | "hash": "f6e31bee48c671466ccf9972401fe4b26ed75580e7c4cb870952d2853ecb69fc" 15 | } 16 | -------------------------------------------------------------------------------- /.sqlx/query-fabcfd346174afa82e766f233674abf031d026bb55c632b459f24517eaa12dc1.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\nSELECT \n discord_id, \n highscore \nFROM \n higherlower_scores \nWHERE \n game_version = $1", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "discord_id", 9 | "type_info": "Int8" 10 | }, 11 | { 12 | "ordinal": 1, 13 | "name": "highscore", 14 | "type_info": "Int4" 15 | } 16 | ], 17 | "parameters": { 18 | "Left": [ 19 | "Int2" 20 | ] 21 | }, 22 | "nullable": [ 23 | false, 24 | false 25 | ] 26 | }, 27 | "hash": "fabcfd346174afa82e766f233674abf031d026bb55c632b459f24517eaa12dc1" 28 | } 29 | -------------------------------------------------------------------------------- /.sqlx/query-fcfc547353fe778233d3a6407bc36fb78992719918d6d3198f1bcf070f72fd00.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\nSELECT \n channel_id, \n user_id \nFROM \n tracked_twitch_streams", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "channel_id", 9 | "type_info": "Int8" 10 | }, 11 | { 12 | "ordinal": 1, 13 | "name": "user_id", 14 | "type_info": "Int8" 15 | } 16 | ], 17 | "parameters": { 18 | "Left": [] 19 | }, 20 | "nullable": [ 21 | false, 22 | false 23 | ] 24 | }, 25 | "hash": "fcfc547353fe778233d3a6407bc36fb78992719918d6d3198f1bcf070f72fd00" 26 | } 27 | -------------------------------------------------------------------------------- /.sqlx/query-feac8cf9d9ba3bc8cb4875c59ec30587a1fec73bc05a929ffa67d32b495f414a.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\nINSERT INTO osu_maps (\n map_id, mapset_id, user_id, checksum, \n map_version, seconds_total, seconds_drain, \n count_circles, count_sliders, count_spinners, \n hp, cs, od, ar, bpm, gamemode\n) \nVALUES \n (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, \n $11, $12, $13, $14, $15, $16\n ) ON CONFLICT (map_id) DO NOTHING", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Left": [ 8 | "Int4", 9 | "Int4", 10 | "Int4", 11 | "Varchar", 12 | "Varchar", 13 | "Int4", 14 | "Int4", 15 | "Int4", 16 | "Int4", 17 | "Int4", 18 | "Float4", 19 | "Float4", 20 | "Float4", 21 | "Float4", 22 | "Float4", 23 | "Int2" 24 | ] 25 | }, 26 | "nullable": [] 27 | }, 28 | "hash": "feac8cf9d9ba3bc8cb4875c59ec30587a1fec73bc05a929ffa67d32b495f414a" 29 | } 30 | -------------------------------------------------------------------------------- /.sqlx/query-ff290f1421104f7ffb98653a0bcd84cdf0b6c1aa3791775c4fbb1d97c65fe60f.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\nDELETE FROM \n tracked_twitch_streams \nWHERE \n channel_id = $1 \n AND user_id = $2", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Left": [ 8 | "Int8", 9 | "Int8" 10 | ] 11 | }, 12 | "nullable": [] 13 | }, 14 | "hash": "ff290f1421104f7ffb98653a0bcd84cdf0b6c1aa3791775c4fbb1d97c65fe60f" 15 | } 16 | -------------------------------------------------------------------------------- /.sqlx/query-ffd820fbdf987b68780011586413f1b8c3eb2f0b93083d839c8de935eb1ab28b.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\nSELECT \n replay \nFROM \n osu_replays \nWHERE \n score_id = $1", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "replay", 9 | "type_info": "Bytea" 10 | } 11 | ], 12 | "parameters": { 13 | "Left": [ 14 | "Int8" 15 | ] 16 | }, 17 | "nullable": [ 18 | false 19 | ] 20 | }, 21 | "hash": "ffd820fbdf987b68780011586413f1b8c3eb2f0b93083d839c8de935eb1ab28b" 22 | } 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2022 Max Ohn 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 10 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 11 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 12 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 13 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 14 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 15 | PERFORMANCE OF THIS SOFTWARE. -------------------------------------------------------------------------------- /bathbot-cache/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bathbot-cache" 3 | version.workspace = true 4 | authors.workspace = true 5 | edition.workspace = true 6 | 7 | [dependencies] 8 | bathbot-model = { path = "../bathbot-model" } 9 | bb8-redis = { version = "0.20.0" } 10 | eyre = { workspace = true } 11 | itoa = { version = "1.0" } 12 | once_cell = { version = "1.0" } 13 | rkyv = { workspace = true } 14 | thiserror = { workspace = true } 15 | tracing = { version = "0.1" } 16 | twilight-model = { workspace = true } 17 | twilight-gateway = { workspace = true } -------------------------------------------------------------------------------- /bathbot-cache/src/key/to_key.rs: -------------------------------------------------------------------------------- 1 | pub trait ToCacheKey { 2 | fn to_key(&self) -> &[u8]; 3 | } 4 | 5 | impl ToCacheKey for [u8] { 6 | #[inline] 7 | fn to_key(&self) -> &[u8] { 8 | self 9 | } 10 | } 11 | 12 | impl ToCacheKey for Vec { 13 | #[inline] 14 | fn to_key(&self) -> &[u8] { 15 | <[u8] as ToCacheKey>::to_key(self.as_slice()) 16 | } 17 | } 18 | 19 | impl ToCacheKey for str { 20 | #[inline] 21 | fn to_key(&self) -> &[u8] { 22 | <[u8] as ToCacheKey>::to_key(self.as_bytes()) 23 | } 24 | } 25 | 26 | impl ToCacheKey for String { 27 | #[inline] 28 | fn to_key(&self) -> &[u8] { 29 | ::to_key(self.as_str()) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /bathbot-cache/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub use self::{ 2 | cache::{Cache, FetchError}, 3 | key::ToCacheKey, 4 | }; 5 | 6 | pub mod model; 7 | pub mod util; 8 | 9 | mod cache; 10 | mod key; 11 | -------------------------------------------------------------------------------- /bathbot-cache/src/model/connection.rs: -------------------------------------------------------------------------------- 1 | use bb8_redis::{RedisConnectionManager, bb8::PooledConnection}; 2 | 3 | /// Provided by `Cache::fetch` to be later used in `Cache::cache_data`. 4 | pub struct CacheConnection<'c>(pub(crate) PooledConnection<'c, RedisConnectionManager>); 5 | -------------------------------------------------------------------------------- /bathbot-cache/src/model/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) use self::stats::CacheStatsInternal; 2 | pub use self::{ 3 | archive::{CachedArchive, ValidatorStrategy}, 4 | connection::CacheConnection, 5 | stats::{CacheChange, CacheStats}, 6 | }; 7 | 8 | mod archive; 9 | mod connection; 10 | mod stats; 11 | -------------------------------------------------------------------------------- /bathbot-cache/src/util/aligned_vec.rs: -------------------------------------------------------------------------------- 1 | use bb8_redis::redis::{ 2 | ErrorKind, FromRedisValue, RedisError, RedisResult, RedisWrite, ToRedisArgs, Value, 3 | }; 4 | use rkyv::util::AlignedVec; 5 | 6 | pub(crate) struct AlignedVecRedisArgs(pub(crate) AlignedVec<8>); 7 | 8 | impl ToRedisArgs for AlignedVecRedisArgs { 9 | #[inline] 10 | fn write_redis_args(&self, out: &mut W) 11 | where 12 | W: ?Sized + RedisWrite, 13 | { 14 | self.0.as_slice().write_redis_args(out) 15 | } 16 | } 17 | 18 | impl FromRedisValue for AlignedVecRedisArgs { 19 | #[inline] 20 | fn from_redis_value(v: &Value) -> RedisResult { 21 | match v { 22 | Value::BulkString(data) => { 23 | let mut bytes = AlignedVec::new(); 24 | bytes.reserve_exact(data.len()); 25 | bytes.extend_from_slice(data); 26 | 27 | Ok(Self(bytes)) 28 | } 29 | _ => Err(RedisError::from(( 30 | ErrorKind::TypeError, 31 | "Response was of incompatible type", 32 | format!("Response type not AlignedVec-compatible (response was {v:?})"), 33 | ))), 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /bathbot-cache/src/util/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) use self::{aligned_vec::AlignedVecRedisArgs, zipped::Zipped}; 2 | 3 | mod aligned_vec; 4 | pub mod serialize; 5 | mod zipped; 6 | -------------------------------------------------------------------------------- /bathbot-cache/src/util/serialize.rs: -------------------------------------------------------------------------------- 1 | use rkyv::{ 2 | Archived, Serialize, 3 | rancor::{BoxedError, Strategy}, 4 | ser::{Serializer, WriterExt, allocator::ArenaHandle}, 5 | util::AlignedVec, 6 | with::With, 7 | }; 8 | 9 | pub type SerializerStrategy<'a> = 10 | Strategy, ArenaHandle<'a>, ()>, BoxedError>; 11 | 12 | pub fn serialize_using_arena(data: &T) -> Result, BoxedError> 13 | where 14 | T: for<'a> Serialize>, 15 | { 16 | rkyv::util::with_arena(|arena| { 17 | let mut serializer = Serializer::new(AlignedVec::new(), arena.acquire(), ()); 18 | rkyv::api::serialize_using(data, Strategy::<_, BoxedError>::wrap(&mut serializer))?; 19 | 20 | Ok(serializer.into_writer()) 21 | }) 22 | } 23 | 24 | pub fn serialize_using_arena_and_with(data: &T) -> Result, BoxedError> 25 | where 26 | T: ?Sized, 27 | With: for<'a> Serialize>, 28 | { 29 | rkyv::util::with_arena(|arena| { 30 | let wrap = With::::cast(data); 31 | let mut serializer = Serializer::new(AlignedVec::new(), arena.acquire(), ()); 32 | let resolver = wrap.serialize(Strategy::wrap(&mut serializer))?; 33 | serializer.align_for::>>()?; 34 | 35 | // SAFETY: A proper resolver is being used and the serializer has been 36 | // aligned 37 | unsafe { serializer.resolve_aligned(wrap, resolver)? }; 38 | 39 | Ok(serializer.into_writer()) 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /bathbot-cache/src/util/zipped.rs: -------------------------------------------------------------------------------- 1 | use std::mem; 2 | 3 | #[derive(Default)] 4 | /// Provides a way to collect an iterator of tuples into specified collections. 5 | /// Similar to `Iterator::unzip` but more flexible. 6 | pub(crate) struct Zipped { 7 | left: C1, 8 | right: C2, 9 | } 10 | 11 | impl Zipped { 12 | pub fn into_parts(self) -> (C1, C2) { 13 | (self.left, self.right) 14 | } 15 | } 16 | 17 | impl FromIterator<(T1, T2)> for Zipped 18 | where 19 | C1: Default + Extend, 20 | C2: Default + Extend, 21 | { 22 | #[inline] 23 | fn from_iter>(iter: T) -> Self { 24 | let mut tuple = (C1::default(), C2::default()); 25 | tuple.extend(iter); 26 | let (left, right) = tuple; 27 | 28 | Self { left, right } 29 | } 30 | } 31 | 32 | impl Extend<(T1, T2)> for Zipped 33 | where 34 | C1: Default + Extend, 35 | C2: Default + Extend, 36 | { 37 | fn extend>(&mut self, iter: T) { 38 | let Self { left, right } = self; 39 | 40 | let mut tuple = (mem::take(left), mem::take(right)); 41 | tuple.extend(iter); 42 | let (left, right) = tuple; 43 | 44 | self.left = left; 45 | self.right = right; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /bathbot-cards/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bathbot-cards" 3 | version.workspace = true 4 | authors.workspace = true 5 | edition.workspace = true 6 | 7 | [dependencies] 8 | itoa = { version = "1.0.9", default-features = false } 9 | rosu-pp = { workspace = true } 10 | rosu-v2 = { workspace = true } 11 | skia-safe = { workspace = true } 12 | thiserror = { workspace = true } 13 | -------------------------------------------------------------------------------- /bathbot-cards/assets/backgrounds/advanced.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxOhn/Bathbot/4eff2f918d716002483b8661fa5a64521b73e155/bathbot-cards/assets/backgrounds/advanced.png -------------------------------------------------------------------------------- /bathbot-cards/assets/backgrounds/apprentice.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxOhn/Bathbot/4eff2f918d716002483b8661fa5a64521b73e155/bathbot-cards/assets/backgrounds/apprentice.png -------------------------------------------------------------------------------- /bathbot-cards/assets/backgrounds/expert.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxOhn/Bathbot/4eff2f918d716002483b8661fa5a64521b73e155/bathbot-cards/assets/backgrounds/expert.png -------------------------------------------------------------------------------- /bathbot-cards/assets/backgrounds/god.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxOhn/Bathbot/4eff2f918d716002483b8661fa5a64521b73e155/bathbot-cards/assets/backgrounds/god.png -------------------------------------------------------------------------------- /bathbot-cards/assets/backgrounds/legendary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxOhn/Bathbot/4eff2f918d716002483b8661fa5a64521b73e155/bathbot-cards/assets/backgrounds/legendary.png -------------------------------------------------------------------------------- /bathbot-cards/assets/backgrounds/master.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxOhn/Bathbot/4eff2f918d716002483b8661fa5a64521b73e155/bathbot-cards/assets/backgrounds/master.png -------------------------------------------------------------------------------- /bathbot-cards/assets/backgrounds/newbie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxOhn/Bathbot/4eff2f918d716002483b8661fa5a64521b73e155/bathbot-cards/assets/backgrounds/newbie.png -------------------------------------------------------------------------------- /bathbot-cards/assets/backgrounds/novice.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxOhn/Bathbot/4eff2f918d716002483b8661fa5a64521b73e155/bathbot-cards/assets/backgrounds/novice.png -------------------------------------------------------------------------------- /bathbot-cards/assets/backgrounds/outstanding.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxOhn/Bathbot/4eff2f918d716002483b8661fa5a64521b73e155/bathbot-cards/assets/backgrounds/outstanding.png -------------------------------------------------------------------------------- /bathbot-cards/assets/backgrounds/professional.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxOhn/Bathbot/4eff2f918d716002483b8661fa5a64521b73e155/bathbot-cards/assets/backgrounds/professional.png -------------------------------------------------------------------------------- /bathbot-cards/assets/backgrounds/rookie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxOhn/Bathbot/4eff2f918d716002483b8661fa5a64521b73e155/bathbot-cards/assets/backgrounds/rookie.png -------------------------------------------------------------------------------- /bathbot-cards/assets/backgrounds/seasoned.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxOhn/Bathbot/4eff2f918d716002483b8661fa5a64521b73e155/bathbot-cards/assets/backgrounds/seasoned.png -------------------------------------------------------------------------------- /bathbot-cards/assets/branding/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxOhn/Bathbot/4eff2f918d716002483b8661fa5a64521b73e155/bathbot-cards/assets/branding/icon.png -------------------------------------------------------------------------------- /bathbot-cards/assets/fonts/Roboto-Black.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxOhn/Bathbot/4eff2f918d716002483b8661fa5a64521b73e155/bathbot-cards/assets/fonts/Roboto-Black.ttf -------------------------------------------------------------------------------- /bathbot-cards/assets/fonts/Roboto-BlackItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxOhn/Bathbot/4eff2f918d716002483b8661fa5a64521b73e155/bathbot-cards/assets/fonts/Roboto-BlackItalic.ttf -------------------------------------------------------------------------------- /bathbot-cards/assets/fonts/Roboto-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxOhn/Bathbot/4eff2f918d716002483b8661fa5a64521b73e155/bathbot-cards/assets/fonts/Roboto-Bold.ttf -------------------------------------------------------------------------------- /bathbot-cards/assets/fonts/Roboto-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxOhn/Bathbot/4eff2f918d716002483b8661fa5a64521b73e155/bathbot-cards/assets/fonts/Roboto-BoldItalic.ttf -------------------------------------------------------------------------------- /bathbot-cards/assets/fonts/Roboto-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxOhn/Bathbot/4eff2f918d716002483b8661fa5a64521b73e155/bathbot-cards/assets/fonts/Roboto-Italic.ttf -------------------------------------------------------------------------------- /bathbot-cards/assets/fonts/Roboto-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxOhn/Bathbot/4eff2f918d716002483b8661fa5a64521b73e155/bathbot-cards/assets/fonts/Roboto-Light.ttf -------------------------------------------------------------------------------- /bathbot-cards/assets/fonts/Roboto-LightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxOhn/Bathbot/4eff2f918d716002483b8661fa5a64521b73e155/bathbot-cards/assets/fonts/Roboto-LightItalic.ttf -------------------------------------------------------------------------------- /bathbot-cards/assets/fonts/Roboto-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxOhn/Bathbot/4eff2f918d716002483b8661fa5a64521b73e155/bathbot-cards/assets/fonts/Roboto-Medium.ttf -------------------------------------------------------------------------------- /bathbot-cards/assets/fonts/Roboto-MediumItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxOhn/Bathbot/4eff2f918d716002483b8661fa5a64521b73e155/bathbot-cards/assets/fonts/Roboto-MediumItalic.ttf -------------------------------------------------------------------------------- /bathbot-cards/assets/fonts/Roboto-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxOhn/Bathbot/4eff2f918d716002483b8661fa5a64521b73e155/bathbot-cards/assets/fonts/Roboto-Regular.ttf -------------------------------------------------------------------------------- /bathbot-cards/assets/fonts/Roboto-Thin.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxOhn/Bathbot/4eff2f918d716002483b8661fa5a64521b73e155/bathbot-cards/assets/fonts/Roboto-Thin.ttf -------------------------------------------------------------------------------- /bathbot-cards/assets/fonts/Roboto-ThinItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxOhn/Bathbot/4eff2f918d716002483b8661fa5a64521b73e155/bathbot-cards/assets/fonts/Roboto-ThinItalic.ttf -------------------------------------------------------------------------------- /bathbot-cards/assets/gamemodes/Standard.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /bathbot-cards/assets/gamemodes/Taiko.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /bathbot-cards/src/builder/card/mod.rs: -------------------------------------------------------------------------------- 1 | mod footer; 2 | mod header; 3 | mod info; 4 | 5 | use std::{fs, path::PathBuf}; 6 | 7 | use itoa::Buffer; 8 | use skia_safe::{Canvas, Data, Image}; 9 | 10 | use crate::{error::BackgroundError, skills::CardTitle}; 11 | 12 | pub(crate) const H: i32 = 1260; 13 | pub(crate) const W: i32 = 980; 14 | 15 | pub(crate) struct CardBuilder<'c> { 16 | canvas: &'c Canvas, 17 | int_buf: Buffer, 18 | } 19 | 20 | impl<'a> CardBuilder<'a> { 21 | pub(crate) fn new(canvas: &'a Canvas) -> Self { 22 | Self { 23 | canvas, 24 | int_buf: Buffer::new(), 25 | } 26 | } 27 | 28 | pub(crate) fn draw_background( 29 | &mut self, 30 | title: &CardTitle, 31 | mut assets: PathBuf, 32 | ) -> Result<&mut Self, BackgroundError> { 33 | assets.push("backgrounds"); 34 | assets.push(title.prefix.filename()); 35 | let bytes = fs::read(assets).map_err(BackgroundError::File)?; 36 | 37 | // SAFETY: `bytes` and `Data` share the same lifetime 38 | let data = unsafe { Data::new_bytes(&bytes) }; 39 | 40 | let img = Image::from_encoded_with_alpha_type(data, None).ok_or(BackgroundError::Image)?; 41 | self.canvas.draw_image(&img, (0, 0), None); 42 | 43 | Ok(self) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /bathbot-cards/src/builder/font.rs: -------------------------------------------------------------------------------- 1 | use skia_safe::{Font, FontMgr, font_style::Slant}; 2 | 3 | use crate::{error::FontError, font::FontData}; 4 | 5 | pub(crate) struct FontBuilder; 6 | 7 | impl FontBuilder { 8 | pub(crate) fn build( 9 | weight: i32, 10 | slant: Slant, 11 | data: &FontData, 12 | size: f32, 13 | ) -> Result { 14 | let font_data = data.get(weight.into(), slant); 15 | 16 | let typeface = FontMgr::new() 17 | .new_from_data(font_data, None) 18 | .ok_or(FontError::Typeface)?; 19 | 20 | Ok(Font::new(typeface, Some(size))) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /bathbot-cards/src/builder/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod card; 2 | pub(crate) mod font; 3 | pub(crate) mod paint; 4 | -------------------------------------------------------------------------------- /bathbot-cards/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod builder; 2 | mod card; 3 | mod error; 4 | mod font; 5 | mod skills; 6 | mod svg; 7 | 8 | pub use self::card::{BathbotCard, RequiredAttributes}; 9 | -------------------------------------------------------------------------------- /bathbot-cards/src/svg.rs: -------------------------------------------------------------------------------- 1 | use std::str::from_utf8 as str_from_utf8; 2 | 3 | use skia_safe::Path; 4 | 5 | use crate::error::SvgError; 6 | 7 | pub(crate) struct Svg { 8 | pub(crate) view_box_w: i32, 9 | pub(crate) view_box_h: i32, 10 | pub(crate) path: Path, 11 | } 12 | 13 | impl Svg { 14 | pub(crate) fn parse(bytes: &[u8]) -> Result { 15 | const VIEW_BOX_NEEDLE: &str = " viewBox=\""; 16 | const PATH_NEEDLE: &str = " d=\""; 17 | 18 | let svg = str_from_utf8(bytes)?; 19 | 20 | let start = 21 | svg.find(VIEW_BOX_NEEDLE).ok_or(SvgError::MissingViewBox)? + VIEW_BOX_NEEDLE.len(); 22 | let end = svg[start..].find('"').ok_or(SvgError::MissingViewBox)?; 23 | let mut iter = svg[start..start + end].split_ascii_whitespace().skip(2); 24 | 25 | let view_box_w = iter 26 | .next() 27 | .ok_or(SvgError::MissingViewBoxW)? 28 | .parse() 29 | .map_err(|_| SvgError::ParseViewBox)?; 30 | 31 | let view_box_h = iter 32 | .next() 33 | .ok_or(SvgError::MissingViewBoxH)? 34 | .parse() 35 | .map_err(|_| SvgError::ParseViewBox)?; 36 | 37 | let start = svg.find(PATH_NEEDLE).ok_or(SvgError::MissingPath)? + PATH_NEEDLE.len(); 38 | let end = svg[start..].find('"').ok_or(SvgError::MissingPath)?; 39 | let path = Path::from_svg(&svg[start..start + end]).ok_or(SvgError::CreatePath)?; 40 | 41 | Ok(Self { 42 | view_box_h, 43 | view_box_w, 44 | path, 45 | }) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /bathbot-client/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bathbot-client" 3 | version.workspace = true 4 | authors.workspace = true 5 | edition.workspace = true 6 | 7 | [dependencies] 8 | bathbot-model = { path = "../bathbot-model" } 9 | bathbot-util = { path = "../bathbot-util" } 10 | bytes = { version = "1.0" } 11 | eyre = { workspace = true} 12 | http = { workspace = true } 13 | http-body-util = { workspace = true } 14 | hyper = { workspace = true } 15 | hyper-rustls = { workspace = true } 16 | hyper-util = { workspace = true } 17 | itoa = { version = "1.0.9", default-features = false } 18 | leaky-bucket-lite = { version = "0.5", features = ["parking_lot"] } 19 | memchr = { workspace = true } 20 | metrics = { workspace = true } 21 | rand = { version = "0.8" } 22 | rosu-v2 = { workspace = true } 23 | rkyv = { workspace = true } 24 | rustls = { workspace = true } 25 | ryu = { version = "1.0.15", default-features = false } 26 | serde = { version = "1.0", features = ["derive"] } 27 | serde_json = { version = "1.0" } 28 | thiserror = { workspace = true } 29 | time = { version = "0.3", features = ["parsing"] } 30 | tokio = { version = "1.20", default-features = false, features = ["fs", "io-util", "macros", "parking_lot", "rt-multi-thread", "signal", "sync", "time"] } 31 | tracing = { version = "0.1" } 32 | twilight-interactions = { workspace = true } 33 | twilight-model = { workspace = true } 34 | 35 | [features] 36 | default = [] 37 | twitch = [] 38 | -------------------------------------------------------------------------------- /bathbot-client/src/discord.rs: -------------------------------------------------------------------------------- 1 | use bytes::Bytes; 2 | use eyre::{Report, Result}; 3 | use twilight_model::channel::Attachment; 4 | 5 | use crate::{Client, Site}; 6 | 7 | impl Client { 8 | pub async fn get_discord_attachment(&self, attachment: &Attachment) -> Result { 9 | self.make_get_request(&attachment.url, Site::DiscordAttachment) 10 | .await 11 | .map_err(Report::new) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /bathbot-client/src/error.rs: -------------------------------------------------------------------------------- 1 | use eyre::Report; 2 | use thiserror::Error; 3 | 4 | #[derive(Debug, Error)] 5 | pub enum ClientError { 6 | #[error("status code 400 - bad request")] 7 | BadRequest, 8 | #[error("status code 404 - not found")] 9 | NotFound, 10 | #[error("status code 429 - ratelimited")] 11 | Ratelimited, 12 | #[error(transparent)] 13 | Report(#[from] Report), 14 | } 15 | -------------------------------------------------------------------------------- /bathbot-client/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate eyre; 3 | 4 | #[macro_use] 5 | extern crate tracing; 6 | 7 | mod client; 8 | mod discord; 9 | mod error; 10 | mod github; 11 | mod metrics; 12 | mod miss_analyzer; 13 | mod multipart; 14 | mod osekai; 15 | mod osu; 16 | mod osustats; 17 | mod osutrack; 18 | mod relax; 19 | mod respektive; 20 | mod site; 21 | mod snipe; 22 | mod twitch; 23 | 24 | use self::site::{Ratelimiters, Site}; 25 | pub use self::{client::Client, error::ClientError}; 26 | 27 | static MY_USER_AGENT: &str = env!("CARGO_PKG_NAME"); 28 | -------------------------------------------------------------------------------- /bathbot-client/src/metrics.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use hyper::StatusCode; 4 | use metrics::{counter, describe_counter, describe_histogram, histogram}; 5 | 6 | use crate::site::Site; 7 | 8 | const CLIENT_RESPONSE_TIME: &str = "client_response_time"; 9 | const CLIENT_INTERNAL_ERRORS: &str = "client_internal_errors"; 10 | 11 | pub(crate) struct ClientMetrics; 12 | 13 | impl ClientMetrics { 14 | pub(crate) fn init() { 15 | describe_histogram!( 16 | CLIENT_RESPONSE_TIME, 17 | "Response time for client requests in seconds" 18 | ); 19 | 20 | describe_counter!( 21 | CLIENT_INTERNAL_ERRORS, 22 | "Number of times an internal error occurred" 23 | ); 24 | } 25 | 26 | pub(crate) fn observe(site: Site, status: StatusCode, latency: Duration) { 27 | histogram!( 28 | CLIENT_RESPONSE_TIME, 29 | "site" => site.as_str(), 30 | "status" => status.as_str().to_owned() 31 | ) 32 | .record(latency); 33 | } 34 | 35 | pub(crate) fn internal_error(site: Site) { 36 | counter!(CLIENT_INTERNAL_ERRORS, "site" => site.as_str()).increment(1); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /bathbot-macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bathbot-macros" 3 | version.workspace = true 4 | authors.workspace = true 5 | edition.workspace = true 6 | 7 | [lib] 8 | proc-macro = true 9 | 10 | [dependencies] 11 | proc-macro2 = "1.0" 12 | quote = "1.0" 13 | syn = { version = "2.0.87", features = ["extra-traits"] } 14 | -------------------------------------------------------------------------------- /bathbot-macros/src/bucket.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::{ToTokens, quote}; 3 | use syn::{ 4 | Ident, Result, 5 | parse::{Parse, ParseStream}, 6 | }; 7 | 8 | pub struct Bucket { 9 | bucket: Ident, 10 | } 11 | 12 | impl Parse for Bucket { 13 | fn parse(input: ParseStream<'_>) -> Result { 14 | input.parse().map(|bucket| Self { bucket }) 15 | } 16 | } 17 | 18 | impl ToTokens for Bucket { 19 | fn to_tokens(&self, tokens: &mut TokenStream) { 20 | let ident = &self.bucket; 21 | tokens.extend(quote!(bathbot_util::BucketName::#ident)); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /bathbot-macros/src/flags.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::{ToTokens, quote}; 3 | use syn::{ 4 | Ident, Result, Token, 5 | parse::{Parse, ParseStream}, 6 | }; 7 | 8 | use crate::util::PunctuatedExt; 9 | 10 | #[derive(Default)] 11 | pub struct Flags { 12 | list: Box<[Ident]>, 13 | } 14 | 15 | impl Parse for Flags { 16 | fn parse(input: ParseStream) -> Result { 17 | Vec::parse_separated_nonempty::(input) 18 | .map(Vec::into_boxed_slice) 19 | .map(|list| Self { list }) 20 | } 21 | } 22 | 23 | impl ToTokens for Flags { 24 | fn to_tokens(&self, tokens: &mut TokenStream) { 25 | let mut flags = self.list.iter(); 26 | 27 | let Some(flag) = flags.next() else { 28 | tokens.extend(quote!(crate::core::commands::CommandFlags::empty())); 29 | 30 | return; 31 | }; 32 | 33 | let mut sum = quote!(crate::core::commands::CommandFlags:: #flag .bits()); 34 | 35 | for bit in flags.map(|flag| quote!(+ crate::core::commands::CommandFlags:: #flag .bits())) { 36 | sum.extend(bit) 37 | } 38 | 39 | tokens.extend(quote!(crate::core::commands::CommandFlags::from_bits_retain(#sum))); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /bathbot-macros/src/slash/mod.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::{format_ident, quote}; 3 | use syn::{DeriveInput, Error, Result, Visibility}; 4 | 5 | use crate::slash::attrs::CommandAttrs; 6 | 7 | mod attrs; 8 | 9 | pub fn derive(input: DeriveInput) -> Result { 10 | match input.vis { 11 | Visibility::Public(_) => {} 12 | _ => return Err(Error::new(input.ident.span(), "type must be pub")), 13 | } 14 | 15 | let CommandAttrs { 16 | bucket, 17 | flags, 18 | name_lit, 19 | } = CommandAttrs::parse_attrs(&input.attrs)?; 20 | 21 | let name = input.ident; 22 | let name_str = name.to_string(); 23 | let static_name = format_ident!("{}", name_str.to_uppercase(), span = name.span()); 24 | let slash_cmd = format_ident!("slash_{}", name_str.to_lowercase(), span = name.span()); 25 | let exec = format_ident!("{slash_cmd}__", span = name.span()); 26 | let path = quote!(crate::core::commands::interaction::SlashCommand); 27 | 28 | let tokens = quote! { 29 | #[linkme::distributed_slice(crate::core::commands::interaction::__SLASH_COMMANDS)] 30 | pub static #static_name: #path = #path { 31 | bucket: #bucket, 32 | create: #name::create_command, 33 | exec: #exec, 34 | flags: #flags, 35 | name: #name_lit, 36 | id: std::sync::OnceLock::new(), 37 | }; 38 | 39 | fn #exec( 40 | command: crate::util::interaction::InteractionCommand, 41 | ) -> crate::core::commands::interaction::CommandResult { 42 | Box::pin(#slash_cmd(command)) 43 | } 44 | }; 45 | 46 | Ok(tokens) 47 | } 48 | -------------------------------------------------------------------------------- /bathbot-model/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bathbot-model" 3 | version.workspace = true 4 | authors.workspace = true 5 | edition.workspace = true 6 | 7 | [dependencies] 8 | bathbot-util = { path = "../bathbot-util" } 9 | bitflags = { workspace = true } 10 | form_urlencoded = { version = "1.2.0", default-features = false, features = ["std"] } 11 | eyre = { workspace = true } 12 | http = { workspace = true } 13 | leaky-bucket-lite = { version = "0.5", features = ["parking_lot"] } 14 | once_cell = { version = "1.0" } 15 | rosu-mods = { workspace = true } 16 | rosu-v2 = { workspace = true } 17 | rkyv = { workspace = true } 18 | serde = { version = "1.0", features = ["derive"] } 19 | serde_json = { version = "1.0", features = ["raw_value"] } 20 | serde_urlencoded = { version = "0.7.1", default-features = false } 21 | time = { version = "0.3", features = ["parsing"] } 22 | twilight-gateway = { workspace = true } 23 | twilight-interactions = { workspace = true } 24 | twilight-model = { workspace = true } 25 | -------------------------------------------------------------------------------- /bathbot-model/src/either.rs: -------------------------------------------------------------------------------- 1 | #[derive(Copy, Clone)] 2 | pub enum Either { 3 | Left(L), 4 | Right(R), 5 | } 6 | -------------------------------------------------------------------------------- /bathbot-model/src/embed_builder/mod.rs: -------------------------------------------------------------------------------- 1 | macro_rules! define_enum { 2 | ( 3 | #[$enum_meta:meta] 4 | pub enum $enum_name:ident { 5 | $( 6 | $( #[$variant_meta:meta] )? 7 | $variant:ident = $discriminant:literal, 8 | )* 9 | } 10 | ) => { 11 | #[$enum_meta] 12 | pub enum $enum_name { 13 | $( 14 | $( #[$variant_meta] )? 15 | $variant = $discriminant, 16 | )* 17 | } 18 | 19 | impl<'de> Deserialize<'de> for $enum_name { 20 | fn deserialize>(d: D) -> Result { 21 | match u8::deserialize(d)? { 22 | $( $discriminant => Ok(Self::$variant), )* 23 | other => Err(serde::de::Error::invalid_value( 24 | serde::de::Unexpected::Unsigned(u64::from(other)), 25 | &stringify!($enum_name), 26 | )), 27 | } 28 | } 29 | } 30 | 31 | impl Serialize for $enum_name { 32 | fn serialize(&self, s: S) -> Result { 33 | s.serialize_u8(*self as u8) 34 | } 35 | } 36 | } 37 | } 38 | 39 | mod settings; 40 | mod value; 41 | 42 | pub use self::{settings::*, value::*}; 43 | 44 | fn is_true(b: &bool) -> bool { 45 | *b 46 | } 47 | -------------------------------------------------------------------------------- /bathbot-model/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod country_code; 2 | mod deser; 3 | mod either; 4 | mod games; 5 | mod github; 6 | mod huismetbenen; 7 | mod kittenroleplay; 8 | mod osekai; 9 | mod osu; 10 | mod osu_stats; 11 | mod osutrack; 12 | mod personal_best; 13 | mod ranking_entries; 14 | mod relax; 15 | mod respektive; 16 | mod score_slim; 17 | mod twitch; 18 | mod user_stats; 19 | 20 | pub mod command_fields; 21 | pub mod embed_builder; 22 | pub mod rosu_v2; 23 | pub mod twilight; 24 | 25 | pub mod rkyv_util; 26 | 27 | pub use self::{ 28 | country_code::*, deser::ModeAsSeed, either::Either, games::*, github::*, huismetbenen::*, 29 | kittenroleplay::*, osekai::*, osu::*, osu_stats::*, osutrack::*, 30 | personal_best::PersonalBestIndex, ranking_entries::*, relax::*, respektive::*, score_slim::*, 31 | twitch::*, user_stats::*, 32 | }; 33 | -------------------------------------------------------------------------------- /bathbot-model/src/osu.rs: -------------------------------------------------------------------------------- 1 | use rkyv::{niche::niching::Null, with::NicheInto}; 2 | use serde::{Deserialize, Deserializer}; 3 | 4 | #[derive(Deserialize)] 5 | pub struct ScrapedUser { 6 | #[serde(rename = "achievements")] 7 | pub medals: Box<[ScrapedMedal]>, 8 | } 9 | 10 | #[derive(Debug, Deserialize, rkyv::Archive, rkyv::Serialize)] 11 | pub struct ScrapedMedal { 12 | pub icon_url: Box, 13 | pub id: u16, 14 | pub name: Box, 15 | pub grouping: Box, 16 | pub ordering: u8, 17 | pub description: Box, 18 | #[serde(default, deserialize_with = "deser_mode")] 19 | #[rkyv(with = NicheInto)] 20 | pub mode: Option>, 21 | #[rkyv(with = NicheInto)] 22 | pub instructions: Option>, 23 | } 24 | 25 | fn deser_mode<'de, D: Deserializer<'de>>(d: D) -> Result>, D::Error> { 26 | match Option::<&str>::deserialize(d) { 27 | Ok(Some("fruits")) => Ok(Some(Box::from("catch"))), 28 | Ok(Some(mode)) => Ok(Some(Box::from(mode))), 29 | Ok(None) => Ok(None), 30 | Err(err) => Err(err), 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /bathbot-model/src/respektive.rs: -------------------------------------------------------------------------------- 1 | use std::{num::NonZeroU32, vec::IntoIter}; 2 | 3 | use serde::{Deserialize, Deserializer}; 4 | use time::OffsetDateTime; 5 | 6 | use crate::deser::datetime_rfc3339; 7 | 8 | #[derive(Clone, Copy, Deserialize, Debug)] 9 | pub struct RespektiveUserRankHighest { 10 | pub rank: u32, 11 | #[serde(with = "datetime_rfc3339")] 12 | pub updated_at: OffsetDateTime, 13 | } 14 | #[derive(Deserialize, Debug)] 15 | pub struct RespektiveUser { 16 | #[serde(deserialize_with = "zero_as_none")] 17 | pub rank: Option, 18 | pub user_id: u32, 19 | pub rank_highest: Option, 20 | pub rank_history: Option>, 21 | } 22 | 23 | #[derive(Deserialize, Debug)] 24 | pub struct RankHistoryEntry { 25 | pub rank: Option, 26 | #[serde(with = "datetime_rfc3339")] 27 | pub date: OffsetDateTime, 28 | } 29 | 30 | pub struct RespektiveUsers { 31 | inner: IntoIter, 32 | } 33 | 34 | impl From> for RespektiveUsers { 35 | fn from(users: Vec) -> Self { 36 | Self { 37 | inner: users.into_iter(), 38 | } 39 | } 40 | } 41 | 42 | impl Iterator for RespektiveUsers { 43 | type Item = Option; 44 | 45 | #[inline] 46 | fn next(&mut self) -> Option { 47 | self.inner 48 | .next() 49 | .map(|user| (user.rank.is_some() || user.rank_highest.is_some()).then_some(user)) 50 | } 51 | } 52 | 53 | fn zero_as_none<'de, D: Deserializer<'de>>(d: D) -> Result, D::Error> { 54 | u32::deserialize(d).map(NonZeroU32::new) 55 | } 56 | -------------------------------------------------------------------------------- /bathbot-model/src/rkyv_util/deref_as_box.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Deref; 2 | 3 | use rkyv::{ 4 | ArchiveUnsized, Archived, Place, Resolver, SerializeUnsized, 5 | boxed::ArchivedBox, 6 | rancor::Fallible, 7 | with::{ArchiveWith, SerializeWith}, 8 | }; 9 | 10 | pub struct DerefAsBox; 11 | 12 | impl ArchiveWith for DerefAsBox 13 | where 14 | U: Deref, 15 | V: ArchiveUnsized + ?Sized, 16 | { 17 | type Archived = Archived>; 18 | type Resolver = Resolver>; 19 | 20 | #[inline] 21 | fn resolve_with(field: &U, resolver: Self::Resolver, out: Place) { 22 | let deref: &V = field; 23 | ArchivedBox::resolve_from_ref(deref, resolver, out); 24 | } 25 | } 26 | 27 | impl SerializeWith for DerefAsBox 28 | where 29 | U: Deref, 30 | V: ArchiveUnsized + SerializeUnsized + ?Sized, 31 | S: Fallible + ?Sized, 32 | { 33 | #[inline] 34 | fn serialize_with( 35 | value: &U, 36 | serializer: &mut S, 37 | ) -> Result::Error> { 38 | let deref: &V = value; 39 | 40 | ArchivedBox::serialize_from_ref(deref, serializer) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /bathbot-model/src/rkyv_util/deref_as_string.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Deref; 2 | 3 | use rkyv::{ 4 | Place, 5 | rancor::{Fallible, Source}, 6 | ser::Writer, 7 | string::{ArchivedString, StringResolver}, 8 | with::{ArchiveWith, DeserializeWith, SerializeWith}, 9 | }; 10 | 11 | pub struct DerefAsString; 12 | 13 | impl ArchiveWith for DerefAsString 14 | where 15 | T: Deref, 16 | { 17 | type Archived = ArchivedString; 18 | type Resolver = StringResolver; 19 | 20 | #[inline] 21 | fn resolve_with(field: &T, resolver: Self::Resolver, out: Place) { 22 | ArchivedString::resolve_from_str(field, resolver, out); 23 | } 24 | } 25 | 26 | impl SerializeWith for DerefAsString 27 | where 28 | S: Writer + Fallible + ?Sized, 29 | T: Deref, 30 | { 31 | #[inline] 32 | fn serialize_with(field: &T, serializer: &mut S) -> Result { 33 | ArchivedString::serialize_from_str(field, serializer) 34 | } 35 | } 36 | 37 | impl DeserializeWith for DerefAsString 38 | where 39 | T: for<'s> From<&'s str>, 40 | D: Fallible + ?Sized, 41 | { 42 | #[inline] 43 | fn deserialize_with(field: &ArchivedString, _: &mut D) -> Result { 44 | Ok(T::from(field.as_str())) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /bathbot-model/src/rkyv_util/map_boxed_slice.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | 3 | use rkyv::{ 4 | Place, 5 | rancor::Fallible, 6 | ser::{Allocator, Writer}, 7 | vec::{ArchivedVec, VecResolver}, 8 | with::{ArchiveWith, SerializeWith, With}, 9 | }; 10 | 11 | /// Basically [`rkyv::with::Map`] but for `Box<[T]>` 12 | pub struct MapBoxedSlice(PhantomData); 13 | 14 | impl ArchiveWith> for MapBoxedSlice 15 | where 16 | A: ArchiveWith, 17 | { 18 | type Archived = ArchivedVec<>::Archived>; 19 | type Resolver = VecResolver; 20 | 21 | #[inline] 22 | fn resolve_with(field: &Box<[O]>, resolver: Self::Resolver, out: Place) { 23 | ArchivedVec::resolve_from_len(field.len(), resolver, out); 24 | } 25 | } 26 | 27 | impl SerializeWith, S> for MapBoxedSlice 28 | where 29 | S: Fallible + Allocator + Writer + ?Sized, 30 | A: ArchiveWith + SerializeWith, 31 | { 32 | #[inline] 33 | fn serialize_with(field: &Box<[O]>, s: &mut S) -> Result { 34 | ArchivedVec::serialize_from_iter::, _, _>(field.iter().map(With::cast), s) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /bathbot-model/src/rkyv_util/mod.rs: -------------------------------------------------------------------------------- 1 | mod as_non_zero; 2 | mod bitflags; 3 | mod deref_as_box; 4 | mod deref_as_string; 5 | mod map_boxed_slice; 6 | mod map_unwrap_or_default; 7 | mod niche_deref_as_box; 8 | mod str_as_string; 9 | mod unwrap_or_default; 10 | 11 | pub mod time; 12 | 13 | pub use self::{ 14 | as_non_zero::AsNonZero, bitflags::BitflagsRkyv, deref_as_box::DerefAsBox, 15 | deref_as_string::DerefAsString, map_boxed_slice::MapBoxedSlice, 16 | map_unwrap_or_default::MapUnwrapOrDefault, niche_deref_as_box::NicheDerefAsBox, 17 | str_as_string::StrAsString, unwrap_or_default::UnwrapOrDefault, 18 | }; 19 | -------------------------------------------------------------------------------- /bathbot-model/src/rkyv_util/niche_deref_as_box.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Deref; 2 | 3 | use rkyv::{ 4 | ArchiveUnsized, ArchivedMetadata, Place, SerializeUnsized, 5 | niche::option_box::{ArchivedOptionBox, OptionBoxResolver}, 6 | rancor::Fallible, 7 | ser::Writer, 8 | with::{ArchiveWith, SerializeWith}, 9 | }; 10 | 11 | pub struct NicheDerefAsBox; 12 | 13 | impl ArchiveWith> for NicheDerefAsBox 14 | where 15 | U: Deref, 16 | V: ArchiveUnsized + ?Sized, 17 | ArchivedMetadata: Default, 18 | { 19 | type Archived = ArchivedOptionBox; 20 | type Resolver = OptionBoxResolver; 21 | 22 | #[inline] 23 | fn resolve_with(field: &Option, resolver: Self::Resolver, out: Place) { 24 | let deref: Option<&V> = field.as_deref(); 25 | ArchivedOptionBox::resolve_from_option(deref, resolver, out); 26 | } 27 | } 28 | 29 | impl SerializeWith, S> for NicheDerefAsBox 30 | where 31 | U: Deref, 32 | V: ArchiveUnsized + SerializeUnsized + ?Sized, 33 | S: Writer + Fallible + ?Sized, 34 | ArchivedMetadata: Default, 35 | { 36 | #[inline] 37 | fn serialize_with( 38 | field: &Option, 39 | serializer: &mut S, 40 | ) -> Result::Error> { 41 | let deref: Option<&V> = field.as_deref(); 42 | 43 | ArchivedOptionBox::serialize_from_option(deref, serializer) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /bathbot-model/src/rkyv_util/str_as_string.rs: -------------------------------------------------------------------------------- 1 | use rkyv::{ 2 | Place, SerializeUnsized, 3 | rancor::{Fallible, Source}, 4 | ser::Writer, 5 | string::{ArchivedString, StringResolver}, 6 | with::{ArchiveWith, SerializeWith}, 7 | }; 8 | 9 | pub struct StrAsString; 10 | 11 | impl ArchiveWith<&str> for StrAsString { 12 | type Archived = ArchivedString; 13 | type Resolver = StringResolver; 14 | 15 | fn resolve_with(field: &&str, resolver: Self::Resolver, out: Place) { 16 | ArchivedString::resolve_from_str(field, resolver, out); 17 | } 18 | } 19 | 20 | impl SerializeWith<&str, S> for StrAsString 21 | where 22 | str: SerializeUnsized, 23 | S: Fallible + Writer + ?Sized, 24 | { 25 | fn serialize_with(field: &&str, serializer: &mut S) -> Result { 26 | ArchivedString::serialize_from_str(field, serializer) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /bathbot-model/src/rkyv_util/unwrap_or_default.rs: -------------------------------------------------------------------------------- 1 | use rkyv::{ 2 | Archive, Archived, Deserialize, Place, Resolver, Serialize, 3 | rancor::Fallible, 4 | with::{ArchiveWith, DeserializeWith, SerializeWith}, 5 | }; 6 | 7 | pub struct UnwrapOrDefault; 8 | 9 | impl ArchiveWith> for UnwrapOrDefault { 10 | type Archived = Archived; 11 | type Resolver = Resolver; 12 | 13 | #[inline] 14 | fn resolve_with(field: &Option, resolver: Self::Resolver, out: Place) { 15 | match field { 16 | Some(value) => Archive::resolve(value, resolver, out), 17 | None => Archive::resolve(&T::default(), resolver, out), 18 | } 19 | } 20 | } 21 | 22 | impl SerializeWith, S> for UnwrapOrDefault 23 | where 24 | T: Archive + Default + Serialize, 25 | S: Fallible + ?Sized, 26 | { 27 | #[inline] 28 | fn serialize_with(field: &Option, serializer: &mut S) -> Result { 29 | match field { 30 | Some(value) => Serialize::serialize(value, serializer), 31 | None => Serialize::serialize(&T::default(), serializer), 32 | } 33 | } 34 | } 35 | 36 | impl DeserializeWith, T, D> for UnwrapOrDefault 37 | where 38 | T: Archive, 39 | Archived: Deserialize, 40 | D: Fallible + ?Sized, 41 | { 42 | #[inline] 43 | fn deserialize_with(field: &Archived, deserializer: &mut D) -> Result { 44 | field.deserialize(deserializer) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /bathbot-model/src/rosu_v2/grade.rs: -------------------------------------------------------------------------------- 1 | use rkyv::{Archive, Deserialize, Serialize}; 2 | use rosu_v2::model::Grade; 3 | 4 | #[derive(Archive, Serialize, Deserialize)] 5 | #[rkyv( 6 | remote = Grade, 7 | archived = ArchivedGrade, 8 | resolver = GradeResolver, 9 | derive(Copy, Clone), 10 | )] 11 | pub enum GradeRkyv { 12 | F, 13 | D, 14 | C, 15 | B, 16 | A, 17 | S, 18 | SH, 19 | X, 20 | XH, 21 | } 22 | 23 | impl From for Grade { 24 | fn from(grade: GradeRkyv) -> Self { 25 | match grade { 26 | GradeRkyv::F => Self::F, 27 | GradeRkyv::D => Self::D, 28 | GradeRkyv::C => Self::C, 29 | GradeRkyv::B => Self::B, 30 | GradeRkyv::A => Self::A, 31 | GradeRkyv::S => Self::S, 32 | GradeRkyv::SH => Self::SH, 33 | GradeRkyv::X => Self::X, 34 | GradeRkyv::XH => Self::XH, 35 | } 36 | } 37 | } 38 | 39 | impl From for Grade { 40 | fn from(grade: ArchivedGrade) -> Self { 41 | match grade { 42 | ArchivedGrade::F => Self::F, 43 | ArchivedGrade::D => Self::D, 44 | ArchivedGrade::C => Self::C, 45 | ArchivedGrade::B => Self::B, 46 | ArchivedGrade::A => Self::A, 47 | ArchivedGrade::S => Self::S, 48 | ArchivedGrade::SH => Self::SH, 49 | ArchivedGrade::X => Self::X, 50 | ArchivedGrade::XH => Self::XH, 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /bathbot-model/src/rosu_v2/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod grade; 2 | pub mod mode; 3 | pub mod ranking; 4 | pub mod user; 5 | -------------------------------------------------------------------------------- /bathbot-model/src/rosu_v2/mode.rs: -------------------------------------------------------------------------------- 1 | use rkyv::{Place, niche::niching::Niching}; 2 | use rosu_mods::GameMode; 3 | 4 | pub struct GameModeNiche; 5 | 6 | impl GameModeNiche { 7 | const NICHED: u8 = u8::MAX; 8 | } 9 | 10 | impl Niching for GameModeNiche { 11 | unsafe fn is_niched(niched: *const GameMode) -> bool { 12 | unsafe { *niched.cast::() == Self::NICHED } 13 | } 14 | 15 | fn resolve_niched(out: Place) { 16 | unsafe { out.cast_unchecked::() }.write(Self::NICHED); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /bathbot-model/src/rosu_v2/ranking.rs: -------------------------------------------------------------------------------- 1 | use rkyv::{ 2 | Archive, Serialize, 3 | niche::niching::NaN, 4 | with::{Map, MapNiche}, 5 | }; 6 | use rosu_v2::prelude::{CountryCode, Rankings, User, UserStatistics, Username}; 7 | 8 | use super::user::UserStatisticsRkyv; 9 | use crate::rkyv_util::{DerefAsString, NicheDerefAsBox}; 10 | 11 | #[derive(Archive, Serialize)] 12 | #[rkyv(remote = Rankings, archived = ArchivedRankings)] 13 | pub struct RankingsRkyv { 14 | #[rkyv(with = Map)] 15 | pub ranking: Vec, 16 | pub total: u32, 17 | } 18 | 19 | #[derive(Archive, Serialize)] 20 | #[rkyv(remote = User, archived = ArchivedRankingsUser)] 21 | pub struct RankingsUserRkyv { 22 | pub avatar_url: String, 23 | #[rkyv(with = DerefAsString)] 24 | pub country_code: CountryCode, 25 | #[rkyv(with = NicheDerefAsBox)] 26 | pub country: Option, 27 | pub user_id: u32, 28 | #[rkyv(with = DerefAsString)] 29 | pub username: Username, 30 | #[rkyv(with = MapNiche)] 31 | pub statistics: Option, 32 | } 33 | -------------------------------------------------------------------------------- /bathbot-model/src/twilight/guild/role.rs: -------------------------------------------------------------------------------- 1 | use rkyv::{ 2 | Archive, Place, Serialize, 3 | munge::munge, 4 | rancor::{Fallible, Source}, 5 | ser::Writer, 6 | with::{ArchiveWith, SerializeWith}, 7 | }; 8 | use twilight_model::{ 9 | guild::{Permissions, Role}, 10 | id::{Id, marker::RoleMarker}, 11 | }; 12 | 13 | use crate::{ 14 | rkyv_util::{BitflagsRkyv, StrAsString}, 15 | twilight::id::IdRkyv, 16 | }; 17 | 18 | #[derive(Archive, Serialize)] 19 | pub struct CachedRole<'a> { 20 | #[rkyv(with = IdRkyv)] 21 | pub id: Id, 22 | #[rkyv(with = StrAsString)] 23 | pub name: &'a str, 24 | #[rkyv(with = BitflagsRkyv)] 25 | pub permissions: Permissions, 26 | } 27 | 28 | impl<'a> ArchiveWith for CachedRole<'a> { 29 | type Archived = ArchivedCachedRole<'a>; 30 | type Resolver = CachedRoleResolver<'a>; 31 | 32 | #[allow(clippy::unit_arg)] 33 | fn resolve_with(role: &Role, resolver: Self::Resolver, out: Place) { 34 | munge!(let ArchivedCachedRole { id, name, permissions } = out); 35 | IdRkyv::resolve_with(&role.id, resolver.id, id); 36 | role.name.resolve(resolver.name, name); 37 | BitflagsRkyv::resolve_with(&role.permissions, resolver.permissions, permissions); 38 | } 39 | } 40 | 41 | impl + Writer + ?Sized> SerializeWith for CachedRole<'_> { 42 | fn serialize_with(role: &Role, serializer: &mut S) -> Result { 43 | Ok(CachedRoleResolver { 44 | id: IdRkyv::serialize_with(&role.id, serializer)?, 45 | name: role.name.serialize(serializer)?, 46 | permissions: BitflagsRkyv::serialize_with(&role.permissions, serializer)?, 47 | }) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /bathbot-model/src/twilight/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod channel; 2 | pub mod guild; 3 | pub mod id; 4 | pub mod session; 5 | pub mod user; 6 | pub mod util; 7 | -------------------------------------------------------------------------------- /bathbot-model/src/twilight/util/mod.rs: -------------------------------------------------------------------------------- 1 | mod image_hash; 2 | 3 | pub use self::image_hash::ImageHashRkyv; 4 | -------------------------------------------------------------------------------- /bathbot-psql/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bathbot-psql" 3 | version.workspace = true 4 | authors.workspace = true 5 | edition.workspace = true 6 | 7 | [dependencies] 8 | bathbot-model = { path = "../bathbot-model" } 9 | bathbot-util = { path = "../bathbot-util" } 10 | eyre = { workspace = true } 11 | futures = { version = "0.3", default-features = false } 12 | rkyv = { workspace = true } 13 | rosu-v2 = { workspace = true } 14 | smallvec = { version = "1.10" } 15 | sqlx = { version = "0.8.3", default-features = false, features = ["json", "macros", "postgres", "runtime-tokio-rustls", "time"] } 16 | time = { version = "0.3" } 17 | tokio = { version = "1.20", default-features = false, features = ["io-util", "time"] } 18 | tracing = { version = "0.1" } 19 | twilight-interactions = { workspace = true } 20 | twilight-model = { workspace = true } 21 | 22 | [dev-dependencies] 23 | dotenvy = { version = "0.15" } 24 | -------------------------------------------------------------------------------- /bathbot-psql/migrations/20221102221131_base.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE osu_maps; 2 | 3 | DROP TABLE osu_map_files; 4 | 5 | DROP TABLE osu_map_difficulty; 6 | 7 | DROP TABLE osu_map_difficulty_taiko; 8 | 9 | DROP TABLE osu_map_difficulty_catch; 10 | 11 | DROP TABLE osu_map_difficulty_mania; 12 | 13 | DROP TABLE osu_mapsets; 14 | 15 | DROP TABLE osu_scores; 16 | 17 | DROP TABLE osu_scores_performance; 18 | 19 | DROP TABLE tracked_twitch_streams; 20 | 21 | DROP TABLE bggame_scores; 22 | 23 | DROP INDEX higherlower_scores_version_index; 24 | 25 | DROP TABLE higherlower_scores; 26 | 27 | DROP TABLE map_tags; 28 | 29 | DROP TABLE guild_configs; 30 | 31 | DROP INDEX user_configs_osu_index; 32 | 33 | DROP TABLE user_configs; 34 | 35 | DROP TABLE tracked_osu_users; 36 | 37 | DROP TABLE huismetbenen_countries; 38 | 39 | DROP INDEX osu_user_names_name_index; 40 | 41 | DROP TABLE osu_user_names; 42 | 43 | DROP TABLE osu_user_stats; 44 | 45 | DROP INDEX osu_user_mode_stats_pp_index; 46 | 47 | DROP INDEX osu_user_mode_stats_global_rank_index; 48 | 49 | DROP INDEX osu_user_mode_stats_mode_index; 50 | 51 | DROP TABLE osu_user_mode_stats; 52 | -------------------------------------------------------------------------------- /bathbot-psql/migrations/20230201231043_inc-version-len.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE osu_maps ALTER COLUMN map_version TYPE VARCHAR(80); -------------------------------------------------------------------------------- /bathbot-psql/migrations/20230201231043_inc-version-len.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE osu_maps ALTER COLUMN map_version TYPE VARCHAR(90); -------------------------------------------------------------------------------- /bathbot-psql/migrations/20230212055648_remove_map_max_combo.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE osu_maps ADD COLUMN max_combo INT4; -------------------------------------------------------------------------------- /bathbot-psql/migrations/20230212055648_remove_map_max_combo.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE osu_maps DROP COLUMN max_combo; -------------------------------------------------------------------------------- /bathbot-psql/migrations/20230504191905_remove_countries.down.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS huismetbenen_countries ( 2 | country_name VARCHAR(32) NOT NULL, 3 | country_code VARCHAR(2) NOT NULL, 4 | CHECK (country_code = UPPER(country_code)), 5 | PRIMARY KEY (country_code) 6 | ); -------------------------------------------------------------------------------- /bathbot-psql/migrations/20230504191905_remove_countries.up.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE huismetbenen_countries; -------------------------------------------------------------------------------- /bathbot-psql/migrations/20230531141825_add_bookmarks_table.down.sql: -------------------------------------------------------------------------------- 1 | DROP INDEX map_bookmarks_user_index; 2 | 3 | DROP TABLE user_map_bookmarks; -------------------------------------------------------------------------------- /bathbot-psql/migrations/20230531141825_add_bookmarks_table.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS user_map_bookmarks ( 2 | user_id INT8 NOT NULL, 3 | map_id INT4 NOT NULL, 4 | insert_date TIMESTAMPTZ NOT NULL DEFAULT NOW(), 5 | PRIMARY KEY (user_id, map_id) 6 | ); 7 | 8 | CREATE INDEX map_bookmarks_user_index ON user_map_bookmarks (user_id); -------------------------------------------------------------------------------- /bathbot-psql/migrations/20230613122212_add_render_support.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE osu_replays; 2 | DROP TABLE user_render_settings; 3 | ALTER TABLE user_configs DROP COLUMN render_button; 4 | ALTER TABLE guild_configs DROP COLUMN render_button, DROP COLUMN allow_custom_skins; 5 | DROP TABLE render_video_urls; -------------------------------------------------------------------------------- /bathbot-psql/migrations/20230708224944_adjust_user_config_skin_color.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE user_render_settings DROP COLUMN use_skin_colors; 2 | 3 | ALTER TABLE guild_configs DROP COLUMN hide_medal_solution; 4 | 5 | ALTER TABLE guild_configs ALTER COLUMN retries TYPE BOOLEAN USING CASE WHEN retries = 0 THEN false ELSE true END; 6 | ALTER TABLE guild_configs RENAME COLUMN retries TO show_retries; 7 | 8 | ALTER TABLE user_configs ALTER COLUMN retries TYPE BOOLEAN USING CASE WHEN retries = 0 THEN false ELSE true END; 9 | ALTER TABLE user_configs RENAME COLUMN retries TO show_retries; 10 | -------------------------------------------------------------------------------- /bathbot-psql/migrations/20230708224944_adjust_user_config_skin_color.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE user_render_settings ADD COLUMN use_skin_colors BOOLEAN NOT NULL DEFAULT false; 2 | ALTER TABLE user_render_settings ALTER COLUMN use_skin_colors DROP DEFAULT; 3 | UPDATE user_render_settings SET use_skin_colors = NOT use_beatmap_colors; 4 | 5 | ALTER TABLE guild_configs ADD COLUMN hide_medal_solution INT2; 6 | 7 | ALTER TABLE guild_configs RENAME COLUMN show_retries TO retries; 8 | ALTER TABLE guild_configs ALTER COLUMN retries TYPE INT2 USING CASE WHEN retries = true THEN 1 ELSE 0 END; 9 | 10 | ALTER TABLE user_configs RENAME COLUMN show_retries TO retries; 11 | ALTER TABLE user_configs ALTER COLUMN retries TYPE INT2 USING CASE WHEN retries = true THEN 1 ELSE 0 END; 12 | -------------------------------------------------------------------------------- /bathbot-psql/migrations/20230906135717_add_render_fields.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE user_render_settings DROP COLUMN show_strain_graph, DROP COLUMN show_slider_breaks, DROP COLUMN ignore_fail; -------------------------------------------------------------------------------- /bathbot-psql/migrations/20230906135717_add_render_fields.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE user_render_settings ADD COLUMN show_strain_graph BOOLEAN NOT NULL DEFAULT false, ADD COLUMN show_slider_breaks BOOLEAN NOT NULL DEFAULT false, ADD COLUMN ignore_fail BOOLEAN NOT NULL DEFAULT false; 2 | ALTER TABLE user_render_settings ALTER COLUMN show_strain_graph DROP DEFAULT, ALTER COLUMN show_slider_breaks DROP DEFAULT, ALTER COLUMN ignore_fail DROP DEFAULT; 3 | -------------------------------------------------------------------------------- /bathbot-psql/migrations/20231020095331_materialized_scores_view.down.sql: -------------------------------------------------------------------------------- 1 | DROP MATERIALIZED VIEW user_scores; 2 | -------------------------------------------------------------------------------- /bathbot-psql/migrations/20231020095331_materialized_scores_view.up.sql: -------------------------------------------------------------------------------- 1 | CREATE MATERIALIZED VIEW user_scores AS 2 | SELECT 3 | osu_scores.*, 4 | osu_user_stats.country_code, 5 | osu_scores_performance.pp 6 | FROM 7 | osu_scores 8 | JOIN osu_user_stats USING (user_id) 9 | JOIN osu_scores_performance USING (score_id); 10 | 11 | CREATE UNIQUE INDEX user_scores_score_id_index ON user_scores (score_id); 12 | CREATE INDEX user_scores_mode_pp_index ON user_scores (gamemode, pp DESC); 13 | CREATE INDEX user_scores_mode_country_user_index ON user_scores (gamemode, country_code, user_id); 14 | CREATE INDEX user_scores_mode_country_pp_index ON user_scores (gamemode, country_code, pp DESC); -------------------------------------------------------------------------------- /bathbot-psql/migrations/20240228202351_attribute_adjustments.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE osu_map_difficulty_taiko DROP COLUMN is_convert; 2 | ALTER TABLE osu_map_difficulty_taiko RENAME COLUMN color TO colour; 3 | ALTER TABLE osu_map_difficulty_catch DROP COLUMN is_convert; 4 | ALTER TABLE osu_map_difficulty_mania DROP COLUMN n_objects; 5 | ALTER TABLE osu_map_difficulty_mania DROP COLUMN is_convert; -------------------------------------------------------------------------------- /bathbot-psql/migrations/20240228202351_attribute_adjustments.up.sql: -------------------------------------------------------------------------------- 1 | -- scary stuff 2 | DELETE FROM osu_map_difficulty_taiko; 3 | DELETE FROM osu_map_difficulty_catch; 4 | DELETE FROM osu_map_difficulty_mania; 5 | 6 | ALTER TABLE osu_map_difficulty_taiko ADD COLUMN is_convert BOOLEAN NOT NULL; 7 | ALTER TABLE osu_map_difficulty_taiko RENAME COLUMN colour TO color; 8 | 9 | ALTER TABLE osu_map_difficulty_catch ADD COLUMN is_convert BOOLEAN NOT NULL; 10 | 11 | ALTER TABLE osu_map_difficulty_mania ADD COLUMN n_objects INT4 NOT NULL; 12 | ALTER TABLE osu_map_difficulty_mania ADD COLUMN is_convert BOOLEAN NOT NULL; -------------------------------------------------------------------------------- /bathbot-psql/migrations/20240407231623_lazer_stable_toggle.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE user_configs DROP COLUMN legacy_scores; 2 | ALTER TABLE guild_configs DROP COLUMN legacy_scores; -------------------------------------------------------------------------------- /bathbot-psql/migrations/20240407231623_lazer_stable_toggle.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE user_configs ADD COLUMN legacy_scores BOOL; 2 | ALTER TABLE guild_configs ADD COLUMN legacy_scores BOOL; -------------------------------------------------------------------------------- /bathbot-psql/migrations/20240724163823_score_data.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE guild_configs ALTER COLUMN score_data TYPE BOOLEAN USING CASE WHEN score_data = 0 THEN true WHEN score_data = NULL THEN NULL ELSE false END; 2 | ALTER TABLE guild_configs RENAME COLUMN score_data TO legacy_scores; 3 | 4 | ALTER TABLE user_configs ALTER COLUMN score_data TYPE BOOLEAN USING CASE WHEN score_data = 0 THEN true WHEN score_data = NULL THEN NULL ELSE false END; 5 | ALTER TABLE user_configs RENAME COLUMN score_data TO legacy_scores; -------------------------------------------------------------------------------- /bathbot-psql/migrations/20240724163823_score_data.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE guild_configs RENAME COLUMN legacy_scores TO score_data; 2 | ALTER TABLE guild_configs ALTER COLUMN score_data TYPE INT2 USING CASE WHEN score_data = true THEN 0 WHEN score_data = false THEN 1 ELSE NULL END; 3 | 4 | ALTER TABLE user_configs RENAME COLUMN legacy_scores TO score_data; 5 | ALTER TABLE user_configs ALTER COLUMN score_data TYPE INT2 USING CASE WHEN score_data = true THEN 0 WHEN score_data = false THEN 1 ELSE NULL END; -------------------------------------------------------------------------------- /bathbot-psql/migrations/20240727204755_score_format.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE user_configs ADD COLUMN score_size INT2; 2 | ALTER TABLE user_configs ADD COLUMN minimized_pp INT2; 3 | ALTER TABLE user_configs DROP COLUMN score_embed; 4 | 5 | ALTER TABLE guild_configs ADD COLUMN score_size INT2; 6 | ALTER TABLE guild_configs ADD COLUMN minimized_pp INT2; -------------------------------------------------------------------------------- /bathbot-psql/migrations/20240727204755_score_format.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE user_configs DROP COLUMN score_size; 2 | ALTER TABLE user_configs DROP COLUMN minimized_pp; 3 | ALTER TABLE user_configs ADD COLUMN score_embed JSONB; 4 | 5 | ALTER TABLE guild_configs DROP COLUMN score_size; 6 | ALTER TABLE guild_configs DROP COLUMN minimized_pp; -------------------------------------------------------------------------------- /bathbot-psql/migrations/20241031101334_pp_update.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE osu_map_difficulty DROP COLUMN aim_difficult_strain_count; 2 | ALTER TABLE osu_map_difficulty DROP COLUMN speed_difficult_strain_count; 3 | ALTER TABLE osu_map_difficulty DROP COLUMN n_large_ticks; 4 | 5 | ALTER TABLE osu_map_difficulty_taiko RENAME COLUMN great_hit_window TO hit_window; 6 | ALTER TABLE osu_map_difficulty_taiko DROP COLUMN ok_hit_window; 7 | ALTER TABLE osu_map_difficulty_taiko DROP COLUMN mono_stamina_factor; 8 | 9 | ALTER TABLE osu_map_difficulty_mania DROP COLUMN n_hold_notes; 10 | 11 | DELETE FROM guild_configs; 12 | ALTER TABLE guild_configs ALTER COLUMN prefixes TYPE BYTEA USING (''::BYTEA); -------------------------------------------------------------------------------- /bathbot-psql/migrations/20241031101334_pp_update.up.sql: -------------------------------------------------------------------------------- 1 | DELETE FROM osu_map_difficulty; 2 | ALTER TABLE osu_map_difficulty ADD COLUMN aim_difficult_strain_count FLOAT8 NOT NULL; 3 | ALTER TABLE osu_map_difficulty ADD COLUMN speed_difficult_strain_count FLOAT8 NOT NULL; 4 | ALTER TABLE osu_map_difficulty ADD COLUMN n_large_ticks INT4 NOT NULL; 5 | 6 | DELETE FROM osu_map_difficulty_taiko; 7 | ALTER TABLE osu_map_difficulty_taiko RENAME COLUMN hit_window TO great_hit_window; 8 | ALTER TABLE osu_map_difficulty_taiko ADD COLUMN ok_hit_window FLOAT8 NOT NULL; 9 | ALTER TABLE osu_map_difficulty_taiko ADD COLUMN mono_stamina_factor FLOAT8 NOT NULL; 10 | 11 | DELETE FROM osu_map_difficulty_mania; 12 | ALTER TABLE osu_map_difficulty_mania ADD COLUMN n_hold_notes INT4 NOT NULL; 13 | 14 | DELETE FROM osu_scores_performance; 15 | 16 | DELETE FROM guild_configs; 17 | ALTER TABLE guild_configs ALTER COLUMN prefixes TYPE JSONB USING (to_jsonb(prefixes)); -------------------------------------------------------------------------------- /bathbot-psql/migrations/20241210012458_remove_scores.up.sql: -------------------------------------------------------------------------------- 1 | DROP MATERIALIZED VIEW user_scores; 2 | DROP TABLE osu_scores; 3 | DROP TABLE osu_scores_performance; 4 | DROP TABLE osu_map_difficulty; 5 | DROP TABLE osu_map_difficulty_catch; 6 | DROP TABLE osu_map_difficulty_mania; 7 | DROP TABLE osu_map_difficulty_taiko; -------------------------------------------------------------------------------- /bathbot-psql/migrations/20250114084143_tracking_rework.down.sql: -------------------------------------------------------------------------------- 1 | DROP INDEX tracked_osu_users_channel_index; 2 | 3 | DROP TABLE tracked_osu_users; 4 | DROP TABLE osu_users_100th_pp; 5 | 6 | CREATE TABLE IF NOT EXISTS tracked_osu_users ( 7 | user_id INT4 NOT NULL, 8 | gamemode INT2 NOT NULL, 9 | channels BYTEA NOT NULL, 10 | last_update TIMESTAMPTZ NOT NULL DEFAULT NOW(), 11 | PRIMARY KEY (user_id, gamemode) 12 | ); 13 | 14 | ALTER TABLE guild_configs ADD COLUMN osu_track_limit INT2; -------------------------------------------------------------------------------- /bathbot-psql/migrations/20250114084143_tracking_rework.up.sql: -------------------------------------------------------------------------------- 1 | -- data is migrated via script beforehand 2 | DROP TABLE tracked_osu_users; 3 | 4 | CREATE TABLE IF NOT EXISTS tracked_osu_users ( 5 | user_id INT4 NOT NULL, 6 | gamemode INT2 NOT NULL, 7 | channel_id INT8 NOT NULL, 8 | min_index INT2, 9 | max_index INT2, 10 | min_pp FLOAT4, 11 | max_pp FLOAT4, 12 | min_combo_percent FLOAT4, 13 | max_combo_percent FLOAT4, 14 | PRIMARY KEY (user_id, gamemode, channel_id) 15 | ); 16 | 17 | CREATE INDEX tracked_osu_users_channel_index ON tracked_osu_users (channel_id); 18 | 19 | CREATE TABLE IF NOT EXISTS osu_users_100th_pp ( 20 | user_id INT4 NOT NULL, 21 | gamemode INT2 NOT NULL, 22 | pp FLOAT4 NOT NULL, 23 | PRIMARY KEY (user_id, gamemode) 24 | ); 25 | 26 | ALTER TABLE guild_configs DROP COLUMN osu_track_limit; -------------------------------------------------------------------------------- /bathbot-psql/migrations/20250120223657_tracking_with_datetime.down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE osu_users_100th_pp DROP COLUMN last_updated; -------------------------------------------------------------------------------- /bathbot-psql/migrations/20250120223657_tracking_with_datetime.up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE osu_users_100th_pp ADD COLUMN last_updated TIMESTAMPTZ NOT NULL DEFAULT NOW(); -------------------------------------------------------------------------------- /bathbot-psql/migrations/20250318160023_osu_file_data.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE osu_map_file_content; 2 | 3 | CREATE TABLE IF NOT EXISTS osu_map_files ( 4 | map_id INT4 NOT NULL, 5 | map_filepath VARCHAR(150) NOT NULL, 6 | PRIMARY KEY (map_id) 7 | ); -------------------------------------------------------------------------------- /bathbot-psql/migrations/20250318160023_osu_file_data.up.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE osu_map_files; 2 | 3 | CREATE TABLE IF NOT EXISTS osu_map_file_content ( 4 | map_id INT4 NOT NULL, 5 | content BYTEA NOT NULL, 6 | PRIMARY KEY (map_id) 7 | ); -------------------------------------------------------------------------------- /bathbot-psql/src/impls/configs/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod guild; 2 | pub mod user; 3 | -------------------------------------------------------------------------------- /bathbot-psql/src/impls/games/mod.rs: -------------------------------------------------------------------------------- 1 | mod bg; 2 | mod hl; 3 | -------------------------------------------------------------------------------- /bathbot-psql/src/impls/mod.rs: -------------------------------------------------------------------------------- 1 | mod bookmarks; 2 | mod configs; 3 | mod games; 4 | mod osu; 5 | mod tracked_streams; 6 | -------------------------------------------------------------------------------- /bathbot-psql/src/impls/osu/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod map; 2 | pub mod mapset; 3 | pub mod name; 4 | pub mod rank_pp; 5 | pub mod render; 6 | pub mod score; 7 | pub mod tracked_users; 8 | pub mod user; 9 | -------------------------------------------------------------------------------- /bathbot-psql/src/impls/osu/score.rs: -------------------------------------------------------------------------------- 1 | use eyre::{Result, WrapErr}; 2 | use rosu_v2::prelude::Score; 3 | 4 | use crate::database::Database; 5 | 6 | impl Database { 7 | pub async fn insert_scores_mapsets(&self, scores: &[Score]) -> Result<()> { 8 | let mut tx = self.begin().await.wrap_err("Failed to begin transaction")?; 9 | 10 | for chunk in scores.chunks(100) { 11 | let mapset_iter = chunk.iter().filter_map(|score| score.mapset.as_deref()); 12 | 13 | Self::update_beatmapsets(&mut *tx, mapset_iter, chunk.len()) 14 | .await 15 | .wrap_err("Failed to update mapset")? 16 | } 17 | 18 | tx.commit().await.wrap_err("Failed to commit transaction")?; 19 | 20 | Ok(()) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /bathbot-psql/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate eyre; 3 | 4 | #[macro_use] 5 | extern crate tracing; 6 | 7 | pub use self::database::Database; 8 | 9 | mod database; 10 | mod impls; 11 | mod util; 12 | 13 | pub mod model; 14 | -------------------------------------------------------------------------------- /bathbot-psql/src/model/configs/hide_solutions.rs: -------------------------------------------------------------------------------- 1 | use twilight_interactions::command::{CommandOption, CreateOption}; 2 | 3 | #[derive(Copy, Clone, Debug, Eq, PartialEq, CommandOption, CreateOption)] 4 | #[repr(u8)] 5 | pub enum HideSolutions { 6 | #[option(name = "Show all solutions", value = "show")] 7 | ShowAll = 0, 8 | #[option(name = "Hide Hush-Hush solutions", value = "hide_hushhush")] 9 | HideHushHush = 1, 10 | #[option(name = "Hide all solutions", value = "hide_all")] 11 | HideAll = 2, 12 | } 13 | 14 | impl From for i16 { 15 | #[inline] 16 | fn from(hide_solutions: HideSolutions) -> Self { 17 | hide_solutions as Self 18 | } 19 | } 20 | 21 | impl TryFrom for HideSolutions { 22 | type Error = (); 23 | 24 | #[inline] 25 | fn try_from(value: i16) -> Result { 26 | match value { 27 | 0 => Ok(Self::ShowAll), 28 | 1 => Ok(Self::HideHushHush), 29 | 2 => Ok(Self::HideAll), 30 | _ => Err(()), 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /bathbot-psql/src/model/configs/mod.rs: -------------------------------------------------------------------------------- 1 | pub use self::{ 2 | authorities::{Authorities, Authority}, 3 | guild::{DbGuildConfig, GuildConfig}, 4 | hide_solutions::HideSolutions, 5 | list_size::ListSize, 6 | retries::Retries, 7 | score_data::ScoreData, 8 | skin::{DbSkinEntry, SkinEntry}, 9 | user::{DbUserConfig, OsuId, OsuUserId, OsuUsername, UserConfig}, 10 | }; 11 | 12 | mod authorities; 13 | mod guild; 14 | mod hide_solutions; 15 | mod list_size; 16 | mod retries; 17 | mod score_data; 18 | mod skin; 19 | mod user; 20 | -------------------------------------------------------------------------------- /bathbot-psql/src/model/configs/retries.rs: -------------------------------------------------------------------------------- 1 | use twilight_interactions::command::{CommandOption, CreateOption}; 2 | 3 | #[derive(Copy, Clone, Debug, Eq, PartialEq, CommandOption, CreateOption)] 4 | #[repr(u8)] 5 | pub enum Retries { 6 | #[option(name = "Hide", value = "hide")] 7 | Hide = 0, 8 | #[option(name = "Consider mods", value = "with_mods")] 9 | ConsiderMods = 1, 10 | #[option(name = "Ignore mods", value = "ignore_mods")] 11 | IgnoreMods = 2, 12 | } 13 | 14 | impl From for i16 { 15 | #[inline] 16 | fn from(retries: Retries) -> Self { 17 | retries as Self 18 | } 19 | } 20 | 21 | impl TryFrom for Retries { 22 | type Error = (); 23 | 24 | #[inline] 25 | fn try_from(value: i16) -> Result { 26 | match value { 27 | 0 => Ok(Self::Hide), 28 | 1 => Ok(Self::ConsiderMods), 29 | 2 => Ok(Self::IgnoreMods), 30 | _ => Err(()), 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /bathbot-psql/src/model/configs/score_data.rs: -------------------------------------------------------------------------------- 1 | use twilight_interactions::command::{CommandOption, CreateOption}; 2 | 3 | #[derive(Copy, Clone, Debug, Default, Eq, PartialEq, CommandOption, CreateOption)] 4 | #[repr(u8)] 5 | pub enum ScoreData { 6 | #[default] 7 | #[option(name = "Lazer", value = "lazer")] 8 | Lazer = 1, 9 | #[option(name = "Stable", value = "stable")] 10 | Stable = 0, 11 | #[option(name = "Lazer (Classic scoring)", value = "lazer_classic")] 12 | LazerWithClassicScoring = 2, 13 | } 14 | 15 | impl ScoreData { 16 | pub fn is_legacy(self) -> bool { 17 | self == Self::Stable 18 | } 19 | } 20 | 21 | impl From for i16 { 22 | fn from(score_data: ScoreData) -> Self { 23 | score_data as Self 24 | } 25 | } 26 | 27 | impl TryFrom for ScoreData { 28 | type Error = (); 29 | 30 | fn try_from(value: i16) -> Result { 31 | match value { 32 | 0 => Ok(Self::Stable), 33 | 1 => Ok(Self::Lazer), 34 | 2 => Ok(Self::LazerWithClassicScoring), 35 | _ => Err(()), 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /bathbot-psql/src/model/configs/skin.rs: -------------------------------------------------------------------------------- 1 | pub struct DbSkinEntry { 2 | pub user_id: i32, 3 | pub username: String, 4 | pub skin_url: Option, 5 | } 6 | 7 | impl From for SkinEntry { 8 | fn from(entry: DbSkinEntry) -> Self { 9 | Self { 10 | user_id: entry.user_id as u32, 11 | username: entry.username.into_boxed_str(), 12 | skin_url: entry.skin_url.expect("query ensures Some").into_boxed_str(), 13 | } 14 | } 15 | } 16 | 17 | pub struct SkinEntry { 18 | pub user_id: u32, 19 | pub username: Box, 20 | pub skin_url: Box, 21 | } 22 | -------------------------------------------------------------------------------- /bathbot-psql/src/model/games/hl.rs: -------------------------------------------------------------------------------- 1 | use sqlx::FromRow; 2 | 3 | #[derive(FromRow)] 4 | pub struct DbHlGameScore { 5 | pub discord_id: i64, 6 | pub highscore: i32, 7 | } 8 | -------------------------------------------------------------------------------- /bathbot-psql/src/model/games/mod.rs: -------------------------------------------------------------------------------- 1 | pub use self::{bg::*, hl::*}; 2 | 3 | mod bg; 4 | mod hl; 5 | -------------------------------------------------------------------------------- /bathbot-psql/src/model/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod configs; 2 | pub mod games; 3 | pub mod osu; 4 | pub mod render; 5 | -------------------------------------------------------------------------------- /bathbot-psql/src/model/osu/bookmark.rs: -------------------------------------------------------------------------------- 1 | use rosu_v2::prelude::{GameMode, Genre, Language, RankStatus}; 2 | use time::OffsetDateTime; 3 | 4 | pub struct MapBookmark { 5 | pub insert_date: OffsetDateTime, 6 | pub map_id: u32, 7 | pub mapset_id: u32, 8 | pub mapper_id: u32, 9 | pub creator_id: u32, 10 | pub creator_name: Box, 11 | pub artist: Box, 12 | pub title: Box, 13 | pub version: Box, 14 | pub mode: GameMode, 15 | pub hp: f32, 16 | pub cs: f32, 17 | pub od: f32, 18 | pub ar: f32, 19 | pub bpm: f32, 20 | pub count_circles: u32, 21 | pub count_sliders: u32, 22 | pub count_spinners: u32, 23 | pub seconds_drain: u32, 24 | pub seconds_total: u32, 25 | pub status: RankStatus, 26 | pub ranked_date: Option, 27 | pub genre: Genre, 28 | pub language: Language, 29 | pub cover_url: Box, 30 | } 31 | -------------------------------------------------------------------------------- /bathbot-psql/src/model/osu/map.rs: -------------------------------------------------------------------------------- 1 | use bathbot_util::{ 2 | CowUtils, 3 | query::{FilterCriteria, RegularCriteria, Searchable}, 4 | }; 5 | use rkyv::{Archive, Deserialize, Serialize}; 6 | 7 | #[derive(Clone)] 8 | pub struct DbBeatmap { 9 | pub map_id: i32, 10 | pub mapset_id: i32, 11 | pub user_id: i32, 12 | pub map_version: String, 13 | pub seconds_drain: i32, 14 | pub count_circles: i32, 15 | pub count_sliders: i32, 16 | pub count_spinners: i32, 17 | pub bpm: f32, 18 | } 19 | 20 | impl Searchable> for DbBeatmap { 21 | fn matches(&self, criteria: &FilterCriteria>) -> bool { 22 | let mut matches = true; 23 | 24 | matches &= criteria.length.contains(self.seconds_drain as f32); 25 | matches &= criteria.bpm.contains(self.bpm); 26 | 27 | if matches && criteria.has_search_terms() { 28 | let version = self.map_version.cow_to_ascii_lowercase(); 29 | 30 | matches &= criteria.search_terms().any(|term| version.contains(term)); 31 | } 32 | 33 | matches 34 | } 35 | } 36 | 37 | #[derive(Debug)] 38 | pub enum DbMapContent { 39 | Present(Vec), 40 | ChecksumMismatch, 41 | Missing, 42 | } 43 | 44 | #[derive(Archive, Deserialize, Serialize)] 45 | pub struct MapVersion { 46 | pub map_id: i32, 47 | pub version: String, 48 | } 49 | -------------------------------------------------------------------------------- /bathbot-psql/src/model/osu/mapset.rs: -------------------------------------------------------------------------------- 1 | use bathbot_util::{ 2 | CowUtils, 3 | query::{FilterCriteria, RegularCriteria, Searchable}, 4 | }; 5 | use time::OffsetDateTime; 6 | 7 | #[derive(Clone)] 8 | pub struct DbBeatmapset { 9 | pub mapset_id: i32, 10 | pub user_id: i32, 11 | pub artist: String, 12 | pub title: String, 13 | pub creator: String, 14 | pub rank_status: i16, 15 | pub ranked_date: Option, 16 | pub thumbnail: String, 17 | pub cover: String, 18 | } 19 | 20 | impl Searchable> for DbBeatmapset { 21 | fn matches(&self, criteria: &FilterCriteria>) -> bool { 22 | let mut matches = true; 23 | 24 | let artist = self.artist.cow_to_ascii_lowercase(); 25 | let creator = self.creator.cow_to_ascii_lowercase(); 26 | let title = self.title.cow_to_ascii_lowercase(); 27 | 28 | matches &= criteria.artist.matches(artist.as_ref()); 29 | matches &= criteria.creator.matches(creator.as_ref()); 30 | matches &= criteria.title.matches(title.as_ref()); 31 | 32 | if matches && criteria.has_search_terms() { 33 | let terms = [artist, creator, title]; 34 | 35 | matches &= criteria 36 | .search_terms() 37 | .all(|term| terms.iter().any(|searchable| searchable.contains(term))); 38 | } 39 | 40 | matches 41 | } 42 | } 43 | 44 | pub struct ArtistTitle { 45 | pub artist: String, 46 | pub title: String, 47 | } 48 | -------------------------------------------------------------------------------- /bathbot-psql/src/model/osu/mod.rs: -------------------------------------------------------------------------------- 1 | pub use self::{bookmark::*, map::*, mapset::*, tracked_user::*, user::*}; 2 | 3 | mod bookmark; 4 | mod map; 5 | mod mapset; 6 | mod tracked_user; 7 | mod user; 8 | -------------------------------------------------------------------------------- /bathbot-psql/src/model/osu/tracked_user.rs: -------------------------------------------------------------------------------- 1 | use time::OffsetDateTime; 2 | 3 | pub struct DbTrackedOsuUser { 4 | pub user_id: i32, 5 | pub gamemode: i16, 6 | pub channel_id: i64, 7 | pub min_index: Option, 8 | pub max_index: Option, 9 | pub min_pp: Option, 10 | pub max_pp: Option, 11 | pub min_combo_percent: Option, 12 | pub max_combo_percent: Option, 13 | pub last_pp: f32, 14 | pub last_updated: OffsetDateTime, 15 | } 16 | 17 | pub struct DbTrackedOsuUserInChannel { 18 | pub user_id: i32, 19 | pub gamemode: i16, 20 | pub min_index: Option, 21 | pub max_index: Option, 22 | pub min_pp: Option, 23 | pub max_pp: Option, 24 | pub min_combo_percent: Option, 25 | pub max_combo_percent: Option, 26 | } 27 | -------------------------------------------------------------------------------- /bathbot-server/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bathbot-server" 3 | version.workspace = true 4 | authors.workspace = true 5 | edition.workspace = true 6 | 7 | [dependencies] 8 | axum = { version = "0.8.1", default-features = false, features = ["http2", "json", "macros", "matched-path", "query", "tokio"] } 9 | bathbot-model = { path = "../bathbot-model" } 10 | bathbot-util = { path = "../bathbot-util" } 11 | eyre = { workspace = true } 12 | flexmap = { git = "https://github.com/MaxOhn/flexmap" } 13 | futures = { version = "0.3", default-features = false } 14 | handlebars = { version = "6.3.0" } 15 | hyper = { workspace = true, features = ["server"] } 16 | http-body-util = { workspace = true } 17 | hyper-rustls = { workspace = true } 18 | hyper-util = { workspace = true } 19 | metrics = { workspace = true } 20 | metrics-exporter-prometheus = { workspace = true } 21 | rosu-v2 = { workspace = true } 22 | rustls = { workspace = true } 23 | serde = { version = "1.0", features = ["derive", "rc"] } 24 | serde_json = { version = "1.0" } 25 | thiserror = { workspace = true } 26 | tokio = { version = "1.0", default-features = false, features = ["sync"] } 27 | tower = { version = "0.5.2", default-features = false } 28 | tower-http = { version = "0.6.2", features = ["cors", "fs", "trace"] } 29 | tracing = { version = "0.1" } 30 | -------------------------------------------------------------------------------- /bathbot-server/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate tracing; 3 | 4 | mod middleware; 5 | mod routes; 6 | mod server; 7 | mod standby; 8 | mod state; 9 | 10 | pub use self::{ 11 | server::Server, 12 | standby::{AuthenticationStandby, AuthenticationStandbyError}, 13 | state::AppStateBuilder, 14 | }; 15 | -------------------------------------------------------------------------------- /bathbot-server/src/middleware/metrics.rs: -------------------------------------------------------------------------------- 1 | use std::time::Instant; 2 | 3 | use axum::{body::Body, extract::MatchedPath, middleware::Next, response::Response}; 4 | use hyper::Request; 5 | use metrics::histogram; 6 | 7 | pub async fn track_metrics(req: Request, next: Next) -> Response { 8 | let path = match req.extensions().get::() { 9 | Some(matched_path) => matched_path.as_str().to_owned(), 10 | None => req.uri().path().to_owned(), 11 | }; 12 | 13 | let method = req.method().to_string(); 14 | 15 | let start = Instant::now(); 16 | let response = next.run(req).await; 17 | let latency = start.elapsed(); 18 | let status = response.status().as_str().to_string(); 19 | 20 | histogram!( 21 | "server_response_time", 22 | "method" => method, 23 | "path" => path, 24 | "status" => status 25 | ) 26 | .record(latency); 27 | 28 | response 29 | } 30 | -------------------------------------------------------------------------------- /bathbot-server/src/middleware/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod metrics; 2 | -------------------------------------------------------------------------------- /bathbot-server/src/routes/guild_count.rs: -------------------------------------------------------------------------------- 1 | use std::{slice, sync::Arc}; 2 | 3 | use axum::{Json, extract::State}; 4 | use metrics::{Key, Label}; 5 | use serde::Serialize; 6 | 7 | use crate::state::AppState; 8 | 9 | static GUILDS_LABEL: Label = Label::from_static_parts("kind", "Guilds"); 10 | 11 | pub async fn get_guild_count(State(state): State>) -> Json { 12 | let key = Key::from_static_parts("bathbot.cache_entries", slice::from_ref(&GUILDS_LABEL)); 13 | let guild_count = state.metrics_reader.gauge_value(&key) as u64; 14 | 15 | Json(GuildCount { guild_count }) 16 | } 17 | 18 | #[derive(Serialize)] 19 | pub struct GuildCount { 20 | guild_count: u64, 21 | } 22 | -------------------------------------------------------------------------------- /bathbot-server/src/routes/metrics.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use axum::{extract::State, http::StatusCode}; 4 | use eyre::Result; 5 | 6 | use crate::state::AppState; 7 | 8 | pub async fn get_metrics(State(state): State>) -> Result { 9 | Ok(state.prometheus.render()) 10 | } 11 | -------------------------------------------------------------------------------- /bathbot-server/src/routes/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod auth; 2 | pub mod guild_count; 3 | pub mod metrics; 4 | pub mod osudirect; 5 | -------------------------------------------------------------------------------- /bathbot-server/src/routes/osudirect.rs: -------------------------------------------------------------------------------- 1 | use axum::{extract::Path, response::Redirect}; 2 | 3 | pub async fn redirect_osudirect(Path(mapset_id): Path) -> Redirect { 4 | let location = format!("osu://dl/{mapset_id}"); 5 | 6 | Redirect::permanent(&location) 7 | } 8 | -------------------------------------------------------------------------------- /bathbot-util/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bathbot-util" 3 | version.workspace = true 4 | authors.workspace = true 5 | edition.workspace = true 6 | 7 | [dependencies] 8 | eyre = { workspace = true } 9 | memchr = { workspace = true } 10 | metrics = { workspace = true } 11 | metrics-util = { workspace = true } 12 | regex = { version = "1.0" } 13 | rosu-v2 = { workspace = true } 14 | time = { version = "0.3", features = ["parsing"] } 15 | tokio = { version = "1.20", default-features = false, features = ["parking_lot", "process"] } 16 | twilight-model = { workspace = true } 17 | -------------------------------------------------------------------------------- /bathbot-util/src/builder/author.rs: -------------------------------------------------------------------------------- 1 | use twilight_model::channel::message::embed::EmbedAuthor; 2 | 3 | #[derive(Clone)] 4 | pub struct AuthorBuilder { 5 | pub icon_url: Option, 6 | pub name: String, 7 | pub url: Option, 8 | } 9 | 10 | impl AuthorBuilder { 11 | pub fn new(name: impl Into) -> Self { 12 | Self { 13 | name: name.into(), 14 | url: None, 15 | icon_url: None, 16 | } 17 | } 18 | 19 | pub fn url(mut self, url: impl Into) -> Self { 20 | self.url = Some(url.into()); 21 | 22 | self 23 | } 24 | 25 | pub fn icon_url(mut self, icon_url: impl Into) -> Self { 26 | let icon_url = icon_url.into(); 27 | self.icon_url = Some(icon_url); 28 | 29 | self 30 | } 31 | 32 | pub fn build(self) -> EmbedAuthor { 33 | EmbedAuthor { 34 | icon_url: self.icon_url, 35 | name: self.name, 36 | proxy_icon_url: None, 37 | url: self.url, 38 | } 39 | } 40 | } 41 | 42 | impl From for EmbedAuthor { 43 | #[inline] 44 | fn from(author: AuthorBuilder) -> Self { 45 | author.build() 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /bathbot-util/src/builder/footer.rs: -------------------------------------------------------------------------------- 1 | use twilight_model::channel::message::embed::EmbedFooter; 2 | 3 | #[derive(Clone)] 4 | pub struct FooterBuilder { 5 | pub icon_url: Option, 6 | pub text: String, 7 | } 8 | 9 | impl FooterBuilder { 10 | pub fn new(text: impl Into) -> Self { 11 | Self { 12 | text: text.into(), 13 | icon_url: None, 14 | } 15 | } 16 | 17 | pub fn icon_url(mut self, icon_url: impl Into) -> Self { 18 | self.icon_url = Some(icon_url.into()); 19 | 20 | self 21 | } 22 | 23 | pub fn build(self) -> EmbedFooter { 24 | EmbedFooter { 25 | icon_url: self.icon_url, 26 | proxy_icon_url: None, 27 | text: self.text, 28 | } 29 | } 30 | } 31 | 32 | pub trait IntoFooterBuilder { 33 | fn into(self) -> FooterBuilder; 34 | } 35 | 36 | impl IntoFooterBuilder for &str { 37 | #[inline] 38 | fn into(self) -> FooterBuilder { 39 | FooterBuilder { 40 | icon_url: None, 41 | text: self.to_owned(), 42 | } 43 | } 44 | } 45 | 46 | impl IntoFooterBuilder for String { 47 | #[inline] 48 | fn into(self) -> FooterBuilder { 49 | FooterBuilder { 50 | icon_url: None, 51 | text: self, 52 | } 53 | } 54 | } 55 | 56 | impl IntoFooterBuilder for FooterBuilder { 57 | #[inline] 58 | fn into(self) -> FooterBuilder { 59 | self 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /bathbot-util/src/builder/mod.rs: -------------------------------------------------------------------------------- 1 | pub use self::{ 2 | author::AuthorBuilder, 3 | embed::{EmbedBuilder, attachment}, 4 | footer::FooterBuilder, 5 | message::MessageBuilder, 6 | }; 7 | 8 | mod author; 9 | mod embed; 10 | mod footer; 11 | mod message; 12 | 13 | pub mod modal; 14 | -------------------------------------------------------------------------------- /bathbot-util/src/exp_backoff.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | #[derive(Debug, Clone)] 4 | pub struct ExponentialBackoff { 5 | current: Duration, 6 | base: u32, 7 | factor: u32, 8 | max_delay: Option, 9 | } 10 | 11 | impl ExponentialBackoff { 12 | pub fn new(base: u32) -> Self { 13 | ExponentialBackoff { 14 | current: Duration::from_millis(base as u64), 15 | base, 16 | factor: 1, 17 | max_delay: None, 18 | } 19 | } 20 | 21 | pub fn factor(mut self, factor: u32) -> Self { 22 | self.factor = factor; 23 | 24 | self 25 | } 26 | 27 | pub fn max_delay(mut self, max_delay: u64) -> Self { 28 | self.max_delay.replace(Duration::from_millis(max_delay)); 29 | 30 | self 31 | } 32 | } 33 | 34 | impl Iterator for ExponentialBackoff { 35 | type Item = Duration; 36 | 37 | fn next(&mut self) -> Option { 38 | let duration = self.current * self.factor; 39 | 40 | if let Some(max_delay) = self.max_delay.filter(|&max_delay| duration > max_delay) { 41 | return Some(max_delay); 42 | } 43 | 44 | self.current *= self.base; 45 | 46 | Some(duration) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /bathbot-util/src/ext/authored.rs: -------------------------------------------------------------------------------- 1 | use eyre::Result; 2 | use twilight_model::{ 3 | channel::Message, 4 | id::{ 5 | Id, 6 | marker::{ChannelMarker, GuildMarker, UserMarker}, 7 | }, 8 | user::User, 9 | }; 10 | 11 | pub trait Authored { 12 | /// Channel id of the event 13 | fn channel_id(&self) -> Id; 14 | 15 | /// Guild id of the event 16 | fn guild_id(&self) -> Option>; 17 | 18 | /// Author of the event 19 | fn user(&self) -> Result<&User>; 20 | 21 | /// Author's user id 22 | #[inline] 23 | fn user_id(&self) -> Result> { 24 | self.user().map(|user| user.id) 25 | } 26 | } 27 | 28 | impl Authored for Message { 29 | #[inline] 30 | fn channel_id(&self) -> Id { 31 | self.channel_id 32 | } 33 | 34 | #[inline] 35 | fn guild_id(&self) -> Option> { 36 | self.guild_id 37 | } 38 | 39 | #[inline] 40 | fn user(&self) -> Result<&User> { 41 | Ok(&self.author) 42 | } 43 | 44 | #[inline] 45 | fn user_id(&self) -> Result> { 46 | Ok(self.author.id) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /bathbot-util/src/ext/mod.rs: -------------------------------------------------------------------------------- 1 | mod authored; 2 | mod score; 3 | 4 | pub use self::{ 5 | authored::Authored, 6 | score::{ScoreExt, ScoreHasEndedAt, ScoreHasMode}, 7 | }; 8 | -------------------------------------------------------------------------------- /bathbot-util/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod buckets; 2 | mod builder; 3 | mod cow; 4 | mod exp_backoff; 5 | mod ext; 6 | mod hasher; 7 | mod macros; 8 | mod matrix; 9 | mod metrics; 10 | mod mods_fmt; 11 | mod msg_origin; 12 | mod tourney_badges; 13 | 14 | pub mod constants; 15 | pub mod datetime; 16 | pub mod html; 17 | pub mod matcher; 18 | pub mod numbers; 19 | pub mod osu; 20 | pub mod query; 21 | pub mod string_cmp; 22 | 23 | pub use self::{ 24 | buckets::{Bucket, BucketName, Buckets}, 25 | builder::{AuthorBuilder, EmbedBuilder, FooterBuilder, MessageBuilder, attachment, modal}, 26 | cow::CowUtils, 27 | exp_backoff::ExponentialBackoff, 28 | ext::*, 29 | hasher::{IntHash, IntHasher}, 30 | matrix::Matrix, 31 | metrics::MetricsReader, 32 | mods_fmt::ModsFormatter, 33 | msg_origin::MessageOrigin, 34 | tourney_badges::TourneyBadges, 35 | }; 36 | -------------------------------------------------------------------------------- /bathbot-util/src/macros.rs: -------------------------------------------------------------------------------- 1 | /// Create or push onto a vector of embed fields. 2 | #[macro_export] 3 | macro_rules! fields { 4 | // Push fields to a vec 5 | ($fields:ident { 6 | $($name:expr, $value:expr, $inline:expr $(;)? )+ 7 | }) => {{ 8 | $( 9 | $fields.push( 10 | twilight_model::channel::message::embed::EmbedField { 11 | name: $name.into(), 12 | value: $value, 13 | inline: $inline, 14 | } 15 | ); 16 | )+ 17 | }}; 18 | 19 | // Create a new vec of fields 20 | ($($name:expr, $value:expr, $inline:expr);+) => { 21 | fields![$($name, $value, $inline;)+] 22 | }; 23 | 24 | ($($name:expr, $value:expr, $inline:expr;)+) => { 25 | vec![ 26 | $( 27 | twilight_model::channel::message::embed::EmbedField { 28 | name: $name.into(), 29 | value: $value, 30 | inline: $inline, 31 | }, 32 | )+ 33 | ] 34 | }; 35 | } 36 | -------------------------------------------------------------------------------- /bathbot-util/src/matrix.rs: -------------------------------------------------------------------------------- 1 | use std::ops::{Index, IndexMut}; 2 | 3 | pub struct Matrix { 4 | inner: Box<[T]>, 5 | width: usize, 6 | } 7 | 8 | impl Matrix { 9 | pub fn new(columns: usize, rows: usize) -> Matrix { 10 | Matrix { 11 | inner: vec![T::default(); columns * rows].into_boxed_slice(), 12 | width: columns, 13 | } 14 | } 15 | 16 | pub fn width(&self) -> usize { 17 | self.width 18 | } 19 | 20 | pub fn height(&self) -> usize { 21 | self.inner.len() / self.width 22 | } 23 | 24 | pub fn count_neighbors(&self, x: usize, y: usize, cell: T) -> u8 25 | where 26 | T: Eq, 27 | { 28 | let h = self.height(); 29 | let mut neighbors = 0; 30 | 31 | for cx in x.saturating_sub(1)..self.width.min(x + 2) { 32 | for cy in y.saturating_sub(1)..h.min(y + 2) { 33 | neighbors += ((cx != x || cy != y) && self[(cx, cy)] == cell) as u8; 34 | } 35 | } 36 | 37 | neighbors 38 | } 39 | } 40 | 41 | impl Index<(usize, usize)> for Matrix { 42 | type Output = T; 43 | 44 | #[inline] 45 | fn index(&self, coords: (usize, usize)) -> &T { 46 | &self.inner[coords.1 * self.width + coords.0] 47 | } 48 | } 49 | 50 | impl IndexMut<(usize, usize)> for Matrix { 51 | #[inline] 52 | fn index_mut(&mut self, coords: (usize, usize)) -> &mut T { 53 | &mut self.inner[coords.1 * self.width + coords.0] 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /bathbot-util/src/msg_origin.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Display, Formatter, Result as FmtResult}; 2 | 3 | use twilight_model::id::{ 4 | Id, 5 | marker::{ChannelMarker, GuildMarker}, 6 | }; 7 | 8 | #[derive(Copy, Clone)] 9 | pub struct MessageOrigin { 10 | guild: Option>, 11 | channel: Id, 12 | } 13 | 14 | impl MessageOrigin { 15 | pub fn new(guild: Option>, channel: Id) -> Self { 16 | Self { guild, channel } 17 | } 18 | } 19 | 20 | impl Display for MessageOrigin { 21 | #[inline] 22 | fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { 23 | let Self { guild, channel } = self; 24 | 25 | match guild { 26 | Some(guild) => write!(f, "https://discord.com/channels/{guild}/{channel}/#"), 27 | None => write!(f, "https://discord.com/channels/@me/{channel}/#"), 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /bathbot-util/src/query/impls/mod.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Debug, Write}; 2 | 3 | pub use self::{bookmark::BookmarkCriteria, regular::RegularCriteria, top::TopCriteria}; 4 | use super::{ 5 | operator::Operator, 6 | optional::{OptionalRange, OptionalText}, 7 | separate_content, 8 | }; 9 | 10 | mod bookmark; 11 | mod regular; 12 | mod top; 13 | 14 | fn try_update_len(length: &mut OptionalRange, op: Operator, value: &str) -> bool { 15 | let Ok(len) = value.trim_end_matches(['m', 's', 'h']).parse::() else { 16 | return false; 17 | }; 18 | 19 | let scale = if value.ends_with("ms") { 20 | 1.0 / 1000.0 21 | } else if value.ends_with('s') { 22 | 1.0 23 | } else if value.ends_with('m') { 24 | 60.0 25 | } else if value.ends_with('h') { 26 | 3_600.0 27 | } else { 28 | 1.0 29 | }; 30 | 31 | length.try_update_value(op, len * scale, scale / 2.0) 32 | } 33 | 34 | fn display_range(content: &mut String, name: &str, range: &OptionalRange) 35 | where 36 | OptionalRange: Debug, 37 | { 38 | if !range.is_empty() { 39 | separate_content(content); 40 | let _ = write!(content, "`{name}: {range:?}`"); 41 | } 42 | } 43 | 44 | fn display_text(content: &mut String, name: &str, text: &OptionalText<'_>) { 45 | if !text.is_empty() { 46 | separate_content(content); 47 | let _ = write!(content, "`{name}: {text:?}`"); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /bathbot-util/src/query/mod.rs: -------------------------------------------------------------------------------- 1 | mod filter; 2 | mod impls; 3 | mod operator; 4 | mod optional; 5 | mod searchable; 6 | 7 | pub use self::{filter::*, impls::*, operator::Operator, searchable::*}; 8 | 9 | fn separate_content(content: &mut String) { 10 | if !content.is_empty() { 11 | content.push_str(" • "); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /bathbot-util/src/query/operator.rs: -------------------------------------------------------------------------------- 1 | pub enum Operator { 2 | Equal, 3 | Less, 4 | LessOrEqual, 5 | Greater, 6 | GreaterOrEqual, 7 | } 8 | 9 | impl From<&str> for Operator { 10 | fn from(s: &str) -> Self { 11 | match s { 12 | "=" | ":" => Self::Equal, 13 | "<" => Self::Less, 14 | "<=" | "<:" => Self::LessOrEqual, 15 | ">" => Self::Greater, 16 | ">=" | ">:" => Self::GreaterOrEqual, 17 | _ => unreachable!(), 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /bathbot/src/active/impls/bg_game/img_reveal.rs: -------------------------------------------------------------------------------- 1 | use std::io::Cursor; 2 | 3 | use eyre::{Result, WrapErr}; 4 | use image::{DynamicImage, GenericImageView, ImageOutputFormat::Png}; 5 | use rand::RngCore; 6 | 7 | pub struct ImageReveal { 8 | dim: (u32, u32), 9 | original: DynamicImage, 10 | x: u32, 11 | y: u32, 12 | radius: u32, 13 | } 14 | 15 | impl ImageReveal { 16 | pub fn new(original: DynamicImage) -> Self { 17 | let (w, h) = original.dimensions(); 18 | let radius = 100; 19 | let mut rng = rand::thread_rng(); 20 | let x = radius + rng.next_u32() % (w - 2 * radius); 21 | let y = radius + rng.next_u32() % (h - 2 * radius); 22 | 23 | Self { 24 | dim: (w, h), 25 | original, 26 | x, 27 | y, 28 | radius, 29 | } 30 | } 31 | 32 | pub fn increase_radius(&mut self) { 33 | self.radius += 75; 34 | } 35 | 36 | pub fn sub_image(&self) -> Result> { 37 | let cx = self.x.saturating_sub(self.radius); 38 | let cy = self.y.saturating_sub(self.radius); 39 | let (w, h) = self.dim; 40 | let w = (self.x + self.radius).min(w) - cx; 41 | let h = (self.y + self.radius).min(h) - cy; 42 | let sub_image = self.original.crop_imm(cx, cy, w, h); 43 | let png_bytes: Vec = Vec::with_capacity((w * h) as usize); 44 | 45 | let mut cursor = Cursor::new(png_bytes); 46 | sub_image 47 | .write_to(&mut cursor, Png) 48 | .wrap_err("Failed to encode image")?; 49 | 50 | Ok(cursor.into_inner()) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /bathbot/src/active/impls/bg_game/util.rs: -------------------------------------------------------------------------------- 1 | use std::collections::VecDeque; 2 | 3 | use bathbot_psql::model::games::{DbMapTagEntry, MapsetTagsEntries}; 4 | 5 | pub fn get_random_mapset<'m>( 6 | entries: &'m MapsetTagsEntries, 7 | previous_ids: &mut VecDeque, 8 | ) -> &'m DbMapTagEntry { 9 | let buffer_size = entries.tags.len() / 2; 10 | 11 | loop { 12 | let random_index = rand::random::() % entries.tags.len(); 13 | let entry = &entries.tags[random_index]; 14 | 15 | if !previous_ids.contains(&entry.mapset_id) { 16 | previous_ids.push_front(entry.mapset_id); 17 | 18 | if previous_ids.len() > buffer_size { 19 | previous_ids.pop_back(); 20 | } 21 | 22 | return entry; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /bathbot/src/active/impls/compare/mod.rs: -------------------------------------------------------------------------------- 1 | pub use self::{ 2 | most_played::CompareMostPlayedPagination, scores::CompareScoresPagination, 3 | top::CompareTopPagination, 4 | }; 5 | 6 | mod most_played; 7 | mod scores; 8 | mod top; 9 | -------------------------------------------------------------------------------- /bathbot/src/active/impls/help/mod.rs: -------------------------------------------------------------------------------- 1 | pub use self::{interaction::HelpInteractionCommand, prefix::HelpPrefixMenu}; 2 | 3 | mod interaction; 4 | mod prefix; 5 | -------------------------------------------------------------------------------- /bathbot/src/active/impls/medals/mod.rs: -------------------------------------------------------------------------------- 1 | pub use self::{ 2 | common::MedalsCommonPagination, list::MedalsListPagination, missing::MedalsMissingPagination, 3 | recent::MedalsRecentPagination, 4 | }; 5 | 6 | mod common; 7 | mod list; 8 | mod missing; 9 | mod recent; 10 | -------------------------------------------------------------------------------- /bathbot/src/active/impls/osekai/mod.rs: -------------------------------------------------------------------------------- 1 | pub use self::{medal_count::MedalCountPagination, rarity::MedalRarityPagination}; 2 | 3 | mod medal_count; 4 | mod rarity; 5 | -------------------------------------------------------------------------------- /bathbot/src/active/impls/osustats/mod.rs: -------------------------------------------------------------------------------- 1 | pub use self::{ 2 | best::OsuStatsBestPagination, players::OsuStatsPlayersPagination, 3 | scores::OsuStatsScoresPagination, 4 | }; 5 | 6 | mod best; 7 | mod players; 8 | mod scores; 9 | -------------------------------------------------------------------------------- /bathbot/src/active/impls/relax/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod top; 2 | -------------------------------------------------------------------------------- /bathbot/src/active/impls/render/mod.rs: -------------------------------------------------------------------------------- 1 | mod cached; 2 | mod import; 3 | mod settings; 4 | 5 | pub use self::{cached::CachedRender, import::SettingsImport, settings::RenderSettingsActive}; 6 | -------------------------------------------------------------------------------- /bathbot/src/active/impls/simulate/attrs.rs: -------------------------------------------------------------------------------- 1 | use rosu_pp::Beatmap; 2 | 3 | #[derive(Copy, Clone, Default)] 4 | pub struct SimulateAttributes { 5 | pub ar: Option, 6 | pub cs: Option, 7 | pub hp: Option, 8 | pub od: Option, 9 | } 10 | 11 | impl From<&Beatmap> for SimulateAttributes { 12 | #[inline] 13 | fn from(map: &Beatmap) -> Self { 14 | Self { 15 | ar: Some(map.ar), 16 | cs: Some(map.cs), 17 | hp: Some(map.hp), 18 | od: Some(map.od), 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /bathbot/src/active/impls/snipe/mod.rs: -------------------------------------------------------------------------------- 1 | pub use self::{ 2 | country_list::SnipeCountryListPagination, difference::SnipeDifferencePagination, 3 | player_list::SnipePlayerListPagination, 4 | }; 5 | 6 | mod country_list; 7 | mod difference; 8 | mod player_list; 9 | -------------------------------------------------------------------------------- /bathbot/src/commands/fun/bg_game/hint.rs: -------------------------------------------------------------------------------- 1 | use bathbot_util::{BucketName, MessageBuilder, constants::GENERAL_ISSUE}; 2 | use eyre::Result; 3 | use twilight_model::{channel::Message, guild::Permissions}; 4 | 5 | use crate::{Context, util::ChannelExt}; 6 | 7 | pub async fn hint(msg: &Message, permissions: Option) -> Result<()> { 8 | let ratelimit = Context::check_ratelimit(msg.author.id, BucketName::BgHint); 9 | 10 | if let Some(cooldown) = ratelimit { 11 | trace!( 12 | "Ratelimiting user {} on bucket `BgHint` for {cooldown} seconds", 13 | msg.author.id 14 | ); 15 | 16 | return Ok(()); 17 | } 18 | 19 | match Context::bg_games().read(&msg.channel_id).await.get() { 20 | Some(game) => match game.hint().await { 21 | Ok(hint) => { 22 | let builder = MessageBuilder::new().content(hint); 23 | msg.create_message(builder, permissions).await?; 24 | } 25 | Err(err) => { 26 | let _ = msg.error(GENERAL_ISSUE).await; 27 | 28 | return Err(err.wrap_err("Failed to get hint")); 29 | } 30 | }, 31 | None => { 32 | let content = "No running game in this channel. Start one with `/bg`."; 33 | msg.error(content).await?; 34 | } 35 | } 36 | 37 | Ok(()) 38 | } 39 | -------------------------------------------------------------------------------- /bathbot/src/commands/fun/bg_game/skip.rs: -------------------------------------------------------------------------------- 1 | use bathbot_util::{ 2 | BucketName, 3 | constants::{GENERAL_ISSUE, INVITE_LINK}, 4 | }; 5 | use eyre::Result; 6 | use twilight_model::channel::Message; 7 | 8 | use crate::{Context, util::ChannelExt}; 9 | 10 | pub async fn skip(msg: &Message) -> Result<()> { 11 | if let Some(cooldown) = Context::check_ratelimit(msg.author.id, BucketName::BgSkip) { 12 | trace!( 13 | "Ratelimiting user {} on bucket `BgSkip` for {cooldown} seconds", 14 | msg.author.id 15 | ); 16 | 17 | let content = format!("Command on cooldown, try again in {cooldown} seconds"); 18 | msg.error(content).await?; 19 | 20 | return Ok(()); 21 | } 22 | 23 | let _ = Context::http().create_typing_trigger(msg.channel_id).await; 24 | 25 | match Context::bg_games().read(&msg.channel_id).await.get() { 26 | Some(game) => match game.restart() { 27 | Ok(_) => {} 28 | Err(err) => { 29 | let _ = msg.error(GENERAL_ISSUE).await; 30 | 31 | return Err(err.wrap_err("Failed to restart game")); 32 | } 33 | }, 34 | None => { 35 | let content = format!( 36 | "The background guessing game must be started with `/bg`.\n\ 37 | If slash commands are not available in your server, \ 38 | try [re-inviting the bot]({INVITE_LINK})." 39 | ); 40 | 41 | msg.error(content).await?; 42 | } 43 | } 44 | 45 | Ok(()) 46 | } 47 | -------------------------------------------------------------------------------- /bathbot/src/commands/fun/bg_game/stop.rs: -------------------------------------------------------------------------------- 1 | use eyre::Result; 2 | use twilight_model::channel::Message; 3 | 4 | use crate::{Context, util::ChannelExt}; 5 | 6 | pub async fn stop(msg: &Message) -> Result<()> { 7 | match Context::bg_games().read(&msg.channel_id).await.get() { 8 | Some(game) => match game.stop() { 9 | Ok(_) => {} 10 | Err(err) => { 11 | let _ = msg.error("Error while stopping game \\:(").await; 12 | 13 | return Err(err.wrap_err("Failed to stop game")); 14 | } 15 | }, 16 | None => { 17 | let content = "No running game in this channel. Start one with `/bg`."; 18 | msg.error(content).await?; 19 | } 20 | } 21 | 22 | Ok(()) 23 | } 24 | -------------------------------------------------------------------------------- /bathbot/src/commands/fun/mod.rs: -------------------------------------------------------------------------------- 1 | pub use self::bg_game::*; 2 | 3 | mod bg_game; 4 | mod higherlower_game; 5 | mod minesweeper; 6 | -------------------------------------------------------------------------------- /bathbot/src/commands/help/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::BTreeMap, fmt::Write}; 2 | 3 | pub use self::interaction::slash_help; 4 | 5 | mod interaction; 6 | mod message; 7 | 8 | fn failed_message_content(dists: BTreeMap) -> String { 9 | let mut names = dists.iter().take(5).map(|(_, &name)| name); 10 | 11 | if let Some(name) = names.next() { 12 | let count = dists.len().min(5); 13 | let mut content = String::with_capacity(14 + count * (5 + 2) + (count - 1) * 2); 14 | content.push_str("Did you mean "); 15 | let _ = write!(content, "`{name}`"); 16 | 17 | for name in names { 18 | let _ = write!(content, ", `{name}`"); 19 | } 20 | 21 | content.push('?'); 22 | 23 | content 24 | } else { 25 | "There is no such command".to_owned() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /bathbot/src/commands/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod fun; 2 | pub mod help; 3 | pub mod osu; 4 | pub mod owner; 5 | pub mod songs; 6 | pub mod tracking; 7 | pub mod utility; 8 | 9 | #[cfg(feature = "twitchtracking")] 10 | pub mod twitch; 11 | 12 | const DISCORD_OPTION_DESC: &str = "Specify a linked discord user"; 13 | 14 | const DISCORD_OPTION_HELP: &str = "Instead of specifying an osu! username with \ 15 | the `name` option, you can use this option to choose a discord user.\nOnly \ 16 | works on users who have used the `/link` command."; 17 | -------------------------------------------------------------------------------- /bathbot/src/commands/osu/bookmarks/mod.rs: -------------------------------------------------------------------------------- 1 | mod message; 2 | mod slash; 3 | -------------------------------------------------------------------------------- /bathbot/src/commands/osu/daily_challenge/mod.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use bathbot_macros::{HasName, SlashCommand}; 4 | use eyre::Result; 5 | use twilight_interactions::command::{CommandModel, CreateCommand}; 6 | use twilight_model::id::{Id, marker::UserMarker}; 7 | 8 | use crate::{commands::{DISCORD_OPTION_DESC, DISCORD_OPTION_HELP}, util::{interaction::InteractionCommand, InteractionCommandExt}}; 9 | 10 | mod user; 11 | 12 | #[derive(CommandModel, CreateCommand, SlashCommand)] 13 | #[command(name = "dailychallenge", desc = "Daily challenge statistics")] 14 | pub enum DailyChallenge<'a> { 15 | #[command(name = "user")] 16 | User(DailyChallengeUser<'a>), 17 | } 18 | 19 | const DC_USER_DESC: &str = "Daily challenge statistics of a user"; 20 | 21 | #[derive(CommandModel, CreateCommand, HasName)] 22 | #[command(name = "user", desc = DC_USER_DESC)] 23 | pub struct DailyChallengeUser<'a> { 24 | #[command(desc = "Specify a username")] 25 | name: Option>, 26 | #[command(desc = DISCORD_OPTION_DESC, help = DISCORD_OPTION_HELP)] 27 | discord: Option>, 28 | } 29 | 30 | async fn slash_dailychallenge(mut command: InteractionCommand) -> Result<()> { 31 | match DailyChallenge::from_interaction(command.input_data())? { 32 | DailyChallenge::User(user) => user::user((&mut command).into(), user).await, 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /bathbot/src/commands/osu/link.rs: -------------------------------------------------------------------------------- 1 | use bathbot_macros::{SlashCommand, command}; 2 | use bathbot_util::constants::INVITE_LINK; 3 | use eyre::Result; 4 | use twilight_interactions::command::CreateCommand; 5 | 6 | use crate::{ 7 | commands::utility::{Config, ConfigLink, config}, 8 | util::{ChannelExt, interaction::InteractionCommand}, 9 | }; 10 | 11 | #[derive(CreateCommand, SlashCommand)] 12 | #[command( 13 | name = "link", 14 | desc = "Link your discord to an osu! profile", 15 | help = "Link your discord to an osu! profile.\n\ 16 | To unlink, use the `/config` command.\n\ 17 | To link your discord to a twitch account you can also use the `/config` command." 18 | )] 19 | #[flags(EPHEMERAL)] 20 | pub struct Link; 21 | 22 | async fn slash_link(command: InteractionCommand) -> Result<()> { 23 | let mut args = Config::default(); 24 | args.osu = Some(ConfigLink::Link); 25 | 26 | config(command, args).await 27 | } 28 | 29 | #[command] 30 | #[desc("Deprecated command, use the slash command `/link` instead")] 31 | #[flags(SKIP_DEFER)] 32 | #[group(AllModes)] 33 | async fn prefix_link(msg: &Message) -> Result<()> { 34 | let content = format!( 35 | "This command is deprecated and no longer works.\n\ 36 | Use the slash command `/link` instead (no need to specify your osu! name).\n\ 37 | If slash commands are not available in your server, \ 38 | try [re-inviting the bot]({INVITE_LINK})." 39 | ); 40 | 41 | msg.error(content).await?; 42 | 43 | Ok(()) 44 | } 45 | -------------------------------------------------------------------------------- /bathbot/src/commands/osu/osekai/rarity.rs: -------------------------------------------------------------------------------- 1 | use bathbot_model::Rarity; 2 | use bathbot_util::{Authored, constants::GENERAL_ISSUE}; 3 | use eyre::{Report, Result}; 4 | 5 | use crate::{ 6 | Context, 7 | active::{ActiveMessages, impls::MedalRarityPagination}, 8 | util::{InteractionCommandExt, interaction::InteractionCommand}, 9 | }; 10 | 11 | pub(super) async fn rarity(mut command: InteractionCommand) -> Result<()> { 12 | let ranking = match Context::redis().osekai_ranking::().await { 13 | Ok(ranking) => ranking, 14 | Err(err) => { 15 | let _ = command.error(GENERAL_ISSUE).await; 16 | 17 | return Err(Report::new(err).wrap_err("Failed to get cached rarity ranking")); 18 | } 19 | }; 20 | 21 | let pagination = MedalRarityPagination::builder() 22 | .ranking(ranking) 23 | .msg_owner(command.user_id()?) 24 | .build(); 25 | 26 | ActiveMessages::builder(pagination) 27 | .start_by_update(true) 28 | .begin(&mut command) 29 | .await 30 | } 31 | -------------------------------------------------------------------------------- /bathbot/src/commands/owner/cache.rs: -------------------------------------------------------------------------------- 1 | use bathbot_util::{EmbedBuilder, FooterBuilder, MessageBuilder, numbers::WithComma}; 2 | use eyre::Result; 3 | 4 | use crate::{ 5 | Context, 6 | util::{InteractionCommandExt, interaction::InteractionCommand}, 7 | }; 8 | 9 | pub async fn cache(command: InteractionCommand) -> Result<()> { 10 | let stats = Context::cache().stats(); 11 | 12 | let description = format!( 13 | "Guilds: {guilds}\n\ 14 | Unavailable guilds: {unavailable_guilds}\n\ 15 | Users: {users}\n\ 16 | Roles: {roles}\n\ 17 | Channels: {channels}", 18 | guilds = WithComma::new(stats.guilds), 19 | unavailable_guilds = WithComma::new(stats.unavailable_guilds), 20 | users = WithComma::new(stats.users), 21 | roles = WithComma::new(stats.roles), 22 | channels = WithComma::new(stats.channels), 23 | ); 24 | 25 | let embed = EmbedBuilder::new() 26 | .description(description) 27 | .footer(FooterBuilder::new("Boot time")) 28 | .timestamp(Context::get().start_time); 29 | 30 | let builder = MessageBuilder::new().embed(embed); 31 | command.callback(builder, false).await?; 32 | 33 | Ok(()) 34 | } 35 | -------------------------------------------------------------------------------- /bathbot/src/commands/owner/request_members.rs: -------------------------------------------------------------------------------- 1 | use bathbot_util::{MessageBuilder, constants::GENERAL_ISSUE}; 2 | use eyre::{Report, Result}; 3 | use twilight_model::id::Id; 4 | 5 | use crate::{ 6 | core::Context, 7 | util::{InteractionCommandExt, interaction::InteractionCommand}, 8 | }; 9 | 10 | pub async fn request_members(command: InteractionCommand, guild_id: &str) -> Result<()> { 11 | let Ok(Some(guild)) = guild_id.parse().map(Id::new_checked) else { 12 | command 13 | .error_callback("Must provide a valid guild id") 14 | .await?; 15 | 16 | return Ok(()); 17 | }; 18 | 19 | let ctx = Context::get(); 20 | 21 | let Some(shard_id) = ctx.guild_shards().pin().get(&guild).copied() else { 22 | let content = format!("No stored shard id for guild {guild}"); 23 | command.error_callback(content).await?; 24 | 25 | return Ok(()); 26 | }; 27 | 28 | ctx.member_requests 29 | .pending_guilds 30 | .lock() 31 | .unwrap() 32 | .insert(guild); 33 | 34 | match ctx.member_requests.tx.send((guild, shard_id)) { 35 | Ok(_) => { 36 | let content = "Successfully enqueued member request"; 37 | let builder = MessageBuilder::new().embed(content); 38 | command.callback(builder, false).await?; 39 | 40 | Ok(()) 41 | } 42 | Err(err) => { 43 | let _ = command.error_callback(GENERAL_ISSUE).await; 44 | 45 | Err(Report::new(err).wrap_err("Failed to forward member request")) 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /bathbot/src/commands/owner/reshard.rs: -------------------------------------------------------------------------------- 1 | use std::sync::OnceLock; 2 | 3 | use bathbot_util::{EmbedBuilder, MessageBuilder}; 4 | use eyre::Result; 5 | use tokio::sync::broadcast::Sender; 6 | 7 | use crate::util::{InteractionCommandExt, interaction::InteractionCommand}; 8 | 9 | pub static RESHARD_TX: OnceLock> = OnceLock::new(); 10 | 11 | pub async fn reshard(command: InteractionCommand) -> Result<()> { 12 | RESHARD_TX 13 | .get() 14 | .expect("RESHARD_TX has not been initialized") 15 | .send(()) 16 | .expect("RESHARD_RX has been dropped"); 17 | 18 | let embed = EmbedBuilder::new().description("Reshard message has been sent"); 19 | let builder = MessageBuilder::new().embed(embed); 20 | 21 | command.callback(builder, false).await?; 22 | 23 | Ok(()) 24 | } 25 | -------------------------------------------------------------------------------- /bathbot/src/commands/songs/bombsaway.rs: -------------------------------------------------------------------------------- 1 | use bathbot_macros::command; 2 | use eyre::Result; 3 | 4 | #[command] 5 | #[desc("https://youtu.be/xpkkakkDhN4?t=65")] 6 | #[group(Songs)] 7 | #[flags(SKIP_DEFER)] 8 | pub async fn prefix_bombsaway(msg: &Message) -> Result<()> { 9 | let (lyrics, delay) = bombsaway_(); 10 | 11 | super::song(lyrics, delay, msg.into()).await 12 | } 13 | 14 | pub fn bombsaway_() -> (&'static [&'static str], u64) { 15 | let lyrics = &[ 16 | "Tick tick tock and it's bombs awayyyy", 17 | "Come ooon, it's the only way", 18 | "Save your-self for a better dayyyy", 19 | "No, no, we are falling dooo-ooo-ooo-ooown", 20 | "I know, you know - this is over", 21 | "Tick tick tock and it's bombs awayyyy", 22 | "Now we're falling -- now we're falling doooown", 23 | ]; 24 | 25 | (lyrics, 2750) 26 | } 27 | -------------------------------------------------------------------------------- /bathbot/src/commands/songs/catchit.rs: -------------------------------------------------------------------------------- 1 | use bathbot_macros::command; 2 | use eyre::Result; 3 | 4 | #[command] 5 | #[desc("https://youtu.be/BjFWk0ncr70?t=12")] 6 | #[group(Songs)] 7 | #[flags(SKIP_DEFER)] 8 | async fn prefix_catchit(msg: &Message) -> Result<()> { 9 | let (lyrics, delay) = catchit_(); 10 | 11 | super::song(lyrics, delay, msg.into()).await 12 | } 13 | 14 | pub fn catchit_() -> (&'static [&'static str], u64) { 15 | let lyrics = &[ 16 | "This song is one you won't forget", 17 | "It will get stuck -- in your head", 18 | "If it does, then you can't blame me", 19 | "Just like I said - too catchy", 20 | ]; 21 | 22 | (lyrics, 2500) 23 | } 24 | -------------------------------------------------------------------------------- /bathbot/src/commands/songs/chicago.rs: -------------------------------------------------------------------------------- 1 | pub fn chicago_() -> (&'static [&'static str], u64) { 2 | let lyrics = &[ 3 | "Kimi no koe ga itsumo hibii teru yo", 4 | "Doramachikku na tenkai matenrou-sa", 5 | "Kimi no kage ga itsumo jama shi teru yo", 6 | "Romanchikku na koukai me no mae sa", 7 | ]; 8 | 9 | (lyrics, 4500) 10 | } 11 | -------------------------------------------------------------------------------- /bathbot/src/commands/songs/ding.rs: -------------------------------------------------------------------------------- 1 | use bathbot_macros::command; 2 | use eyre::Result; 3 | 4 | #[command] 5 | #[desc("https://youtu.be/_yWU0lFghxU?t=54")] 6 | #[group(Songs)] 7 | #[flags(SKIP_DEFER)] 8 | async fn prefix_ding(msg: &Message) -> Result<()> { 9 | let (lyrics, delay) = ding_(); 10 | 11 | super::song(lyrics, delay, msg.into()).await 12 | } 13 | 14 | pub fn ding_() -> (&'static [&'static str], u64) { 15 | let lyrics = &[ 16 | "Oh-oh-oh, hübsches Ding", 17 | "Ich versteck' mein' Ehering", 18 | "Klinglingeling, wir könnten's bring'n", 19 | "Doch wir nuckeln nur am Drink", 20 | "Oh-oh-oh, hübsches Ding", 21 | "Du bist Queen und ich bin King", 22 | "Wenn ich dich seh', dann muss ich sing'n", 23 | "Tingalingaling, you pretty thing!", 24 | ]; 25 | 26 | (lyrics, 2500) 27 | } 28 | -------------------------------------------------------------------------------- /bathbot/src/commands/songs/fireandflames.rs: -------------------------------------------------------------------------------- 1 | use bathbot_macros::command; 2 | use eyre::Result; 3 | 4 | #[command] 5 | #[desc("https://youtu.be/0jgrCKhxE1s?t=77")] 6 | #[group(Songs)] 7 | #[flags(SKIP_DEFER)] 8 | async fn prefix_fireandflames(msg: &Message) -> Result<()> { 9 | let (lyrics, delay) = fireandflames_(); 10 | 11 | super::song(lyrics, delay, msg.into()).await 12 | } 13 | 14 | pub fn fireandflames_() -> (&'static [&'static str], u64) { 15 | let lyrics = &[ 16 | "So far away we wait for the day-yay", 17 | "For the lives all so wasted and gooone", 18 | "We feel the pain of a lifetime lost in a thousand days", 19 | "Through the fire and the flames we carry ooooooon", 20 | ]; 21 | 22 | (lyrics, 3000) 23 | } 24 | -------------------------------------------------------------------------------- /bathbot/src/commands/songs/fireflies.rs: -------------------------------------------------------------------------------- 1 | use bathbot_macros::command; 2 | use eyre::Result; 3 | 4 | #[command] 5 | #[desc("https://youtu.be/psuRGfAaju4?t=25")] 6 | #[group(Songs)] 7 | #[flags(SKIP_DEFER)] 8 | async fn prefix_fireflies(msg: &Message) -> Result<()> { 9 | let (lyrics, delay) = fireflies_(); 10 | 11 | super::song(lyrics, delay, msg.into()).await 12 | } 13 | 14 | pub fn fireflies_() -> (&'static [&'static str], u64) { 15 | let lyrics = &[ 16 | "You would not believe your eyes", 17 | "If ten million fireflies", 18 | "Lit up the world as I fell asleep", 19 | "'Cause they'd fill the open air", 20 | "And leave teardrops everywhere", 21 | "You'd think me rude, but I would just stand and -- stare", 22 | ]; 23 | 24 | (lyrics, 2500) 25 | } 26 | -------------------------------------------------------------------------------- /bathbot/src/commands/songs/flamingo.rs: -------------------------------------------------------------------------------- 1 | use bathbot_macros::command; 2 | use eyre::Result; 3 | 4 | #[command] 5 | #[desc("https://youtu.be/la9C0n7jSsI")] 6 | #[group(Songs)] 7 | #[flags(SKIP_DEFER)] 8 | async fn prefix_flamingo(msg: &Message) -> Result<()> { 9 | let (lyrics, delay) = flamingo_(); 10 | 11 | super::song(lyrics, delay, msg.into()).await 12 | } 13 | 14 | pub fn flamingo_() -> (&'static [&'static str], u64) { 15 | let lyrics = &[ 16 | "How many shrimps do you have to eat", 17 | "before you make your skin turn pink?", 18 | "Eat too much and you'll get sick", 19 | "Shrimps are pretty rich", 20 | ]; 21 | 22 | (lyrics, 2500) 23 | } 24 | -------------------------------------------------------------------------------- /bathbot/src/commands/songs/glorydays.rs: -------------------------------------------------------------------------------- 1 | pub fn glorydays_() -> (&'static [&'static str], u64) { 2 | let lyrics = &[ 3 | "To seek the glory days", 4 | "We'll fight the lion's way", 5 | "Then let the rain wash", 6 | "All of our pride away", 7 | "So if this victory", 8 | "Is our last odyssey", 9 | "Then let the power within us deciiiide", 10 | ]; 11 | 12 | (lyrics, 2750) 13 | } 14 | -------------------------------------------------------------------------------- /bathbot/src/commands/songs/harumachi.rs: -------------------------------------------------------------------------------- 1 | pub fn harumachi_() -> (&'static [&'static str], u64) { 2 | let lyrics = &[ 3 | "Me no mae no tobira o ake tara harukaze", 4 | "Tori tachi mo kigi de machiawase", 5 | "Kimi e mukau shingō wa aozora iro", 6 | "Kakedase ba ii", 7 | "Usotsuki kakuritsu ron toka", 8 | "Ichi purasu ichi ga mugen toka", 9 | "Oshie te kure ta kimi to sagashi ni ikou", 10 | "Haru machi cloveeeeer", 11 | ]; 12 | 13 | (lyrics, 3650) 14 | } 15 | -------------------------------------------------------------------------------- /bathbot/src/commands/songs/hitorigoto.rs: -------------------------------------------------------------------------------- 1 | pub fn hitorigoto_() -> (&'static [&'static str], u64) { 2 | let lyrics = &[ 3 | "Futo shita toki ni sagashiteiru yo", 4 | "Kimi no egao wo sagashiteiru yo", 5 | "Muishiki no naka sono riyuu -", 6 | "wa mada ienai kedo", 7 | "Hitori de iru to aitakunaru yo", 8 | "Dare to itatte aitakunaru yo", 9 | "Tatta hitokoto nee, doushite Aah", 10 | "Ienai sono kotoba -", 11 | "ienai kono kimochi Aaah", 12 | "Hayaku kizuite hoshii no niiii", 13 | ]; 14 | 15 | (lyrics, 3200) 16 | } 17 | -------------------------------------------------------------------------------- /bathbot/src/commands/songs/lionheart.rs: -------------------------------------------------------------------------------- 1 | pub fn lionheart_() -> (&'static [&'static str], u64) { 2 | let lyrics = &[ 3 | "Like a lion we fight, together we will die", 4 | "For the glory of our god --", 5 | "Justice on our side, this cross will lead the light", 6 | "Follow Richard Lionheart --", 7 | ]; 8 | 9 | (lyrics, 5400) 10 | } 11 | -------------------------------------------------------------------------------- /bathbot/src/commands/songs/mylove.rs: -------------------------------------------------------------------------------- 1 | pub fn mylove_() -> (&'static [&'static str], u64) { 2 | let lyrics = &[ 3 | "I wanna be your man,", 4 | "your lover and your friend.", 5 | "I'm gonna love you true.", 6 | "I wanna be the one -", 7 | "you come - home - to", 8 | "I'm gonna treat you right.", 9 | "I'll do ya every night,", 10 | "myyyy looove", 11 | ]; 12 | 13 | (lyrics, 1800) 14 | } 15 | -------------------------------------------------------------------------------- /bathbot/src/commands/songs/padoru.rs: -------------------------------------------------------------------------------- 1 | pub fn padoru_() -> (&'static [&'static str], u64) { 2 | let lyrics = &[ 3 | "HASHIRE SORI YO", 4 | "KAZE NO YOU NI", 5 | "TSUKIMIHARA WO", 6 | "PADORU PADORUUUU", 7 | ]; 8 | 9 | (lyrics, 2500) 10 | } 11 | -------------------------------------------------------------------------------- /bathbot/src/commands/songs/pretender.rs: -------------------------------------------------------------------------------- 1 | use bathbot_macros::command; 2 | use eyre::Result; 3 | 4 | #[command] 5 | #[desc("https://youtu.be/SBjQ9tuuTJQ?t=83")] 6 | #[group(Songs)] 7 | #[flags(SKIP_DEFER)] 8 | async fn prefix_pretender(msg: &Message) -> Result<()> { 9 | let (lyrics, delay) = pretender_(); 10 | 11 | super::song(lyrics, delay, msg.into()).await 12 | } 13 | 14 | pub fn pretender_() -> (&'static [&'static str], u64) { 15 | let lyrics = &[ 16 | "What if I say I'm not like the others?", 17 | "What if I say I'm not just another oooone of your plays?", 18 | "You're the pretender", 19 | "What if I say that I will never surrender?", 20 | ]; 21 | 22 | (lyrics, 3000) 23 | } 24 | -------------------------------------------------------------------------------- /bathbot/src/commands/songs/rockefeller.rs: -------------------------------------------------------------------------------- 1 | use bathbot_macros::command; 2 | use eyre::Result; 3 | 4 | #[command] 5 | #[desc("https://youtu.be/hjGZLnja1o8?t=41")] 6 | #[group(Songs)] 7 | #[alias("1273")] 8 | #[flags(SKIP_DEFER)] 9 | pub async fn prefix_rockefeller(msg: &Message) -> Result<()> { 10 | let (lyrics, delay) = rockefeller_(); 11 | 12 | super::song(lyrics, delay, msg.into()).await 13 | } 14 | 15 | pub fn rockefeller_() -> (&'static [&'static str], u64) { 16 | let lyrics = &[ 17 | "1 - 2 - 7 - 3", 18 | "down the Rockefeller street.", 19 | "Life is marchin' on, do you feel that?", 20 | "1 - 2 - 7 - 3", 21 | "down the Rockefeller street.", 22 | "Everything is more than surreal", 23 | ]; 24 | 25 | (lyrics, 2250) 26 | } 27 | -------------------------------------------------------------------------------- /bathbot/src/commands/songs/saygoodbye.rs: -------------------------------------------------------------------------------- 1 | use bathbot_macros::command; 2 | use eyre::Result; 3 | 4 | #[command] 5 | #[desc("https://youtu.be/SyJMQg3spck?t=43")] 6 | #[group(Songs)] 7 | #[flags(SKIP_DEFER)] 8 | async fn prefix_saygoodbye(msg: &Message) -> Result<()> { 9 | let (lyrics, delay) = saygoodbye_(); 10 | 11 | super::song(lyrics, delay, msg.into()).await 12 | } 13 | 14 | pub fn saygoodbye_() -> (&'static [&'static str], u64) { 15 | let lyrics = &[ 16 | "It still kills meeee", 17 | "(it - still - kills - me)", 18 | "That I can't change thiiiings", 19 | "(that I - can't - change - things)", 20 | "But I'm still dreaming", 21 | "I'll rewrite the ending", 22 | "So you'll take back the lies", 23 | "Before we say our goodbyes", 24 | "\\~\\~\\~ say our goodbyyeees \\~\\~\\~", 25 | ]; 26 | 27 | (lyrics, 2500) 28 | } 29 | -------------------------------------------------------------------------------- /bathbot/src/commands/songs/startagain.rs: -------------------------------------------------------------------------------- 1 | use bathbot_macros::command; 2 | use eyre::Result; 3 | 4 | #[command] 5 | #[desc("https://youtu.be/g7VNvg_QTMw&t=29")] 6 | #[group(Songs)] 7 | #[flags(SKIP_DEFER)] 8 | async fn prefix_startagain(msg: &Message) -> Result<()> { 9 | let (lyrics, delay) = startagain_(); 10 | 11 | super::song(lyrics, delay, msg.into()).await 12 | } 13 | 14 | pub fn startagain_() -> (&'static [&'static str], u64) { 15 | let lyrics = &[ 16 | "I'm not always perfect, but I'm always myself.", 17 | "If you don't think I'm worth it - find someone eeeelse.", 18 | "I won't say I'm sorry, for being who I aaaaaam.", 19 | "Is the eeeend a chance to start agaaaaain?", 20 | ]; 21 | 22 | (lyrics, 5500) 23 | } 24 | -------------------------------------------------------------------------------- /bathbot/src/commands/songs/tijdmachine.rs: -------------------------------------------------------------------------------- 1 | use bathbot_macros::command; 2 | use eyre::Result; 3 | 4 | #[command] 5 | #[desc("https://youtu.be/DT6tpUbWOms?t=47")] 6 | #[group(Songs)] 7 | #[flags(SKIP_DEFER)] 8 | async fn prefix_tijdmachine(msg: &Message) -> Result<()> { 9 | let (lyrics, delay) = tijdmachine_(); 10 | 11 | super::song(lyrics, delay, msg.into()).await 12 | } 13 | 14 | pub fn tijdmachine_() -> (&'static [&'static str], u64) { 15 | let lyrics = &[ 16 | "Als ik denk aan al die dagen,", 17 | "dat ik mij zo heb misdragen.", 18 | "Dan denk ik, - had ik maar een tijdmachine -- tijdmachine", 19 | "Maar die heb ik niet,", 20 | "dus zal ik mij gedragen,", 21 | "en zal ik blijven sparen,", 22 | "sparen voor een tiiijdmaaachine.", 23 | ]; 24 | 25 | (lyrics, 2500) 26 | } 27 | -------------------------------------------------------------------------------- /bathbot/src/commands/songs/time_traveler.rs: -------------------------------------------------------------------------------- 1 | pub fn time_traveler_() -> (&'static [&'static str], u64) { 2 | let lyrics = &[ 3 | "You're like a \\~\\~ time traveler", 4 | "You like to \\~\\~ go backwards", 5 | "You're like a \\~\\~ time traveler", 6 | "Running from \\~\\~ the future", 7 | ]; 8 | 9 | (lyrics, 3100) 10 | } 11 | -------------------------------------------------------------------------------- /bathbot/src/commands/songs/wordsneversaid.rs: -------------------------------------------------------------------------------- 1 | pub fn wordsneversaid_() -> (&'static [&'static str], u64) { 2 | let lyrics = &[ 3 | "It's so loooouuud insiiide my head.", 4 | "With words that I - should have said.", 5 | "As I drooooown in my regrets.", 6 | "I can't take back - the words I never said.", 7 | "I never saaaiid...", 8 | ]; 9 | 10 | (lyrics, 6000) 11 | } 12 | -------------------------------------------------------------------------------- /bathbot/src/commands/songs/zenzenzense.rs: -------------------------------------------------------------------------------- 1 | pub fn zenzenzense_() -> (&'static [&'static str], u64) { 2 | let lyrics = &[ 3 | "Kimi no zen zen zense kara boku wa kimi wo sagashi hajimeta yo", 4 | "Sono bukiccho na waraikata wo megakete yatte kitanda yo", 5 | "Kimi ga zenzen zenbu naku natte chirijiri ni nattatte", 6 | "Mou mayowanai mata ichi kara sagashi hajimeru sa", 7 | "Mushiro zero kara mata uchuu wo hajimete miyou ka", 8 | ]; 9 | 10 | (lyrics, 4900) 11 | } 12 | -------------------------------------------------------------------------------- /bathbot/src/commands/twitch/tracked.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Write; 2 | 3 | use bathbot_macros::command; 4 | use bathbot_util::{MessageBuilder, constants::GENERAL_ISSUE}; 5 | use eyre::Result; 6 | 7 | use crate::{Context, core::commands::CommandOrigin}; 8 | 9 | #[command] 10 | #[desc("List all streams that are tracked in a channel")] 11 | #[alias("tracked")] 12 | #[group(Twitch)] 13 | async fn prefix_trackedstreams(msg: &Message) -> Result<()> { 14 | tracked(msg.into()).await 15 | } 16 | 17 | pub async fn tracked(orig: CommandOrigin<'_>) -> Result<()> { 18 | let twitch_ids = Context::tracked_users_in(orig.channel_id()); 19 | 20 | let mut twitch_users: Vec<_> = match Context::client().get_twitch_users(&twitch_ids).await { 21 | Ok(users) => users.into_iter().map(|user| user.display_name).collect(), 22 | Err(err) => { 23 | let _ = orig.error(GENERAL_ISSUE).await; 24 | 25 | return Err(err.wrap_err("failed to get twitch users")); 26 | } 27 | }; 28 | 29 | twitch_users.sort_unstable(); 30 | let mut content = "Tracked twitch streams in this channel:\n".to_owned(); 31 | let mut users = twitch_users.into_iter(); 32 | 33 | if let Some(user) = users.next() { 34 | let _ = write!(content, "`{user}`"); 35 | 36 | for user in users { 37 | let _ = write!(content, ", `{user}`"); 38 | } 39 | } else { 40 | content.push_str("None"); 41 | } 42 | 43 | let builder = MessageBuilder::new().embed(content); 44 | orig.create_message(builder).await?; 45 | 46 | Ok(()) 47 | } 48 | -------------------------------------------------------------------------------- /bathbot/src/commands/utility/invite.rs: -------------------------------------------------------------------------------- 1 | use bathbot_macros::{SlashCommand, command}; 2 | use bathbot_util::{ 3 | EmbedBuilder, FooterBuilder, MessageBuilder, 4 | constants::{BATHBOT_WORKSHOP, INVITE_LINK}, 5 | }; 6 | use eyre::Result; 7 | use twilight_interactions::command::CreateCommand; 8 | 9 | use crate::{core::commands::CommandOrigin, util::interaction::InteractionCommand}; 10 | 11 | #[derive(CreateCommand, SlashCommand)] 12 | #[command(name = "invite", desc = "Invite me to your server")] 13 | #[flags(SKIP_DEFER)] 14 | pub struct Invite; 15 | 16 | #[command] 17 | #[desc("Invite me to your server")] 18 | #[alias("inv")] 19 | #[flags(SKIP_DEFER)] 20 | #[group(Utility)] 21 | async fn prefix_invite(msg: &Message) -> Result<()> { 22 | invite(msg.into()).await 23 | } 24 | 25 | pub async fn slash_invite(mut command: InteractionCommand) -> Result<()> { 26 | invite((&mut command).into()).await 27 | } 28 | 29 | async fn invite(orig: CommandOrigin<'_>) -> Result<()> { 30 | let embed = EmbedBuilder::new() 31 | .description(INVITE_LINK) 32 | .footer(FooterBuilder::new("The initial prefix will be <")) 33 | .title("Invite me to your server!"); 34 | 35 | let builder = MessageBuilder::new().content(BATHBOT_WORKSHOP).embed(embed); 36 | orig.callback(builder).await?; 37 | 38 | Ok(()) 39 | } 40 | -------------------------------------------------------------------------------- /bathbot/src/commands/utility/mod.rs: -------------------------------------------------------------------------------- 1 | mod authorities; 2 | mod changelog; 3 | mod commands; 4 | mod config; 5 | mod embed_builder; 6 | mod invite; 7 | mod ping; 8 | mod prefix; 9 | mod roll; 10 | mod server_config; 11 | mod skin; 12 | 13 | #[allow(unused_imports)] 14 | pub use self::{authorities::*, changelog::*, config::*, embed_builder::*, skin::*}; 15 | -------------------------------------------------------------------------------- /bathbot/src/core/commands/flags.rs: -------------------------------------------------------------------------------- 1 | bitflags::bitflags! { 2 | #[derive(Copy, Clone)] 3 | pub struct CommandFlags: u8 { 4 | const AUTHORITY = 1 << 0; 5 | const EPHEMERAL = 1 << 1; 6 | const ONLY_GUILDS = 1 << 2; 7 | const ONLY_OWNER = 1 << 3; 8 | const SKIP_DEFER = 1 << 4; 9 | } 10 | } 11 | 12 | impl CommandFlags { 13 | pub fn authority(self) -> bool { 14 | self.contains(CommandFlags::AUTHORITY) 15 | } 16 | 17 | pub fn defer(self) -> bool { 18 | !self.contains(CommandFlags::SKIP_DEFER) 19 | } 20 | 21 | pub fn ephemeral(self) -> bool { 22 | self.contains(CommandFlags::EPHEMERAL) 23 | } 24 | 25 | pub fn only_guilds(self) -> bool { 26 | self.contains(CommandFlags::ONLY_GUILDS) 27 | } 28 | 29 | pub fn only_owner(self) -> bool { 30 | self.contains(CommandFlags::ONLY_OWNER) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /bathbot/src/core/commands/mod.rs: -------------------------------------------------------------------------------- 1 | pub use self::{ 2 | flags::CommandFlags, 3 | origin::{CommandOrigin, OwnedCommandOrigin}, 4 | }; 5 | 6 | mod flags; 7 | mod origin; 8 | 9 | pub mod checks; 10 | pub mod interaction; 11 | pub mod prefix; 12 | -------------------------------------------------------------------------------- /bathbot/src/core/commands/prefix/command.rs: -------------------------------------------------------------------------------- 1 | use bathbot_util::BucketName; 2 | use twilight_model::{channel::Message, guild::Permissions}; 3 | 4 | use super::{Args, CommandResult, PrefixCommandGroup}; 5 | use crate::core::commands::flags::CommandFlags; 6 | 7 | pub struct PrefixCommand { 8 | pub names: &'static [&'static str], 9 | pub desc: &'static str, 10 | pub help: Option<&'static str>, 11 | pub usage: Option<&'static str>, 12 | pub examples: &'static [&'static str], 13 | pub bucket: Option, 14 | pub flags: CommandFlags, 15 | pub group: PrefixCommandGroup, 16 | pub exec: for<'f> fn(&'f Message, Args<'f>, Option) -> CommandResult<'f>, 17 | } 18 | 19 | impl PrefixCommand { 20 | pub fn name(&self) -> &str { 21 | self.names[0] 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /bathbot/src/core/context/games.rs: -------------------------------------------------------------------------------- 1 | use super::BgGames; 2 | use crate::Context; 3 | 4 | impl Context { 5 | pub fn bg_games() -> &'static BgGames { 6 | &Context::get().data.games.bg 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /bathbot/src/core/context/osutrack.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use papaya::HashMap as PapayaMap; 4 | use rosu_v2::model::GameMode; 5 | use time::OffsetDateTime; 6 | 7 | use super::Context; 8 | 9 | /// Mapping user ids to the last timestamp that osutrack was notified of that 10 | /// user's activity. 11 | pub type OsuTrackUserNotifTimestamps = PapayaMap<(u32, GameMode), OffsetDateTime>; 12 | 13 | impl Context { 14 | pub async fn notify_osutrack_of_user_activity(&self, user_id: u32, mode: GameMode) { 15 | const DAY: Duration = Duration::from_secs(60 * 60 * 24); 16 | 17 | let key = (user_id, mode); 18 | 19 | let should_notify = match self.data.osutrack_user_notif_timestamps.pin().get(&key) { 20 | Some(timestamp) => *timestamp < OffsetDateTime::now_utc() - DAY, 21 | None => true, 22 | }; 23 | 24 | if !should_notify || !cfg!(feature = "notify_osutrack") { 25 | return; 26 | } 27 | 28 | let notify_fut = self 29 | .clients 30 | .custom 31 | .notify_osutrack_user_activity(user_id, mode); 32 | 33 | if let Err(err) = notify_fut.await { 34 | warn!( 35 | user_id, 36 | %mode, 37 | ?err, 38 | "Failed to notify osutrack of user activity", 39 | ); 40 | } 41 | 42 | self.data 43 | .osutrack_user_notif_timestamps 44 | .pin() 45 | .insert(key, OffsetDateTime::now_utc()); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /bathbot/src/core/events/interaction/autocomplete.rs: -------------------------------------------------------------------------------- 1 | use std::{mem, time::Instant}; 2 | 3 | use crate::{ 4 | commands::{ 5 | help::slash_help, 6 | osu::{slash_badges, slash_cs, slash_medal}, 7 | }, 8 | core::{BotMetrics, events::EventKind}, 9 | util::interaction::InteractionCommand, 10 | }; 11 | 12 | pub async fn handle_autocomplete(mut command: InteractionCommand) { 13 | let start = Instant::now(); 14 | 15 | let name = mem::take(&mut command.data.name); 16 | EventKind::Autocomplete.log(&command, &name).await; 17 | 18 | let res = match name.as_str() { 19 | "help" => slash_help(command).await, 20 | "badges" => slash_badges(command).await, 21 | "medal" => slash_medal(command).await, 22 | "cs" | "compare" | "score" => slash_cs(command).await, 23 | _ => return error!(name, "Unknown autocomplete command"), 24 | }; 25 | 26 | if let Err(err) = res { 27 | BotMetrics::inc_command_error("autocomplete", name.clone()); 28 | error!(name, ?err, "Failed to process autocomplete"); 29 | } 30 | 31 | let elapsed = start.elapsed(); 32 | BotMetrics::observe_command("autocomplete", name, elapsed); 33 | } 34 | -------------------------------------------------------------------------------- /bathbot/src/core/mod.rs: -------------------------------------------------------------------------------- 1 | pub use self::{ 2 | config::BotConfig, 3 | context::Context, 4 | events::{EventKind, event_loop}, 5 | metrics::BotMetrics, 6 | }; 7 | 8 | mod config; 9 | mod context; 10 | mod events; 11 | mod metrics; 12 | 13 | pub mod commands; 14 | pub mod logging; 15 | -------------------------------------------------------------------------------- /bathbot/src/embeds/mod.rs: -------------------------------------------------------------------------------- 1 | use bathbot_util::EmbedBuilder; 2 | 3 | pub use self::{osu::*, utility::*}; 4 | 5 | mod osu; 6 | mod utility; 7 | 8 | pub trait EmbedData { 9 | fn build(self) -> EmbedBuilder; 10 | } 11 | -------------------------------------------------------------------------------- /bathbot/src/embeds/utility/mod.rs: -------------------------------------------------------------------------------- 1 | mod config; 2 | mod server_config; 3 | 4 | pub use self::{config::ConfigEmbed, server_config::ServerConfigEmbed}; 5 | -------------------------------------------------------------------------------- /bathbot/src/manager/bookmarks.rs: -------------------------------------------------------------------------------- 1 | use bathbot_psql::{Database, model::osu::MapBookmark}; 2 | use eyre::{Result, WrapErr}; 3 | use twilight_model::id::{Id, marker::UserMarker}; 4 | 5 | use crate::core::Context; 6 | 7 | #[derive(Copy, Clone)] 8 | pub struct BookmarkManager { 9 | psql: &'static Database, 10 | } 11 | 12 | impl BookmarkManager { 13 | pub fn new() -> Self { 14 | Self { 15 | psql: Context::psql(), 16 | } 17 | } 18 | 19 | pub async fn get(self, user: Id) -> Result> { 20 | self.psql 21 | .select_user_bookmarks(user) 22 | .await 23 | .wrap_err("Failed to get bookmarks") 24 | } 25 | 26 | pub async fn add(self, user: Id, map_id: u32) -> Result<()> { 27 | self.psql 28 | .insert_user_bookmark(user, map_id) 29 | .await 30 | .wrap_err("Failed to insert user bookmark") 31 | } 32 | 33 | pub async fn remove(self, user: Id, map_id: u32) -> Result<()> { 34 | self.psql 35 | .delete_user_bookmark(user, map_id) 36 | .await 37 | .wrap_err("Failed to delete user bookmark") 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /bathbot/src/manager/huismetbenen_country.rs: -------------------------------------------------------------------------------- 1 | use bathbot_util::CowUtils; 2 | use rosu_v2::model::GameMode; 3 | 4 | use super::redis::RedisManager; 5 | 6 | #[derive(Copy, Clone)] 7 | pub struct HuismetbenenCountryManager; 8 | 9 | impl HuismetbenenCountryManager { 10 | pub fn new() -> Self { 11 | Self 12 | } 13 | 14 | #[allow(clippy::wrong_self_convention)] 15 | pub async fn is_supported(self, country_code: &str, mode: GameMode) -> bool { 16 | let country_code = country_code.cow_to_ascii_uppercase(); 17 | 18 | match RedisManager::new().snipe_countries(mode).await { 19 | Ok(countries) => countries.contains(country_code.as_ref()), 20 | Err(err) => { 21 | warn!( 22 | country_code = country_code.as_ref(), 23 | ?err, 24 | "Failed to check if country code contained" 25 | ); 26 | 27 | false 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /bathbot/src/manager/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "twitch")] 2 | pub use self::twitch::TwitchManager; 3 | pub use self::{ 4 | bookmarks::BookmarkManager, 5 | games::GameManager, 6 | github::GithubManager, 7 | guild_config::GuildConfigManager, 8 | huismetbenen_country::HuismetbenenCountryManager, 9 | osu_map::{MapError, MapManager, OsuMap, OsuMapSlim}, 10 | osu_scores::ScoresManager, 11 | osu_user::OsuUserManager, 12 | pp::{Mods, PpManager}, 13 | rank_pp_approx::ApproxManager, 14 | replay::{ReplayError, ReplayManager, ReplaySettings}, 15 | user_config::UserConfigManager, 16 | }; 17 | 18 | pub mod redis; 19 | 20 | mod bookmarks; 21 | mod games; 22 | mod github; 23 | mod guild_config; 24 | mod huismetbenen_country; 25 | mod osu_map; 26 | mod osu_scores; 27 | mod osu_user; 28 | mod pp; 29 | mod rank_pp_approx; 30 | mod replay; 31 | mod user_config; 32 | 33 | #[cfg(feature = "twitch")] 34 | mod twitch; 35 | -------------------------------------------------------------------------------- /bathbot/src/manager/rank_pp_approx.rs: -------------------------------------------------------------------------------- 1 | use bathbot_psql::Database; 2 | use eyre::{Result, WrapErr}; 3 | use rosu_v2::prelude::GameMode; 4 | 5 | use crate::core::Context; 6 | 7 | #[derive(Copy, Clone)] 8 | pub struct ApproxManager { 9 | psql: &'static Database, 10 | } 11 | 12 | impl ApproxManager { 13 | pub fn new() -> Self { 14 | Self { 15 | psql: Context::psql(), 16 | } 17 | } 18 | 19 | pub async fn rank(self, pp: f32, mode: GameMode) -> Result { 20 | self.psql 21 | .select_rank_approx_by_pp(pp, mode) 22 | .await 23 | .wrap_err("failed to approximate rank") 24 | } 25 | 26 | pub async fn pp(self, rank: u32, mode: GameMode) -> Result { 27 | self.psql 28 | .select_pp_approx_by_rank(rank, mode) 29 | .await 30 | .wrap_err("failed to approximate pp") 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /bathbot/src/tracking/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "twitch")] 2 | pub use self::twitch::online_streams::OnlineTwitchStreams; 3 | #[cfg(feature = "twitchtracking")] 4 | pub use self::twitch::twitch_loop::twitch_tracking_loop; 5 | pub use self::{ 6 | ordr::{Ordr, OrdrReceivers}, 7 | osu::{OsuTracking, TrackEntryParams}, 8 | scores_ws::{ScoresWebSocket, ScoresWebSocketDisconnect}, 9 | }; 10 | 11 | mod ordr; 12 | mod osu; 13 | mod scores_ws; 14 | 15 | #[cfg(feature = "twitch")] 16 | mod twitch; 17 | -------------------------------------------------------------------------------- /bathbot/src/tracking/twitch/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "twitch")] 2 | pub mod online_streams; 3 | 4 | #[cfg(feature = "twitchtracking")] 5 | pub mod twitch_loop; 6 | -------------------------------------------------------------------------------- /bathbot/src/tracking/twitch/online_streams.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Borrow; 2 | 3 | use bathbot_model::TwitchStream; 4 | use bathbot_util::IntHasher; 5 | use papaya::{Guard, HashMap as PapayaMap}; 6 | 7 | #[derive(Copy, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)] 8 | pub struct TwitchUserId(u64); 9 | 10 | impl From for TwitchUserId { 11 | fn from(user_id: u64) -> Self { 12 | Self(user_id) 13 | } 14 | } 15 | 16 | impl Borrow for TwitchUserId { 17 | fn borrow(&self) -> &u64 { 18 | &self.0 19 | } 20 | } 21 | 22 | pub struct TwitchStreamId( 23 | // false positive; used when logging 24 | #[allow(unused)] u64, 25 | ); 26 | 27 | impl From for TwitchStreamId { 28 | fn from(stream_id: u64) -> Self { 29 | Self(stream_id) 30 | } 31 | } 32 | 33 | #[derive(Default)] 34 | pub struct OnlineTwitchStreams { 35 | user_streams: PapayaMap, 36 | } 37 | 38 | impl OnlineTwitchStreams { 39 | pub fn guard(&self) -> impl Guard + '_ { 40 | self.user_streams.guard() 41 | } 42 | 43 | pub fn set_online(&self, stream: &TwitchStream, guard: &impl Guard) { 44 | self.user_streams 45 | .insert(stream.user_id.into(), stream.stream_id.into(), guard); 46 | } 47 | 48 | pub fn set_offline(&self, stream: &TwitchStream, guard: &impl Guard) { 49 | self.user_streams.remove(&stream.user_id, guard); 50 | } 51 | 52 | pub fn set_offline_by_user(&self, user: u64) { 53 | self.user_streams.pin().remove(&user); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /bathbot/src/util/check_permissions.rs: -------------------------------------------------------------------------------- 1 | use twilight_model::guild::Permissions; 2 | 3 | use super::interaction::{InteractionCommand, InteractionComponent, InteractionModal}; 4 | use crate::core::commands::CommandOrigin; 5 | 6 | pub trait CheckPermissions { 7 | fn permissions(&self) -> Option; 8 | 9 | fn can_attach_file(&self) -> bool { 10 | self.has_permission_to(Permissions::ATTACH_FILES) 11 | } 12 | 13 | fn can_create_thread(&self) -> bool { 14 | self.has_permission_to(Permissions::CREATE_PUBLIC_THREADS) 15 | } 16 | 17 | fn can_view_channel(&self) -> bool { 18 | self.has_permission_to(Permissions::VIEW_CHANNEL) 19 | } 20 | 21 | fn has_permission_to(&self, permission: Permissions) -> bool { 22 | self.permissions().is_none_or(|p| p.contains(permission)) 23 | } 24 | } 25 | 26 | impl CheckPermissions for CommandOrigin<'_> { 27 | fn permissions(&self) -> Option { 28 | match self { 29 | CommandOrigin::Message { permissions, .. } => *permissions, 30 | CommandOrigin::Interaction { command } => command.permissions, 31 | } 32 | } 33 | } 34 | 35 | impl CheckPermissions for InteractionCommand { 36 | fn permissions(&self) -> Option { 37 | self.permissions 38 | } 39 | } 40 | 41 | impl CheckPermissions for InteractionComponent { 42 | fn permissions(&self) -> Option { 43 | self.permissions 44 | } 45 | } 46 | 47 | impl CheckPermissions for InteractionModal { 48 | fn permissions(&self) -> Option { 49 | self.permissions 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /bathbot/src/util/ext/mod.rs: -------------------------------------------------------------------------------- 1 | pub use self::{ 2 | cached_user::CachedUserExt, 3 | channel::ChannelExt, 4 | component::ComponentExt, 5 | interaction_command::{InteractionCommandExt, InteractionToken}, 6 | message::MessageExt, 7 | modal::*, 8 | }; 9 | 10 | mod cached_user; 11 | mod channel; 12 | mod component; 13 | mod interaction_command; 14 | mod message; 15 | mod modal; 16 | -------------------------------------------------------------------------------- /bathbot/src/util/mod.rs: -------------------------------------------------------------------------------- 1 | pub use self::{ 2 | check_permissions::CheckPermissions, 3 | emote::{CustomEmote, Emote}, 4 | ext::*, 5 | monthly::Monthly, 6 | searchable::NativeCriteria, 7 | }; 8 | 9 | pub mod interaction; 10 | pub mod osu; 11 | 12 | mod check_permissions; 13 | mod emote; 14 | mod ext; 15 | mod monthly; 16 | mod searchable; 17 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | 3 | services: 4 | psql: 5 | container_name: bathbot-db 6 | image: postgres:alpine 7 | restart: unless-stopped 8 | environment: 9 | - POSTGRES_USER=bathbot 10 | - POSTGRES_PASSWORD=bathbot 11 | - POSTGRES_DB=bathbot 12 | ports: 13 | - 2345:5432 14 | volumes: 15 | - ./docker-volume/psql:/var/lib/postgresql/data 16 | networks: 17 | - bathbot-net 18 | 19 | redis: 20 | container_name: bathbot-redis 21 | image: redis:alpine 22 | restart: unless-stopped 23 | ports: 24 | - 9736:6379 25 | volumes: 26 | - ./docker-volume/redis:/data 27 | networks: 28 | - bathbot-net 29 | 30 | networks: 31 | bathbot-net: 32 | driver: bridge -------------------------------------------------------------------------------- /media/emotes/A_.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxOhn/Bathbot/4eff2f918d716002483b8661fa5a64521b73e155/media/emotes/A_.png -------------------------------------------------------------------------------- /media/emotes/B_.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxOhn/Bathbot/4eff2f918d716002483b8661fa5a64521b73e155/media/emotes/B_.png -------------------------------------------------------------------------------- /media/emotes/C_.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxOhn/Bathbot/4eff2f918d716002483b8661fa5a64521b73e155/media/emotes/C_.png -------------------------------------------------------------------------------- /media/emotes/D_.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxOhn/Bathbot/4eff2f918d716002483b8661fa5a64521b73e155/media/emotes/D_.png -------------------------------------------------------------------------------- /media/emotes/F_.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxOhn/Bathbot/4eff2f918d716002483b8661fa5a64521b73e155/media/emotes/F_.png -------------------------------------------------------------------------------- /media/emotes/SH.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxOhn/Bathbot/4eff2f918d716002483b8661fa5a64521b73e155/media/emotes/SH.png -------------------------------------------------------------------------------- /media/emotes/S_.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxOhn/Bathbot/4eff2f918d716002483b8661fa5a64521b73e155/media/emotes/S_.png -------------------------------------------------------------------------------- /media/emotes/XH.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxOhn/Bathbot/4eff2f918d716002483b8661fa5a64521b73e155/media/emotes/XH.png -------------------------------------------------------------------------------- /media/emotes/X_.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxOhn/Bathbot/4eff2f918d716002483b8661fa5a64521b73e155/media/emotes/X_.png -------------------------------------------------------------------------------- /media/emotes/bpm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxOhn/Bathbot/4eff2f918d716002483b8661fa5a64521b73e155/media/emotes/bpm.png -------------------------------------------------------------------------------- /media/emotes/count_objects.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxOhn/Bathbot/4eff2f918d716002483b8661fa5a64521b73e155/media/emotes/count_objects.png -------------------------------------------------------------------------------- /media/emotes/count_sliders.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxOhn/Bathbot/4eff2f918d716002483b8661fa5a64521b73e155/media/emotes/count_sliders.png -------------------------------------------------------------------------------- /media/emotes/count_spinners.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxOhn/Bathbot/4eff2f918d716002483b8661fa5a64521b73e155/media/emotes/count_spinners.png -------------------------------------------------------------------------------- /media/emotes/end.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxOhn/Bathbot/4eff2f918d716002483b8661fa5a64521b73e155/media/emotes/end.png -------------------------------------------------------------------------------- /media/emotes/expand.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxOhn/Bathbot/4eff2f918d716002483b8661fa5a64521b73e155/media/emotes/expand.png -------------------------------------------------------------------------------- /media/emotes/minimize.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxOhn/Bathbot/4eff2f918d716002483b8661fa5a64521b73e155/media/emotes/minimize.png -------------------------------------------------------------------------------- /media/emotes/miss.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxOhn/Bathbot/4eff2f918d716002483b8661fa5a64521b73e155/media/emotes/miss.png -------------------------------------------------------------------------------- /media/emotes/my_position.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxOhn/Bathbot/4eff2f918d716002483b8661fa5a64521b73e155/media/emotes/my_position.png -------------------------------------------------------------------------------- /media/emotes/osu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxOhn/Bathbot/4eff2f918d716002483b8661fa5a64521b73e155/media/emotes/osu.png -------------------------------------------------------------------------------- /media/emotes/osu_ctb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxOhn/Bathbot/4eff2f918d716002483b8661fa5a64521b73e155/media/emotes/osu_ctb.png -------------------------------------------------------------------------------- /media/emotes/osu_mania.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxOhn/Bathbot/4eff2f918d716002483b8661fa5a64521b73e155/media/emotes/osu_mania.png -------------------------------------------------------------------------------- /media/emotes/osu_std.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxOhn/Bathbot/4eff2f918d716002483b8661fa5a64521b73e155/media/emotes/osu_std.png -------------------------------------------------------------------------------- /media/emotes/osu_taiko.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxOhn/Bathbot/4eff2f918d716002483b8661fa5a64521b73e155/media/emotes/osu_taiko.png -------------------------------------------------------------------------------- /media/emotes/single_step.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxOhn/Bathbot/4eff2f918d716002483b8661fa5a64521b73e155/media/emotes/single_step.png -------------------------------------------------------------------------------- /media/emotes/single_step_back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxOhn/Bathbot/4eff2f918d716002483b8661fa5a64521b73e155/media/emotes/single_step_back.png -------------------------------------------------------------------------------- /media/emotes/start.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxOhn/Bathbot/4eff2f918d716002483b8661fa5a64521b73e155/media/emotes/start.png -------------------------------------------------------------------------------- /media/emotes/tracking.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxOhn/Bathbot/4eff2f918d716002483b8661fa5a64521b73e155/media/emotes/tracking.png -------------------------------------------------------------------------------- /media/emotes/twitch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxOhn/Bathbot/4eff2f918d716002483b8661fa5a64521b73e155/media/emotes/twitch.png -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | # https://rust-lang.github.io/rustfmt/ 2 | 3 | edition = "2021" 4 | newline_style = "Unix" 5 | use_field_init_shorthand = true 6 | 7 | # nightly configs 8 | condense_wildcard_suffixes = true 9 | format_code_in_doc_comments = true 10 | reorder_impl_items = true 11 | group_imports = "StdExternalCrate" 12 | imports_granularity = "Crate" 13 | wrap_comments = true --------------------------------------------------------------------------------