Merge pull request #1 from mcinj/noninteractive

Noninteractive
This commit is contained in:
mcinj 2022-04-23 15:01:44 -04:00 committed by GitHub
commit c41c21116d
Signed by: GitHub
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 222 additions and 166 deletions

1
.dockerignore Normal file
View file

@ -0,0 +1 @@
src/config.ini

2
.gitignore vendored
View file

@ -1,3 +1,5 @@
env/ env/
*.ini *.ini
__pycache__/ __pycache__/
.idea
config

19
Dockerfile Normal file
View 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"]

View file

@ -1,22 +1,13 @@
![](https://i.imgur.com/oCob3wQ.gif)
### About ### About
The bot is specially designed for [SteamGifts.com](https://www.steamgifts.com/) The bot is specially designed for [SteamGifts.com](https://www.steamgifts.com/)
### Features ### Features
- Automatically enters giveaways. - Automatically enters giveaways.
- Undetectable. - Undetectable.
- Сonvenient user interface.
- Сonfigurable. - Сonfigurable.
- Sleeps to restock the points. - Sleeps to restock the points.
- Can run 24/7. - 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 ### Run from sources
```bash ```bash
python -m venv env python -m venv env
@ -25,5 +16,29 @@ pip install -r requirements.txt
python src/cli.py 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 ### Help
Please leave your feedback and bugs in `Issues` page. Please leave your feedback and bugs in `Issues` page.

View file

@ -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
View 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
View 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)

View file

@ -1,27 +1,27 @@
import sys
import configparser
import requests
import json 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 requests.adapters import HTTPAdapter
from urllib3.util import Retry 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: 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 = { self.cookie = {
'PHPSESSID': cookie 'PHPSESSID': cookie
} }
self.gifts_type = gifts_type self.gifts_type = gifts_type
self.pinned = pinned self.pinned = pinned
self.min_points = int(min_points) 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.base = "https://www.steamgifts.com"
self.session = requests.Session() self.session = requests.Session()
@ -66,15 +66,36 @@ class SteamGifts:
self.xsrf_token = soup.find('input', {'name': 'xsrf_token'})['value'] self.xsrf_token = soup.find('input', {'name': 'xsrf_token'})['value']
self.points = int(soup.find('span', {'class': 'nav__points'}).text) # storage points self.points = int(soup.find('span', {'class': 'nav__points'}).text) # storage points
except TypeError: except TypeError:
log("⛔ Cookie is not valid.", "red") logger.error("⛔ Cookie is not valid.")
sleep(10) sleep(10)
exit() 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): def get_game_content(self, page=1):
n = page n = page
while True: while True:
txt = "⚙️ Retrieving games from %d page." % n txt = "⚙️ Retrieving games from %d page." % n
log(txt, "magenta") logger.info(txt)
filtered_url = self.filter_url[self.gifts_type] % n filtered_url = self.filter_url[self.gifts_type] % n
paginated_url = f"{self.base}/giveaways/{filtered_url}" 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'}) game_list = soup.find_all('div', {'class': 'giveaway__row-inner-wrap'})
if not len(game_list): if not len(game_list):
log("⛔ Page is empty. Please, select another type.", "red") random_seconds = randint(900, 1400)
sleep(10) txt = f"We have run out of gifts to consider. Trying again in {random_seconds} seconds."
exit() logger.info(txt)
sleep(random_seconds)
self.start()
continue
for item in game_list: 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 continue
if self.points == 0 or self.points < self.min_points: 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." random_seconds = randint(900, 1400)
log(txt, "yellow") txt = f"🛋️ Sleeping {random_seconds} seconds to get more points. We have {self.points} points, but we need {self.min_points} to start."
sleep(900) logger.info(txt)
sleep(random_seconds)
self.start() self.start()
break 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] game_cost = item.find_all('span', {'class': 'giveaway__heading__thin'})[-1]
if game_cost: if game_cost:
game_cost = game_cost.getText().replace('(', '').replace(')', '').replace('P', '') game_cost = game_cost.getText().replace('(', '').replace(')', '').replace('P', '')
else: else:
continue 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: if self.points - int(game_cost) < 0:
txt = f"⛔ Not enough points to enter: {game_name}" txt = f"⛔ Not enough points to enter: {game_name}"
log(txt, "red") logger.info(txt)
continue continue
if self.max_time_left > game_remaining_in_minutes:
elif self.points - int(game_cost) >= 0: txt = f"Game {game_name} has {game_remaining_in_minutes} left and is above your cutoff of {self.max_time_left} minutes."
game_id = item.find('a', {'class': 'giveaway__heading__name'})['href'].split('/')[2] 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) res = self.entry_gift(game_id)
if res: if res:
self.points -= int(game_cost) self.points -= int(game_cost)
txt = f"🎉 One more game! Has just entered {game_name}" txt = f"🎉 One more game! Has just entered {game_name}"
log(txt, "green") logger.info(txt)
sleep(randint(3, 7)) sleep(randint(3, 7))
n = n+1 n = n+1
logger.info("🛋️ List of games is ended. Waiting 2 mins to update...")
log("🛋️ List of games is ended. Waiting 2 mins to update...", "yellow")
sleep(120) sleep(120)
self.start() self.start()
@ -142,6 +181,6 @@ class SteamGifts:
if self.points > 0: if self.points > 0:
txt = "🤖 Hoho! I am back! You have %d points. Lets hack." % self.points txt = "🤖 Hoho! I am back! You have %d points. Lets hack." % self.points
log(txt, "blue") logger.info(txt)
self.get_game_content() self.get_game_content()

83
src/run.py Normal file
View 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()