diff --git a/lib/changelog.ts b/lib/changelog.ts index d481e32..f157c84 100644 --- a/lib/changelog.ts +++ b/lib/changelog.ts @@ -3,13 +3,9 @@ export namespace Changelog { created_at: Date display_version: string id: number - /** - * How many users are playing on this version of the game? (if lazer/web, should be 0, lazer doesn't show such stats) - */ + /** How many users are playing on this version of the game? (if lazer/web, should be 0, lazer doesn't show such stats) */ users: number - /** - * The name of the version - */ + /** The name of the version */ version: string | null /** * If a video is showcased on the changelog @@ -19,9 +15,7 @@ export namespace Changelog { } export namespace Build { - /** - * Expected from ChangelogBuildWithChangelogentriesVersions - */ + /** @obtainableFrom {@link Changelog.Build.WithChangelogentriesVersions} */ export interface WithUpdatestreams extends Build { update_stream: UpdateStream } @@ -37,13 +31,9 @@ export namespace Changelog { category: string title: string | null major: boolean - /** - * Can be January 1st 1970! - */ + /** @remarks Can be January 1st 1970! */ created_at: Date - /** - * Doesn't exist if no github user is associated with who's credited with the change - */ + /** @remarks Doesn't exist if no github user is associated with who's credited with the change */ github_user?: { display_name: string github_url: string | null @@ -54,27 +44,25 @@ export namespace Changelog { user_url: string | null } /** - * Entry message in Markdown format, embedded HTML is allowed, exists only if Markdown was requested + * Entry message in Markdown format, embedded HTML is allowed + * @remarks Exists only if Markdown was requested */ message?: string | null /** - * Entry message in HTML format, exists only if HTML was requested + * Entry message in HTML format + * @remarks Exists only if HTML was requested */ message_html?: string | null }[] } - /** - * Expected from api.getChangelogBuilds() - */ + /** @obtainableFrom {@link API.getChangelogBuilds} */ export interface WithUpdatestreamsChangelogentries extends WithUpdatestreams, WithChangelogentries { } - /** - * Expected from api.getChangelogBuild() - */ + /** @obtainableFrom {@link API.getChangelogBuild} */ export interface WithChangelogentriesVersions extends WithChangelogentries { versions: { next: WithUpdatestreams | null @@ -83,9 +71,7 @@ export namespace Changelog { } } - /** - * Expected from ChangelogBuildWithUpdatestreams - */ + /** @obtainableFrom {@link Changelog.Build.WithUpdatestreams} */ export interface UpdateStream { id: number name: string @@ -94,13 +80,12 @@ export namespace Changelog { } export namespace UpdateStream { - /** - * Expected from api.getChangelogStreams() - */ + /** @obtainableFrom {@link API.getChangelogStreams} */ export interface WithLatestbuildUsercount extends UpdateStream { latest_build: Build | null /** - * How many users are playing on this? (if lazer/web, should be 0, lazer doesn't show such stats) + * How many users are playing on this? + * @remarks Should be 0 if web */ user_count: number } diff --git a/lib/event.ts b/lib/event.ts index 5426522..46bf5cf 100644 --- a/lib/event.ts +++ b/lib/event.ts @@ -9,32 +9,22 @@ export namespace Event { export interface User extends Event { user: { username: string - /** - * What goes after the website's URL, so for example, it could be the `/u/7276846` of `https://osu.ppy.sh/u/7276846` (or `users` instead of `u`) - */ + /** What goes after the website's URL, so for example, it could be the `/u/7276846` of `https://osu.ppy.sh/u/7276846` (or `users` instead of `u`) */ url: string } } export interface Beatmap extends Event { - /** - * {artist} - {title} [{difficulty_name}] - */ + /** {artist} - {title} [{difficulty_name}] */ title: string - /** - * What goes after the website's URL, like it could be the `/b/2980857?m=0` of `https://osu.ppy.sh/b/2980857?m=0` (/{beatmap_id}?m={ruleset_id}) - */ + /** What goes after the website's URL, like it could be the `/b/2980857?m=0` of `https://osu.ppy.sh/b/2980857?m=0` (/{beatmap_id}?m={ruleset_id}) */ url: string } export interface Beatmapset extends Event { - /** - * {artist} - {title} - */ + /** {artist} - {title} */ title: string - /** - * What goes after the website's URL, like it could be the `/s/689155` of `https://osu.ppy.sh/s/689155` (/{beatmapset_id}) - */ + /** What goes after the website's URL, like it could be the `/s/689155` of `https://osu.ppy.sh/s/689155` (/{beatmapset_id}) */ url: string } @@ -49,13 +39,9 @@ export namespace Event { ordering: number slug: string description: string - /** - * If the achievement is for a specific mode only (such as pass a 2* beatmap in taiko) - */ + /** If the achievement is for a specific mode only (such as pass a 2* beatmap in taiko) */ mode: string | null - /** - * May contain HTML (like have the text between ) - */ + /** @remarks May contain HTML (like have the text between ) */ instructions: string } } @@ -88,13 +74,9 @@ export namespace Event { export interface Rank extends User, Beatmap { type: "rank" - /** - * The grade, like "S" - */ + /** The grade, like "S" */ scoreRank: string - /** - * The position achieved, like 14 - */ + /** The position achieved, like 14 */ rank: number mode: Rulesets } @@ -120,9 +102,7 @@ export namespace Event { type: "usernameChange" user: { username: string - /** - * What goes after the website's URL, so for example, it could be the `/u/7276846` of `https://osu.ppy.sh/u/7276846` - */ + /** What goes after the website's URL, so for example, it could be the `/u/7276846` of `https://osu.ppy.sh/u/7276846` */ url: string previousUsername: string } diff --git a/lib/forum.ts b/lib/forum.ts index f4bb9ef..653de7b 100644 --- a/lib/forum.ts +++ b/lib/forum.ts @@ -1,6 +1,10 @@ export namespace Forum { /** - * Expected from api.replyForumTopic(), api.createForumTopic(), api.getForumTopicAndPosts(), api.editForumPost() + * @obtainableFrom + * {@link API.replyForumTopic} / + * {@link API.createForumTopic} / + * {@link API.getForumTopicAndPosts} / + * {@link API.editForumPost} */ export interface Post { created_at: Date @@ -14,13 +18,16 @@ export namespace Forum { body: { /** Post content in HTML format */ html: string - /** Post content in BBCode format */ + /** Post content in BBCode format */ raw: string } } /** - * Expected from api.createForumTopic(), api.getForumTopicAndPosts(), api.editForumTopicTitle() + * @obtainableFrom + * {@link API.createForumTopic} / + * {@link API.getForumTopicAndPosts} / + * {@link API.editForumTopicTitle} */ export interface Topic { created_at: Date @@ -37,7 +44,7 @@ export namespace Forum { user_id: number poll: { allow_vote_change: boolean - /** Can be in the future */ + /** @remarks Can be in the future */ ended_at: Date | null hide_incomplete_results: boolean last_vote_at: Date | null @@ -48,7 +55,7 @@ export namespace Forum { bbcode: string html: string } - /** Not present if the poll is incomplete and results are hidden */ + /** @remarks Not present if the poll is incomplete and results are hidden */ vote_count?: number }[] started_at: Date @@ -61,7 +68,7 @@ export namespace Forum { } } -/** Feel free to use this interface to help you create polls with `api.createForumTopic()`! */ +/** Feel free to use this interface to help you create polls with {@link API.createForumTopic}! */ export interface PollConfig { title: string /** The things the users can vote for */ diff --git a/lib/home.ts b/lib/home.ts index 4634475..119bcc6 100644 --- a/lib/home.ts +++ b/lib/home.ts @@ -7,17 +7,13 @@ interface SearchResult { } export namespace SearchResult { - /** - * Expected from api.searchUser() - */ + /** @obtainableFrom {@link API.searchUser} */ export interface User extends SearchResult { /** The Users that have been found */ data: UserInterface[] } - /** - * Expected from api.searchWiki() - */ + /** @obtainableFrom {@link API.searchWiki} */ export interface Wiki extends SearchResult { /** The WikiPages that have been found */ data: WikiPage[] diff --git a/lib/index.ts b/lib/index.ts index 8482f1a..ef86dfc 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -5,7 +5,7 @@ import querystring from "querystring" import { User } from "./user.js" import { Beatmap, Beatmapset, RankStatus } from "./beatmap.js" -import { Room, Leader, PlaylistItem, MultiplayerScore, MultiplayerScores, Match, MatchInfo } from "./multiplayer.js" +import { Multiplayer } from "./multiplayer.js" import { Score, BeatmapUserScore } from "./score.js" import { Rankings, Spotlight } from "./ranking.js" import { Event } from "./event.js" @@ -23,7 +23,7 @@ import { WebSocketEvent } from "./websocket.js" export { User } from "./user.js" export { Beatmap, Beatmapset, RankStatus } from "./beatmap.js" -export { Room, Leader, PlaylistItem, MultiplayerScore, MultiplayerScores, Match, MatchInfo } from "./multiplayer.js" +export { Multiplayer } from "./multiplayer.js" export { Score, BeatmapUserScore } from "./score.js" export { Rankings, Spotlight } from "./ranking.js" export { Event } from "./event.js" @@ -376,8 +376,8 @@ export class API { /** * Get extensive user data about the authorized user + * @scope {@link Scope"identify"} * @param ruleset Defaults to the user's default/favourite Ruleset - * @scope identify */ async getResourceOwner(ruleset?: Rulesets): Promise { return await this.request("get", "me", {mode: ruleset}) @@ -465,7 +465,7 @@ export class API { /** * Get user data of each friend of the authorized user - * @scope friends.read + * @scope {@link Scope"friends.read"} */ async getFriends(): Promise { return await this.request("get", "friends") @@ -630,11 +630,12 @@ export class API { /** * An effective way to get all available streams, as well as their latest version! - * @example ```ts + * @example + * ```ts * const names_of_streams = (await api.getChangelogStreams()).map(s => s.name) * ``` */ - async getChangelogStreams(): Promise { + async getChangelogStreams(): Promise { const response = await this.request("get", "changelog", {max_id: 0}) return response.streams } @@ -646,25 +647,25 @@ export class API { * Get data about a lazer multiplayer room (realtime or playlists)! * @param room An object with the id of the room, is at the end of its URL (after `/multiplayer/rooms/`) */ - async getRoom(room: {id: number} | Room): Promise { + async getRoom(room: {id: number} | Multiplayer.Room): Promise { return await this.request("get", `rooms/${room.id}`) } /** * Get rooms that are active, that have ended, that the user participated in, that the user made, or just simply any room! + * @scope {@link Scope"public"} * @param mode Self-explanatory enough, defaults to `active` - * @scope public */ - async getRooms(mode: "active" | "all" | "ended" | "participated" | "owned" = "active"): Promise { + async getRooms(mode: "active" | "all" | "ended" | "participated" | "owned" = "active"): Promise { return await this.request("get", "rooms", {mode}) } /** * Get the room stats of all the users of that room! + * @scope {@link Scope"public"} * @param room An object with the id of the room in question - * @scope public */ - async getRoomLeaderboard(room: {id: number} | Room): Promise { + async getRoomLeaderboard(room: {id: number} | Multiplayer.Room): Promise { const response = await this.request("get", `rooms/${room.id}/leaderboard`) return response.leaderboard } @@ -677,9 +678,11 @@ export class API { * @param cursor_string Use a MultiplayerScores' `params` and `cursor_string` to get the next page (scores 51 to 100 for example) * @remarks (2023-11-10) Items are broken for multiplayer (real-time) rooms, not for playlists (like spotlights), that's an osu! bug * https://github.com/ppy/osu-web/issues/10725 + * @remarks (2024-03-04) Straight up doesn't work anymore, that's an osu! bug + * https://github.com/ppy/osu-web/issues/11064 */ - async getPlaylistItemScores(item: {id: number, room_id: number} | PlaylistItem, limit: number = 50, - sort: "score_asc" | "score_desc" = "score_desc", cursor_string?: string): Promise { + async getPlaylistItemScores(item: {id: number, room_id: number} | Multiplayer.PlaylistItem, limit: number = 50, + sort: "score_asc" | "score_desc" = "score_desc", cursor_string?: string): Promise { return await this.request("get", `rooms/${item.room_id}/playlist/${item.id}/scores`, {limit, sort, cursor_string}) } @@ -687,8 +690,8 @@ export class API { * Get data of a multiplayer lobby from the stable (non-lazer) client that have URLs with `community/matches` or `mp` * @param id Can be found at the end of the URL of said match */ - async getMatch(id: number): Promise { - const response = await this.request("get", `matches/${id}`) as Match + async getMatch(id: number): Promise { + const response = await this.request("get", `matches/${id}`) as Multiplayer.Match // I know `events[i].game.scores[e].perfect` can at least be 0 instead of being false; fix that for (let i = 0; i < response.events.length; i++) { for (let e = 0; e < Number(response.events[i].game?.scores.length); e++) { @@ -701,7 +704,7 @@ export class API { /** * Gets the info of the 50 most recently created stable (non-lazer) matches, descending order (most recent is at index 0) */ - async getMatches(): Promise { + async getMatches(): Promise { const response = await this.request("get", "matches") return response.matches } @@ -821,10 +824,10 @@ export class API { /** * Make and send a ForumPost in a ForumTopic! + * @scope {@link Scope"forum.write"} * @param topic An object with the id of the topic you're making your reply in * @param text Your reply! Your message! * @returns The reply you've made! - * @scope forum.write */ async replyForumTopic(topic: {id: number} | Forum.Topic, text: string): Promise { return await this.request("post", `forums/topics/${topic.id}/reply`, {body: text}) @@ -832,13 +835,13 @@ export class API { /** * Create a new ForumTopic in the forum of your choice! + * @scope {@link Scope"forum.write"} * @remarks Some users may not be allowed to do that, such as newly registered users, so this can 403 even with the right scopes * @param forum_id The id of the forum you're creating your topic in * @param title The topic's title * @param text The first post's content/message * @param poll If you want to make a poll, specify the parameters of that poll! * @returns An object with the topic you've made, and its first initial post (which uses your `text`) - * @scope forum.write */ async createForumTopic(forum_id: number, title: string, text: string, poll?: PollConfig): Promise<{topic: Forum.Topic, post: Forum.Post}> { const with_poll = poll !== undefined @@ -872,11 +875,11 @@ export class API { /** * Edit the title of a ForumTopic! + * @scope {@link Scope"forum.write"} * @remarks Use `editForumPost` if you wanna edit the post at the top of the topic * @param topic An object with the id of the topic in question * @param new_title The new title of the topic * @returns The edited ForumTopic - * @scope forum.write */ async editForumTopicTitle(topic: {id: number} | Forum.Topic, new_title: string): Promise { return await this.request("put", `forums/topics/${topic.id}`, {forum_topic: {topic_title: new_title}}) @@ -884,10 +887,10 @@ export class API { /** * Edit a ForumPost! Note that it can be the initial one of a ForumTopic! + * @scope {@link Scope"forum.write"} * @param post An object with the id of the post in question * @param new_text The new content of the post (replaces the old content) * @returns The edited ForumPost - * @scope forum.write */ async editForumPost(post: {id: number} | Forum.Post, new_text: string): Promise { return await this.request("put", `forums/posts/${post.id}`, {body: new_text}) @@ -898,10 +901,10 @@ export class API { /** * Needs to be done periodically to reset chat activity timeout + * @scope {@link Scope"chat.read"} * @remarks Every 30 seconds is a good idea * @param since UserSilences that are before that will not be returned! * @returns A list of recent silences - * @scope chat.read */ async keepChatAlive(since?: {user_silence?: {id: number} | Chat.UserSilence, message?: {message_id: number} | Chat.Message}): Promise { return await this.request("post", "chat/ack", {history_since: since?.user_silence?.id, since: since?.message?.message_id}) @@ -909,13 +912,13 @@ export class API { /** * Send a private message to someone! + * @scope {@link Scope"chat.write"} * @remarks You don't need to use `createChatPrivateChannel` before sending a message * @param user_target The User you wanna send your message to! * @param message The message you wanna send * @param is_action (defaults to false) Is it a command? Like `/me dances` * @param uuid A client-side message identifier * @returns The message you sent - * @scope chat.write */ async sendChatPrivateMessage(user_target: {id: number} | User, message: string, is_action: boolean = false, uuid?: string): Promise<{channel: Chat.Channel, message: Chat.Message}> { @@ -924,11 +927,11 @@ export class API { /** * Get the recent messages of a specific ChatChannel! + * @scope {@link Scope"chat.read"} * @param channel The Channel you wanna get the messages from * @param limit (defaults to 20, max 50) The maximum amount of messages you want to get! * @param since Get the messages sent after this message * @param until Get the messages sent up to but not including this message - * @scope chat.read */ async getChatMessages(channel: {channel_id: number} | Chat.Channel, limit: number = 20, since?: {message_id: number} | Chat.Message, until?: {message_id: number} | Chat.Message): Promise { @@ -937,11 +940,11 @@ export class API { /** * Send a message in a ChatChannel! + * @scope {@link Scope"chat.write"} * @param channel The channel in which you want to send your message * @param message The message you wanna send * @param is_action (defaults to false) Is it a command? Like `/me dances` * @returns The newly sent ChatMessage! - * @scope chat.write */ async sendChatMessage(channel: {channel_id: number} | Chat.Channel, message: string, is_action: boolean = false): Promise { return await this.request("post", `chat/channels/${channel.channel_id}/messages`, {message, is_action}) @@ -949,9 +952,9 @@ export class API { /** * Join a public or multiplayer ChatChannel, allowing you to interact with it! + * @scope {@link Scope"chat.write_manage"} * @param channel The channel you wanna join * @param user (defaults to the presumed authorized user) The user joining the channel - * @scope chat.write_manage */ async joinChatChannel(channel: {channel_id: number} | Chat.Channel, user?: {id: number} | User): Promise { return await this.request("put", `chat/channels/${channel.channel_id}/users/${user?.id || this.user}`) @@ -959,9 +962,9 @@ export class API { /** * Leave/Close a public ChatChannel! + * @scope {@link Scope"chat.write_manage"} * @param channel The channel you wanna join * @param user (defaults to the presumed authorized user) The user joining the channel - * @scope chat.write_manage */ async leaveChatChannel(channel: {channel_id: number} | Chat.Channel, user?: {id: number} | User): Promise { return await this.request("delete", `chat/channels/${channel.channel_id}/users/${user?.id || this.user}`) @@ -969,9 +972,9 @@ export class API { /** * Mark a certain channel as read up to a given message! + * @scope {@link Scope"chat.read"} * @param channel The channel in question * @param message You're marking this and all the messages before it as read! - * @scope chat.read */ async markChatChannelAsRead(channel: {channel_id: number} | Chat.Channel, message: {message_id: number} | Chat.Message): Promise { return await this.request("put", @@ -980,7 +983,7 @@ export class API { /** * Get a list of all publicly joinable channels! - * @scope chat.read + * @scope {@link Scope"chat.read"} */ async getChatChannels(): Promise { return await this.request("get", "chat/channels") @@ -988,9 +991,9 @@ export class API { /** * Create/Open/Join a private messages chat channel! + * @scope {@link Scope"chat.read"} * @param user_target The other user able to read and send messages in this channel * @returns The newly created channel! - * @scope chat.write_manage */ async createChatPrivateChannel(user_target: {id: number} | User): Promise { return await this.request("post", "chat/channels", {type: "PM", target_id: user_target.id}) @@ -998,12 +1001,12 @@ export class API { /** * Create a new announcement! + * @scope {@link Scope"chat.write_manage"} * @remarks From my understanding, this WILL 403 unless the user is kinda special * @param channel Details of the channel you're creating * @param user_targets The people that will receive your message * @param message The message to send with the announcement * @returns The newly created channel! - * @scope chat.write_manage */ async createChatAnnouncementChannel(channel: {name: string, description: string}, user_targets: Array<{id: number} | User>, message: string): Promise { @@ -1013,9 +1016,9 @@ export class API { /** * Get a ChatChannel, and the users in it if it is a private channel! + * @scope {@link Scope"chat.read"} * @remarks Will 404 if the user has not joined the channel (use `joinChatChannel` for that) * @param channel The channel in question - * @scope chat.read */ async getChatChannel(channel: {channel_id: number} | Chat.Channel): Promise { const response = await this.request("get", `chat/channels/${channel.channel_id}`) diff --git a/lib/multiplayer.ts b/lib/multiplayer.ts index 7965d07..95ec58d 100644 --- a/lib/multiplayer.ts +++ b/lib/multiplayer.ts @@ -1,208 +1,196 @@ import { Beatmap } from "./beatmap.js" import { Rulesets, Mod } from "./misc.js" -import { Score } from "./score.js" +import { Score as ScoreInterface } from "./score.js" import { User } from "./user.js" -/** - * Expected from api.getRoom() - */ -export interface Room { - active: boolean - auto_skip: boolean - category: string - channel_id: number - ends_at: Date | null - has_password: boolean - host: User.WithCountry - id: number - max_attempts: number | null - name: string - participant_count: number - playlist: PlaylistItem[] - queue_mode: string - recent_participants: User[] - starts_at: Date - type: string - user_id: number +export namespace Multiplayer { /** - * Only exists if authorized user + * Expected from api.getRoom() */ - current_user_score?: { - /** - * In a format where `96.40%` would be `0.9640` (likely with some numbers after the zero) - */ - accuracy: number - attempts: number - completed: number - pp: number - room_id: number - total_score: number + export interface Room { + active: boolean + auto_skip: boolean + category: string + channel_id: number + ends_at: Date | null + has_password: boolean + host: User.WithCountry + id: number + max_attempts: number | null + name: string + participant_count: number + playlist: PlaylistItem[] + queue_mode: string + recent_participants: User[] + starts_at: Date + type: string user_id: number - /** - * How many (completed?) attempts on each item? Empty array if the multiplayer room is the realtime kind - */ - playlist_item_attempts: { + /** Only exists if authorized user */ + current_user_score?: { + /** In a format where `96.40%` would be `0.9640` (with some numbers after the zero) */ + accuracy: number attempts: number - id: number - }[] + completed: number + pp: number + room_id: number + total_score: number + user_id: number + /** How many (completed (I think)) attempts on each item? Empty array if the multiplayer room is the realtime kind */ + playlist_item_attempts: { + attempts: number + id: number + }[] + } } -} -/** - * Expected from Room - */ -export interface PlaylistItem { - id: number - room_id: number - beatmap_id: number - ruleset_id: number - allowed_mods: Mod[] - required_mods: Mod[] - expired: boolean - owner_id: number - /** - * Should be null if the room isn't the realtime multiplayer kind - */ - playlist_order: number | null /** - * Should be null if the room isn't the realtime multiplayer kind + * Expected from Room */ - played_at: Date | null - beatmap: Beatmap.WithBeatmapsetChecksumMaxcombo -} + export interface PlaylistItem { + id: number + room_id: number + beatmap_id: number + ruleset_id: number + allowed_mods: Mod[] + required_mods: Mod[] + expired: boolean + owner_id: number + /** + * Should be null if the room isn't the realtime multiplayer kind + */ + playlist_order: number | null + /** + * Should be null if the room isn't the realtime multiplayer kind + */ + played_at: Date | null + beatmap: Beatmap.WithBeatmapsetChecksumMaxcombo + } -/** - * Expected from MultiplayerScores - * @remarks This particular interface seems really unstable, beware - */ -export interface MultiplayerScore { /** - * In a format where `96.40%` would be `0.9640` (and no number afterwards) + * Expected from MultiplayerScores + * @remarks This particular interface seems really unstable, beware */ - accuracy: number - beatmap_id: number - ended_at: Date - max_combo: number - maximum_statistics: { - great: number - ignore_hit: number - large_tick_hit: number - small_tick_hit: number + export interface Score { + /** In a format where `96.40%` would be `0.9640` (and no number afterwards) */ + accuracy: number + beatmap_id: number + ended_at: Date + max_combo: number + maximum_statistics: { + great: number + ignore_hit: number + large_tick_hit: number + small_tick_hit: number + } + mods: Mod[] + passed: boolean + rank: string + ruleset_id: number + started_at: Date + /** + * All of its properties are optional because instead of being 0, the property actually disappears instead! + * (so if the score has no miss, the miss property is simply not there) + * @privateRemarks lmao wtf nanaya + */ + statistics: { + great?: number + large_bonus?: number + large_tick_hit?: number + meh?: number + miss?: number + ok?: number + small_bonus?: number + small_tick_hit?: number + small_tick_miss?: number + } + total_score: number + user_id: number + playlist_item_id: number + room_id: number + id: number + pp: number | null + replay: boolean + type: string + user: User.WithCountryCover } - mods: Mod[] - passed: boolean - rank: string - ruleset_id: number - started_at: Date + /** - * All of its properties are optional because instead of being 0, the property actually disappears instead! - * (so if the score has no miss, the miss property is simply not there) - * @privateRemarks lmao wtf nanaya + * Expected from api.getPlaylistItemScores() */ - statistics: { - great?: number - large_bonus?: number - large_tick_hit?: number - meh?: number - miss?: number - ok?: number - small_bonus?: number - small_tick_hit?: number - small_tick_miss?: number + export interface Scores { + params: { + limit: number + sort: string + } + scores: Score[] + /** How many scores there are across all pages, not necessarily `scores.length` */ + total: number + /** Will be null if not an authorized user or if the authorized user has no score */ + user_score: Score | null + /** Will be null if there is no next page */ + cursor_string: string | null } - total_score: number - user_id: number - playlist_item_id: number - room_id: number - id: number - pp: number | null - replay: boolean - type: string - user: User.WithCountryCover -} -/** - * Expected from api.getPlaylistItemScores() - */ -export interface MultiplayerScores { - params: { - limit: number - sort: string + export interface Leader { + /** + * In a format where `96.40%` would be `0.9640` (likely with some numbers after the zero) + */ + accuracy: number + attempts: number + completed: number + pp: number + room_id: number + total_score: number + user_id: number + user: User.WithCountry } - scores: MultiplayerScore[] - /** - * How many scores there are across all pages, not necessarily `scores.length` - */ - total: number - /** - * Will be null if not an authorized user or if the authorized user has no score - */ - user_score: MultiplayerScore | null + /** - * Will be null if there is no next page + * Expected from api.getMatches(), Match */ - cursor_string: string | null -} + export interface MatchInfo { + id: number + start_time: Date + end_time: Date | null + name: string + } -export interface Leader { /** - * In a format where `96.40%` would be `0.9640` (likely with some numbers after the zero) + * Expected from api.getMatch() */ - accuracy: number - attempts: number - completed: number - pp: number - room_id: number - total_score: number - user_id: number - user: User.WithCountry -} - -/** - * Expected from api.getMatches(), Match - */ -export interface MatchInfo { - id: number - start_time: Date - end_time: Date | null - name: string -} - -/** - * Expected from api.getMatch() - */ -export interface Match { - match: MatchInfo - events: { - id: number - detail: { - type: string + export interface Match { + match: MatchInfo + events: { + id: number + detail: { + type: string + /** + * If `detail.type` is `other`, this exists and will be the name of the room + */ + text?: string + } + timestamp: Date + user_id: number | null /** - * If `detail.type` is `other`, this exists and will be the name of the room + * If `detail.type` is `other`, then this should exist! */ - text?: string - } - timestamp: Date - user_id: number | null - /** - * If `detail.type` is `other`, then this should exist! - */ - game?: { - beatmap_id: number - id: number - start_time: Date - end_time: Date | null - mode: string - mode_int: Rulesets - scoring_type: string - team_type: string - mods: string[] - beatmap: Beatmap.WithBeatmapset - scores: Score.WithMatch[] - } - }[] - users: User.WithCountry[] - first_event_id: number - latest_event_id: number - current_game_id: number | null + game?: { + beatmap_id: number + id: number + start_time: Date + end_time: Date | null + mode: string + mode_int: Rulesets + scoring_type: string + team_type: string + mods: string[] + beatmap: Beatmap.WithBeatmapset + scores: ScoreInterface.WithMatch[] + } + }[] + users: User.WithCountry[] + first_event_id: number + latest_event_id: number + current_game_id: number | null + } } diff --git a/lib/score.ts b/lib/score.ts index 0eae5e0..812649b 100644 --- a/lib/score.ts +++ b/lib/score.ts @@ -20,7 +20,7 @@ export interface Score { passed: boolean perfect: boolean /** - * null when Beatmap is Loved (for example) + * @remarks Is null when Beatmap is Loved (for example) */ pp: number | null /** @@ -33,11 +33,14 @@ export interface Score { replay: boolean score: number statistics: { - count_50: number + /** + * @remarks Is null if the score's gamemode is Taiko + */ + count_50: number | null count_100: number count_300: number - count_geki: number - count_katu: number + count_geki: number | null + count_katu: number | null count_miss: number } type: string diff --git a/lib/tests/test.ts b/lib/tests/test.ts index 79858a3..a74337e 100644 --- a/lib/tests/test.ts +++ b/lib/tests/test.ts @@ -187,24 +187,25 @@ const testMultiplayerStuff = async (multi_gen: tsj.SchemaGenerator): Promise | false>>attempt("\ngetRoom (realtime): ", api.getRoom({id: 231069})) - if (!isOk(d1, !d1 || (d1.recent_participants.length === 4 && validate(d1, "Room", multi_gen)))) okay = false - let d2 = await | false>>attempt("getRoom (playlist): ", api.getRoom({id: 51693})) - if (!isOk(d2, !d2 || (d2.participant_count === 159 && validate(d2, "Room", multi_gen)))) okay = false - if (d1) { // can't bother getting and writing down the id of a playlist item + if (!isOk(d1, !d1 || (d1.recent_participants.length === 4 && validate(d1, "Multiplayer.Room", multi_gen)))) okay = false + let d2 = await | false>>attempt("getRoom (playlist): ", api.getRoom({id: 499640})) + if (!isOk(d2, !d2 || (d2.participant_count === 70 && validate(d2, "Multiplayer.Room", multi_gen)))) okay = false + if (d1) { let d3 = await | false>>attempt( "getPlaylistItemScores (realtime): ", api.getPlaylistItemScores({id: d1.playlist[0].id, room_id: d1.id})) - !isOk(d3, !d3 || (d3.scores.length > 0 && validate(d3, "MultiplayerScores", multi_gen)), 1) ? + !isOk(d3, !d3 || (d3.scores.length > 0 && validate(d3, "Multiplayer.Scores", multi_gen)), 1) ? console.log("Bug not fixed yet...") : console.log("Bug fixed!!! :partying_face:") } - if (d2) { // still can't bother getting and writing down the id of a playlist item + if (d2) { let d4 = await | false>>attempt( "getPlaylistItemScores (playlist): ", api.getPlaylistItemScores({id: d2.playlist[0].id, room_id: d2.id})) - if (!isOk(d4, !d4 || (d4.scores.length >= 50 && validate(d4, "MultiplayerScores", multi_gen)), 1)) okay = false + !isOk(d4, !d4 || (d4.scores.length >= 50 && validate(d4, "Multiplayer.Scores", multi_gen)), 1) ? + console.log("Bug not fixed yet...") : console.log("Bug fixed!!! :partying_face:") } let d5 = await | false>>attempt("getMatch: ", api.getMatch(62006076)) - if (!isOk(d5, !d5 || (d5.match.name === "CWC2020: (Italy) vs (Indonesia)" && validate(d5, "Match", multi_gen)), 3)) okay = false + if (!isOk(d5, !d5 || (d5.match.name === "CWC2020: (Italy) vs (Indonesia)" && validate(d5, "Multiplayer.Match", multi_gen)), 3)) okay = false let d6 = await | false>>attempt("getMatches: ", api.getMatches()) - if (!isOk(d6, !d6 || (d6[0].id > 111250329 && validate(d6, "MatchInfo", multi_gen)))) okay = false + if (!isOk(d6, !d6 || (d6[0].id > 111250329 && validate(d6, "Multiplayer.MatchInfo", multi_gen)))) okay = false return okay } diff --git a/lib/tests/test_authorized.ts b/lib/tests/test_authorized.ts index e12d276..4643f4a 100644 --- a/lib/tests/test_authorized.ts +++ b/lib/tests/test_authorized.ts @@ -21,7 +21,7 @@ async function test(id: string | undefined, secret: string | undefined, redirect if (secret === undefined) {throw new Error("no SECRET env var")} if (redirect_uri === undefined) {throw new Error("no REDIRECT_URI env var")} - let url = osu.generateAuthorizationURL(Number(id), redirect_uri, ["public", "chat.read", "chat.write_manage"], server) + let url = osu.generateAuthorizationURL(Number(id), redirect_uri, ["public", "chat.read", "chat.write_manage", "forum.write"], server) exec(`xdg-open "${url}"`) let code = prompt(`What code do you get from: ${url}\n\n`) let api = await osu.API.createAsync({id: Number(id), secret}, {code, redirect_uri}, "errors", server) diff --git a/yarn.lock b/yarn.lock index e5e8748..7fdcbf7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8,9 +8,9 @@ integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== "@types/node@*", "@types/node@^20.8.10": - version "20.10.3" - resolved "https://registry.yarnpkg.com/@types/node/-/node-20.10.3.tgz#4900adcc7fc189d5af5bb41da8f543cea6962030" - integrity sha512-XJavIpZqiXID5Yxnxv3RUDKTN5b81ddNC3ecsA0SoFXz/QU8OGBwZGMomiq0zw+uuqbL/krztv/DINAQ/EV4gg== + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== dependencies: undici-types "~5.26.4" @@ -69,9 +69,9 @@ data-uri-to-buffer@^4.0.0: integrity sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A== dotenv@^16.3.1: - version "16.3.1" - resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.3.1.tgz#369034de7d7e5b120972693352a3bf112172cc3e" - integrity sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ== + version "16.4.5" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f" + integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg== fast-deep-equal@^3.1.1: version "3.1.3" @@ -133,9 +133,9 @@ json5@^2.2.3: integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== jsonc-parser@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.2.0.tgz#31ff3f4c2b9793f89c67212627c51c6394f88e76" - integrity sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w== + version "3.2.1" + resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.2.1.tgz#031904571ccf929d7670ee8c547545081cb37f1a" + integrity sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA== lunr@^2.3.9: version "2.3.9" @@ -209,10 +209,10 @@ safe-stable-stringify@^2.4.3: resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz#138c84b6f6edb3db5f8ef3ef7115b8f55ccbf886" integrity sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g== -shiki@^0.14.1: - version "0.14.5" - resolved "https://registry.yarnpkg.com/shiki/-/shiki-0.14.5.tgz#375dd214e57eccb04f0daf35a32aa615861deb93" - integrity sha512-1gCAYOcmCFONmErGTrS1fjzJLA7MGZmKzrBNX7apqSwhyITJg2O102uFzXUeBxNnEkDA9vHIKLyeKq0V083vIw== +shiki@^0.14.7: + version "0.14.7" + resolved "https://registry.yarnpkg.com/shiki/-/shiki-0.14.7.tgz#c3c9e1853e9737845f1d2ef81b31bcfb07056d4e" + integrity sha512-dNPAPrxSc87ua2sKJ3H5dQ/6ZaY8RNnaAqK+t0eG7p0Soi2ydiqbGOTaZCqaYvA/uZYfS1LJnemt3Q+mSfcPCg== dependencies: ansi-sequence-parser "^1.1.0" jsonc-parser "^3.2.0" @@ -227,9 +227,9 @@ strip-ansi@^5.0.0: ansi-regex "^4.1.0" ts-json-schema-generator@^1.4.0: - version "1.4.1" - resolved "https://registry.yarnpkg.com/ts-json-schema-generator/-/ts-json-schema-generator-1.4.1.tgz#83a72dace79c8684b6d0fcad1c7d49c656e985d2" - integrity sha512-wnhPMtskk9KvsTuU8AYx0TNdm1YrLVUEontT22+jL12JIPqPXdaoxPgsYBhlqDXsR9R9Nm2bJgH5r4IrTMbTSg== + version "1.5.0" + resolved "https://registry.yarnpkg.com/ts-json-schema-generator/-/ts-json-schema-generator-1.5.0.tgz#9f5cea606c27ebcf13060157542ac1d3b225430f" + integrity sha512-RkiaJ6YxGc5EWVPfyHxszTmpGxX8HC2XBvcFlAl1zcvpOG4tjjh+eXioStXJQYTvr9MoK8zCOWzAUlko3K0DiA== dependencies: "@types/json-schema" "^7.0.12" commander "^11.0.0" @@ -240,19 +240,19 @@ ts-json-schema-generator@^1.4.0: typescript "~5.3.2" typedoc@^0.25.3: - version "0.25.4" - resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.25.4.tgz#5c2c0677881f504e41985f29d9aef0dbdb6f1e6f" - integrity sha512-Du9ImmpBCw54bX275yJrxPVnjdIyJO/84co0/L9mwe0R3G4FSR6rQ09AlXVRvZEGMUg09+z/usc8mgygQ1aidA== + version "0.25.10" + resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.25.10.tgz#572f566498e4752fdbc793ccc14b8eb517944770" + integrity sha512-v10rtOFojrjW9og3T+6wAKeJaGMuojU87DXGZ33sfs+554wgPTRG+s07Ag1BjPZI85Y5QPVouPI63JQ6fcQM5w== dependencies: lunr "^2.3.9" marked "^4.3.0" minimatch "^9.0.3" - shiki "^0.14.1" + shiki "^0.14.7" typescript@^5.2.2, typescript@~5.3.2: - version "5.3.2" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.2.tgz#00d1c7c1c46928c5845c1ee8d0cc2791031d4c43" - integrity sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ== + version "5.3.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37" + integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw== undici-types@~5.26.4: version "5.26.5" @@ -277,9 +277,9 @@ vscode-textmate@^8.0.0: integrity sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg== web-streams-polyfill@^3.0.3: - version "3.2.1" - resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz#71c2718c52b45fd49dbeee88634b3a60ceab42a6" - integrity sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q== + version "3.3.3" + resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz#2073b91a2fdb1fbfbd401e7de0ac9f8214cecb4b" + integrity sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw== wrappy@1: version "1.0.2" @@ -287,6 +287,6 @@ wrappy@1: integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== ws@^8.14.2: - version "8.14.2" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.14.2.tgz#6c249a806eb2db7a20d26d51e7709eab7b2e6c7f" - integrity sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g== + version "8.16.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.16.0.tgz#d1cd774f36fbc07165066a60e40323eab6446fd4" + integrity sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==