Hey everyone,
I'm developing a discord bot announcing changes in apps (kinda like steamdb).
Everything works perfectly fine for the first few hours, but then appUpdate no longer fires.
I haven't seen any messages thrown by steamCommunity "sessionExpired" event, client "disconnected" and "error" events.
This scenario happened every time, but yesterday after running bot for about a week, I got an app update notification
which is strange for me, because it was always completely silent after a few hours.
I was searching through github issues and forum as well, but found nothing.
I've tried implementing reconnecting to steamcommunity, but can't go past "Must not be anonymous user to use webLogOn (check to see you passed in valid credentials to logOn)".
I think that is because it's logging via refreshToken.
I don't even know if appUpdate is managed by steamcommunity.
steam-client.ts
steamClient: SteamUser;
steamCommunity: SteamCommunity;
listener: SteamListener;
private RECONNECT_INTERVAL = 10000;
private issuedRefreshToken: string | null;
private useAnonymous: boolean;
private refreshToken: string | null;
private accountName: string | null;
private password: string | null;
private machineName: string | null;
private shouldReconnect = false;
constructor() {
this.useAnonymous = Config.default.steam.anonymous;
this.refreshToken = Config.default.steam.refresh_token;
this.accountName = Config.default.steam.account_name;
this.password = Config.default.steam.password;
this.machineName = Config.default.steam.machine_name;
this.issuedRefreshToken = null;
this.listener = new SteamListener();
this.steamCommunity = new SteamCommunity();
this.steamClient = new SteamUser({
"enablePicsCache": true,
"autoRelogin": true,
"changelistUpdateInterval": 15000,
"renewRefreshTokens": true,
});
this.steamClient.on("webSession", (_, cookies) => {
console.log(chalk.green("Steam reconnected."));
this.steamCommunity.setCookies(cookies);
this.steamCommunity.startConfirmationChecker(10000, '=');
this.shouldReconnect = false;
})
this.steamCommunity.on("sessionExpired", () => {
console.log(chalk.red("Steam session expired."));
this.shouldReconnect = true;
});
this.steamClient.on("disconnected", (eresult, msg) => {
console.log(chalk.red(`Disconnected from steam: [ EResult: ${eresult} ] ${msg}`));
});
this.steamClient.on("error", (error) => {
console.log(chalk.red(error));
});
}
private attemptToReconnectTick() {
console.log("b")
if(this.shouldReconnect) {
if(this.steamClient.steamID) {
this.steamClient.webLogOn();
}
this.steamClient.logOn();
}
setTimeout(this.attemptToReconnectTick.bind(this), this.RECONNECT_INTERVAL);
}
async connect() : Promise<void> {
console.log("[/] Connecting to Steam...");
if(this.useAnonymous) {
return await this.loginAsAnonymous();
}
if(this.refreshToken != null && this.refreshToken != "") {
await this.loginWithRefreshToken();
// TEST - start reconnect task
this.shouldReconnect = true;
return this.attemptToReconnectTick();
}
await this.loginWithUsernameAndPassword();
}
async loginAsAnonymous() {
this.steamClient.logOn({
"anonymous": true,
"machineName": this.machineName ? this.machineName : undefined
});
await this.waitForLogin();
this.listener.registerListeners(this);
console.log(chalk.green(`Steam Client connected as anonymous.`));
}
private async loginWithUsernameAndPassword() {
if(this.accountName == null || this.password == null
|| this.accountName.includes(" ") || this.password.includes(" ")
|| this.accountName.replace(" ", "") == "" || this.password.replace(" ", "") == ""
) {
console.log(chalk.bgRed("ERROR") + " " + chalk.red("This steam authentication method requires username and password."))
process.exit(0);
}
this.steamClient.logOn({
"accountName": this.accountName!,
"password": this.password!,
"machineName": this.machineName ? this.machineName : undefined
});
await this.waitForLogin();
this.listener.registerListeners(this);
console.log(chalk.green(`Steam Client connected using username/password as ${this.steamClient.accountInfo?.name}.`));
if(this.issuedRefreshToken != null && this.issuedRefreshToken != ""
) {
console.log(chalk.blue("Steam uses refresh tokens when logging in."));
console.log(chalk.blue("Your refresh token is: ") + chalk.bgBlue(this.issuedRefreshToken));
console.log(chalk.blue("It's recommended to use it instead of username/password. You can change that in the config file."));
}
}
private async loginWithRefreshToken() {
if(
this.refreshToken == null || this.refreshToken == "" || this.refreshToken.includes(" ")
) {
console.log(chalk.bgRed("ERROR") + " " + chalk.red("This steam authentication method requires a refresh token."))
process.exit(0);
}
this.steamClient.logOn({
"accountName": this.accountName!,
"password": this.password!,
"machineName": this.machineName ? this.machineName : undefined
});
await this.waitForLogin();
this.listener.registerListeners(this);
console.log(chalk.green(`Steam Client connected using refresh token as ${this.steamClient.accountInfo?.name}.`));
}
// library provides any type, so we need to use it
// deno-lint-ignore no-explicit-any
private waitForLogin() : Promise<Record<string, any>> {
return new Promise((resolve) => {
this.steamClient.once("loggedOn", (loginInfo) => {
resolve(loginInfo);
});
this.steamClient.once("refreshToken", (token) => {
this.issuedRefreshToken = token;
});
this.steamClient.once("error", (error) => {
throw error;
});
})
}
getApplicationInfo(appId: number) : Promise<AppInfo | null> {
return new Promise((resolve) => {
this.steamClient.getProductInfo([appId], [], false, (_err, apps) => {
resolve(apps[appId]);
})
});
}
steam-listener.ts
registerListeners(steamClient: SteamClient) {
steamClient.steamClient.on("appUpdate", this.onAppUpdateReceived.bind(this));
}
/**
* Called when an app is updated.
* @param appid The id of the app that was updated.
* @param data The new app info.
*/
async onAppUpdateReceived(appId: number, data: AppInfo): Promise<void> {
if(await UserApplicationsController.isApplicationPresentAnywhere(appId) == false &&
await ServerApplicationsController.isApplicationPresentAnywhere(appId) == false) {
return;
}
const cachedApp: ICachedApplications | null = await CachedApplicationsController.getCachedApplication(appId);
const newAppInfo: AppInfo | null = await SteamClientInstance.getApplicationInfo(appId);
console.log(`[*] Received update for application ${appId}.`);
if(cachedApp != null && cachedApp.changenumber == data.changenumber) {
console.log(`[*] No changes detected.`);
return;
}
if(cachedApp == null || newAppInfo == null) {
return CachedApplicationsController.cacheApplication(newAppInfo);
}
// don't announce if only the changenumber changed
const changes = ApplicationDataComparator.compareApplicationData(cachedApp, newAppInfo);
if(!this.isChangeOnlyChangeNumber(changes)) {
await AnnouncementsManager.announceApplicationChangelist(data.changenumber, appId, changes);
}
// finally save the new app info
await CachedApplicationsController.cacheApplication(newAppInfo);
}
private isChangeOnlyChangeNumber(changes: ApplicationChanges): boolean {
let isOnlyChangeNumber = true;
keys<ApplicationChanges>(changes).forEach(key => {
const change = changes[key];
// changeNumber changes every time a change is detected
// so we don't need to check if changeNumber actually changed
// we are skipping branches because they are handled separately
if(key == "branches" || key == "changenumber") {
return;
}
if(this.didChange(change)) {
isOnlyChangeNumber = false;
}
});
// check for branch changes
Object.keys(changes.branches).forEach(branchName => {
const branchChanges = changes.branches[branchName];
keys<BranchChanges>(branchChanges).forEach(key => {
const change = changes.branches[branchName][key];
if(this.didChange(change)) {
isOnlyChangeNumber = false;
}
});
});
return isOnlyChangeNumber;
}
private didChange(change: ChangeFromTo): boolean {
if((change.from == null || change.from == "") && (change.to != null && change.to != "")) {
return true;
}
if((change.to == null || change.to == "") && (change.from != null && change.from != "")) {
return true;
}
if(change.from != change.to) {
return true;
}
return false;
}
private getCurrentDateAsString() : string {
const options: Intl.DateTimeFormatOptions = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' };
return new Date().toLocaleDateString("pl-PL", options);
}