├── .travis.yml ├── README.md ├── bin └── stem ├── config.example.json ├── index.js ├── lib ├── api.js ├── handlers │ ├── chatMsg.js │ ├── error.js │ ├── friendMsg.js │ ├── index.js │ ├── loggedOn.js │ ├── offerChanged.js │ ├── sentry.js │ ├── servers.js │ ├── sessionStart.js │ ├── tradeError.js │ ├── tradeMsg.js │ └── webSession.js ├── index.js ├── inventory.js ├── logger.js └── storage.js ├── package.json └── test ├── api.js └── storage.js /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Stem 2 | ==== 3 | [![Build Status](https://travis-ci.org/alvinl/stem.svg?branch=master)](https://travis-ci.org/alvinl/stem) [![Dependency Status](https://david-dm.org/alvinl/stem.svg)](https://david-dm.org/alvinl/stem) 4 | 5 | A simple Steam bot based on [node-steam](https://github.com/seishun/node-steam) that can run on Windows, Mac and Linux. The bot is still in its early stages but is stable enough to perform basic actions. 6 | ![Stem Screenshot](http://alvinl.com/cache/stem-github.png?v=0.25) 7 | ## Plugins 8 | As of [v0.25](https://github.com/alvinl/stem/releases/tag/v0.25) Stem is now plugin based. All previous features are now in their own seperate plugins. 9 | 10 | ### Plugin list 11 | A list of plugins can be found [here](https://github.com/alvinl/stem/wiki/Plugins). Feel free to edit the wiki page to include plugins you've made. 12 | 13 | ### Installing plugins 14 | Instructions on how to install plugins for Stem can be found [here](https://github.com/alvinl/stem/wiki/Installing-plugins). 15 | 16 | ### Creating plugins 17 | Instructions on how to create plugins can be found in the wiki [here](https://github.com/alvinl/stem/wiki/Creating-plugins). 18 | 19 | ## Installing 20 | - [Installing on Mac](https://github.com/alvinl/stem/wiki/Installing-on-Mac) 21 | - Installing on Windows (coming soon) 22 | - [Installing on Linux](https://github.com/alvinl/stem/wiki/Installing-on-Linux) 23 | 24 | ## Configuring 25 | Refer to this [wiki page](https://github.com/alvinl/stem/wiki/Configuring-the-bot) for info on configuring the bot. 26 | 27 | ## Changelog 28 | You can find the changelog [here](https://github.com/alvinl/stem/releases) 29 | 30 | ## Contact 31 | If you need help or have any questions feel free to add me via [Steam](http://steamcommunity.com/id/Alvinlz) or my Github email. 32 | -------------------------------------------------------------------------------- /bin/stem: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var config = process.argv[2], 4 | Stem = require('../lib'), 5 | configPath = process.cwd() + '/' + config; 6 | 7 | if (!config) { 8 | 9 | console.error('Config path required'); 10 | return process.exit(1); 11 | 12 | } 13 | 14 | var bot = new Stem(), 15 | botConfig = require(configPath); 16 | 17 | botConfig.username = process.env.BOT_USERNAME || botConfig.username; 18 | botConfig.password = process.env.BOT_PASSWORD || botConfig.password; 19 | 20 | bot.init(botConfig); 21 | -------------------------------------------------------------------------------- /config.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "botname": "", 3 | "username": "", 4 | "password": "", 5 | "admins": ["76561198042819371", "203819413413"], 6 | "plugins": [], 7 | "disabledPlugins": [] 8 | } 9 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | var config = require('./config'), 3 | Stem = require('./lib/'), 4 | bot = new Stem(); 5 | 6 | bot.init(config); -------------------------------------------------------------------------------- /lib/api.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * A module that adds helper metheds to `Stem` 4 | * 5 | * @module StemAPI 6 | */ 7 | 8 | /** 9 | * Dependencies 10 | */ 11 | 12 | var request = require('request'); 13 | 14 | /** 15 | * Export `StemAPI` 16 | */ 17 | 18 | module.exports = StemAPI; 19 | 20 | /** 21 | * Creates a new `StemAPI` instance 22 | * @class 23 | */ 24 | function StemAPI (stem) { 25 | 26 | this.stem = stem; 27 | this._jar = request.jar(); 28 | this.request = request.defaults({ jar: this._jar }); 29 | 30 | } 31 | 32 | /** 33 | * Finds a matching command and returns the commands details as 34 | * an object. 35 | * 36 | * @param {String} eventType Event type to match 37 | * @param {Boolean} isAdmin Should we search for admin only commands 38 | * @return {Object} Details about the matched command 39 | */ 40 | StemAPI.prototype._matchCommand = function(message, eventType, isAdmin) { 41 | 42 | var stem = this.stem; 43 | 44 | // Check if the message matches any registered commands 45 | for (var i = stem.commands.length - 1; i >= 0; i--) { 46 | 47 | /** 48 | * Command listener details 49 | * @type {Object} 50 | */ 51 | var listenerInfo = stem.commands[i]; 52 | 53 | if (listenerInfo.eventType !== eventType) 54 | continue; 55 | 56 | // Admin command 57 | if (isAdmin && listenerInfo.permission === 'admin' && listenerInfo.listener.test(message)) 58 | return listenerInfo; 59 | 60 | // Normal command 61 | else if (listenerInfo.permission === 'normal' && listenerInfo.listener.test(message)) 62 | return listenerInfo; 63 | 64 | } 65 | 66 | }; 67 | 68 | /** 69 | * Applies cookies to `api.request` 70 | * 71 | * @param {Array} cookies Cookies to be applied 72 | */ 73 | StemAPI.prototype.setupRequest = function(cookies) { 74 | 75 | var self = this; 76 | 77 | cookies.forEach(function (cookie) { 78 | 79 | self._jar.setCookie(request.cookie(cookie), 'http://steamcommunity.com'); 80 | 81 | }); 82 | 83 | }; 84 | 85 | /** 86 | * Post a comment on a users profile 87 | * 88 | * @param {String} steamID User to post comment to 89 | * @param {String} comment Comment to post 90 | * @param {Function} cb 91 | */ 92 | StemAPI.prototype.postComment = function(steamID, comment, cb) { 93 | 94 | var postURL = 'http://steamcommunity.com/comment/Profile/post/%steamID/-1/' 95 | .replace('%steamID', steamID), 96 | self = this; 97 | 98 | self.request.post({ url: postURL, form: { comment: comment, count: 1, sessionid: self.stem.botTrade.sessionID }, json: true }, function (err, response, body) { 99 | 100 | if (err || body.error) 101 | return cb(err || new Error(body.error)); 102 | 103 | // Comment was not posted 104 | else if (!body.success) 105 | return cb(null, false); 106 | 107 | return cb(null, true); 108 | 109 | }); 110 | 111 | }; 112 | 113 | /** 114 | * Checks if the given steamID is an admin. 115 | * 116 | * @param {String} steamID SteamID to check 117 | * @return {Boolean} Whether or not the user is an admin 118 | */ 119 | StemAPI.prototype.isAdmin = function(steamID) { 120 | 121 | return (~this.stem.config.admins.indexOf(steamID)) ? true : false; 122 | 123 | }; 124 | 125 | /** 126 | * Registers a new command 127 | * 128 | * @param {String} command String to be matched as a command 129 | * @param {Function} commandHandler Function to be called when command is called 130 | * @param {Boolean} adminCommand Should the command be for admins only 131 | */ 132 | StemAPI.prototype.addCommand = function(command, commandHandler, commandOptions) { 133 | 134 | var stem = this.stem; 135 | 136 | if (!(command instanceof RegExp)) 137 | throw new TypeError('First parameter must be a RegeExp'); 138 | 139 | else if (!(commandHandler instanceof Function)) 140 | throw new TypeError('Second parameter must be a callback function'); 141 | 142 | else if (commandOptions && !(commandOptions instanceof Object)) 143 | throw new TypeError('Third parameter must be an object'); 144 | 145 | commandOptions = commandOptions || {}; 146 | 147 | /** 148 | * Valid permision scopes 149 | * @type {Array} 150 | */ 151 | var validPermissions = ['admin', 'normal']; 152 | 153 | /** 154 | * Valid event types 155 | * @type {Array} 156 | */ 157 | var validEventTypes = ['message', 'group', 'trade']; 158 | 159 | // Validate permission type 160 | if (commandOptions.hasOwnProperty('permission') && !~validPermissions.indexOf(commandOptions.permission)) 161 | throw new Error('Invalid permission type: ' + commandOptions.permission); 162 | 163 | else if (commandOptions.hasOwnProperty('eventType') && !~validEventTypes.indexOf(commandOptions.eventType)) 164 | throw new Error('Invalid event type: ' + commandOptions.eventType); 165 | 166 | /** 167 | * Is the command listener already registered 168 | * @type {Number} 169 | */ 170 | var commandExists = stem.commands.filter(function (listenerInfo) { 171 | 172 | return (listenerInfo.listener.toString() === command.toString() && 173 | (listenerInfo.eventType || 'message') === (commandOptions.eventType || 'message')); 174 | 175 | }).length; 176 | 177 | if (commandExists) 178 | throw new Error('Command `' + command + '` is already registered'); 179 | 180 | stem.commands.push({ listener: command, 181 | handler: commandHandler, 182 | permission: commandOptions.permission || 'normal', 183 | eventType: commandOptions.eventType || 'message' }); 184 | 185 | }; 186 | 187 | /** 188 | * Validates a 64bit steamID 189 | * 190 | * @param {String} steamID SteamID to validate 191 | * @return {Boolean} Whether or not the SteamID is valid 192 | */ 193 | StemAPI.prototype.validateSteamID = function(steamID) { 194 | 195 | if (!steamID || steamID.length !== 17 || isNaN(steamID)) 196 | return false; 197 | 198 | return true; 199 | 200 | }; 201 | 202 | /** 203 | * Attaches a handler to a given botType instance while passing `Stem` as `this`. 204 | * 205 | * @param {String} botType Bot type (bot / botTrade / stem) 206 | * @param {String} eventName Event name 207 | * @param {Function} handler Event handler 208 | */ 209 | StemAPI.prototype.addHandler = function(eventType, eventName, eventHandler) { 210 | 211 | var stem = this.stem; 212 | 213 | switch (eventType) { 214 | 215 | case 'bot': 216 | stem.bot.on(eventName, eventHandler.bind(stem)); 217 | break; 218 | 219 | case 'botTrade': 220 | stem.botTrade.on(eventName, eventHandler.bind(stem)); 221 | break; 222 | 223 | case 'stem': 224 | stem.on(eventName, eventHandler.bind(stem)); 225 | break; 226 | 227 | default: 228 | throw new Error('Invalid event type: ' + eventType); 229 | 230 | } 231 | 232 | }; 233 | 234 | /** 235 | * Validates items in the current trade session 236 | * 237 | * @return {Boolean} Whether or not the trade items are valid 238 | */ 239 | StemAPI.prototype.validateTrade = function() { 240 | 241 | var stem = this.stem; 242 | 243 | // Real items in trade do not match items we tracked from trade events 244 | // - This is usually due to trade lag 245 | if (!stem.trade.eventItems || !stem.botTrade.themAssets) 246 | return false; 247 | 248 | // Total items from real trade and trade events do not match 249 | else if (stem.trade.eventItems.length !== stem.botTrade.themAssets.length) 250 | return false; 251 | 252 | // Item positions do not match up 253 | for (var i = stem.trade.eventItems.length; i--;) { 254 | 255 | if (!~stem.botTrade.themAssets.indexOf(stem.trade.eventItems[i])) 256 | return false; 257 | 258 | } 259 | 260 | return true; 261 | 262 | }; 263 | -------------------------------------------------------------------------------- /lib/handlers/chatMsg.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = function (groupID, message, messageType, userID) { 3 | 4 | var Stem = this, 5 | isAdmin = this.api.isAdmin(userID), 6 | playerName = ((this.bot.users[userID]) ? this.bot.users[userID].playerName : userID); 7 | 8 | // Only listen for `ChatMsg` types 9 | if (messageType !== 1) 10 | return; 11 | 12 | Stem.log.info((isAdmin ? '[Admin] ' : '') + playerName + ': ' + message); 13 | 14 | /** 15 | * Details about the matched command 16 | * @type {Object} 17 | */ 18 | var matchedCommand = Stem.api._matchCommand(message, 'group', isAdmin); 19 | 20 | if (matchedCommand) 21 | return matchedCommand.handler.call(Stem, userID, { match: matchedCommand.listener.exec(message) }, groupID); 22 | 23 | }; 24 | -------------------------------------------------------------------------------- /lib/handlers/error.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = function(e) { 3 | 4 | var Stem = this, 5 | log = this.log, 6 | bot = this.bot, 7 | errorCode = e.eresult, 8 | config = this.config; 9 | 10 | // A sentry is requested 11 | if (errorCode === 63) { 12 | 13 | log.warn('Enter SteamGuard code below...'); 14 | process.stdout.write('Code: '); 15 | process.stdin.resume(); 16 | process.stdin.setEncoding('utf8'); 17 | process.stdin.once('data', function(data) { 18 | 19 | // Attempt to login again with the Auth code 20 | bot.logOn({ 21 | accountName: config.username, 22 | password: config.password, 23 | authCode: data.replace('\n', '').replace('\r', '') 24 | }); 25 | 26 | // Sanitize config 27 | delete Stem.config.password; 28 | 29 | }); 30 | 31 | } 32 | 33 | // Password is wrong 34 | else if (errorCode === 5) { 35 | 36 | log.error('Invalid password, please check that the password in your config is correct'); 37 | 38 | } 39 | 40 | // Invalid Auth code 41 | else if (errorCode === 65) { 42 | 43 | log.error('Invalid Auth code'); 44 | process.exit(1); 45 | 46 | } 47 | 48 | // Unkown error 49 | else { 50 | 51 | log.error(e); 52 | 53 | } 54 | 55 | }; 56 | -------------------------------------------------------------------------------- /lib/handlers/friendMsg.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = function (steamID, message, messageType) { 3 | 4 | var Stem = this, 5 | log = this.log, 6 | isAdmin = this.api.isAdmin(steamID), 7 | playerName = ((this.bot.users[steamID]) ? this.bot.users[steamID].playerName : steamID); 8 | 9 | // Only listen for `ChatMsg` types 10 | if (messageType !== 1) 11 | return; 12 | 13 | log.info((isAdmin ? '[Admin] ' : '') + playerName + ': ' + message); 14 | 15 | /** 16 | * Details about the matched command 17 | * @type {Object} 18 | */ 19 | var matchedCommand = Stem.api._matchCommand(message, 'message', isAdmin); 20 | 21 | if (matchedCommand) 22 | return matchedCommand.handler.call(Stem, steamID, { match: matchedCommand.listener.exec(message) }); 23 | 24 | }; 25 | -------------------------------------------------------------------------------- /lib/handlers/index.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = function(Stem) { 3 | 4 | /** 5 | * General handlers 6 | */ 7 | 8 | Stem.api.addHandler('bot', 'sessionStart', require('./sessionStart')); 9 | 10 | Stem.api.addHandler('bot', 'webSessionID', require('./webSession')); 11 | 12 | Stem.api.addHandler('bot', 'friendMsg', require('./friendMsg')); 13 | 14 | Stem.api.addHandler('bot', 'loggedOn', require('./loggedOn')); 15 | 16 | Stem.api.addHandler('bot', 'servers', require('./servers')); 17 | 18 | Stem.api.addHandler('bot', 'chatMsg', require('./chatMsg')); 19 | 20 | Stem.api.addHandler('bot', 'sentry', require('./sentry')); 21 | 22 | Stem.api.addHandler('bot', 'error', require('./error')); 23 | 24 | /** 25 | * Trade related handlers 26 | */ 27 | 28 | Stem.api.addHandler('botTrade', 'offerChanged', require('./offerChanged')); 29 | 30 | Stem.api.addHandler('botTrade', 'error', require('./tradeError')); 31 | 32 | Stem.api.addHandler('botTrade', 'chatMsg', require('./tradeMsg')); 33 | 34 | }; 35 | -------------------------------------------------------------------------------- /lib/handlers/loggedOn.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = function() { 3 | 4 | var Stem = this, 5 | log = this.log, 6 | bot = this.bot, 7 | customBotname = this.config.botname; 8 | 9 | // Sanitize config 10 | delete Stem.config.password; 11 | 12 | // Log successful login 13 | log.info('Bot successfully logged in'); 14 | 15 | // Set the bots status to 'Online' 16 | bot.setPersonaState(1); 17 | 18 | // Change botname if set in config 19 | if (customBotname) { 20 | 21 | log.info('Changing botname to: %s', customBotname); 22 | 23 | bot.setPersonaName(customBotname); 24 | 25 | } 26 | 27 | }; 28 | -------------------------------------------------------------------------------- /lib/handlers/offerChanged.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = function (itemAdded, item) { 3 | 4 | var Stem = this, 5 | itemPos = Stem.trade.eventItems.indexOf(item); 6 | 7 | // Add item to `eventItems` to be validated later 8 | if (itemAdded) 9 | Stem.trade.eventItems.push(item); 10 | 11 | // Remove item from `eventItems` if it exists and the item was removed 12 | // from trade 13 | else if (!itemAdded && ~itemPos) 14 | Stem.trade.eventItems.splice(itemPos, 1); 15 | 16 | }; -------------------------------------------------------------------------------- /lib/handlers/sentry.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = function(sentry) { 3 | 4 | var log = this.log, 5 | config = this.config, 6 | fs = require('fs'), 7 | sentryPath = this.path + '/.' + config.username; 8 | 9 | // Save the sentry 10 | fs.writeFile(sentryPath, sentry, function(err) { 11 | 12 | // Error saving sentry 13 | if (err) return log.error('Failed to save sentry:', err.stack); 14 | 15 | log.info('Sentry saved!'); 16 | 17 | }); 18 | 19 | }; -------------------------------------------------------------------------------- /lib/handlers/servers.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = function(serverlist) { 3 | 4 | var log = this.log, 5 | fs = require('fs'), 6 | serversPath = this.path + '/.servers'; 7 | 8 | // Save the server list 9 | fs.writeFile(serversPath, JSON.stringify(serverlist), function (err) { 10 | 11 | // Error saving server list 12 | if (err) return log.error('Error saving server list'); 13 | 14 | }); 15 | 16 | }; -------------------------------------------------------------------------------- /lib/handlers/sessionStart.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = function (steamID) { 3 | 4 | this.trade.client = steamID; 5 | this.trade.eventItems = []; 6 | 7 | }; -------------------------------------------------------------------------------- /lib/handlers/tradeError.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = function (err) { 3 | 4 | var Stem = this; 5 | 6 | Stem.log.error('Trade error:', err.message); 7 | 8 | }; 9 | -------------------------------------------------------------------------------- /lib/handlers/tradeMsg.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = function (message) { 3 | 4 | var Stem = this, 5 | log = this.log, 6 | steamID = this.trade.client, 7 | isAdmin = this.api.isAdmin(steamID), 8 | playerName = ((this.bot.users[steamID]) ? this.bot.users[steamID].playerName : steamID); 9 | 10 | log.info((isAdmin ? '[Admin] ' : '') + playerName + ': ' + message); 11 | 12 | /** 13 | * Details about the matched command 14 | * @type {Object} 15 | */ 16 | var matchedCommand = Stem.api._matchCommand(message, 'trade', isAdmin); 17 | 18 | if (matchedCommand) 19 | return matchedCommand.handler.call(Stem, steamID, { match: matchedCommand.listener.exec(message) }); 20 | 21 | }; 22 | -------------------------------------------------------------------------------- /lib/handlers/webSession.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = function(sessionID) { 3 | 4 | var Stem = this, 5 | bot = this.bot, 6 | botTrade = this.botTrade, 7 | botOffers = this.botOffers; 8 | 9 | // Save sessionID 10 | botTrade.sessionID = sessionID; 11 | 12 | // Login to Steam Community 13 | bot.webLogOn(function(cookies) { 14 | 15 | // Apply cookies to `botTrade` 16 | cookies.forEach(function(cookie) { 17 | 18 | botTrade.setCookie(cookie); 19 | 20 | }); 21 | 22 | // Apply cookies to api.request 23 | Stem.api.setupRequest(cookies); 24 | 25 | // Change trading state 26 | Stem.states.tradeReady = true; 27 | 28 | // Change community state 29 | Stem.states.communityReady = true; 30 | 31 | // Setup trade offers 32 | botOffers.setup({ sessionID: sessionID, webCookie: cookies }, function (err) { 33 | 34 | if (err) 35 | Stem.log.error('Error setting up trade offers:', err.message); 36 | 37 | Stem.emit('communityReady'); 38 | 39 | }); 40 | 41 | }); 42 | 43 | }; 44 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Dependencies 4 | */ 5 | 6 | var EventEmitter = require('events').EventEmitter, 7 | SteamTradeOffers = require('steam-tradeoffers'), 8 | package = require('../package.json'), 9 | SteamTrade = require('steam-trade'), 10 | Inventory = require('./inventory'), 11 | Storage = require('./storage'), 12 | StemAPI = require('./api'), 13 | Steam = require('steam'), 14 | util = require('util'), 15 | fs = require('fs'); 16 | 17 | /** 18 | * Export `Stem` 19 | */ 20 | 21 | module.exports = Stem; 22 | 23 | /** 24 | * Creates a `Stem` instance 25 | * 26 | * @class 27 | */ 28 | function Stem() { 29 | 30 | if (!(this instanceof Stem)) 31 | return new Stem(); 32 | 33 | this.bot = new Steam.SteamClient(); 34 | this.botOffers = new SteamTradeOffers(); 35 | this.inventory = new Inventory(this); 36 | this.api = new StemAPI(this); 37 | this.storage = new Storage(this); 38 | this.botTrade = new SteamTrade(); 39 | this.version = package.version; 40 | this.path = process.cwd(); 41 | this.loadedPlugins = []; 42 | this.config = {}; 43 | this.states = {}; 44 | this.trade = {}; 45 | this.configs = {}; 46 | this.inventories = {}; 47 | this.commands = []; 48 | this._inventories = {}; 49 | 50 | EventEmitter.call(this); 51 | 52 | } 53 | 54 | util.inherits(Stem, EventEmitter); 55 | 56 | /** 57 | * Initialize `Stem` with the provided config 58 | * 59 | * @param {Object} config Config to use 60 | */ 61 | Stem.prototype.init = function(config) { 62 | 63 | var Stem = this, 64 | serverList = this.path + '/.servers'; 65 | 66 | this.config = config; 67 | this.log = require('./logger')(config); 68 | 69 | // Check if config is an object 70 | if (typeof(config) !== 'object') { 71 | 72 | Stem.log.error('Invalid config'); 73 | return process.exit(1); 74 | 75 | } 76 | 77 | // Check that password and username exist. 78 | if (!config.username || !config.password) { 79 | 80 | Stem.log.error('Check that your Steam username / Steam password is set correctly.'); 81 | return process.exit(1); 82 | 83 | } 84 | 85 | // If a servers file exists, use it. 86 | if (fs.existsSync(serverList) ) { 87 | 88 | // Attempt to parse the serverlist file 89 | try { 90 | 91 | Steam.servers = JSON.parse(fs.readFileSync(serverList)); 92 | 93 | } 94 | 95 | // Error parsing serverlist. 96 | catch (e) { 97 | 98 | Stem.log.error('Error parsing serverlist:', e); 99 | 100 | } 101 | 102 | } 103 | 104 | // Attempt to login 105 | this._login(config.username, config.password); 106 | 107 | }; 108 | 109 | /** 110 | * Attempts to login the bot 111 | * 112 | * @param {String} username Bots username 113 | * @param {String} password Bots password 114 | */ 115 | Stem.prototype._login = function(username, password) { 116 | 117 | var sentryPath = this.path + '/.' + username, 118 | Stem = this; 119 | 120 | /** 121 | * Does the user have plugins to load 122 | * 123 | * @type {Boolean} 124 | */ 125 | var isTherePlugins = (Stem.config.plugins) ? 126 | Stem.config.plugins.length : 127 | false; 128 | 129 | /** 130 | * Array of disabled plugins 131 | * 132 | * @type {Array} 133 | */ 134 | var disabledPlugins = Stem.config.disabledPlugins || []; 135 | 136 | // Initiate core handlers 137 | require('./handlers')(this); 138 | 139 | // Load plugins if needed 140 | if (isTherePlugins) { 141 | 142 | Stem.config.plugins.forEach(function (pluginName) { 143 | 144 | // Skip disabled plugins and plugins already loaded 145 | if (~disabledPlugins.indexOf(pluginName) || 146 | ~Stem.loadedPlugins.indexOf(pluginName)) 147 | return; 148 | 149 | // Attempt to load plugin 150 | try { 151 | 152 | require(pluginName)(Stem); 153 | 154 | } 155 | 156 | // Error loading plugin 157 | catch (err) { 158 | 159 | Stem.log.error('Failed to load plugin: %s\n', pluginName, err.stack); 160 | return; 161 | 162 | } 163 | 164 | // Mark plugin as loaded 165 | Stem.loadedPlugins.push(pluginName); 166 | 167 | // Log plugin load 168 | Stem.log.info('Plugin loaded:', pluginName); 169 | 170 | }); 171 | 172 | } 173 | 174 | // Login 175 | this.bot.logOn({ 176 | 177 | accountName: username, 178 | password: password, 179 | shaSentryfile: (fs.existsSync(sentryPath)) ? fs.readFileSync(sentryPath) : null 180 | 181 | }); 182 | 183 | }; 184 | -------------------------------------------------------------------------------- /lib/inventory.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * A module to handle inventories 4 | * 5 | * @module Inventory 6 | */ 7 | 8 | /** 9 | * Export `Inventory` 10 | */ 11 | 12 | module.exports = Inventory; 13 | 14 | /** 15 | * Creates a new `Inventory` instance 16 | * @class 17 | */ 18 | function Inventory (stem) { 19 | 20 | this.stem = stem; 21 | this.data = {}; 22 | 23 | } 24 | 25 | /** 26 | * Loads and caches the specified inventory 27 | * 28 | * @param {String} appID 29 | * @param {String} contextID 30 | * @param {Function} cb 31 | */ 32 | Inventory.prototype.load = function(appID, contextID, cb) { 33 | 34 | var self = this, 35 | stem = this.stem; 36 | 37 | stem.botOffers.loadMyInventory(appID, contextID, function (err, inventory) { 38 | 39 | if (err) 40 | return cb(err); 41 | 42 | self.data[appID + ':' + contextID] = inventory; 43 | 44 | self.rebuildAll(); 45 | 46 | return cb(null, inventory); 47 | 48 | }); 49 | 50 | }; 51 | 52 | /** 53 | * Searches for the specified item by id. 54 | * 55 | * @param {String} itemID Id to search for 56 | * @param {String} [appID] Limit search scope to this game 57 | * @param {String} [contextID] Furhter search scope to inventory context 58 | * @return {Object} Item if found otherwise null 59 | */ 60 | Inventory.prototype.findItemById = function(itemID, appID, contextID) { 61 | 62 | var self = this; 63 | 64 | return self.data.all.filter(function (item) { 65 | 66 | return (item.id === itemID.toString() && 67 | (appID ? item.appid === appID.toString() : true) && 68 | (contextID ? item.contextid === parseInt(contextID) : true)); 69 | 70 | })[0] || null; 71 | 72 | }; 73 | 74 | /** 75 | * Rebuilds `Inventory.data.all` from all cached invs. 76 | * 77 | * @return {Array} Rebuilt inventory 78 | */ 79 | Inventory.prototype.rebuildAll = function() { 80 | 81 | var self = this; 82 | 83 | // Reset full inventory 84 | self.data.all = []; 85 | 86 | for (var inventory in self.data) { 87 | 88 | if (inventory !== 'all') { 89 | 90 | for (var i = self.data[inventory].length - 1; i >= 0; i--) { 91 | 92 | self.data.all.push(self.data[inventory][i]); 93 | 94 | } 95 | 96 | } 97 | 98 | } 99 | 100 | return self.data.all; 101 | 102 | }; 103 | -------------------------------------------------------------------------------- /lib/logger.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Dependencies 4 | */ 5 | 6 | var winston = require('winston'), 7 | moment = require('moment'), 8 | path = require('path'), 9 | fs = require('fs'); 10 | 11 | /** 12 | * Creates and returns a logging instance 13 | * @param {Object} config 14 | * @return {Object} 15 | */ 16 | 17 | module.exports = function (config) { 18 | 19 | // Check if log directory exists 20 | var logDirectoryExists = fs.existsSync(process.cwd() + '/logs'); 21 | 22 | // Create log directory if it doesn't exist 23 | if (!logDirectoryExists) 24 | fs.mkdirSync(process.cwd() + '/logs'); 25 | 26 | var username = (config.username || 'error'); 27 | 28 | var logLevels = { 29 | 30 | levels: { 31 | 32 | info: 1, 33 | warn: 2, 34 | error: 3, 35 | debug: 4 36 | 37 | }, 38 | 39 | colors: { 40 | 41 | debug: 'grey', 42 | info: 'green', 43 | warn: 'yellow', 44 | error: 'red' 45 | 46 | } 47 | 48 | }; 49 | 50 | winston.addColors(logLevels.colors); 51 | 52 | return new (winston.Logger)({ 53 | 54 | levels: logLevels.levels, 55 | 56 | transports: [ 57 | new (winston.transports.Console)({ timestamp: function() { return moment().format('YYYY-MM-DD HH:mm:ss'); } , colorize: true }), 58 | new (winston.transports.File)({ filename: 'logs/' + username + '.log' }) 59 | ] 60 | 61 | }); 62 | 63 | }; 64 | -------------------------------------------------------------------------------- /lib/storage.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * A module to handle bot storage 4 | * 5 | * @module Storage 6 | */ 7 | 8 | /** 9 | * Dependencies 10 | */ 11 | 12 | var util = require('util'), 13 | events = require('events'); 14 | 15 | /** 16 | * Export `Storage` 17 | */ 18 | 19 | module.exports = Storage; 20 | 21 | /** 22 | * Creates a new `Storage` instance 23 | * 24 | * @class 25 | */ 26 | function Storage (stem) { 27 | 28 | this.stem = stem; 29 | this.data = {}; 30 | 31 | events.EventEmitter.call(this); 32 | 33 | } 34 | 35 | util.inherits(Storage, events.EventEmitter); 36 | 37 | /** 38 | * Saves key / value pair and emits `change` 39 | * 40 | * @param {String} key 41 | * @param {Mixed} value 42 | */ 43 | Storage.prototype.set = function(key, value) { 44 | 45 | this.data[key] = value; 46 | 47 | this.emit('change', this.data); 48 | 49 | }; 50 | 51 | /** 52 | * Returns the value of the specified key 53 | * 54 | * @param {String} key 55 | * @return {Mixed} Key data or null if key doesn't exist 56 | */ 57 | Storage.prototype.get = function(key) { 58 | 59 | return this.data.hasOwnProperty(key) ? this.data[key] : null; 60 | 61 | }; 62 | 63 | /** 64 | * Removes the specified key from storage 65 | * 66 | * @param {String} key Key to remove 67 | * @return {Boolean} True if a key was removed, otherwise false 68 | */ 69 | Storage.prototype.remove = function(key) { 70 | 71 | // Key doesn't exist 72 | if (!this.data.hasOwnProperty(key)) 73 | return false; 74 | 75 | delete this.data[key]; 76 | 77 | this.emit('change', this.data); 78 | 79 | return true; 80 | 81 | }; 82 | 83 | /** 84 | * Loads and merges the given object to `this.data`, 85 | * emits `loaded` once completed. 86 | * 87 | * @param {Object} dataToLoad Data to load 88 | */ 89 | Storage.prototype.load = function(dataToLoad) { 90 | 91 | var self = this; 92 | 93 | for (var data in dataToLoad) 94 | self.data[data] = dataToLoad[data]; 95 | 96 | self.emit('loaded', self.data); 97 | 98 | }; 99 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stem", 3 | "version": "0.48.0", 4 | "author": "Alvin Lazaro (http://alvinl.com/)", 5 | "main": "./lib/index", 6 | "license": "MIT", 7 | "scripts": { 8 | "test": "NODE_ENV=test node_modules/.bin/mocha -R spec" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/alvinl/stem.git" 13 | }, 14 | "dependencies": { 15 | "moment": "^2.7.0", 16 | "request": "^2.47.0", 17 | "steam": "0.6.8", 18 | "steam-trade": "git://github.com/seishun/node-steam-trade.git", 19 | "steam-tradeoffers": "^1.0.0", 20 | "winston": "^1.0.0" 21 | }, 22 | "devDependencies": { 23 | "mocha": "^1.20.0", 24 | "should": "^3.3.2" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test/api.js: -------------------------------------------------------------------------------- 1 | 2 | /* global describe, it */ 3 | 4 | var should = require('should'), 5 | Stem = require('../lib'); 6 | 7 | describe('API', function () { 8 | 9 | it('api.addHandler should attach an event handler to a event and bind `Stem`', function (done) { 10 | 11 | var bot = new Stem(); 12 | 13 | // Attach a fake handler 14 | bot.api.addHandler('bot', 'friendMsg', function () { 15 | 16 | // Verify that `this` is Stem 17 | this.should.have.property('config'); 18 | return done(); 19 | 20 | }); 21 | 22 | // Call fake handler 23 | bot.bot.emit('friendMsg'); 24 | 25 | }); 26 | 27 | it('api.addCommand should create a admin command for all message types', function (done) { 28 | 29 | var bot = new Stem(); 30 | 31 | bot.api.addCommand(/^help/i, function () { 32 | 33 | }, { permission: 'admin' }); 34 | 35 | bot.api.addCommand(/^help/i, function () { 36 | 37 | }, { permission: 'admin', eventType: 'group' }); 38 | 39 | bot.api.addCommand(/^help/i, function () { 40 | 41 | }, { permission: 'admin', eventType: 'trade' }); 42 | 43 | var messageMatchedCommand = bot.api._matchCommand('help', 'message', true), 44 | groupMatchedCommand = bot.api._matchCommand('help', 'group', true), 45 | tradeMatchedCommand = bot.api._matchCommand('help', 'trade', true); 46 | 47 | should.exist(messageMatchedCommand); 48 | should.exist(groupMatchedCommand); 49 | should.exist(tradeMatchedCommand); 50 | return done(); 51 | 52 | }); 53 | 54 | it('api.addCommand should create a normal command for all message types', function (done) { 55 | 56 | var bot = new Stem(); 57 | 58 | bot.api.addCommand(/^help/i, function () { 59 | 60 | }); 61 | 62 | bot.api.addCommand(/^help/i, function () { 63 | 64 | }, { eventType: 'group' }); 65 | 66 | bot.api.addCommand(/^help/i, function () { 67 | 68 | }, { eventType: 'trade' }); 69 | 70 | var messageMatchedCommand = bot.api._matchCommand('help', 'message'), 71 | groupMatchedCommand = bot.api._matchCommand('help', 'group'), 72 | tradeMatchedCommand = bot.api._matchCommand('help', 'trade'); 73 | 74 | should.exist(messageMatchedCommand); 75 | should.exist(groupMatchedCommand); 76 | should.exist(tradeMatchedCommand); 77 | return done(); 78 | 79 | }); 80 | 81 | it('api.addCommand should fail to create a command for an invalid eventType', function (done) { 82 | 83 | var bot = new Stem(); 84 | 85 | try { 86 | 87 | bot.api.addCommand(/^help/i, function () { 88 | 89 | }, { eventType: 'invalid' }); 90 | 91 | } catch (err) { 92 | 93 | return done(); 94 | 95 | } 96 | 97 | }); 98 | 99 | it('api.addCommand should fail to create a command for an invalid permission group', function (done) { 100 | 101 | var bot = new Stem(); 102 | 103 | try { 104 | 105 | bot.api.addCommand(/^help/i, function () { 106 | 107 | }, { permission: 'invalid' }); 108 | 109 | } catch (err) { 110 | 111 | return done(); 112 | 113 | } 114 | 115 | }); 116 | 117 | it('api.addCommand should fail to create a command if the command already exists for that eventType', function (done) { 118 | 119 | var bot = new Stem(); 120 | 121 | try { 122 | 123 | bot.api.addCommand(/^help/i, function () { 124 | 125 | }); 126 | 127 | bot.api.addCommand(/^help/i, function () { 128 | 129 | }); 130 | 131 | } catch (err) { 132 | 133 | return done(); 134 | 135 | } 136 | 137 | }); 138 | 139 | it('api.addCommand should fail to create a command when passed invalid first param', function (done) { 140 | 141 | var bot = new Stem(); 142 | 143 | try { 144 | 145 | bot.api.addCommand('/^help/i', function () { 146 | 147 | }); 148 | 149 | } catch (err) { 150 | 151 | return done(); 152 | 153 | } 154 | 155 | }); 156 | 157 | it('api.addCommand should fail to create a command when passed invalid second param', function (done) { 158 | 159 | var bot = new Stem(); 160 | 161 | try { 162 | 163 | bot.api.addCommand(/^help/i, 'invalid'); 164 | 165 | } catch (err) { 166 | 167 | return done(); 168 | 169 | } 170 | 171 | }); 172 | 173 | it('api.addCommand should fail to create a command when passed invalid third param', function (done) { 174 | 175 | var bot = new Stem(); 176 | 177 | try { 178 | 179 | bot.api.addCommand(/^help/i, function () { 180 | 181 | }, 'invalid'); 182 | 183 | } catch (err) { 184 | 185 | return done(); 186 | 187 | } 188 | 189 | }); 190 | 191 | it('api.isAdmin should return true if a given steamID is an admin', function (done) { 192 | 193 | var bot = new Stem(); 194 | 195 | // Create admin 196 | bot.config.admins = ['76561198042819371']; 197 | 198 | bot.api.isAdmin('76561198042819371').should.be.true; 199 | 200 | return done(); 201 | 202 | }); 203 | 204 | it('api.isAdmin should return false if a given steamID is not an admin', function (done) { 205 | 206 | var bot = new Stem(); 207 | 208 | // Create admin 209 | bot.config.admins = ['76561198042819371']; 210 | 211 | bot.api.isAdmin('76561198042819372').should.be.false; 212 | 213 | return done(); 214 | 215 | }); 216 | 217 | it('api.validateSteamID should return true if a given string is a valid steamID', function (done) { 218 | 219 | var bot = new Stem(); 220 | 221 | bot.api.validateSteamID('76561198042819371').should.be.true; 222 | 223 | return done(); 224 | 225 | }); 226 | 227 | it('api.validateSteamID should return false if a given string is an invalid steamID', function (done) { 228 | 229 | var bot = new Stem(); 230 | 231 | // No string passed 232 | bot.api.validateSteamID(null).should.be.false; 233 | 234 | // Invalid length 235 | bot.api.validateSteamID('123456789').should.be.false; 236 | 237 | // Non-number 238 | bot.api.validateSteamID('abcd').should.be.false; 239 | 240 | return done(); 241 | 242 | }); 243 | 244 | it('api.validateTrade should return false if no trade session data is available', function (done) { 245 | 246 | var bot = new Stem(); 247 | 248 | bot.api.validateTrade().should.be.false; 249 | 250 | return done(); 251 | 252 | }); 253 | 254 | it('api.validateTrade should return false if inventory lengths do not match', function (done) { 255 | 256 | var bot = new Stem(); 257 | 258 | bot.trade.eventItems = [1, 2, 3]; 259 | bot.botTrade.themAssets = [1, 2]; 260 | 261 | bot.api.validateTrade().should.be.false; 262 | 263 | return done(); 264 | 265 | }); 266 | 267 | it('api.validateTrade should return false if inventories do not match', function (done) { 268 | 269 | var bot = new Stem(); 270 | 271 | bot.trade.eventItems = [1, 2, 3]; 272 | bot.botTrade.themAssets = [1, 2, 4]; 273 | 274 | bot.api.validateTrade().should.be.false; 275 | 276 | return done(); 277 | 278 | }); 279 | 280 | it('api.validateTrade should return true if inventories match', function (done) { 281 | 282 | var bot = new Stem(); 283 | 284 | bot.trade.eventItems = [1, 2, 3]; 285 | bot.botTrade.themAssets = [1, 2, 3]; 286 | 287 | bot.api.validateTrade().should.be.true; 288 | 289 | return done(); 290 | 291 | }); 292 | 293 | }); 294 | -------------------------------------------------------------------------------- /test/storage.js: -------------------------------------------------------------------------------- 1 | 2 | /* global describe, it */ 3 | 4 | var should = require('should'), 5 | Stem = require('../lib'); 6 | 7 | describe('Storage', function () { 8 | 9 | it('storage.set should store the given key and its value', function (done) { 10 | 11 | var bot = new Stem(); 12 | 13 | bot.storage.set('bool', true); 14 | bot.storage.set('string', 'test'); 15 | bot.storage.set('array', [1, 2, 3]); 16 | bot.storage.set('num', 101); 17 | bot.storage.set('obj', { 'test1': true, 'test2': false }); 18 | 19 | bot.storage.data.should.have.property('bool'); 20 | bot.storage.data.should.have.property('string'); 21 | bot.storage.data.should.have.property('array'); 22 | bot.storage.data.should.have.property('num'); 23 | bot.storage.data.should.have.property('obj'); 24 | 25 | return done(); 26 | 27 | }); 28 | 29 | it('storage.get should return the given keys value', function (done) { 30 | 31 | var bot = new Stem(); 32 | 33 | bot.storage.set('bool', true); 34 | bot.storage.set('string', 'test'); 35 | bot.storage.set('array', [1, 2, 3]); 36 | bot.storage.set('num', 101); 37 | bot.storage.set('obj', { 'test1': true, 'test2': false }); 38 | 39 | var string = bot.storage.get('string'), 40 | array = bot.storage.get('array'), 41 | bool = bot.storage.get('bool'), 42 | obj = bot.storage.get('obj'), 43 | num = bot.storage.get('num'); 44 | 45 | string.should.eql('test'); 46 | array.should.eql([1, 2, 3]); 47 | bool.should.eql(true); 48 | obj.should.eql({ 'test1': true, 'test2': false }); 49 | num.should.eql(101); 50 | 51 | return done(); 52 | 53 | }); 54 | 55 | it('storage.remove should remove the given key from storage', function (done) { 56 | 57 | var bot = new Stem(); 58 | 59 | bot.storage.set('bool', true); 60 | bot.storage.set('string', 'test'); 61 | bot.storage.set('array', [1, 2, 3]); 62 | bot.storage.set('num', 101); 63 | bot.storage.set('obj', { 'test1': true, 'test2': false }); 64 | 65 | Object.keys(bot.storage.data).length.should.eql(5); 66 | 67 | var keysToRemove = ['bool', 'string', 'array', 'num', 'obj']; 68 | 69 | keysToRemove.forEach(function (keyToRemove) { 70 | 71 | bot.storage.remove(keyToRemove); 72 | 73 | }); 74 | 75 | Object.keys(bot.storage.data).length.should.eql(0); 76 | 77 | return done(); 78 | 79 | }); 80 | 81 | it('storage.set should emit `change`', function (done) { 82 | 83 | var bot = new Stem(); 84 | 85 | bot.storage.on('change', function (data) { 86 | 87 | Object.keys(data).length.should.eql(1); 88 | return done(); 89 | 90 | }); 91 | 92 | bot.storage.set('string', 'test'); 93 | 94 | }); 95 | 96 | it('storage.remove should emit `change`', function (done) { 97 | 98 | var bot = new Stem(); 99 | 100 | bot.storage.set('string', 'test'); 101 | 102 | bot.storage.on('change', function (data) { 103 | 104 | Object.keys(data).length.should.eql(0); 105 | return done(); 106 | 107 | }); 108 | 109 | bot.storage.remove('string'); 110 | 111 | }); 112 | 113 | it('storage.load should merge given data and emit `loaded`', function (done) { 114 | 115 | var bot = new Stem(); 116 | 117 | bot.storage.set('test', false); 118 | bot.storage.set('123', 'abc'); 119 | 120 | bot.storage.once('loaded', function (data) { 121 | 122 | Object.keys(data).length.should.eql(2); 123 | return done(); 124 | 125 | }); 126 | 127 | bot.storage.load({ test: true }); 128 | 129 | }); 130 | 131 | }); 132 | --------------------------------------------------------------------------------