Last active
June 15, 2024 18:12
-
-
Save virtuallyunknown/fa0dfba15cc6791f1b0d4e203ee4c80f to your computer and use it in GitHub Desktop.
TM API
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import axios from 'axios'; | |
import { decode } from 'jsonwebtoken'; | |
import { z } from 'zod'; | |
const cotdListSchema = z.object({ | |
monthList: z.array( | |
z.object({ | |
year: z.number(), | |
month: z.number(), | |
lastDay: z.number(), | |
days: z.array( | |
z.object({ | |
campaignId: z.number(), | |
mapUid: z.string(), | |
day: z.number(), | |
monthDay: z.number(), | |
seasonUid: z.string(), | |
leaderboardGroup: z.null(), | |
startTimestamp: z.number(), | |
endTimestamp: z.number(), | |
relativeStart: z.number(), | |
relativeEnd: z.number(), | |
}) | |
), | |
media: z.object({ | |
buttonBackgroundUrl: z.string(), | |
buttonForegroundUrl: z.string(), | |
decalUrl: z.string(), | |
popUpBackgroundUrl: z.string(), | |
popUpImageUrl: z.string(), | |
liveButtonBackgroundUrl: z.string(), | |
liveButtonForegroundUrl: z.string() | |
}) | |
}) | |
), | |
itemCount: z.number(), | |
nextRequestTimestamp: z.number(), | |
relativeNextRequest: z.number() | |
}); | |
const authTicketResponseSchema = z.object({ | |
ticket: z.string().min(2000), | |
}); | |
const tokenResponseSchema = z.object({ | |
accessToken: z.string(), | |
refreshToken: z.string(), | |
}); | |
const jwtTokenSchema = z.object({ | |
exp: z.number(), | |
}); | |
type Credentials = { | |
email: string; | |
password: string; | |
}; | |
class TMApi { | |
private email: Credentials['email']; | |
private password: Credentials['password']; | |
private authToken: string; | |
private userAgent: string; | |
private tokens?: { | |
accessToken: { | |
token: string; | |
expires: number; | |
}; | |
refreshToken: { | |
token: string; | |
expires: number; | |
}; | |
}; | |
constructor({ email, password, userAgent }: Credentials & { userAgent: string }) { | |
this.email = email; | |
this.password = password; | |
this.authToken = Buffer.from(`${this.email}:${this.password}`).toString('base64'); | |
this.userAgent = userAgent; | |
} | |
private async getTokens() { | |
const ticketRes = await axios({ | |
url: 'https://public-ubiservices.ubi.com/v3/profiles/sessions', | |
method: 'post', | |
headers: { | |
'User-Agent': this.userAgent, | |
'Content-Type': 'application/json', | |
'Ubi-AppId': '86263886-327a-4328-ac69-527f0d20a237', | |
'Authorization': `Basic ${this.authToken}` | |
} | |
}); | |
const ticketData = authTicketResponseSchema.parse(ticketRes.data); | |
const tokensRes = await axios({ | |
url: 'https://prod.trackmania.core.nadeo.online/v2/authentication/token/ubiservices', | |
method: 'post', | |
headers: { | |
'User-Agent': this.userAgent, | |
'Content-Type': 'application/json', | |
'Authorization': `ubi_v1 t=${ticketData.ticket}` | |
}, | |
data: { | |
audience: 'NadeoLiveServices' | |
} | |
}); | |
const parsedTokens = tokenResponseSchema.parse(tokensRes.data); | |
return { | |
accessToken: { | |
token: parsedTokens.accessToken, | |
expires: jwtTokenSchema.parse(decode(parsedTokens.accessToken)).exp * 1000 | |
}, | |
refreshToken: { | |
token: parsedTokens.refreshToken, | |
expires: jwtTokenSchema.parse(decode(parsedTokens.refreshToken)).exp * 1000 | |
} | |
}; | |
} | |
private async refreshAccessToken(refreshToken: string) { | |
const tokenRes = await axios({ | |
url: 'https://prod.trackmania.core.nadeo.online/v2/authentication/token/refresh', | |
method: 'post', | |
headers: { | |
'User-Agent': this.userAgent, | |
'Content-Type': 'application/json', | |
'Authorization': `nadeo_v1 t=${refreshToken}` | |
}, | |
}); | |
const parsedTokens = tokenResponseSchema.parse(tokenRes.data); | |
return { | |
token: parsedTokens.accessToken, | |
expires: jwtTokenSchema.parse(decode(parsedTokens.accessToken)).exp * 1000 | |
}; | |
} | |
private async ensureAccessToken() { | |
if (!this.tokens) { | |
const tokens = await this.getTokens(); | |
this.tokens = tokens; | |
} | |
if (this.tokens.accessToken.expires > Date.now()) { | |
return; | |
} | |
if (this.tokens.refreshToken.expires <= Date.now()) { | |
const tokens = await this.getTokens(); | |
this.tokens = tokens; | |
return; | |
} | |
if (this.tokens.accessToken.expires <= Date.now()) { | |
const accessToken = await this.refreshAccessToken(this.tokens.refreshToken.token); | |
this.tokens = ({ accessToken, refreshToken: this.tokens.refreshToken }); | |
return; | |
} | |
} | |
public async getCOTDmaps() { | |
await this.ensureAccessToken(); | |
const res = await axios({ | |
url: 'https://live-services.trackmania.nadeo.live/api/token/campaign/month?length=99&offset=0', | |
method: 'get', | |
headers: { | |
'User-Agent': this.userAgent, | |
'Content-Type': 'application/json', | |
'Authorization': `nadeo_v1 t=${this.tokens?.accessToken.token}` | |
}, | |
}); | |
return cotdListSchema.parse(res.data) | |
} | |
} | |
export const tmApi = new TMApi({ | |
userAgent: 'Your user agent', | |
email: 'Ubisoft account email', | |
password: 'Ubisoft account pass', | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment