commit
c41c21116d
9 changed files with 222 additions and 166 deletions
1
.dockerignore
Normal file
1
.dockerignore
Normal file
|
@ -0,0 +1 @@
|
|||
src/config.ini
|
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -1,3 +1,5 @@
|
|||
env/
|
||||
*.ini
|
||||
__pycache__/
|
||||
__pycache__/
|
||||
.idea
|
||||
config
|
19
Dockerfile
Normal file
19
Dockerfile
Normal file
|
@ -0,0 +1,19 @@
|
|||
FROM python:3.9-alpine
|
||||
|
||||
RUN mkdir -p /app
|
||||
WORKDIR /app
|
||||
|
||||
# resolves gcc issue with installing regex dependency
|
||||
RUN apk add build-base --no-cache
|
||||
|
||||
ENV VIRTUAL_ENV=/app/env
|
||||
RUN python -m venv $VIRTUAL_ENV
|
||||
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip3 install -r requirements.txt
|
||||
|
||||
COPY ./src/* /app/
|
||||
VOLUME /config
|
||||
|
||||
CMD ["python", "run.py"]
|
33
README.md
33
README.md
|
@ -1,22 +1,13 @@
|
|||
![](https://i.imgur.com/oCob3wQ.gif)
|
||||
|
||||
### About
|
||||
The bot is specially designed for [SteamGifts.com](https://www.steamgifts.com/)
|
||||
|
||||
### Features
|
||||
- Automatically enters giveaways.
|
||||
- Undetectable.
|
||||
- Сonvenient user interface.
|
||||
- Сonfigurable.
|
||||
- Sleeps to restock the points.
|
||||
- Can run 24/7.
|
||||
|
||||
### How to run
|
||||
1. Download the latest version: https://github.com/stilManiac/steamgifts-bot/releases
|
||||
2. Sign in on [SteamGifts.com](https://www.steamgifts.com/) by Steam.
|
||||
3. Find `PHPSESSID` cookie in your browser.
|
||||
4. Start the bot and follow instructions.
|
||||
|
||||
### Run from sources
|
||||
```bash
|
||||
python -m venv env
|
||||
|
@ -25,5 +16,29 @@ pip install -r requirements.txt
|
|||
python src/cli.py
|
||||
```
|
||||
|
||||
### Docker
|
||||
|
||||
#### Run it
|
||||
```bash
|
||||
# Run the container
|
||||
docker run --name steamgifts -d -it mcinj/docker-steamgifts-bot:latest
|
||||
# Attach to it to fill in the questions
|
||||
docker attach steamgifts # to detach, you must use ctrl+p then ctrl+q.
|
||||
# ctrl+c will kill the container
|
||||
```
|
||||
|
||||
#### Or build it yourself locally
|
||||
```bash
|
||||
# Build the image
|
||||
docker build -t steamgifts:latest .
|
||||
# Run the container
|
||||
docker run --name steamgifts -d -it steamgifts:latest
|
||||
# Attach to it to fill in the questions
|
||||
docker attach steamgifts # to detach, you must use ctrl+p then ctrl+q.
|
||||
# ctrl+c will kill the container
|
||||
```
|
||||
|
||||
|
||||
|
||||
### Help
|
||||
Please leave your feedback and bugs in `Issues` page.
|
||||
|
|
127
src/cli.py
127
src/cli.py
|
@ -1,127 +0,0 @@
|
|||
import six
|
||||
import configparser
|
||||
import re
|
||||
|
||||
from pyfiglet import figlet_format
|
||||
from pyconfigstore import ConfigStore
|
||||
from PyInquirer import (Token, ValidationError, Validator, print_json, prompt,
|
||||
style_from_dict)
|
||||
from prompt_toolkit import document
|
||||
|
||||
try:
|
||||
import colorama
|
||||
colorama.init()
|
||||
except ImportError:
|
||||
colorama = None
|
||||
|
||||
try:
|
||||
from termcolor import colored
|
||||
except ImportError:
|
||||
colored = None
|
||||
|
||||
config = configparser.ConfigParser()
|
||||
|
||||
style = style_from_dict({
|
||||
Token.QuestionMark: '#fac731 bold',
|
||||
Token.Answer: '#4688f1 bold',
|
||||
Token.Selected: '#0abf5b', # default
|
||||
Token.Pointer: '#673ab7 bold',
|
||||
})
|
||||
|
||||
def log(string, color, font="slant", figlet=False):
|
||||
if colored:
|
||||
if not figlet:
|
||||
six.print_(colored(string, color))
|
||||
else:
|
||||
six.print_(colored(figlet_format(
|
||||
string, font=font), color))
|
||||
else:
|
||||
six.print_(string)
|
||||
|
||||
|
||||
class PointValidator(Validator):
|
||||
def validate(self, document: document.Document):
|
||||
value = document.text
|
||||
try:
|
||||
value = int(value)
|
||||
except Exception:
|
||||
raise ValidationError(message = 'Value should be greater than 0', cursor_position = len(document.text))
|
||||
|
||||
if value <= 0:
|
||||
raise ValidationError(message = 'Value should be greater than 0', cursor_position = len(document.text))
|
||||
return True
|
||||
|
||||
|
||||
def ask(type, name, message, validate=None, choices=[]):
|
||||
questions = [
|
||||
{
|
||||
'type': type,
|
||||
'name': name,
|
||||
'message': message,
|
||||
'validate': validate,
|
||||
},
|
||||
]
|
||||
if choices:
|
||||
questions[0].update({
|
||||
'choices': choices,
|
||||
})
|
||||
answers = prompt(questions, style=style)
|
||||
return answers
|
||||
|
||||
|
||||
def run():
|
||||
from main import SteamGifts as SG
|
||||
|
||||
def askCookie():
|
||||
cookie = ask(type='input',
|
||||
name='cookie',
|
||||
message='Enter PHPSESSID cookie (Only needed to provide once):')
|
||||
config['DEFAULT']['cookie'] = cookie['cookie']
|
||||
|
||||
with open('config.ini', 'w') as configfile:
|
||||
config.write(configfile)
|
||||
return cookie['cookie']
|
||||
|
||||
log("SteamGifts Bot", color="blue", figlet=True)
|
||||
log("Welcome to SteamGifts Bot!", "green")
|
||||
log("Created by: github.com/stilManiac", "white")
|
||||
|
||||
config.read('config.ini')
|
||||
if not config['DEFAULT'].get('cookie'):
|
||||
cookie = askCookie()
|
||||
else:
|
||||
re_enter_cookie = ask(type='confirm',
|
||||
name='reenter',
|
||||
message='Do you want to enter new cookie?')['reenter']
|
||||
if re_enter_cookie:
|
||||
cookie = askCookie()
|
||||
else:
|
||||
cookie = config['DEFAULT'].get('cookie')
|
||||
|
||||
pinned_games = ask(type='confirm',
|
||||
name='pinned',
|
||||
message='Should bot enter pinned games?')['pinned']
|
||||
|
||||
gift_type = ask(type='list',
|
||||
name='gift_type',
|
||||
message='Select type:',
|
||||
choices=[
|
||||
'All',
|
||||
'Wishlist',
|
||||
'Recommended',
|
||||
'Copies',
|
||||
'DLC',
|
||||
'New'
|
||||
])['gift_type']
|
||||
|
||||
min_points = ask(type='input',
|
||||
name='min_points',
|
||||
message='Enter minimum points to start working (bot will try to enter giveaways until minimum value is reached):',
|
||||
validate=PointValidator)['min_points']
|
||||
|
||||
s = SG(cookie, gift_type, pinned_games, min_points)
|
||||
s.start()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
run()
|
11
src/config.ini.example
Normal file
11
src/config.ini.example
Normal file
|
@ -0,0 +1,11 @@
|
|||
[DEFAULT]
|
||||
cookie = PHPSESSIONCOOKIEGOESHERE
|
||||
# gift_types options: All, Wishlist, Recommended, Copies, DLC, New
|
||||
gift_types = All
|
||||
pinned = true
|
||||
# minimum number of points in your account before entering into giveaways
|
||||
minimum_points = 50
|
||||
# max number of entries in a giveaway for it to be considered
|
||||
max_entries = 900
|
||||
# time left in minutes of a giveaway for it to be considered
|
||||
max_time_left = 180
|
13
src/log.py
Normal file
13
src/log.py
Normal file
|
@ -0,0 +1,13 @@
|
|||
import logging
|
||||
import sys
|
||||
|
||||
Log_Format = "%(levelname)s %(asctime)s - %(message)s"
|
||||
|
||||
logging.basicConfig(stream = sys.stdout,
|
||||
filemode = "w",
|
||||
format = Log_Format,
|
||||
level = logging.INFO)
|
||||
|
||||
|
||||
def get_logger(name):
|
||||
return logging.getLogger(name)
|
97
src/main.py
97
src/main.py
|
@ -1,27 +1,27 @@
|
|||
import sys
|
||||
import configparser
|
||||
import requests
|
||||
import json
|
||||
import threading
|
||||
import re
|
||||
from random import randint
|
||||
from time import sleep
|
||||
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
from requests.adapters import HTTPAdapter
|
||||
from urllib3.util import Retry
|
||||
from time import sleep
|
||||
from random import randint
|
||||
from requests import RequestException
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
from cli import log
|
||||
import log
|
||||
|
||||
logger = log.get_logger(__name__)
|
||||
|
||||
class SteamGifts:
|
||||
def __init__(self, cookie, gifts_type, pinned, min_points):
|
||||
def __init__(self, cookie, gifts_type, pinned, min_points, max_entries, max_time_left):
|
||||
self.cookie = {
|
||||
'PHPSESSID': cookie
|
||||
}
|
||||
self.gifts_type = gifts_type
|
||||
self.pinned = pinned
|
||||
self.min_points = int(min_points)
|
||||
self.max_entries = int(max_entries)
|
||||
self.max_time_left = int(max_time_left)
|
||||
|
||||
self.base = "https://www.steamgifts.com"
|
||||
self.session = requests.Session()
|
||||
|
@ -66,15 +66,36 @@ class SteamGifts:
|
|||
self.xsrf_token = soup.find('input', {'name': 'xsrf_token'})['value']
|
||||
self.points = int(soup.find('span', {'class': 'nav__points'}).text) # storage points
|
||||
except TypeError:
|
||||
log("⛔ Cookie is not valid.", "red")
|
||||
logger.error("⛔ Cookie is not valid.")
|
||||
sleep(10)
|
||||
exit()
|
||||
|
||||
def determine_time_in_minutes(self, string_time):
|
||||
if not string_time:
|
||||
logger.error(f"Could not determine time from string {string_time}")
|
||||
return None
|
||||
match = re.search('(?P<number>[0-9]+) (?P<time_unit>(hour|day|minute|second))', string_time)
|
||||
if match:
|
||||
number = int(match.group('number'))
|
||||
time_unit = match.group('time_unit')
|
||||
if time_unit == 'hour':
|
||||
return number * 60
|
||||
elif time_unit == 'day':
|
||||
return number * 24 * 60
|
||||
elif time_unit == 'minute':
|
||||
return number
|
||||
elif time_unit == 'second':
|
||||
return 1
|
||||
else:
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
|
||||
def get_game_content(self, page=1):
|
||||
n = page
|
||||
while True:
|
||||
txt = "⚙️ Retrieving games from %d page." % n
|
||||
log(txt, "magenta")
|
||||
logger.info(txt)
|
||||
|
||||
filtered_url = self.filter_url[self.gifts_type] % n
|
||||
paginated_url = f"{self.base}/giveaways/{filtered_url}"
|
||||
|
@ -84,48 +105,66 @@ class SteamGifts:
|
|||
game_list = soup.find_all('div', {'class': 'giveaway__row-inner-wrap'})
|
||||
|
||||
if not len(game_list):
|
||||
log("⛔ Page is empty. Please, select another type.", "red")
|
||||
sleep(10)
|
||||
exit()
|
||||
random_seconds = randint(900, 1400)
|
||||
txt = f"We have run out of gifts to consider. Trying again in {random_seconds} seconds."
|
||||
logger.info(txt)
|
||||
sleep(random_seconds)
|
||||
self.start()
|
||||
continue
|
||||
|
||||
for item in game_list:
|
||||
if len(item.get('class', [])) == 2 and not self.pinned:
|
||||
if len(item.get('lass', [])) == 2 and not self.pinned:
|
||||
continue
|
||||
|
||||
if self.points == 0 or self.points < self.min_points:
|
||||
txt = f"🛋️ Sleeping to get 6 points. We have {self.points} points, but we need {self.min_points} to start."
|
||||
log(txt, "yellow")
|
||||
sleep(900)
|
||||
random_seconds = randint(900, 1400)
|
||||
txt = f"🛋️ Sleeping {random_seconds} seconds to get more points. We have {self.points} points, but we need {self.min_points} to start."
|
||||
logger.info(txt)
|
||||
sleep(random_seconds)
|
||||
self.start()
|
||||
break
|
||||
|
||||
game_name = item.find('a', {'class': 'giveaway__heading__name'}).text
|
||||
game_id = item.find('a', {'class': 'giveaway__heading__name'})['href'].split('/')[2]
|
||||
game_cost = item.find_all('span', {'class': 'giveaway__heading__thin'})[-1]
|
||||
|
||||
if game_cost:
|
||||
game_cost = game_cost.getText().replace('(', '').replace(')', '').replace('P', '')
|
||||
else:
|
||||
continue
|
||||
times = item.select('div span[data-timestamp]')
|
||||
game_remaining = times[0].text
|
||||
game_remaining_in_minutes = self.determine_time_in_minutes(game_remaining)
|
||||
game_created = times[1].text
|
||||
game_created_in_minutes = self.determine_time_in_minutes(game_created)
|
||||
game_entries = int(item.select('div.giveaway__links span')[0].text.split(' ')[0].replace(',' ,''))
|
||||
|
||||
game_name = item.find('a', {'class': 'giveaway__heading__name'}).text
|
||||
txt = f"{game_name} {game_cost} - {game_entries} - Created {game_created} ago with {game_remaining} remaining."
|
||||
logger.debug(txt)
|
||||
|
||||
if self.points - int(game_cost) < 0:
|
||||
txt = f"⛔ Not enough points to enter: {game_name}"
|
||||
log(txt, "red")
|
||||
logger.info(txt)
|
||||
continue
|
||||
|
||||
elif self.points - int(game_cost) >= 0:
|
||||
game_id = item.find('a', {'class': 'giveaway__heading__name'})['href'].split('/')[2]
|
||||
if self.max_time_left > game_remaining_in_minutes:
|
||||
txt = f"Game {game_name} has {game_remaining_in_minutes} left and is above your cutoff of {self.max_time_left} minutes."
|
||||
logger.info(txt)
|
||||
continue
|
||||
if self.max_entries > game_entries:
|
||||
txt = f"Game {game_name} has {game_entries} entries is above your cutoff of {self.max_entries} entries."
|
||||
logger.info(txt)
|
||||
continue
|
||||
# defensive move
|
||||
if self.points - int(game_cost) >= 0:
|
||||
res = self.entry_gift(game_id)
|
||||
if res:
|
||||
self.points -= int(game_cost)
|
||||
txt = f"🎉 One more game! Has just entered {game_name}"
|
||||
log(txt, "green")
|
||||
logger.info(txt)
|
||||
sleep(randint(3, 7))
|
||||
|
||||
n = n+1
|
||||
|
||||
|
||||
log("🛋️ List of games is ended. Waiting 2 mins to update...", "yellow")
|
||||
logger.info("🛋️ List of games is ended. Waiting 2 mins to update...")
|
||||
sleep(120)
|
||||
self.start()
|
||||
|
||||
|
@ -142,6 +181,6 @@ class SteamGifts:
|
|||
|
||||
if self.points > 0:
|
||||
txt = "🤖 Hoho! I am back! You have %d points. Lets hack." % self.points
|
||||
log(txt, "blue")
|
||||
logger.info(txt)
|
||||
|
||||
self.get_game_content()
|
||||
|
|
83
src/run.py
Normal file
83
src/run.py
Normal file
|
@ -0,0 +1,83 @@
|
|||
import configparser
|
||||
from configparser import ConfigParser
|
||||
|
||||
import log
|
||||
|
||||
logger = log.get_logger(__name__)
|
||||
config = configparser.ConfigParser()
|
||||
|
||||
|
||||
class MyException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def value_range(min, max):
|
||||
return [str(x) for x in [*range(min, max + 1)]]
|
||||
|
||||
|
||||
class MyConfig(ConfigParser):
|
||||
def __init__(self, config_file):
|
||||
super(MyConfig, self).__init__()
|
||||
|
||||
self.read(config_file)
|
||||
self.validate_config()
|
||||
|
||||
def validate_config(self):
|
||||
required_values = {
|
||||
'DEFAULT': {
|
||||
'gift_types': ('All', 'Wishlist', 'Recommended', 'Copies', 'DLC', 'New'),
|
||||
'pinned': ('true', 'false'),
|
||||
'minimum_points': '%s' % (value_range(0,400)),
|
||||
'max_entries': '%s' % (value_range(0,10000)),
|
||||
'max_time_left': '%s' % (value_range(0,21600))
|
||||
}
|
||||
}
|
||||
|
||||
for section, keys in required_values.items():
|
||||
if section not in self:
|
||||
raise MyException(
|
||||
'Missing section %s in the config file' % section)
|
||||
|
||||
for key, values in keys.items():
|
||||
if key not in self[section] or self[section][key] == '':
|
||||
raise MyException((
|
||||
'Missing value for %s under section %s in ' +
|
||||
'the config file') % (key, section))
|
||||
|
||||
if values:
|
||||
if self[section][key] not in values:
|
||||
raise MyException((
|
||||
'Invalid value for %s under section %s in ' +
|
||||
'the config file') % (key, section))
|
||||
|
||||
|
||||
def run():
|
||||
from main import SteamGifts as SG
|
||||
|
||||
file_name = '../config/config.ini'
|
||||
try:
|
||||
with open(file_name) as f:
|
||||
config.read_file(f)
|
||||
MyConfig(file_name)
|
||||
except IOError:
|
||||
txt = f"{file_name} doesn't exist. Rename {file_name}.example to {file_name} and fill out."
|
||||
logger.warning(txt)
|
||||
exit(-1)
|
||||
except MyException as e:
|
||||
logger.error(e)
|
||||
exit(-1)
|
||||
|
||||
config.read(file_name)
|
||||
cookie = config['DEFAULT'].get('cookie')
|
||||
pinned_games = config['DEFAULT'].getboolean('pinned')
|
||||
gift_types = config['DEFAULT'].get('gift_types')
|
||||
minimum_points = config['DEFAULT'].getint('minimum_points')
|
||||
max_entries = config['DEFAULT'].getint('max_entries')
|
||||
max_time_left = config['DEFAULT'].getint('max_time_left')
|
||||
|
||||
s = SG(cookie, gift_types, pinned_games, minimum_points, max_entries, max_time_left)
|
||||
s.start()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
run()
|
Loading…
Reference in a new issue