- π Introduction
- π Getting Started
- π‘ Improvements to Make
- π Helpful Resources
Butler is a library for actively interfacing with Steam APIs to perform actions on the platform in a simplified fashion.
The large majority of "Steam bots" currently couple together passive interfacing (i.e listening to API events on poll) and active interfacing (i.e actions such as making and sending a Steam trade offer) within a single application. This causes coupling, make testing long and difficult.
By separating the logic into a passive component @automatedtf/sentinel
and an active component @automatedtf/butler
, it allows for cross-applicational systems to exist with newly-found opportunities for scalability, robustness and maintenance.
This library is designed to provide functions built for ephemeral execution, meaning that it is suitable for usage in stateful settings (i.e handler functions for a 24/7 bot instance) and stateless settings (i.e Cloud / Lambda functions).
To use the library, you will need to install it via npm
:
npm install @automatedtf/butler
This will add the library to your package.json
file.
Below are a few examples of how to use the library. There are more functionalities available, which are listed later on.
import { withLoginDetails } from '@automatedtf/butler';
const loginDetails: SteamLoginDetails = { ... };
const tradeURL: string = ...;
const draftOffer: OutgoingTradeOffer = await withLoginDetails(loginDetails).createOffer(tradeURL);
// Trade offer construction
const myItemsToAdd: Item[] = [...];
const theirItemsToAdd: Item[] = [...];
const tradeMessage: string = "Please accept!";
await draftOffer
.addMyItems(myItemsToAdd) // Add my items
.addTheirItems(theirItemsToAdd) // Add their items
.setMessage(tradeMessage) // Set trade message
.send(); // Send the offer!
console.log(draftOffer.offer.id); // The offer is given an ID after it is sent
import { withLoginDetails } from '@automatedtf/butler';
const loginDetails: SteamLoginDetails = { ... };
const offerid: string = ...;
const incomingOffer: IncomingTradeOffer = await withLoginDetails(loginDetails).getOffer(offerid);
console.log(incomingOffer.offer.id); // incomingOffer.offer.id === offerid
The interface SteamLoginDetails
details the various login details that may be needed to login.
interface SteamLoginDetails {
accountName: string;
password: string;
steamguard?: string; // Steam Guard code for those logging in on Steam Guard Authorisation email
authCode?: string; // Steam Guard code on first email authentication
twoFactorCode?: string;
captcha?: string;
disableMobile?: boolean;
logonID?: number;
}
These login details are used for logging into the Steam platform programmatically, being handled by Dr. Mckay's steamcommunity
.
π Read more about logging in with SteamCommunity
For the bare minimum to login, you should set the required fields for the SteamSecrets
interface.
const loginDetails: SteamSecrets = {
accountName: 'username',
password: 'password',
identitySecret: 'identitySecret',
sharedSecret: 'sharedSecret'
};
π Definition of SteamSecrets interface
The usage of a logonID
is optional.
When logging into an instance of a bot from @automatedtf/sentinel
, @automatedtf/butler
or by any other modules, it will kick all other sessions of the bot with the same logonID
.
To allow for multiple sessions of the bot to run, a unique logonID
for that running bot instance. This can be set explicitly or randomly. The module will generate a deterministic logonID
if not set, using the hash Date.now() % 2 ** 16
.
Logging in with any suitable form of login details will return a BotInstance
.
import { withLoginDetails, BotInstance } from '@automatedtf/butler';
const loginDetails: SteamLoginDetails = { ... };
const botInstance: BotInstance = withLoginDetails(loginDetails);
This class has its own set of methods that can be interfaced with to perform various additional actions on the Steam platform. Generally, this class will be used for accessing offers via BotInstance.getOffer
and BotInstance.createOffer
.
π Accessing other Steam Functionalities
Cookies are given when a steam-user
client joins a webSession
. These cookies can be used to log back into a previous session without having to login again. This is highly recommended when you are logging into multiple instances of a bot, as you may the receive the AlreadyLoggedInElsewhere
error at times.
To login with cookies, you can use the withCookies
function instead of withLoginDetails
.
import { withCookies } from '@automatedtf/butler';
const loginDetails: SteamLoginDetails = { ... };
const cookies: string[] = [...];
const botInstance: BotInstance = withCookies(cookies);
π Read more about logging in with Cookies
When an incoming trade offer is received from a user (e.g from receiving the event from @automatedtf/sentinel
or as part of an event handler), you can process it using the following methods attached to the IncomingTradeOffer
object that is returned when you decide to get the offer from using BotInstance.getOffer
.
import { withLoginDetails } from '@automatedtf/butler';
const offerid = '...';
const loginDetails: SteamLoginDetails = { ... };
const incomingOffer: IncomingTradeOffer = await withLoginDetails(loginDetails).getOffer(offerid);
// Accepting an incoming trade offer
await incomingOffer.accept();
import { withLoginDetails } from '@automatedtf/butler';
const offerid = '...';
const loginDetails: SteamLoginDetails = { ... };
const incomingOffer: IncomingTradeOffer = await withLoginDetails(loginDetails).getOffer(offerid);
// Decline an incoming trade offer
await incomingOffer.decline();
import { withLoginDetails } from '@automatedtf/butler';
const offerid = '...';
const loginDetails: SteamLoginDetails = { ... };
const incomingOffer: IncomingTradeOffer = await withLoginDetails(loginDetails).getOffer(offerid);
// Countering an incoming trade offer
const draftOutgoingOffer = await incomingOffer.counter();
// Next: Add items, set message, etc.
You can use the BotInstance.createOffer
method or IncomingTradeOffer.counter
method (as shown above) to create an outgoing trade offer.
import { withLoginDetails } from '@automatedtf/butler';
const tradeURL: string = ...;
const loginDetails: SteamLoginDetails = { ... };
const outgoingOffer: OutgoingTradeOffer = await withLoginDetails(loginDetails).createOffer(tradeURL);
const outgoingOffer: OutgoingTradeOffer = ...; // get somehow
const myItemsToAdd: Item[] = [...];
const theirItemsToAdd: Item[] = [...];
const message: string = "Please accept!";
outgoingOffer.addMyItems(myItemsToAdd); // Add my items
outgoingOffer.addTheirItems(theirItemsToAdd); // Add their items
The Item
interface is defined as follows:
interface Item {
assetid: string;
appid: number;
contextid: string;
amount: number;
}
You can fetch instances of Item
from a supported API or module, but generally, you can set this for yourself.
// Creating an `Item` for Team Fortress 2 items
import { ItemInstance } from '@automatedtf/sherpa';
const item: ItemInstance = ...; // Get somehow
function toItem(itemInstance: ItemInstance): Item {
return {
assetid: itemInstance.assetid,
appid: itemInstance.appid, // 440 for TF2
contextid: '2', // ContextID for all TF2 items
amount: 1
} as Item;
}
When you are ready, you can then send the trade offer.
// Set trade message (optional!)
outgoingOffer.setMessage(message);
// Send the offer!
await outgoingOffer.send();
Confirmation for the trade offer object is done automatically for convenience, meaning that you will not need to do anything else to ensure that the trade offer is sent to the other party.
You can get the new assetid
s for the items in the trade offer using the TradeOffer.getExchangeDetails
method that is provided for any OutgoingTradeOffer
or IncomingTradeOffer
instance.
π Further Details on getExchangeDetails Returned Data
import { withLoginDetails } from '@automatedtf/butler';
const outgoingOffer: OutgoingTradeOffer = ...; // get somehow
...
// ASSERT: outgoingOffer has been sent previously
...
const {
status,
tradeInitTime,
receivedItems,
sentItems
} = outgoingOffer.getExchangeDetails();
import { withLoginDetails } from '@automatedtf/butler';
const incomingOffer: IncomingTradeOffer = ...; // get somehow
...
// ASSERT: incomingOffer has been accepted previously
...
const {
status,
tradeInitTime,
receivedItems,
sentItems
} = incomingOffer.getExchangeDetails();
For ease, several methods are offered under the BotInstance
class to facilitate changing profile details.
import { withLoginDetails } from '@automatedtf/butler';
const loginDetails: SteamLoginDetails = { ... };
const botInstance: BotInstance = await withLoginDetails(loginDetails);
const profileDetails: Partial<SteamProfileDetails> = {
name: "New Name",
realName: "New Real Name",
summary: "New Summary",
country: "UK",
state: "FL",
city: "Orlando",
customURL: "myNewCustomURLToMyProfile",
}
await botInstance.editProfile(profileDetails);
This can be used to change the public profile details of the account.
import { withLoginDetails } from '@automatedtf/butler';
const loginDetails: SteamLoginDetails = { ... };
const botInstance: BotInstance = await withLoginDetails(loginDetails);
const photoURL: string = "...";
await botInstance.changeProfilePicture(photoURL);
For now, support is only provided for a https://
or http://
url to a photo.
You can access other Steam functionalities by using the methods of the BotInstance
class to access relevant clients. These will have already been instantiated for you, logged in, and ready to use.
BotInstance.accessTradeManager()
- Access the trade manager client.
BotInstance.accessCommunity()
- Access the community client.
As said, the library is designed to be for ephemeral executions, but usage can be tailored for persistent bot instances by holding the running BotInstance
object in memory.
import { withLoginDetails } from '@automatedtf/butler';
const loginDetails: SteamLoginDetails = { ... };
const botInstance = await withLoginDetails(loginDetails);
// Do stuff with botInstance
As such, there are potential opportunities of improvement to take advantage of to reduce the cold start time and improve the overall performance for using the library in a stateful setting.
Holding a bot instance in memory is a potential improvement when multiple (but centralised) accesses have to be made to the Steam account that is being operated programmatically as a bot.
For example, one may have a dedicated Express
server for executing actions for the bot to perform using @automatedtf/butler
. Rather than a new login being performed always every single time when using withLoginDetails
, withLoginDetails
can first access a cache of BotInstance
objects that have been created previously and then return the cached instance if present.