Skip to content

Commit

Permalink
Merge branch 'master' into IPC
Browse files Browse the repository at this point in the history
  • Loading branch information
idinium96 committed Mar 31, 2024
2 parents 2ec05cb + 23923d0 commit f0e6c68
Show file tree
Hide file tree
Showing 8 changed files with 83 additions and 145 deletions.
5 changes: 2 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "tf2autobot",
"version": "5.10.0",
"version": "5.11.0",
"description": "Fully automated TF2 trading bot advertising on www.backpack.tf using prices from www.prices.tf, Originally made by Nicklason.",
"main": "dist/app.js",
"scripts": {
Expand Down Expand Up @@ -65,7 +65,6 @@
"retry": "^0.13.1",
"semver": "^7.5.4",
"socket.io-client": "^4.7.2",
"steam-session": "^1.7.1",
"steam-totp": "^2.1.2",
"steam-user": "^5.0.4",
"steamid": "^2.0.0",
Expand Down
191 changes: 67 additions & 124 deletions src/classes/Bot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,6 @@ import fs from 'fs';
import path from 'path';
import * as files from '../lib/files';

// Reference: https://github.com/tf2-automatic/tf2-automatic/commit/cf7b807cae11eb172a78ef184bbafdb4ebe86501#diff-58f39591209025b16105c9f25a34c119332983a0d8cea7819b534d9d408324c4L329
// Credit to @Nicklason
import { EAuthSessionGuardType, EAuthTokenPlatformType, LoginSession } from 'steam-session';
import jwt from 'jsonwebtoken';
import DiscordBot from './DiscordBot';
import { Message as DiscordMessage } from 'discord.js';
Expand Down Expand Up @@ -68,8 +65,6 @@ export default class Bot {

readonly manager: TradeOfferManager;

session: LoginSession | null = null;

readonly community: SteamCommunity;

tradeOfferUrl: string;
Expand Down Expand Up @@ -842,13 +837,14 @@ export default class Bot {
let cookies: string[];

this.addListener(this.client, 'loggedOn', this.handler.onLoggedOn.bind(this.handler), false);
this.addListener(this.client, 'refreshToken', this.handler.onRefreshToken.bind(this.handler), false);
this.addAsyncListener(this.client, 'friendMessage', this.onMessage.bind(this), true);
this.addListener(this.client, 'friendRelationship', this.handler.onFriendRelationship.bind(this.handler), true);
this.addListener(this.client, 'groupRelationship', this.handler.onGroupRelationship.bind(this.handler), true);
this.addListener(this.client, 'newItems', this.onNewItems.bind(this), true);
this.addListener(this.client, 'webSession', this.onWebSession.bind(this), false);
this.addListener(this.client, 'steamGuard', this.onSteamGuard.bind(this), false);
this.addListener(this.client, 'error', this.onError.bind(this), false);
this.addAsyncListener(this.client, 'error', this.onError.bind(this), false);

this.addListener(this.community, 'sessionExpired', this.onSessionExpired.bind(this), false);
this.addListener(this.community, 'confKeyNeeded', this.onConfKeyNeeded.bind(this), false);
Expand Down Expand Up @@ -894,10 +890,10 @@ export default class Bot {
return callback(null);
});
},
(callback): void => {
async (callback): Promise<void> => {
log.info('Signing in to Steam...');

this.login()
this.login(await this.getRefreshToken())
.then(() => {
log.info('Signed in to Steam!');
if (this.options.IPC) this.ipc.init();
Expand Down Expand Up @@ -1381,113 +1377,7 @@ export default class Bot {
});
}

private async startSession(): Promise<string> {
this.session = new LoginSession(EAuthTokenPlatformType.SteamClient);
// will think about proxy later
//,{
// httpProxy: this.configService.getOrThrow<SteamAccountConfig>('steam').proxyUrl
// });

this.session.on('debug', (message: string) => {
log.debug(`Session debug: ${message}`);
});

const oldTokens = (await files.readFile(this.handler.getPaths.files.loginToken, true).catch(err => {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-unsafe-member-access
log.warn(`Failed to read tokens: ${err.message}`);
return null;
})) as SteamTokens;

if (oldTokens !== null) {
// Figure out if the refresh token expired
const { refreshToken, accessToken } = oldTokens;

this.session.refreshToken = refreshToken;
this.session.accessToken = accessToken;

const decoded = jwt.decode(refreshToken, {
complete: true
});

if (decoded) {
const { exp } = decoded.payload as { exp: number };

if (exp < Date.now() / 1000) {
// Refresh token expired, log in again
log.debug('Refresh token expired, logging in again');
} else {
// Refresh token is still valid, use it
return refreshToken;
}
}
}

const result = await this.session.startWithCredentials({
accountName: this.options.steamAccountName,
password: this.options.steamPassword
});

if (result.actionRequired) {
const actions = result.validActions ?? [];

if (actions.length !== 1) {
throw new Error(`Unexpected number of valid actions: ${actions.length}`);
}

const action = actions[0];

if (action.type !== EAuthSessionGuardType.DeviceCode) {
throw new Error(`Unexpected action type: ${action.type}`);
}

await this.session.submitSteamGuardCode(SteamTotp.generateAuthCode(this.options.steamSharedSecret));
}

this.session.on('error', err => {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-unsafe-member-access
log.warn(`Error in session: ${err.message}`);
});

/* eslint-disable @typescript-eslint/no-non-null-assertion */
return new Promise<void>((resolve, reject) => {
const handleAuth = () => {
log.debug('Session authenticated');
removeListeners();
resolve();
};

const handleTimeout = () => {
removeListeners();
reject(new Error('Login session timed out'));
};

const handleError = (err: Error) => {
removeListeners();
reject(err);
};

const removeListeners = () => {
this.session.removeListener('authenticated', handleAuth);
this.session.removeListener('timeout', handleTimeout);
this.session.removeListener('error', handleError);
};

this.session.once('authenticated', handleAuth);
this.session.once('error', handleError);
}).then(() => {
const refreshToken = this.session.refreshToken;

this.handler.onLoginToken({ refreshToken, accessToken: this.session.accessToken });

this.session.removeAllListeners();
this.session = null;

return refreshToken;
});
/* eslint-enable @typescript-eslint/no-non-null-assertion */
}

private async login(): Promise<void> {
private async login(refreshToken?: string): Promise<void> {
log.debug('Starting login attempt');
// loginKey: loginKey,
// private: true
Expand All @@ -1497,19 +1387,12 @@ export default class Bot {
this.handler.onLoginThrottle(wait);
}

const refreshToken = await this.startSession();

return new Promise((resolve, reject) => {
setTimeout(() => {
const listeners = this.client.listeners('error');

this.client.removeAllListeners('error');

const details = { refreshToken };

this.newLoginAttempt();
this.client.logOn(details);

const gotEvent = (): void => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
Expand Down Expand Up @@ -1543,16 +1426,74 @@ export default class Bot {
this.client.removeListener('error', errorEvent);

log.debug('Did not get login response from Steam');
this.client.logOff();

reject(new Error('Did not get login response (Steam might be down)'));
}, 60 * 1000);

this.client.once('loggedOn', loggedOnEvent);
this.client.once('error', errorEvent);

let loginDetails: { refreshToken: string } | { accountName: string; password: string };

if (refreshToken) {
log.debug('Attempting to login to Steam with refresh token...');
loginDetails = { refreshToken };
} else {
log.debug('Attempting to login to Steam...');
loginDetails = {
accountName: this.options.steamAccountName,
password: this.options.steamPassword
};
}

this.newLoginAttempt();
this.client.logOn(loginDetails);
}, wait);
});
}

private calculateBackoff(delay: number, attempts: number): number {
return delay * Math.pow(2, attempts - 1) + Math.floor(Math.random() * 1000);
}

private async getRefreshToken(): Promise<string | null> {
const tokenPath = this.handler.getPaths.files.refreshToken;

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const refreshToken = (await files.readFile(tokenPath, false).catch(err => null)) as string;

if (!refreshToken) {
return null;
}

const decoded = jwt.decode(refreshToken, {
complete: true
});

if (!decoded) {
// Invalid token
return null;
}

const { exp } = decoded.payload as { exp: number };

if (exp < Date.now() / 1000) {
// Refresh token expired
return null;
}

return refreshToken;
}

private async deleteRefreshToken(): Promise<void> {
const tokenPath = this.handler.getPaths.files.refreshToken;

await files.writeFile(tokenPath, '', false).catch(() => {
// Ignore error
});
}

sendMessage(steamID: SteamID | string, message: string): void {
if (steamID instanceof SteamID && steamID.redirectAnswerTo) {
const origMessage = steamID.redirectAnswerTo;
Expand Down Expand Up @@ -1675,7 +1616,7 @@ export default class Bot {
});
}

private onError(err: CustomError): void {
private async onError(err: CustomError): Promise<void> {
if (err.eresult === EResult.LoggedInElsewhere) {
log.warn('Signed in elsewhere, stopping the bot...');
this.botManager.stop(err, false, true);
Expand All @@ -1690,7 +1631,9 @@ export default class Bot {

log.warn('Login session replaced, relogging...');

this.login().catch(err => {
await this.deleteRefreshToken();

this.login(await this.getRefreshToken()).catch(err => {
if (err) {
throw err;
}
Expand Down
5 changes: 0 additions & 5 deletions src/classes/BotManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,11 +195,6 @@ export default class BotManager {
this.bot.client.setPersona(EPersonaState.Snooze);
this.bot.client.autoRelogin = false;

if (this.bot.session) {
this.bot.session.removeAllListeners();
this.bot.session.cancelLoginAttempt();
}

// Stop polling offers
this.bot.manager.pollInterval = -1;

Expand Down
2 changes: 2 additions & 0 deletions src/classes/Commands/sub-classes/PricelistManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,8 @@ export default class PricelistManagerCommands {
if (params.id) {
priceKey = String(params.id);
params.id = String(params.id);
// force intent sell for assetid added
params.intent = 1;
}
priceKey = priceKey ? priceKey : params.sku;
return this.bot.pricelist
Expand Down
4 changes: 2 additions & 2 deletions src/classes/Handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import SteamID from 'steamid';
import TradeOfferManager, { PollData, Meta, CustomError } from '@tf2autobot/tradeoffer-manager';
import Bot, { SteamTokens } from './Bot';
import Bot from './Bot';
import { Entry, PricesDataObject, PricesObject } from './Pricelist';
import { Blocked } from './MyHandler/interfaces';

Expand Down Expand Up @@ -47,7 +47,7 @@ export default abstract class Handler {
* Called when a new login key has been issued
* @param loginKey - The new login key
*/
abstract onLoginToken(loginToken: SteamTokens): void;
abstract onRefreshToken(token: string): void;

/**
* Called when a new trade offer is being processed
Expand Down
14 changes: 7 additions & 7 deletions src/classes/MyHandler/MyHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import { keepMetalSupply, craftDuplicateWeapons, craftClassWeapons } from './uti
import { Blocked, BPTFGetUserInfo } from './interfaces';

import Handler, { OnRun } from '../Handler';
import Bot, { SteamTokens } from '../Bot';
import Bot from '../Bot';
import Pricelist, { Entry, PricesDataObject, PricesObject } from '../Pricelist';
import Commands from '../Commands/Commands';
import CartQueue from '../Carts/CartQueue';
Expand Down Expand Up @@ -536,19 +536,19 @@ export default class MyHandler extends Handler {
if (!respondChat) return resp;
}

onLoginToken(loginToken: SteamTokens): void {
log.debug('New login key');
onRefreshToken(token: string): void {
log.debug('New refresh key');

files.writeFile(this.paths.files.loginToken, loginToken, true).catch(err => {
log.warn('Failed to save login token: ', err);
files.writeFile(this.paths.files.refreshToken, token, false).catch(err => {
log.warn('Failed to save refresh token: ', err);
});
}

onLoginError(err: CustomError): void {
if (err.eresult === EResult.AccessDenied) {
// Access denied during login
files.deleteFile(this.paths.files.loginToken).catch(err => {
log.warn('Failed to delete login token file: ', err);
files.deleteFile(this.paths.files.refreshToken).catch(err => {
log.warn('Failed to delete refresh token file: ', err);
});
}
}
Expand Down
Loading

0 comments on commit f0e6c68

Please sign in to comment.