├── handlers ├── offerChanged.js ├── ready.js ├── communityReady.js ├── tradeError.js ├── tradeResult.js ├── tradeProposed.js ├── end.js └── sessionStart.js ├── package.json ├── scripts ├── import-hash.js └── import-ssfn.js ├── README.md └── index.js /handlers/offerChanged.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = function () { 3 | 4 | var stem = this; 5 | 6 | stem.log.info('Item added, readying up'); 7 | 8 | stem.botTrade.ready(); 9 | 10 | }; 11 | -------------------------------------------------------------------------------- /handlers/ready.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = function () { 3 | 4 | var stem = this; 5 | 6 | stem.log.info('Other bot is now ready, confirming trade'); 7 | 8 | stem.botTrade.ready(function () { 9 | 10 | stem.botTrade.confirm(function () { 11 | 12 | stem.log.info('Trade confirmed'); 13 | 14 | }); 15 | 16 | }); 17 | 18 | }; 19 | -------------------------------------------------------------------------------- /handlers/communityReady.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = function () { 3 | 4 | var stem = this; 5 | 6 | stem.states.isCommunityReady = true; 7 | stem.states.isGiver = (stem.config.initBot === stem.bot.steamID); 8 | 9 | // Wait for the `initBot` to send the trade first 10 | if (stem.config.initBot !== stem.bot.steamID) 11 | return; 12 | 13 | stem.log.info('Sending trade to', stem.config.initTradeBot); 14 | 15 | stem.bot.trade(stem.config.initTradeBot); 16 | 17 | }; 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "auto-trader", 3 | "version": "2.1.1", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/alvinl/auto-trader.git" 12 | }, 13 | "author": "Alvin Lazaro ", 14 | "license": "MIT", 15 | "dependencies": { 16 | "async": "^0.9.0", 17 | "lodash": "^2.4.1" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /handlers/tradeError.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = function () { 3 | 4 | var stem = this; 5 | 6 | stem.log.warn('Refreshing cookies'); 7 | 8 | stem.bot.webLogOn(function (cookies) { 9 | 10 | stem.log.warn('Refreshed cookies'); 11 | 12 | // Apply new cookies 13 | cookies.forEach(function (cookie) { 14 | 15 | stem.botTrade.setCookie(cookie); 16 | 17 | }); 18 | 19 | // Reopen trade session if the bot was trading 20 | if (stem.states.isTrading) 21 | stem.botTrade.open(stem.states.lastTraded); 22 | 23 | }); 24 | 25 | }; 26 | -------------------------------------------------------------------------------- /handlers/tradeResult.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = function (tradeID, tradeResponse, steamID) { 3 | 4 | var stem = this; 5 | 6 | // Trade request was accepted 7 | if (!tradeResponse) { 8 | 9 | stem.states.isPendingTradeResult = false; 10 | return; 11 | 12 | } 13 | 14 | stem.log.warn('Trade request was not accepted, resending in 3 seconds'); 15 | 16 | // Trade request wasn't accepted, retry in a few seconds 17 | setTimeout(function () { 18 | 19 | stem.states.isPendingTradeResult = true; 20 | stem.bot.trade(steamID); 21 | 22 | }, 3000); 23 | 24 | }; 25 | -------------------------------------------------------------------------------- /scripts/import-hash.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var botName = process.argv[2], 4 | automaticHash = process.argv[3], 5 | cwd = process.cwd(), 6 | fs = require('fs'); 7 | 8 | if (!botName || !automaticHash) 9 | return console.error('Usage: node import-hash.js '); 10 | 11 | console.log('Importing hash for %s and saving to %s', botName, cwd + '/.' + botName); 12 | 13 | fs.writeFile(cwd + '/.' + botName, new Buffer(automaticHash, 'base64'), function (err) { 14 | 15 | if (err) 16 | return console.error('Failed to import hash:', err.message); 17 | 18 | console.log('Hash has been imported!'); 19 | 20 | }); 21 | -------------------------------------------------------------------------------- /handlers/tradeProposed.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = function (tradeID, steamID) { 3 | 4 | var stem = this; 5 | 6 | // Only accept trades from `initBot` & `initTradeBot` 7 | if (!~stem.states.tradeWhitelist.indexOf(steamID)) 8 | return stem.bot.respondToTrade(tradeID, false); 9 | 10 | // Deny trade if not yet logged into Steam community 11 | else if (!stem.states.isCommunityReady) 12 | return stem.bot.respondToTrade(tradeID, false); 13 | 14 | // Deny trade request if the current session is still in progress 15 | else if (stem.states.isTrading) 16 | return stem.bot.respondToTrade(tradeID, false); 17 | 18 | stem.log.info('Accepting trade request from', steamID); 19 | 20 | // Accept trade 21 | stem.bot.respondToTrade(tradeID, true); 22 | 23 | }; 24 | -------------------------------------------------------------------------------- /scripts/import-ssfn.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var sha = require('crypto').createHash('sha1'), 4 | path = require('path'), 5 | botName = process.argv[2], 6 | ssfnLocation = process.argv[3], 7 | cwd = process.cwd(), 8 | fs = require('fs'); 9 | 10 | if (!botName || !ssfnLocation) 11 | return console.error('Usage: node import-ssfn.js '); 12 | 13 | console.log('Importing hash for %s and saving to %s', botName, cwd + '/.' + botName); 14 | 15 | fs.readFile(path.resolve(cwd, ssfnLocation), function (err, ssfnData) { 16 | 17 | if (err) 18 | return console.error('Failed to import ssfn:', err.message); 19 | 20 | sha.update(ssfnData); 21 | sha = new Buffer(sha.digest(), 'binary'); 22 | 23 | fs.writeFile(cwd + '/.' + botName, sha, function (err) { 24 | 25 | if (err) 26 | return console.error('Failed to save hash:', err.message); 27 | 28 | console.log('Ssfn has been imported!'); 29 | 30 | }); 31 | 32 | }); 33 | -------------------------------------------------------------------------------- /handlers/end.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = function () { 3 | 4 | var stem = this; 5 | 6 | stem.log.info('Trade ended'); 7 | 8 | // Ignore `end` event if trade was cancelled by the desync check 9 | if (stem.states.isCancelledByDesync) { 10 | 11 | stem.states.isCancelledByDesync = false; 12 | return; 13 | 14 | } 15 | 16 | stem.states.tradesCompleted++; 17 | stem.states.tradesPerMin++; 18 | 19 | stem.states.isTrading = false; 20 | stem.states.isGiver = !stem.states.isGiver; 21 | 22 | if (!stem.states.isGiver) 23 | return; 24 | 25 | // Check if `notifyThreshold` was reached 26 | if (stem.states.tradesCompleted === stem.config.notifyThreshold && !stem.states.isAdminNotified) { 27 | 28 | stem.log.warn('Notify threshold reached, notifying admins'); 29 | 30 | stem.config.admins.forEach(function (steamID) { 31 | 32 | stem.bot.sendMessage(steamID, 'Notify threshold reached! Trades completed: ' + stem.states.tradesCompleted); 33 | 34 | }); 35 | 36 | stem.states.isAdminNotified = true; 37 | 38 | } 39 | 40 | stem.log.info('Sending trade to', stem.states.lastTraded); 41 | stem.bot.trade(stem.states.lastTraded); 42 | 43 | }; 44 | -------------------------------------------------------------------------------- /handlers/sessionStart.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Dependencies 4 | */ 5 | 6 | var async = require('async'), 7 | _ = require('lodash'); 8 | 9 | 10 | /** 11 | * Internal names for cases, chests and crates 12 | * @type {Array} 13 | */ 14 | var INTERNAL_CRATE_NAMES = ['Supply Crate', 15 | 'TF_LockedCrate', 16 | 'CSGO_Type_WeaponCase', 17 | 'treasure_chest', 18 | 'retired_treasure_chest']; 19 | 20 | module.exports = function (steamID) { 21 | 22 | var stem = this; 23 | 24 | stem.states.lastTraded = steamID; 25 | stem.states.isTrading = true; 26 | 27 | stem.log.info('Trade session started'); 28 | 29 | // Open trade session 30 | stem.botTrade.open(steamID); 31 | 32 | if (!stem.states.isGiver) 33 | return; 34 | 35 | // Load inventories 36 | async.parallel(stem.states.invsToLoad, function (err, inventories) { 37 | 38 | // An inventory failed to load 39 | if (err) { 40 | 41 | stem.log.error('Failed to load inventory, cancelling trade. (%s)', err.message); 42 | return stem.botTrade.cancel(function () { 43 | 44 | stem.states.isTrading = false; 45 | stem.states.isGiver = !stem.states.isGiver; 46 | 47 | }); 48 | 49 | } 50 | 51 | /** 52 | * Flattened inventory of valid items that we can use to trade. 53 | * @type {Array} 54 | */ 55 | var inventory = _.flatten(inventories).filter(function (item) { 56 | 57 | // Only return crates if required 58 | if (stem.config.cratesOnly) { 59 | 60 | /** 61 | * Is this item a crate, chest, or a case. 62 | * @type {Boolean} 63 | */ 64 | var isCrate = item.tags.some(function (itemTag) { 65 | 66 | return (~INTERNAL_CRATE_NAMES.indexOf(itemTag.internal_name)); 67 | 68 | }); 69 | 70 | return (item.tradable && isCrate); 71 | 72 | } 73 | 74 | /** 75 | * Is this a strange item 76 | * @type {Boolean} 77 | */ 78 | var isItemStrange = item.tags.some(function (itemTag) { 79 | 80 | return (itemTag.internal_name === 'strange'); 81 | 82 | }); 83 | 84 | return (item.tradable && !isItemStrange); 85 | 86 | }); 87 | 88 | // No tradable items found 89 | if (!inventory.length) { 90 | 91 | stem.log.warn('No tradable items found, cancelling trade.'); 92 | return stem.botTrade.cancel(function () { 93 | 94 | stem.states.isTrading = false; 95 | stem.states.isGiver = !stem.states.isGiver; 96 | 97 | }); 98 | 99 | } 100 | 101 | stem.botTrade.addItem(inventory[0], function () { 102 | 103 | stem.log.info('Added item', inventory[0].market_hash_name); 104 | 105 | setTimeout(function () { 106 | 107 | stem.botTrade.ready(); 108 | 109 | }, 3000); 110 | 111 | }); 112 | 113 | }); 114 | 115 | }; 116 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Auto-trader 2 | === 3 | A [Stem](https://github.com/alvinl/stem) plugin that allows 2 accounts to trade one another to remove the captcha restriction. 4 | 5 | ## Installation 6 | 7 | [Installing on Windows](https://github.com/alvinl/auto-trader/wiki/Installing-on-Windows) 8 | 9 | Linux and OS X steps below: 10 | 11 | 1. Install [Stem](https://github.com/alvinl/stem) 12 | 1. `$ git clone https://github.com/alvinl/stem.git && cd stem` 13 | 2. `$ npm install` 14 | 2. Install the auto-trader plugin 15 | 1. `$ npm install git://github.com/alvinl/auto-trader.git` 16 | 3. Create a config file called auto.json (this can be named anything as long as it's a json file) with the [contents below](#config). 17 | - Make sure this file is created inside the `stem` folder. 18 | 4. (Optional) If you have previous [sentry](https://github.com/seishun/node-steam#sentry) files saved for your accounts you will need to copy them over. Rename them to `.` and place in the stem folder, where `` is the bot's username (*not* display name). 19 | This is to avoid having to login again with SteamGuard and avoid the 7 day trade ban. 20 | - Example: If you have a sentry file for your account named `bot_1.sentry` then you can copy it over inside the `stem` folder like so `$ cp bot_1.sentry stem/.bot_1`. 21 | - If you use [backpack.tf-automatic](https://bitbucket.org/srabouin/backpack.tf-automatic/src), run the [import-hash.js](#import-hashjs) script to create a sentry file that can be used with Stem. 22 | - You can also use the [import-ssfn.js](#import-ssfnjs) script to import the sentry file (ssfn) that the Steam client uses. 23 | 5. Start the bot 24 | - `$ BOT_USERNAME=bot1 BOT_PASSWORD=botpass node bin/stem auto.json` repeat this command for the second bot. Remember to change `BOT_USERNAME` and `BOT_PASSWORD` accordingly. 25 | - Example: Start the first bot with the following command `$ BOT_USERNAME=bot1 BOT_PASSWORD=botpass node bin/stem auto.json` and the second via `$ BOT_USERNAME=bot2 BOT_PASSWORD=botpass node bin/stem auto.json` 26 | 27 | Remember to check the [notes](#notes) section and available [commands](#commands) 28 | 29 | ## Updating 30 | Updates can be installed by going into the `stem` folder and running the following command: 31 | 32 | `$ npm install git://github.com/alvinl/auto-trader.git`. 33 | 34 | ## Scripts 35 | The following scripts are located in the `scripts` folder and can be used to import and convert sentry files into a format that Stem can use. 36 | ### import-hash.js 37 | Use this script to create a sentry file from the `shaSentryfile` field in your `settings.json` file if you use [BP.tf's automatic bot](https://bitbucket.org/srabouin/backpack.tf-automatic/). While inside the `stem` folder run the following command to create the sentry file. 38 | 39 | `$ node node_modules/auto-trader/scripts/import-hash.js ` 40 | - `username` - The accounts username 41 | - `shaSentryfile` - The value of `shaSentryfile` in your `settings.json` file. 42 | 43 | ### import-ssfn.js 44 | Use this script to create a sentry file that is compatible with stem from the sentry file that the Steam client uses (also known as a ssfn file). While inside the `stem` folder run the following command to create the sentry file. 45 | 46 | `$ node node_modules/auto-trader/scripts/import-ssfn.js ` 47 | - `username` - The accounts username 48 | - `ssfnLocation` - The location of the ssfn file. 49 | 50 | ### Config 51 | ```json 52 | { 53 | 54 | "admins": ["76561198042819371", "76561198042819371"], 55 | "plugins": ["auto-trader"], 56 | 57 | "initBot": "76561198089129440", 58 | "initTradeBot": "76561198089063899", 59 | 60 | "inventories": ["tf2"], 61 | 62 | "cratesOnly": false, 63 | 64 | "notifyThreshold": 5500 65 | 66 | } 67 | ``` 68 | **Config values** 69 | 70 | The following properties need to be filled out: 71 | - `admins` - An array of strings containing the steamID's of admins 72 | - `initBot` - The steamID of the first bot that will be trading 73 | - `initTradeBot` - The steamID that the first bot will be trading with 74 | - `inventories` - An array of inventories you want to trade items from. 75 | - The following games are currently supported: `tf2`, `cs:go`, `dota2`, `steam` 76 | - Multiple inventories example: `"inventories": ["tf2", "cs:go"]` 77 | - `cratesOnly` - Setting this to `true` will only allow crates, chests, or cases to be traded. 78 | - `notifyThreshold` - The bot will message users in the admins array once this number of trades has been reached 79 | 80 | ## Notes 81 | 1. Verify that the 2 accounts that are trading each other are friends with one another and have at least 5 TF2 items in each account. 82 | 2. There are some cases where the bots may stall while trading each other, restarting both bots will fix this. 83 | 3. If there are any crashes please open an issue with the crash log. 84 | 4. If you have any questions please open an issue regarding them. 85 | 86 | ## Commands 87 | Commands that you can message the bots 88 | > .status 89 | 90 | This command will return the amount of trades per minute and trades completed since starting the bot. 91 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = function (stem) { 3 | 4 | /** 5 | * Validate config 6 | */ 7 | 8 | if (!stem.config.hasOwnProperty('initBot')) 9 | throw new Error('Missing initBot property in config.'); 10 | 11 | else if (!stem.config.hasOwnProperty('initTradeBot')) 12 | throw new Error('Missing initTradeBot property in config.'); 13 | 14 | else if (!stem.config.hasOwnProperty('inventories')) 15 | throw new Error('Missing inventories property in config.'); 16 | 17 | else if (!stem.config.inventories.length) 18 | throw new Error('Inventories property should have at least 1 inventory specified.'); 19 | 20 | /** 21 | * Set default states 22 | */ 23 | 24 | stem.states.isCommunityReady = false; 25 | stem.states.isAdminNotified = false; 26 | stem.states.isTrading = false; 27 | stem.states.isGiver = false; 28 | stem.states.lastTraded = ''; 29 | 30 | stem.states.startupTime = Date.now(); 31 | stem.states.prevTradesPerMin = 0; 32 | stem.states.tradesCompleted = 0; 33 | stem.states.tradesPerMin = 0; 34 | 35 | /** 36 | * Inventory codes assigned to their app and context id's 37 | * @type {Object} 38 | */ 39 | var inventoryCodes ={ 40 | 41 | 'TF2': '440:2', 42 | 'CS:GO': '730:2', 43 | 'DOTA2': '570:2', 44 | 'STEAM': '753:6' 45 | 46 | }; 47 | 48 | stem.states.invsToLoad = []; 49 | 50 | // Setup async tasks to load inventories 51 | stem.config.inventories.forEach(function (inventoryCode) { 52 | 53 | inventoryCode = inventoryCode.toUpperCase(); 54 | 55 | // Invalid inventory code 56 | if (!inventoryCodes.hasOwnProperty(inventoryCode)) 57 | return stem.log.error('Invalid inventory:', inventoryCode); 58 | 59 | stem.states.invsToLoad.push(function (cb) { 60 | 61 | var loadInvOpts = { 62 | 63 | appId: inventoryCodes[inventoryCode].split(':')[0], 64 | contextId: inventoryCodes[inventoryCode].split(':')[1] 65 | 66 | }; 67 | 68 | // Load inventory 69 | stem.botOffers.loadMyInventory(loadInvOpts, cb); 70 | 71 | }); 72 | 73 | }); 74 | 75 | // No valid inventories 76 | if (!stem.states.invsToLoad.length) 77 | throw new Error('Inventories property should have at least 1 valid inventory specified.'); 78 | 79 | // Setup a trade whitelist 80 | stem.states.tradeWhitelist = []; 81 | 82 | stem.states.tradeWhitelist.push(stem.config.initBot); 83 | stem.states.tradeWhitelist.push(stem.config.initTradeBot); 84 | 85 | stem.tpmInterval = setInterval(function () { 86 | 87 | stem.states.prevTradesPerMin = stem.states.tradesPerMin; 88 | stem.states.tradesPerMin = 0; 89 | 90 | stem.log.info('Trades per minute:', stem.states.prevTradesPerMin); 91 | 92 | }, 60000); 93 | 94 | // Check for possible desyncs every 2 minutes 95 | stem.desyncCheck = setInterval(function () { 96 | 97 | // Possible desync, reset trading 98 | if (!stem.states.tradesPerMin && !stem.states.prevTradesPerMin && 99 | stem.states.isCommunityReady && !stem.states.isPendingTradeResult) { 100 | 101 | stem.log.warn('Possible desync detected, resetting states.'); 102 | 103 | // Cancel trade if currently in a trade session 104 | if (stem.states.isTrading) 105 | return stem.botTrade.cancel(function () { 106 | 107 | stem.states.isCancelledByDesync = true; 108 | stem.states.isTrading = false; 109 | stem.emit('communityReady'); 110 | 111 | }); 112 | 113 | stem.emit('communityReady'); 114 | 115 | } 116 | 117 | }, 120000); 118 | 119 | /** 120 | * Register handlers 121 | */ 122 | 123 | stem.api.addHandler('stem', 'communityReady', require('./handlers/communityReady')); 124 | 125 | stem.api.addHandler('botTrade', 'offerChanged', require('./handlers/offerChanged')); 126 | 127 | stem.api.addHandler('bot', 'tradeProposed', require('./handlers/tradeProposed')); 128 | 129 | stem.api.addHandler('bot', 'sessionStart', require('./handlers/sessionStart')); 130 | 131 | stem.api.addHandler('bot', 'tradeResult', require('./handlers/tradeResult')); 132 | 133 | stem.api.addHandler('botTrade', 'error', require('./handlers/tradeError')); 134 | 135 | stem.api.addHandler('botTrade', 'ready', require('./handlers/ready')); 136 | 137 | stem.api.addHandler('botTrade', 'end', require('./handlers/end')); 138 | 139 | /** 140 | * Register commands 141 | */ 142 | 143 | stem.api.addCommand(/.status/, function (steamID) { 144 | 145 | var timeNow = Date.now(); 146 | 147 | stem.bot.sendMessage(steamID, 'Status:' + 148 | '\nTrades completed: ' + stem.states.tradesCompleted + 149 | '\nTrades per minute: ' + stem.states.prevTradesPerMin + 150 | '\nUptime: %hours hours (%minutes minutes)' 151 | .replace('%hours', ((timeNow - stem.states.startupTime) / 3600000) | 0) 152 | .replace('%minutes', ((timeNow - stem.states.startupTime) / 60000) | 0)); 153 | 154 | }, { permission: 'admin' }); 155 | 156 | }; 157 | --------------------------------------------------------------------------------