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==