speedruncom.js 1.2.9 → 1.2.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,49 +1,49 @@
1
- ## `npm i retrozy1/speedruncom.js`
2
-
3
- # About
4
-
5
- Speedrun.com has two API versions. Version 1 is officially documented, REST, public and directly exposed to users. Version 2 is a pre-production RPC API designed only for internal use of site functions, and is much more complicated and less user-friendly, especially regarding authentication and browser CORS.
6
-
7
- Version 1 is mainly read-only, and when it isn't read only it may just be broken. If you want to use any of the modern features of the site now, you'll use version 2. This is a NodeJS wrapper for Speedrun API version 2, with parameters and responses all documented in TypeScript and JSDoc.
8
-
9
- # Usage
10
-
11
- ## Installation
12
-
13
- **`npm i retrozy1/speedruncom.js`**
14
-
15
- ## Importing
16
-
17
- `Client` is the default export, and enums and interfaces are available in named exports for TypeScript users.
18
-
19
- ```js
20
- import SpeedrunClient from 'speedruncom.js';
21
-
22
- const client = new SpeedrunClient({
23
- userAgent: 'RunCollabGetter'
24
- });
25
-
26
- const getCollabs = async (userUrl) => {
27
- // Fetch user summary
28
- const {
29
- user: { id: userId, name }
30
- } = await client.GetUserSummary({ url: userUrl });
31
-
32
- // Fetch leaderboard and extract player names
33
- const { players } = await client.GetUserLeaderboard({ userId });
34
- const otherPlayerNames = players.map(player => player.name).filter(n => n !== name); // Exclude self
35
-
36
- if (otherPlayerNames.length === 0) {
37
- return `${name} hasn't ran with anyone.`;
38
- }
39
-
40
- const formattedNames =
41
- otherPlayerNames.length === 1
42
- ? otherPlayerNames[0]
43
- : `${otherPlayerNames.slice(0, -1).join(', ')} and ${otherPlayerNames.slice(-1)}`;
44
-
45
- return `${name} has ran with ${formattedNames}.`;
46
- };
47
-
48
- console.log(await getCollabs('retrozy'));
49
- ```
1
+ ## `npm i retrozy1/speedruncom.js`
2
+
3
+ # About
4
+
5
+ Speedrun.com has two API versions. Version 1 is officially documented, REST, public and directly exposed to users. Version 2 is a pre-production RPC API designed only for internal use of site functions, and is much more complicated and less user-friendly, especially regarding authentication and browser CORS.
6
+
7
+ Version 1 is mainly read-only, and when it isn't read only it may just be broken. If you want to use any of the modern features of the site now, you'll use version 2. This is a NodeJS wrapper for Speedrun API version 2, with parameters and responses all documented in TypeScript and JSDoc.
8
+
9
+ # Usage
10
+
11
+ ## Installation
12
+
13
+ **`npm i retrozy1/speedruncom.js`**
14
+
15
+ ## Importing
16
+
17
+ `Client` is the default export, and enums and interfaces are available in named exports for TypeScript users.
18
+
19
+ ```js
20
+ import SpeedrunClient from 'speedruncom.js';
21
+
22
+ const client = new SpeedrunClient({
23
+ userAgent: 'RunCollabGetter'
24
+ });
25
+
26
+ const getCollabs = async (userUrl) => {
27
+ // Fetch user summary
28
+ const {
29
+ user: { id: userId, name }
30
+ } = await client.GetUserSummary({ url: userUrl });
31
+
32
+ // Fetch leaderboard and extract player names
33
+ const { players } = await client.GetUserLeaderboard({ userId });
34
+ const otherPlayerNames = players.map(player => player.name).filter(n => n !== name); // Exclude self
35
+
36
+ if (otherPlayerNames.length === 0) {
37
+ return `${name} hasn't ran with anyone.`;
38
+ }
39
+
40
+ const formattedNames =
41
+ otherPlayerNames.length === 1
42
+ ? otherPlayerNames[0]
43
+ : `${otherPlayerNames.slice(0, -1).join(', ')} and ${otherPlayerNames.slice(-1)}`;
44
+
45
+ return `${name} has ran with ${formattedNames}.`;
46
+ };
47
+
48
+ console.log(await getCollabs('retrozy'));
49
+ ```
@@ -449,6 +449,7 @@ export interface PutRunDelete {
449
449
  export interface PutRunVerification {
450
450
  runId: string;
451
451
  verified: Enums.RunStatus;
452
+ reason?: string;
452
453
  }
453
454
  /**
454
455
  * Assigns a video-at-risk state to a run.
package/package.json CHANGED
@@ -1,30 +1,30 @@
1
- {
2
- "name": "speedruncom.js",
3
- "version": "1.2.9",
4
- "description": "WIP NodeJS module for Speedrun's version 2 API.",
5
- "type": "module",
6
- "author": {
7
- "name": "retrozy"
8
- },
9
- "engines": {
10
- "node": ">=18"
11
- },
12
- "dependencies": {
13
- "axios": "^1.9.0",
14
- "ts-morph": "^26.0.0"
15
- },
16
- "types": "dist/index.d.ts",
17
- "scripts": {
18
- "clean": "rimraf dist src/Client.ts",
19
- "build": "tsc",
20
- "render": "tsx src/build-client.ts",
21
- "prepare": "npm run render && npm run build"
22
- },
23
- "devDependencies": {
24
- "@types/node": "^22.15.24",
25
- "rimraf": "^6.0.1",
26
- "ts-node": "^10.9.2",
27
- "typescript": "^5.8.3"
28
- },
29
- "main": "dist/index.js"
30
- }
1
+ {
2
+ "name": "speedruncom.js",
3
+ "version": "1.2.10",
4
+ "description": "WIP NodeJS module for Speedrun's version 2 API.",
5
+ "type": "module",
6
+ "author": {
7
+ "name": "retrozy"
8
+ },
9
+ "engines": {
10
+ "node": ">=18"
11
+ },
12
+ "dependencies": {
13
+ "axios": "^1.9.0",
14
+ "ts-morph": "^26.0.0"
15
+ },
16
+ "types": "dist/index.d.ts",
17
+ "scripts": {
18
+ "clean": "rimraf dist",
19
+ "build": "tsc",
20
+ "render": "tsx src/build-client.ts",
21
+ "prepare": "npm run render && npm run build"
22
+ },
23
+ "devDependencies": {
24
+ "@types/node": "^22.15.24",
25
+ "rimraf": "^6.0.1",
26
+ "tsx": "^4.20.3",
27
+ "typescript": "^5.8.3"
28
+ },
29
+ "main": "dist/index.js"
30
+ }
package/src/BaseClient.ts CHANGED
@@ -1,108 +1,108 @@
1
- import axios, { AxiosResponse, AxiosInstance, AxiosError } from 'axios';
2
- import * as GetEndpoints from './endpoints/endpoints.get.js';
3
- import * as PostEndpoints from './endpoints/endpoints.post.js'
4
- import * as Responses from './responses.js';
5
-
6
- const BASE_USER_AGENT = 'speedruncom.js';
7
- const BASE_URL = 'https://www.speedrun.com/api/v2/';
8
- const HEADERS = {
9
- 'Accept-Language': 'en',
10
- 'Accept': 'application/json'
11
- }
12
-
13
- const isBrowser = typeof window !== 'undefined';
14
-
15
- const objectToBase64 = (obj: object) => {
16
- const jsonString = JSON.stringify(obj).replace(/\s+/g, '');
17
- return Buffer.from(jsonString).toString('base64');
18
- }
19
-
20
- export interface config {
21
- PHPSESSID?: string;
22
- userAgent?: string;
23
- }
24
-
25
- export class APIError extends Error {
26
- status: number;
27
-
28
- constructor(message: string, status: number) {
29
- super(message);
30
- this.message = message;
31
- this.status = status;
32
- }
33
- }
34
-
35
- export default class Client {
36
-
37
- /**
38
- * `AxiosInstance` used on instance-called methods (called with `POST`).
39
- */
40
- axiosClient = axios.create({
41
- baseURL: BASE_URL,
42
- method: 'POST',
43
- withCredentials: true,
44
- headers: HEADERS
45
- });
46
-
47
- /**
48
- * `AxiosInstance` used on Client-called methods (called with `GET`).
49
- */
50
- static axiosClient = axios.create({
51
- baseURL: BASE_URL,
52
- method: 'GET',
53
- headers: HEADERS
54
- });
55
-
56
- private username!: string;
57
- private password!: string;
58
-
59
- private headers = this.axiosClient.defaults.headers.common;
60
-
61
- constructor(config?: config) {
62
- if (config) this.config(config);
63
-
64
- this.axiosClient.interceptors.response.use(
65
- (response: AxiosResponse) => response,
66
- (error: AxiosError) => {
67
- const data = error.response.data as { error?: string };
68
- throw new APIError(data.error || 'Unknown error', error.response.status);
69
- }
70
- );
71
- }
72
-
73
- config(config: config) {
74
- if (!isBrowser) this.headers['User-Agent'] = BASE_USER_AGENT + (config.userAgent ? `/${config.userAgent}` : '');
75
-
76
- if (config.PHPSESSID) {
77
- if (isBrowser) {
78
- console.error('You cannot use a PHPSESSID to authenticate in a browser environment.');
79
- } else {
80
- this.headers['Cookie'] = `PHPSESSID=${config.PHPSESSID}`;
81
- }
82
- }
83
- }
84
-
85
- async request<T>(endpoint: string, params: object = {}): Promise<T> {
86
- const response = await this.axiosClient.post<T>(endpoint, params);
87
-
88
- const cookie = response.headers['set-cookie'];
89
- if (cookie && !isBrowser) this.headers['Cookie'] = cookie[0].split(';')[0];
90
-
91
- return response.data;
92
- }
93
-
94
- static async request<T>(endpoint: string, params: object = {}): Promise<T> {
95
- return (await this.axiosClient.get(`${endpoint}?_r=${objectToBase64(params)}`)).data;
96
- }
97
-
98
- /**
99
- * Attempts to remove the PHPSESSID cookie if using a browser, otherwise removes your Client's authentication.
100
- */
101
- async logout() {
102
- if (isBrowser) return await this.request('PutAuthLogout');
103
-
104
- delete this.headers['Cookie'];
105
- }
106
-
107
- // Endpoints (auto-generated with build-client)
1
+ import axios, { AxiosResponse, AxiosInstance, AxiosError } from 'axios';
2
+ import * as GetEndpoints from './endpoints/endpoints.get.js';
3
+ import * as PostEndpoints from './endpoints/endpoints.post.js'
4
+ import * as Responses from './responses.js';
5
+
6
+ const BASE_USER_AGENT = 'speedruncom.js';
7
+ const BASE_URL = 'https://www.speedrun.com/api/v2/';
8
+ const HEADERS = {
9
+ 'Accept-Language': 'en',
10
+ 'Accept': 'application/json'
11
+ }
12
+
13
+ const isBrowser = typeof window !== 'undefined';
14
+
15
+ const objectToBase64 = (obj: object) => {
16
+ const jsonString = JSON.stringify(obj).replace(/\s+/g, '');
17
+ return Buffer.from(jsonString).toString('base64');
18
+ }
19
+
20
+ export interface config {
21
+ PHPSESSID?: string;
22
+ userAgent?: string;
23
+ }
24
+
25
+ export class APIError extends Error {
26
+ status: number;
27
+
28
+ constructor(message: string, status: number) {
29
+ super(message);
30
+ this.message = message;
31
+ this.status = status;
32
+ }
33
+ }
34
+
35
+ export default class Client {
36
+
37
+ /**
38
+ * `AxiosInstance` used on instance-called methods (called with `POST`).
39
+ */
40
+ axiosClient = axios.create({
41
+ baseURL: BASE_URL,
42
+ method: 'POST',
43
+ withCredentials: true,
44
+ headers: HEADERS
45
+ });
46
+
47
+ /**
48
+ * `AxiosInstance` used on Client-called methods (called with `GET`).
49
+ */
50
+ static axiosClient = axios.create({
51
+ baseURL: BASE_URL,
52
+ method: 'GET',
53
+ headers: HEADERS
54
+ });
55
+
56
+ private username!: string;
57
+ private password!: string;
58
+
59
+ private headers = this.axiosClient.defaults.headers.common;
60
+
61
+ constructor(config?: config) {
62
+ if (config) this.config(config);
63
+
64
+ this.axiosClient.interceptors.response.use(
65
+ (response: AxiosResponse) => response,
66
+ (error: AxiosError) => {
67
+ const data = error.response.data as { error?: string };
68
+ throw new APIError(data.error || 'Unknown error', error.response.status);
69
+ }
70
+ );
71
+ }
72
+
73
+ config(config: config) {
74
+ if (!isBrowser) this.headers['User-Agent'] = BASE_USER_AGENT + (config.userAgent ? `/${config.userAgent}` : '');
75
+
76
+ if (config.PHPSESSID) {
77
+ if (isBrowser) {
78
+ console.error('You cannot use a PHPSESSID to authenticate in a browser environment.');
79
+ } else {
80
+ this.headers['Cookie'] = `PHPSESSID=${config.PHPSESSID}`;
81
+ }
82
+ }
83
+ }
84
+
85
+ async request<T>(endpoint: string, params: object = {}): Promise<T> {
86
+ const response = await this.axiosClient.post<T>(endpoint, params);
87
+
88
+ const cookie = response.headers['set-cookie'];
89
+ if (cookie && !isBrowser) this.headers['Cookie'] = cookie[0].split(';')[0];
90
+
91
+ return response.data;
92
+ }
93
+
94
+ static async request<T>(endpoint: string, params: object = {}): Promise<T> {
95
+ return (await this.axiosClient.get(`${endpoint}?_r=${objectToBase64(params)}`)).data;
96
+ }
97
+
98
+ /**
99
+ * Attempts to remove the PHPSESSID cookie if using a browser, otherwise removes your Client's authentication.
100
+ */
101
+ async logout() {
102
+ if (isBrowser) return await this.request('PutAuthLogout');
103
+
104
+ delete this.headers['Cookie'];
105
+ }
106
+
107
+ // Endpoints (auto-generated with build-client)
108
108
  }
@@ -1,80 +1,80 @@
1
- import { Project, SourceFile, Node, OptionalKind, MethodDeclarationStructure } from 'ts-morph';
2
-
3
- const project = new Project({
4
- tsConfigFilePath: "tsconfig.json",
5
- });
6
-
7
- const isInterfaceEmpty = (interfaceName: string, sourceFile: SourceFile) => {
8
- const declarations = sourceFile.getExportedDeclarations().get(interfaceName);
9
- if (!declarations || declarations.length === 0) return false;
10
-
11
- const decl = declarations[0];
12
- if (!Node.isInterfaceDeclaration(decl)) return false;
13
-
14
- const type = decl.getType();
15
- const allProperties = type.getProperties();
16
-
17
- return allProperties.length === 0;
18
- };
19
-
20
-
21
- const isInterfaceAllOptional = (name: string, sourceFile: SourceFile) => {
22
- const iface = sourceFile.getInterface(name);
23
- if (iface) {
24
- return iface.getType().getProperties().every(p => p.isOptional());
25
- }
26
-
27
- const typeNode = sourceFile.getTypeAliasOrThrow(name).getType();
28
- return typeNode
29
- .getProperties()
30
- .every(p => p.isOptional());
31
- };
32
-
33
- const baseClient = project.getSourceFileOrThrow('src/BaseClient.ts');
34
- const clientFile = project.createSourceFile('src/Client.ts', baseClient.getFullText(), { overwrite: true });
35
- const clientClass = clientFile.getClasses()[1];
36
-
37
- const getEndpointsFile = project.getSourceFileOrThrow('src/endpoints/endpoints.get.ts');
38
- const postEndpointsFile = project.getSourceFileOrThrow('src/endpoints/endpoints.post.ts');
39
- const responsesFile = project.getSourceFileOrThrow('src/responses.ts');
40
-
41
- const getEndpointNames = Array.from(getEndpointsFile.getExportedDeclarations().keys());
42
- const postEndpointNames = Array.from(postEndpointsFile.getExportedDeclarations().keys());
43
-
44
- const responseNames = new Set(Array.from(responsesFile.getExportedDeclarations().keys()));
45
-
46
- const makeMethod = (name: string, isStatic: boolean, returnType: string, isEmpty: boolean, interfaces: string, isOptional: boolean) => {
47
- const method: OptionalKind<MethodDeclarationStructure> = {
48
- name,
49
- isStatic,
50
- isAsync: true,
51
- returnType,
52
- statements: [`return await this.request('${name}'${!isEmpty ? ', params' : ''});`]
53
- }
54
- if (!isEmpty) method.parameters = [{
55
- name: 'params',
56
- type: `${interfaces}.${name}`,
57
- hasQuestionToken: isOptional
58
- }];
59
-
60
- clientClass.addMethod(method);
61
- }
62
-
63
- for (const endpointName of getEndpointNames) {
64
- const returnType = responseNames.has(endpointName) ? `Promise<Readonly<Responses.${endpointName}>>` : 'Promise<void>';
65
- const isEmpty = isInterfaceEmpty(endpointName, getEndpointsFile);
66
- const isAllOptional = isInterfaceAllOptional(endpointName, getEndpointsFile);
67
-
68
- makeMethod(endpointName, false, returnType, isEmpty, 'GetEndpoints', isAllOptional);
69
- makeMethod(endpointName, true, returnType, isEmpty, 'GetEndpoints', isAllOptional);
70
- }
71
-
72
- for (const endpointName of postEndpointNames) {
73
- const returnType = responseNames.has(endpointName) ? `Promise<Readonly<Responses.${endpointName}>>` : 'Promise<void>';
74
- const isEmpty = isInterfaceEmpty(endpointName, postEndpointsFile);
75
- const isAllOptional = isInterfaceAllOptional(endpointName, postEndpointsFile);
76
-
77
- makeMethod(endpointName, false, returnType, isEmpty, 'PostEndpoints', isAllOptional);
78
- }
79
-
1
+ import { Project, SourceFile, Node, OptionalKind, MethodDeclarationStructure } from 'ts-morph';
2
+
3
+ const project = new Project({
4
+ tsConfigFilePath: "tsconfig.json",
5
+ });
6
+
7
+ const isInterfaceEmpty = (interfaceName: string, sourceFile: SourceFile) => {
8
+ const declarations = sourceFile.getExportedDeclarations().get(interfaceName);
9
+ if (!declarations || declarations.length === 0) return false;
10
+
11
+ const decl = declarations[0];
12
+ if (!Node.isInterfaceDeclaration(decl)) return false;
13
+
14
+ const type = decl.getType();
15
+ const allProperties = type.getProperties();
16
+
17
+ return allProperties.length === 0;
18
+ };
19
+
20
+
21
+ const isInterfaceAllOptional = (name: string, sourceFile: SourceFile) => {
22
+ const iface = sourceFile.getInterface(name);
23
+ if (iface) {
24
+ return iface.getType().getProperties().every(p => p.isOptional());
25
+ }
26
+
27
+ const typeNode = sourceFile.getTypeAliasOrThrow(name).getType();
28
+ return typeNode
29
+ .getProperties()
30
+ .every(p => p.isOptional());
31
+ };
32
+
33
+ const baseClient = project.getSourceFileOrThrow('src/BaseClient.ts');
34
+ const clientFile = project.createSourceFile('src/Client.ts', baseClient.getFullText(), { overwrite: true });
35
+ const clientClass = clientFile.getClasses()[1];
36
+
37
+ const getEndpointsFile = project.getSourceFileOrThrow('src/endpoints/endpoints.get.ts');
38
+ const postEndpointsFile = project.getSourceFileOrThrow('src/endpoints/endpoints.post.ts');
39
+ const responsesFile = project.getSourceFileOrThrow('src/responses.ts');
40
+
41
+ const getEndpointNames = Array.from(getEndpointsFile.getExportedDeclarations().keys());
42
+ const postEndpointNames = Array.from(postEndpointsFile.getExportedDeclarations().keys());
43
+
44
+ const responseNames = new Set(Array.from(responsesFile.getExportedDeclarations().keys()));
45
+
46
+ const makeMethod = (name: string, isStatic: boolean, returnType: string, isEmpty: boolean, interfaces: string, isOptional: boolean) => {
47
+ const method: OptionalKind<MethodDeclarationStructure> = {
48
+ name,
49
+ isStatic,
50
+ isAsync: true,
51
+ returnType,
52
+ statements: [`return await this.request('${name}'${!isEmpty ? ', params' : ''});`]
53
+ }
54
+ if (!isEmpty) method.parameters = [{
55
+ name: 'params',
56
+ type: `${interfaces}.${name}`,
57
+ hasQuestionToken: isOptional
58
+ }];
59
+
60
+ clientClass.addMethod(method);
61
+ }
62
+
63
+ for (const endpointName of getEndpointNames) {
64
+ const returnType = responseNames.has(endpointName) ? `Promise<Readonly<Responses.${endpointName}>>` : 'Promise<void>';
65
+ const isEmpty = isInterfaceEmpty(endpointName, getEndpointsFile);
66
+ const isAllOptional = isInterfaceAllOptional(endpointName, getEndpointsFile);
67
+
68
+ makeMethod(endpointName, false, returnType, isEmpty, 'GetEndpoints', isAllOptional);
69
+ makeMethod(endpointName, true, returnType, isEmpty, 'GetEndpoints', isAllOptional);
70
+ }
71
+
72
+ for (const endpointName of postEndpointNames) {
73
+ const returnType = responseNames.has(endpointName) ? `Promise<Readonly<Responses.${endpointName}>>` : 'Promise<void>';
74
+ const isEmpty = isInterfaceEmpty(endpointName, postEndpointsFile);
75
+ const isAllOptional = isInterfaceAllOptional(endpointName, postEndpointsFile);
76
+
77
+ makeMethod(endpointName, false, returnType, isEmpty, 'PostEndpoints', isAllOptional);
78
+ }
79
+
80
80
  clientFile.saveSync();