├── .gitattributes ├── .gitignore ├── README.md ├── farmer.js ├── html └── index.html ├── include ├── css │ ├── bootstrap.min.css │ ├── font-awesome.min.css │ ├── main.css │ ├── normalize.css │ └── style.css ├── fonts │ ├── BrandonText-Light.otf │ ├── BrandonText-Medium.otf │ ├── BrandonText-Regular.otf │ ├── BrandonText-Thin.otf │ ├── FontAwesome.otf │ ├── Lobster_1_4.ttf │ ├── fontawesome-webfont.eot │ ├── fontawesome-webfont.svg │ ├── fontawesome-webfont.ttf │ ├── fontawesome-webfont.woff │ └── fontawesome-webfont.woff2 └── js │ ├── bootstrap.min.js │ ├── jquery.min.js │ └── tether.min.js ├── main.js └── package.json /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.js text eol=lf -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | sentry.*.sha1 3 | servers.json 4 | builds/* -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Steam Card Farmer 2 | 3 | ## A work-in-progress [Electron](http://electron.atom.io) app 4 | 5 | *Looking for the old CLI version? It's still available on 6 | [GitHub](https://github.com/DoctorMcKay/Steam-Card-Farmer/tree/old-cli) and 7 | [npm](https://www.npmjs.com/package/steam-card-farmer). 8 | 9 | ## Usage 10 | 11 | ### Run / Test app 12 | `npm test` 13 | 14 | ### Build app 15 | `npm start` 16 | 17 | ## License 18 | 19 | Released under [the GPLv3 license](https://opensource.org/licenses/GPL-3.0). 20 | -------------------------------------------------------------------------------- /farmer.js: -------------------------------------------------------------------------------- 1 | 2 | var SteamUser = require('steam-user'); 3 | var Steam = SteamUser.Steam; 4 | var request = require('request'); 5 | var Cheerio = require('cheerio'); 6 | var fs = require("fs"); 7 | 8 | var client = new SteamUser({"enablePicsCache": true,"promptSteamGuardCode":false}); 9 | 10 | var Electron = require('electron'); 11 | var Main = Electron.remote.require('./main.js'); 12 | 13 | var g_Jar = request.jar(); 14 | request = request.defaults({"jar": g_Jar}); 15 | var g_Page = 1; 16 | var g_Start; 17 | var g_CheckTimer; 18 | var g_OwnedApps = []; 19 | 20 | function log(message) { 21 | var date = new Date(); 22 | var time = [date.getFullYear(), date.getMonth() + 1, date.getDate(), date.getHours(), date.getMinutes(), date.getSeconds()]; 23 | 24 | for(var i = 1; i < 6; i++) { 25 | if(time[i] < 10) { 26 | time[i] = '0' + time[i]; 27 | } 28 | } 29 | 30 | console.log(time[0] + '-' + time[1] + '-' + time[2] + ' ' + time[3] + ':' + time[4] + ':' + time[5] + ' - ' + message); 31 | } 32 | 33 | 34 | 35 | function login() { 36 | $('.Window').fadeOut(250); 37 | var username = $("#username").val(); 38 | var password = $("#password").val(); 39 | $('#LoadingWindow p').html("Initializing Steam client..."); 40 | $('#LoadingWindow').fadeIn(250); 41 | //Clear Password Field 42 | log("Initializing Steam client..."); 43 | //Login to steam client 44 | client.logOn({ 45 | "accountName": username, 46 | "password": password 47 | }); 48 | $("#password").val(""); 49 | } 50 | 51 | client.on('steamGuard', function(domain, callback, lastcode) { 52 | if (lastcode===true){ 53 | $('#LoadingWindow .Error').html(' Invalid Code'); 54 | }else{ 55 | $('#LoadingWindow .Error').html("");; 56 | } 57 | 58 | if (domain != null ){ 59 | auth_msg = "Auth Code\nEmailed to address " + domain + ":"; 60 | } else { 61 | auth_msg = "Mobile Auth Code:"; 62 | } 63 | 64 | $('#LoadingWindow p').html(auth_msg + '
'); 65 | $("#authCode").focus(); 66 | $("#authCodeForm").on("submit",function(e){ 67 | e.preventDefault();e.stopPropagation(); 68 | code = $("#authCode").val(); 69 | $('#LoadingWindow p').html("Initializing Steam client..."); 70 | callback(code); 71 | }); 72 | }); 73 | 74 | client.on('loggedOn', function() { 75 | client.setPersona(SteamUser.EPersonaState.Offline); 76 | $('.Error').html(""); 77 | log("Logged into Steam!"); 78 | $("#AppLogout").fadeIn(250); 79 | log("Waiting for license info..."); 80 | $('#LoadingWindow p').html("Waiting for license info..."); 81 | console.log(client); 82 | }); 83 | 84 | client.once('appOwnershipCached', function() { 85 | log("Got app ownership info"); 86 | checkMinPlaytime(); 87 | }); 88 | 89 | client.on('error', function(e) { 90 | $("#password").val(""); 91 | log("Error: " + e); 92 | if(e == "Error: InvalidPassword"){ 93 | client.logOff(); 94 | $("#LoginWindow .Error").html(' Wrong username/password'); 95 | $('.Window').fadeOut(250); 96 | $("#LoginWindow").fadeIn(250); 97 | } else if (e == "Error: LoggedInElsewhere" || e=="Error: LogonSessionReplaced"){ 98 | client.logOff(); 99 | $("#LoginWindow .Error").html(' In Game Elsewhere!'); 100 | $('.Window').fadeOut(250); 101 | $("#LoginWindow").fadeIn(250); 102 | }else{ 103 | $("#LoginWindow .Error").html(' '+e); 104 | $('.Window').fadeOut(250); 105 | $("#LoginWindow").fadeIn(250); 106 | } 107 | }); 108 | 109 | function checkMinPlaytime(){ 110 | log("Checking app playtime..."); 111 | $('#LoadingWindow p').html('Checking app playtime...'); 112 | client.webLogOn(); 113 | client.once('webSession', function(sessionID, cookies) { 114 | cookies.forEach(function(cookie) { 115 | g_Jar.setCookie(cookie, 'https://steamcommunity.com'); 116 | }); 117 | request("https://steamcommunity.com/my/badges/?p="+g_Page, function(err, response, body) { 118 | if(err || response.statusCode != 200) { 119 | log("Couldn't request badge page: " + (err || "HTTP error " + response.statusCode) + ". Retrying in 10 seconds..."); 120 | setTimeout(checkMinPlaytime, 10000); 121 | return; 122 | } 123 | 124 | var lowHourApps = []; 125 | var ownedPackages = client.licenses.map(function(license) { 126 | var pkg = client.picsCache.packages[license.package_id].packageinfo; 127 | pkg.time_created = license.time_created; 128 | pkg.payment_method = license.payment_method; 129 | return pkg; 130 | }).filter(function(pkg) { 131 | return !(pkg.extended && pkg.extended.freeweekend); 132 | }); 133 | $_ = Cheerio.load(body); 134 | $_('.badge_row').each(function(i) { 135 | var row = $_(this); 136 | var overlay = row.find('.badge_row_overlay'); 137 | if(!overlay) { 138 | return; 139 | } 140 | 141 | var match = overlay.attr('href').match(/\/gamecards\/(\d+)/); 142 | if(!match) { 143 | return; 144 | } 145 | 146 | var appid = parseInt(match[1], 10); 147 | 148 | var name = row.find('.badge_title'); 149 | name.find('.badge_view_details').remove(); 150 | name = name.text().replace(/\n/g, '').replace(/\r/g, '').replace(/\t/g, '').trim(); 151 | 152 | // Check if app is owned 153 | if(!client.picsCache.apps.hasOwnProperty(appid)) { 154 | log("Skipping app " + appid + " \"" + name + "\", not owned"); 155 | return; 156 | } 157 | 158 | var newlyPurchased = false; 159 | // Find the package(s) in which we own this app 160 | ownedPackages.filter(function(pkg) { 161 | return pkg.appids && pkg.appids.indexOf(appid) != -1; 162 | }).forEach(function(pkg) { 163 | var timeCreatedAgo = Math.floor(Date.now() / 1000) - pkg.time_created; 164 | if(timeCreatedAgo < (60 * 60 * 24 * 14) && [Steam.EPaymentMethod.ActivationCode, Steam.EPaymentMethod.GuestPass, Steam.EPaymentMethod.Complimentary].indexOf(pkg.payment_method) == -1) { 165 | newlyPurchased = true; 166 | } 167 | }); 168 | 169 | // Find out if we have drops left 170 | var drops = row.find('.progress_info_bold').text().match(/(\d+) card drops? remaining/); 171 | if(!drops) { 172 | return; 173 | } 174 | 175 | drops = parseInt(drops[1], 10); 176 | if(isNaN(drops) || drops < 1) { 177 | return; 178 | } 179 | 180 | // Find out playtime 181 | var playtime = row.find('.badge_title_stats').html().match(/(\d+\.\d+) hrs on record/); 182 | if(!playtime) { 183 | playtime = 0.0; 184 | } else { 185 | playtime = parseFloat(playtime[1], 10); 186 | if(isNaN(playtime)) { 187 | playtime = 0.0; 188 | } 189 | } 190 | 191 | if(playtime < 2.0) { 192 | // It needs hours! 193 | 194 | lowHourApps.push({ 195 | "appid": appid, 196 | "name": name, 197 | "playtime": playtime, 198 | "newlyPurchased": newlyPurchased, 199 | "icon": client.picsCache.apps[appid].appinfo.common.icon 200 | }); 201 | } 202 | 203 | if(playtime >= 2.0 || !newlyPurchased) { 204 | g_OwnedApps.push(appid); 205 | } 206 | }); 207 | 208 | if(lowHourApps.length > 0) { 209 | var minPlaytime = 2.0; 210 | var newApps = []; 211 | 212 | lowHourApps.forEach(function(app) { 213 | if(app.playtime < minPlaytime) { 214 | minPlaytime = app.playtime; 215 | } 216 | 217 | if(app.newlyPurchased) { 218 | newApps.push(app); 219 | } 220 | }); 221 | 222 | var lowAppsToIdle = []; 223 | 224 | if(newApps.length > 0) { 225 | function getResponseNewApps(){ 226 | switch(prompt("WARNING: Proceeding will waive your right to a refund on\nthe following apps:\n - " + newApps.map(function(app) { return app.name; }).join("\n - ") + 227 | "\n\nDo you wish to continue?\n" + 228 | " y = yes, idle all of these apps and lose my refund\n" + 229 | " n = no, don't idle any of these apps and keep my refund\n" + 230 | " c = choose which apps to idle").toLowerCase()) { 231 | case 'y': 232 | lowAppsToIdle = lowHourApps.map(function(app) { return app.appid; }); 233 | startErUp(); 234 | break; 235 | 236 | case 'n': 237 | lowAppsToIdle = []; 238 | startErUp(); 239 | break; 240 | 241 | case 'c': 242 | lowAppsToIdle = []; 243 | lowHourApps.forEach(function(app) { 244 | switch(prompt("Idle " + app.name + "? [y/n]").toLowerCase()){ 245 | case 'y': 246 | lowAppsToIdle.push(app); 247 | break; 248 | case 'n': 249 | break; 250 | default: 251 | break; 252 | } 253 | }); 254 | startErUp(); 255 | default: 256 | getResponseNewApps(); 257 | } 258 | } 259 | getResponseNewApps(); 260 | } else { 261 | lowAppsToIdle = lowHourApps.map(function(app) { return app.appid; }); 262 | startErUp(); 263 | } 264 | 265 | function startErUp() { 266 | if(lowAppsToIdle.length < 1) { 267 | checkCardApps(); 268 | } else { 269 | g_OwnedApps = g_OwnedApps.concat(lowAppsToIdle); 270 | new Notification("Steam Card Farmer",{body:"Idling " + lowAppsToIdle.length + " app" + (lowAppsToIdle.length == 1 ? '' : 's') + " up to 2 hours.\nYou likely won't receive any card drops in this time.\nThis will take " + (2.0 - minPlaytime) + " hours.",icon:"logo.png"}); 271 | for(i=0;i'+lowHourApps[i].playtime+' hrs on record