Skip to content

Commit

Permalink
Make WebSocket stuff more accessible (hopefully)
Browse files Browse the repository at this point in the history
  • Loading branch information
TTTaevas committed Oct 25, 2024
1 parent eb987e4 commit fc15d32
Show file tree
Hide file tree
Showing 4 changed files with 65 additions and 23 deletions.
53 changes: 42 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,18 +85,46 @@ Your `refresh_token` can actually also expire at a (purposefully) unknown time,
```typescript
// TypeScript
import * as osu from "osu-api-v2-js"
import promptSync from "prompt-sync"
import * as http from "http"
import { exec } from "child_process"

const prompt = promptSync({sigint: true})

const id = "<client_id>"
// This should be from an application registered on https://osu.ppy.sh/home/account/edit#oauth
const id = "<client_id>" // as a number
const secret = "<client_secret>"
const redirect_uri = "<application_callback_url>"
const redirect_uri = "<application_callback_url>" // assuming localhost with any port for convenience

// Because we need to act as an authenticated user, we need to go through the authorization procedure
// This function largely takes care of it by itself
async function getCode(authorization_url: string): Promise<string> {
// Open a temporary server to receive the code when the browser is sent to the redirect_uri after confirming authorization
const httpserver = http.createServer()
const host = redirect_uri.substring(redirect_uri.indexOf("/") + 2, redirect_uri.lastIndexOf(":"))
const port = Number(redirect_uri.substring(redirect_uri.lastIndexOf(":") + 1).split("/")[0])
httpserver.listen({host, port})

// Open the browser to the page on osu!web where you click a button to say you authorize your application
console.log("Waiting for code...")
const command = (process.platform == "darwin" ? "open" : process.platform == "win32" ? "start" : "xdg-open")
exec(`${command} "${authorization_url}"`)

// Check the URL for a `code` GET parameter, get it if it's there
const code: string = await new Promise((resolve) => {
httpserver.on("request", (request, response) => {
if (request.url) {
console.log("Received code!")
response.end("Worked! You may now close this tab.", "utf-8")
httpserver.close() // Close the temporary server as it is no longer needed
resolve(request.url.substring(request.url.indexOf("code=") + 5))
}
})
})
return code
}

async function readChat() {
// Somehow get the code so the application can read the messages as your osu! user
const url = osu.generateAuthorizationURL(id, redirect_uri, ["public", "chat.read"]) // "chat.read" is 100% needed in our case
const code = prompt(`Paste the "code" in the URL you're redicted to by accessing: ${url}\n\n`)
const code = await getCode(url)
const api = await osu.API.createAsync({id, secret}, {code, redirect_uri}, {verbose: "errors"})

// Get a WebSocket object to interact with and get messages from
Expand All @@ -110,11 +138,14 @@ async function readChat() {
})

// Listen for chat messages (and other stuff)
socket.on("message", (m: MessageEvent) => { // Mind you, "message" doesn't mean "chat message" here, it's more like a raw event
const event: osu.WebSocket.Event.Any = JSON.parse(m.toString())
if (event.event === "chat.message.new") { // Filter out things that aren't new chat messages and get type safety
const message = event.data.messages.map((message) => message.content).join(" | ")
const user = event.data.users.map((user) => user.username).join(" | ")
socket.on("message", (m) => { // Mind you, "message" doesn't mean "chat message" here, it's more like a raw event
const parsed: osu.WebSocket.Event.Any = JSON.parse(m.toString())

if (!parsed.event) { // Should only mean we've gotten an error message
throw new Error((parsed as osu.WebSocket.Event.Error).error) // Assertion in case of false positive TS2339 error at build time
} else if (parsed.event === "chat.message.new") { // Filter out things that aren't new chat messages and get type safety
const message = parsed.data.messages.map((message) => message.content).join(" | ")
const user = parsed.data.users.map((user) => user.username).join(" | ")
console.log(`${user}: ${message}`)
}
})
Expand Down
11 changes: 8 additions & 3 deletions lib/WebSocket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ export namespace WebSocket {

/** Those are what you'll get from WebSocket's `MessageEvent`s! */
export namespace Event {
export interface Error {
error: string
event: undefined
data: undefined
}

export interface ChatChannelJoin {
event: "chat.channel.join"
data: Chat.Channel.WithDetails
Expand All @@ -33,19 +39,18 @@ export namespace WebSocket {
}

/** That's the type of `JSON.parse(m.toString())` where `m` is a WebSocket's `MessageEvent`! */
export type Any = ChatChannelJoin | ChatChannelLeave | ChatMessageNew
export type Any = Error | ChatChannelJoin | ChatChannelLeave | ChatMessageNew
}

/**
* Get a websocket to get WebSocket events from!
* @param server The "notification websocket/server" URL (defaults to **wss://notify.ppy.sh**)
*/
export function generate(this: API, server = "wss://notify.ppy.sh"): WebSocketType {
console.log("type:", this.token_type)
return new WebSocketType(server, [], {
headers: {
"User-Agent": "osu-api-v2-js (https://github.com/TTTaevas/osu-api-v2-js)",
"Authorization": `${this.token_type} ${this.access_token}`
Authorization: `${this.token_type} ${this.access_token}`
}
})
}
Expand Down
20 changes: 12 additions & 8 deletions lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -743,6 +743,12 @@ export class API {
readonly getSpotlightRanking = Ranking.getSpotlight


// SCORE STUFF

/** {@inheritDoc Score.getReplay} @group Score Functions */
readonly getReplay = Score.getReplay


// SPOTLIGHTS STUFF

/** {@inheritDoc Spotlight.getAll} @group Spotlights Functions */
Expand Down Expand Up @@ -782,19 +788,17 @@ export class API {
readonly getFriends = User.getFriends


// WEBSOCKET STUFF

/** {@inheritDoc WebSocket.generate} @group WebSocket Functions */
readonly generateWebSocket = WebSocket.generate


// WIKI STUFF

/** {@inheritDoc WikiPage.getOne} @group WikiPage Functions */
readonly getWikiPage = WikiPage.getOne


// OTHER STUFF

/** {@inheritDoc WebSocket.generate} @group Other Functions */
readonly generateWebSocket = WebSocket.generate

/** {@inheritDoc Score.getReplay} @group Other Functions */
readonly getReplay = Score.getReplay

/**
* Get the backgrounds made and selected for this season or for last season!
Expand Down
4 changes: 3 additions & 1 deletion lib/tests/authenticated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,10 @@ async function getCode(url: string): Promise<string> {
const host = redirect_uri.substring(redirect_uri.indexOf("/") + 2, redirect_uri.lastIndexOf(":"))
const port = Number(redirect_uri.substring(redirect_uri.lastIndexOf(":") + 1).split("/")[0])
httpserver.listen({host, port})

console.log("Waiting for code...")
exec(`xdg-open "${url}"`)
const command = (process.platform == "darwin" ? "open" : process.platform == "win32" ? "start" : "xdg-open")
exec(`${command} "${url}"`)

const code: string = await new Promise((resolve) => {
httpserver.on("request", (request, response) => {
Expand Down

0 comments on commit fc15d32

Please sign in to comment.