Compare commits

..

No commits in common. "main" and "v3.3.0" have entirely different histories.
main ... v3.3.0

31 changed files with 235 additions and 930 deletions

View file

@ -2,4 +2,5 @@ config/config.ini
config/debug.log config/debug.log
config/sqlite.db config/sqlite.db
config/info.log config/info.log
alembic.ini
README.ini README.ini

3
.gitignore vendored
View file

@ -1,8 +1,9 @@
env/ env/
venv/ venv/
*.ini
__pycache__/ __pycache__/
.idea .idea
config/config.ini config/config.ini
config/*.log* config/*.log*
config/sqlite* config/sqlite.db
.DS_STORE .DS_STORE

View file

@ -4,32 +4,15 @@ The bot is specially designed for [SteamGifts.com](https://www.steamgifts.com/)
### Features ### Features
- Automatically enters giveaways. - Automatically enters giveaways.
- Undetectable. - Undetectable.
- Сonfigurable - Сonfigurable.
- Can look at your steam wishlist games or the main page for games to enter
- When evaluating a giveaway
- `max_time_left` - if the time left on giveaway is > max time left, then don't enter it
- `max_entries` - if the entries on a giveaway are > than max entries, then don't enter it
- `minimum_points` - minimum number of points **in your account** needed before considering any giveaway
- `minimum_game_points` - if steamgifts.com point cost (ex. 1P, 5P, etc) is below this, don't enter it
- `blacklist_keywords` - if the giveaway name contains any of these words, don't enter it. this list can be blank.
- Notifications
- A pushover notifications can be sent to you when a win is detected.
- Webserver - A simple, simple, simple webserver than can be enabled (disabled by default) to show the config and logs
- `web.host` - the IP to listen on (ex. localhost, 0.0.0.0, 192.168.1.1, etc)
- `web.port` - the port to listen on
- `web.app_root` - the folder to serve up which can be used for reverse proxying this behind nginx/apache/etc
- `web.ssl` - if the traffic will be encrypted (http or https) using a self-signed cert
- `web.basic_auth` - simple basic auth settings can be enabled
- Sleeps to restock the points. - Sleeps to restock the points.
- Can run 24/7. - Can run 24/7.
## Instructions ## Instructions
1. Sign in on SteamGifts.com by Steam. 1. Rename `config/config.ini.example` to `config/config.ini`.
2. Find PHPSESSID cookie in your browser. 2. Add your PHPSESSION cookie to it.
3. Rename `config/config.ini.example` to `config/config.ini`. 3. Modifying the other settings is optional.
4. Add your PHPSESSION cookie to `cookie` in `config/config.ini`
5. Modifying the other settings is optional as defaults are set.
### Run from sources ### Run from sources
@ -58,3 +41,6 @@ docker run --name steamgifts -e TZ=America/New_York -d -v /path/to/the/config/fo
``` ```
### Help
Please leave your feedback and bugs in `Issues` page.

View file

@ -1,102 +0,0 @@
# A generic, single database configuration.
[alembic]
# path to migration scripts
script_location = src/alembic
# template used to generate migration files
file_template = %%(year)d_%%(month).2d_%%(day).2d-%%(hour).2d_%%(minute).2d_%%(second).2d-%%(rev)s_%%(slug)s
# sys.path path, will be prepended to sys.path if present.
# defaults to the current working directory.
prepend_sys_path = .
# timezone to use when rendering the date within the migration file
# as well as the filename.
# If specified, requires the python-dateutil library that can be
# installed by adding `alembic[tz]` to the pip requirements
# string value is passed to dateutil.tz.gettz()
# leave blank for localtime
# timezone =
# max length of characters to apply to the
# "slug" field
# truncate_slug_length = 40
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# set to 'true' to allow .pyc and .pyo files without
# a source .py file to be detected as revisions in the
# versions/ directory
# sourceless = false
# version location specification; This defaults
# to alembic/versions. When using multiple version
# directories, initial revisions must be specified with --version-path.
# The path separator used here should be the separator specified by "version_path_separator" below.
# version_locations = %(here)s/bar:%(here)s/bat:alembic/versions:src/alembic/versions
# version path separator; As mentioned above, this is the character used to split
# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
# Valid values for version_path_separator are:
#
# version_path_separator = :
# version_path_separator = ;
# version_path_separator = space
# version_path_separator = os # Use os.pathsep. Default configuration used for new projects.
# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = utf-8
sqlalchemy.url = sqlite:///config/sqlite.db
[post_write_hooks]
# post_write_hooks defines scripts or Python functions that are run
# on newly generated revision scripts. See the documentation for further
# detail and examples
# format using "black" - use the console_scripts runner, against the "black" entrypoint
# hooks = black
# black.type = console_scripts
# black.entrypoint = black
# black.options = -l 79 REVISION_SCRIPT_FILENAME
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

View file

@ -2,20 +2,16 @@
cookie = PHPSESSIONCOOKIEGOESHERE cookie = PHPSESSIONCOOKIEGOESHERE
# should we consider giveaways on the 'ALL' page? it is the main page # should we consider giveaways on the 'ALL' page? it is the main page
enabled = true enabled = true
[ALL]
# minimum number of points in your account before entering into giveaways # minimum number of points in your account before entering into giveaways
all.minimum_points = 50 minimum_points = 50
# max number of entries in a giveaway for it to be considered # max number of entries in a giveaway for it to be considered
all.max_entries = 2000 max_entries = 2000
# time left in minutes of a giveaway for it to be considered # time left in minutes of a giveaway for it to be considered
all.max_time_left = 300 max_time_left = 300
# points after that giveaway is considered regardless of entries and time left
all.max_points = 350
# the minimum point value for a giveaway to be considered # the minimum point value for a giveaway to be considered
all.minimum_game_points = 1 minimum_game_points = 1
# a comma separated list of keywords in game titles to ignore # a comma separated list of keywords in game titles to ignore
all.blacklist_keywords = hentai,adult blacklist_keywords = hentai,adult
[WISHLIST] [WISHLIST]
# should we consider giveaways on the 'Wishlist' page? # should we consider giveaways on the 'Wishlist' page?
@ -26,20 +22,6 @@ wishlist.minimum_points = 1
wishlist.max_entries = 10000 wishlist.max_entries = 10000
# time left in minutes of a giveaway for it to be considered # time left in minutes of a giveaway for it to be considered
wishlist.max_time_left = 300 wishlist.max_time_left = 300
# points after that giveaway is considered regardless of entries and time left
wishlist.max_points = 350
[DLC]
# should we consider giveaways on the 'DLC' page?
dlc.enabled = true
# minimum number of points in your account before entering into giveaways
dlc.minimum_points = 1
# max number of entries in a giveaway for it to be considered
dlc.max_entries = 10000
# time left in minutes of a giveaway for it to be considered
dlc.max_time_left = 300
# points after that giveaway is considered regardless of entries and time left
dlc.max_points = 350
[NOTIFICATIONS] [NOTIFICATIONS]
# a prefix for messages sent via notifications # a prefix for messages sent via notifications

View file

@ -1,4 +1,4 @@
from src import run from src.bot import run
if __name__ == '__main__': if __name__ == '__main__':

View file

@ -3,11 +3,8 @@ beautifulsoup4==4.11.1
urllib3==1.26.9 urllib3==1.26.9
sqlalchemy==1.4.36 sqlalchemy==1.4.36
sqlalchemy_utils==0.38.2 sqlalchemy_utils==0.38.2
paginate-sqlalchemy==0.3.1
alembic==1.7.7
python-dateutil==2.8.2 python-dateutil==2.8.2
Flask==2.1.2 Flask==2.1.2
Flask-BasicAuth==0.2.0 Flask-BasicAuth==0.2.0
pyopenssl==22.0.0 pyopenssl==22.0.0
apscheduler==3.9.1 alembic==1.7.7
werkzeug==2.2.2

View file

@ -1,31 +0,0 @@
"""won column removed
Revision ID: 15c028536ef5
Revises: 1da33402b659
Create Date: 2022-05-21 10:25:41.647723
"""
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision = '15c028536ef5'
down_revision = '1da33402b659'
branch_labels = None
depends_on = None
def upgrade():
op.drop_column('giveaway', 'won')
def downgrade():
# add columns as nullable as existing records don't have that column value set
op.add_column('giveaway', sa.Column('won', sa.Boolean(), nullable=True))
# set value on new columns for all records
with op.get_context().autocommit_block():
op.execute("UPDATE giveaway SET won=false WHERE won is null;")
# SQLite doesn't support ALTERs so alembic uses a batch mode
# Set columns to non-nullable now that all records have a value
with op.batch_alter_table('giveaway') as batch_op:
batch_op.alter_column('won', nullable=False)

View file

@ -1,27 +0,0 @@
"""add games_won column
Revision ID: ff0a728ba3da
Revises: 15c028536ef5
Create Date: 2022-05-24 08:31:25.684099
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'ff0a728ba3da'
down_revision = '15c028536ef5'
branch_labels = None
depends_on = None
def upgrade():
op.add_column('notification', sa.Column('games_won', sa.Integer(), nullable=True))
# set a default for previous won notifications
with op.get_context().autocommit_block():
op.execute("UPDATE notification SET games_won=1 WHERE type='won';")
def downgrade():
op.drop_column('notification', 'games_won')

View file

@ -1,31 +0,0 @@
"""won column added backed
Revision ID: 8c1784114d65
Revises: ff0a728ba3da
Create Date: 2022-06-01 09:20:36.762279
"""
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision = '8c1784114d65'
down_revision = 'ff0a728ba3da'
branch_labels = None
depends_on = None
def upgrade():
# add columns as nullable as existing records don't have that column value set
op.add_column('giveaway', sa.Column('won', sa.Boolean(), nullable=True))
# set value on new columns for all records
with op.get_context().autocommit_block():
op.execute("UPDATE giveaway SET won=false WHERE won is null;")
# SQLite doesn't support ALTERs so alembic uses a batch mode
# Set columns to non-nullable now that all records have a value
with op.batch_alter_table('giveaway') as batch_op:
batch_op.alter_column('won', nullable=False)
def downgrade():
op.drop_column('giveaway', 'won')

View file

@ -29,28 +29,17 @@ def choose_user_agent():
class ConfigReader(ConfigParser): class ConfigReader(ConfigParser):
required_values = { required_values = {
'DEFAULT': { 'DEFAULT': {
}, 'enabled': ('true', 'false'),
'ALL': { 'minimum_points': '%s' % (value_range(0, 400)),
'all.enabled': ('true', 'false'), 'max_entries': '%s' % (value_range(0, 100000)),
'all.minimum_points': '%s' % (value_range(0, 400)), 'max_time_left': '%s' % (value_range(0, 21600)),
'all.max_entries': '%s' % (value_range(0, 100000)), 'minimum_game_points': '%s' % (value_range(0, 50))
'all.max_time_left': '%s' % (value_range(0, 21600)),
'all.max_points': '%s' % (value_range(0, 400)),
'all.minimum_game_points': '%s' % (value_range(0, 50))
}, },
'WISHLIST': { 'WISHLIST': {
'wishlist.enabled': ('true', 'false'), 'wishlist.enabled': ('true', 'false'),
'wishlist.minimum_points': '%s' % (value_range(0, 400)), 'wishlist.minimum_points': '%s' % (value_range(0, 400)),
'wishlist.max_entries': '%s' % (value_range(0, 100000)), 'wishlist.max_entries': '%s' % (value_range(0, 100000)),
'wishlist.max_time_left': '%s' % (value_range(0, 21600)), 'wishlist.max_time_left': '%s' % (value_range(0, 21600))
'wishlist.max_points': '%s' % (value_range(0, 400)),
},
'DLC': {
'dlc.enabled': ('true', 'false'),
'dlc.minimum_points': '%s' % (value_range(0, 400)),
'dlc.max_entries': '%s' % (value_range(0, 100000)),
'dlc.max_time_left': '%s' % (value_range(0, 21600)),
'dlc.max_points': '%s' % (value_range(0, 400)),
}, },
'NOTIFICATIONS': { 'NOTIFICATIONS': {
'pushover.enabled': ('true', 'false'), 'pushover.enabled': ('true', 'false'),
@ -63,30 +52,19 @@ class ConfigReader(ConfigParser):
default_values = { default_values = {
'DEFAULT': { 'DEFAULT': {
'cookie': '', 'cookie': '',
'user_agent': f"{choose_user_agent()}" 'user_agent': f"{choose_user_agent()}",
}, 'enabled': 'true',
'ALL': { 'minimum_points': f"{randint(20, 50)}",
'all.enabled': 'true', 'max_entries': f"{randint(1000, 2500)}",
'all.minimum_points': f"{randint(20, 50)}", 'max_time_left': f"{randint(180, 500)}",
'all.max_entries': f"{randint(1000, 2500)}", 'minimum_game_points': "0",
'all.max_time_left': f"{randint(180, 500)}", 'blacklist_keywords': 'hentai,adult'
'all.max_points': f"{randint(300, 400)}",
'all.minimum_game_points': "0",
'all.blacklist_keywords': 'hentai,adult'
}, },
'WISHLIST': { 'WISHLIST': {
'wishlist.enabled': 'true', 'wishlist.enabled': 'true',
'wishlist.minimum_points': '1', 'wishlist.minimum_points': '1',
'wishlist.max_entries': f"{randint(10000, 100000)}", 'wishlist.max_entries': f"{randint(10000, 100000)}",
'wishlist.max_time_left': f"{randint(180, 500)}", 'wishlist.max_time_left': f"{randint(180, 500)}"
'wishlist.max_points': f"{randint(300, 400)}",
},
'DLC': {
'dlc.enabled': 'true',
'dlc.minimum_points': '1',
'dlc.max_entries': f"{randint(10000, 100000)}",
'dlc.max_time_left': f"{randint(180, 500)}",
'dlc.max_points': f"{randint(300, 400)}",
}, },
'NOTIFICATIONS': { 'NOTIFICATIONS': {
'notification.prefix': '', 'notification.prefix': '',

View file

@ -1,13 +1,12 @@
import os import os
from datetime import datetime, timedelta from datetime import datetime, timedelta
import paginate_sqlalchemy
import sqlalchemy import sqlalchemy
from alembic import command from alembic import command
from alembic.config import Config from alembic.config import Config
from dateutil import tz from dateutil import tz
from sqlalchemy import func, select from sqlalchemy import func
from sqlalchemy.orm import Session, joinedload from sqlalchemy.orm import Session
from sqlalchemy_utils import database_exists from sqlalchemy_utils import database_exists
from .log import get_logger from .log import get_logger
@ -18,18 +17,23 @@ engine = sqlalchemy.create_engine(f"{os.getenv('BOT_DB_URL', 'sqlite:///./config
engine.connect() engine.connect()
def run_db_migrations(script_location: str, db_url: str) -> None: def create_engine(db_url: str):
return engine
def run_migrations(script_location: str, db_url: str) -> None:
logger.debug('Running DB migrations in %r on %r', script_location, db_url) logger.debug('Running DB migrations in %r on %r', script_location, db_url)
alembic_cfg = Config() alembic_cfg = Config()
alembic_cfg.set_main_option('script_location', script_location) alembic_cfg.set_main_option('script_location', script_location)
alembic_cfg.set_main_option('sqlalchemy.url', db_url) alembic_cfg.set_main_option('sqlalchemy.url', db_url)
if not database_exists(db_url): if not database_exists(db_url):
logger.debug(f"'{db_url}' does not exist. Running normal migration to create db and tables.") logger.debug(f"'{db_url}' does not exist. Running normal migration to create db and tables." )
command.upgrade(alembic_cfg, 'head') command.upgrade(alembic_cfg, 'head')
elif database_exists(db_url): if database_exists(db_url):
logger.debug(f"'{db_url}' exists.") logger.debug(f"'{db_url} exists.")
insp = sqlalchemy.inspect(engine) e = create_engine(db_url)
insp = sqlalchemy.inspect(e)
alembic_version_table_name = 'alembic_version' alembic_version_table_name = 'alembic_version'
has_alembic_table = insp.has_table(alembic_version_table_name) has_alembic_table = insp.has_table(alembic_version_table_name)
if has_alembic_table: if has_alembic_table:
@ -46,15 +50,9 @@ def run_db_migrations(script_location: str, db_url: str) -> None:
class NotificationHelper: class NotificationHelper:
@classmethod @classmethod
def get(cls): def insert(cls, type_of_error, message, medium, success):
with Session(engine) as session: with Session(engine) as session:
return session.query(TableNotification).order_by(TableNotification.created_at.desc()).all() n = TableNotification(type=type_of_error, message=message, medium=medium, success=success)
@classmethod
def insert(cls, type_of_error, message, medium, success, number_won):
with Session(engine) as session:
n = TableNotification(type=type_of_error, message=message, medium=medium, success=success,
games_won=number_won)
session.add(n) session.add(n)
session.commit() session.commit()
@ -66,7 +64,7 @@ class NotificationHelper:
within_3_days = session.query(TableNotification) \ within_3_days = session.query(TableNotification) \
.filter(func.DATE(TableNotification.created_at) >= (datetime.utcnow().date() - timedelta(days=1))) \ .filter(func.DATE(TableNotification.created_at) >= (datetime.utcnow().date() - timedelta(days=1))) \
.filter(func.DATE(TableNotification.created_at) <= (datetime.utcnow().date() + timedelta(days=1))) \ .filter(func.DATE(TableNotification.created_at) <= (datetime.utcnow().date() + timedelta(days=1))) \
.filter_by(type='won').order_by(TableNotification.created_at.asc()).all() .filter_by(type='won').all()
actual = [] actual = []
for r in within_3_days: for r in within_3_days:
if r.created_at.replace(tzinfo=tz.tzutc()).astimezone(tz.tzlocal()).date() == datetime.now( if r.created_at.replace(tzinfo=tz.tzutc()).astimezone(tz.tzlocal()).date() == datetime.now(
@ -91,41 +89,6 @@ class NotificationHelper:
class GiveawayHelper: class GiveawayHelper:
@classmethod
def get(cls):
with Session(engine) as session:
return session.query(TableGiveaway).options(joinedload('steam_item')) \
.order_by(TableGiveaway.giveaway_ended_at.desc()).all()
@classmethod
def paginate(cls, page=1):
with Session(engine) as session:
paginated_giveaways = paginate_sqlalchemy.SqlalchemyOrmPage(session.query(TableGiveaway)
.options(joinedload('steam_item'))
.order_by(
TableGiveaway.giveaway_ended_at.desc()), page=page)
return paginated_giveaways
@classmethod
def total_giveaways(cls):
with Session(engine) as session:
return session.execute(select(func.count(TableGiveaway.giveaway_id))).scalar_one()
@classmethod
def total_entered(cls):
with Session(engine) as session:
return session.query(TableGiveaway).filter_by(entered=True).count()
@classmethod
def total_won(cls):
with Session(engine) as session:
return session.query(TableGiveaway).filter_by(won=True).count()
@classmethod
def get_by_giveaway_id(cls, game_id):
with Session(engine) as session:
return session.query(TableGiveaway).filter_by(giveaway_id=game_id).one_or_none()
@classmethod @classmethod
def unix_timestamp_to_utc_datetime(cls, timestamp): def unix_timestamp_to_utc_datetime(cls, timestamp):
return datetime.utcfromtimestamp(timestamp) return datetime.utcfromtimestamp(timestamp)
@ -137,18 +100,7 @@ class GiveawayHelper:
steam_id=giveaway.steam_app_id).all() steam_id=giveaway.steam_app_id).all()
@classmethod @classmethod
def mark_game_as_won(cls, game_id): def insert(cls, giveaway, entered):
with Session(engine) as session:
result = session.query(TableGiveaway).filter_by(giveaway_id=game_id).all()
if result:
won_giveaway = result[0]
if not won_giveaway.won:
won_giveaway.won = True
session.add(won_giveaway)
session.commit()
@classmethod
def insert(cls, giveaway, entered, won):
with Session(engine) as session: with Session(engine) as session:
result = session.query(TableSteamItem).filter_by(steam_id=giveaway.steam_app_id).all() result = session.query(TableSteamItem).filter_by(steam_id=giveaway.steam_app_id).all()
if result: if result:
@ -172,16 +124,16 @@ class GiveawayHelper:
copies=giveaway.copies, copies=giveaway.copies,
contributor_level=giveaway.contributor_level, contributor_level=giveaway.contributor_level,
entered=entered, entered=entered,
won=won, won=False,
game_entries=giveaway.game_entries) game_entries=giveaway.game_entries)
session.add(g) session.add(g)
session.commit() session.commit()
@classmethod @classmethod
def upsert_giveaway_with_details(cls, giveaway, entered, won): def upsert_giveaway(cls, giveaway, entered):
result = GiveawayHelper.get_by_ids(giveaway) result = GiveawayHelper.get_by_ids(giveaway)
if not result: if not result:
GiveawayHelper.insert(giveaway, entered, won) GiveawayHelper.insert(giveaway, entered)
else: else:
with Session(engine) as session: with Session(engine) as session:
g = TableGiveaway( g = TableGiveaway(
@ -195,28 +147,7 @@ class GiveawayHelper:
copies=giveaway.copies, copies=giveaway.copies,
contributor_level=giveaway.contributor_level, contributor_level=giveaway.contributor_level,
entered=entered, entered=entered,
won=won, won=False,
game_entries=giveaway.game_entries)
session.merge(g)
session.commit()
@classmethod
def upsert_giveaway(cls, giveaway):
result = GiveawayHelper.get_by_ids(giveaway)
if not result:
GiveawayHelper.insert(giveaway, False, False)
else:
with Session(engine) as session:
g = TableGiveaway(
giveaway_id=giveaway.giveaway_game_id,
steam_id=result[0].steam_id,
giveaway_uri=giveaway.giveaway_uri,
user=giveaway.user,
giveaway_created_at=GiveawayHelper.unix_timestamp_to_utc_datetime(giveaway.time_created_timestamp),
giveaway_ended_at=GiveawayHelper.unix_timestamp_to_utc_datetime(giveaway.time_remaining_timestamp),
cost=giveaway.cost,
copies=giveaway.copies,
contributor_level=giveaway.contributor_level,
game_entries=giveaway.game_entries) game_entries=giveaway.game_entries)
session.merge(g) session.merge(g)
session.commit() session.commit()

View file

@ -9,7 +9,7 @@ from urllib3.util import Retry
from .log import get_logger from .log import get_logger
from .database import NotificationHelper, GiveawayHelper from .database import NotificationHelper, GiveawayHelper
from .giveaway_entry import GiveawayEntry from .giveaway import Giveaway
logger = get_logger(__name__) logger = get_logger(__name__)
@ -19,30 +19,28 @@ class SteamGiftsException(Exception):
class EnterGiveaways: class EnterGiveaways:
def __init__(self, cookie, user_agent, gifts_type, pinned, min_points, max_entries, def __init__(self, cookie, user_agent, gifts_type, pinned, min_points, max_entries,
max_time_left, max_points, minimum_game_points, blacklist, notification): max_time_left, minimum_game_points, blacklist, notification):
self._contributor_level = None self.contributor_level = None
self._xsrf_token = None self.xsrf_token = None
self._points = None self.points = None
self._cookie = { self.cookie = {
'PHPSESSID': cookie 'PHPSESSID': cookie
} }
self._user_agent = user_agent self.user_agent = user_agent
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_entries = int(max_entries)
self._max_time_left = int(max_time_left) self.max_time_left = int(max_time_left)
self._max_points = int(max_points) self.minimum_game_points = int(minimum_game_points)
self._minimum_game_points = int(minimum_game_points) self.blacklist = blacklist.split(',')
self._blacklist = blacklist.split(',') self.notification = notification
self._notification = notification
self._base = "https://www.steamgifts.com" self.base = "https://www.steamgifts.com"
self._session = requests.Session() self.session = requests.Session()
self._filter_url = { self.filter_url = {
'All': "search?page=%d", 'All': "search?page=%d",
'Wishlist': "search?page=%d&type=wishlist", 'Wishlist': "search?page=%d&type=wishlist",
'Recommended': "search?page=%d&type=recommended", 'Recommended': "search?page=%d&type=recommended",
@ -51,23 +49,12 @@ class EnterGiveaways:
'New': "search?page=%d&type=new" 'New': "search?page=%d&type=new"
} }
def start(self): def requests_retry_session(
self._update_info()
if self._points >= self._min_points:
txt = f"〰 You have {self._points} points. Evaluating '{self._gifts_type}' giveaways..."
logger.info(txt)
self._evaluate_giveaways()
else:
txt = f"🟡 You have {self._points} points which is below your minimum point threshold of " \
f"{self._min_points} points for '{self._gifts_type}' giveaways. Not evaluating right now."
logger.info(txt)
def _requests_retry_session(
self, self,
retries=5, retries=5,
backoff_factor=0.3 backoff_factor=0.3
): ):
session = self._session or requests.Session() session = self.session or requests.Session()
retry = Retry( retry = Retry(
total=retries, total=retries,
read=retries, read=retries,
@ -80,93 +67,85 @@ class EnterGiveaways:
session.mount('https://', adapter) session.mount('https://', adapter)
return session return session
def _get_soup_from_page(self, url): def get_soup_from_page(self, url):
headers = { headers = {
'User-Agent': self._user_agent 'User-Agent': self.user_agent
} }
self._requests_retry_session().get(url, headers=headers) self.requests_retry_session().get(url, headers=headers)
r = requests.get(url, cookies=self._cookie) r = requests.get(url, cookies=self.cookie)
soup = BeautifulSoup(r.text, 'html.parser') soup = BeautifulSoup(r.text, 'html.parser')
return soup return soup
def _update_info(self): def update_info(self):
soup = self._get_soup_from_page(self._base) soup = self.get_soup_from_page(self.base)
try: try:
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
self._contributor_level = int(float(soup.select_one('nav a>span[title]')['title'])) self.contributor_level = int(float(soup.select_one('nav a>span[title]')['title']))
except TypeError: except TypeError:
logger.error("⛔⛔⛔ Cookie is not valid. A new one must be added.⛔⛔⛔") logger.error("⛔⛔⛔ Cookie is not valid. A new one must be added.⛔⛔⛔")
raise SteamGiftsException("Cookie is not valid. A new one must be added.") raise SteamGiftsException("Cookie is not valid. A new one must be added.")
won = soup.select("a[title='Giveaways Won'] div") won = soup.select("a[title='Giveaways Won'] div")
if won: if won:
number_won = int(soup.select_one("a[title='Giveaways Won'] div").text) number_won = soup.select_one("a[title='Giveaways Won'] div").text
won_notifications = NotificationHelper.get_won_notifications_today() won_notifications = NotificationHelper.get_won_notifications_today()
if won_notifications and len(won_notifications) >= 1: if won_notifications and len(won_notifications) >= 1:
if number_won == won_notifications[-1].games_won:
logger.info("🆒️ Win(s) detected, but we have already notified that there are won games waiting " logger.info("🆒️ Win(s) detected, but we have already notified that there are won games waiting "
"to be received. Doing nothing.") "to be received. Doing nothing.")
elif number_won > won_notifications[-1].games_won:
logger.info("🔥🔥 MORE win(s) detected. Notifying again.")
self._notification.send_won(f"You won ANOTHER game. You now have {number_won} game(s) "
f"waiting to be claimed.", number_won)
else: # we have less games waiting to be claimed than notified, meaning some have been claimed
logger.info("🆒️ Win(s) detected, but we have already notified that there are won games waiting "
"to be received. Some have been claimed. Doing nothing.")
else: else:
logger.info(f"💰💰 WINNER! You have {number_won} game(s) waiting to be claimed. 💰💰") logger.info(f"💰💰 WINNER! You have {number_won} game(s) waiting to be claimed. 💰💰")
self._notification.send_won(f"WINNER! You have {number_won} game(s) waiting to be claimed.", number_won) self.notification.send_won(f"WINNER! You have {number_won} game(s) waiting to be claimed.")
else: else:
logger.debug('No wins detected. Doing nothing.') logger.debug('No wins detected. Doing nothing.')
def _should_we_enter_giveaway(self, giveaway): def should_we_enter_giveaway(self, giveaway):
if giveaway.time_remaining_in_minutes is None: if giveaway.time_remaining_in_minutes is None:
return False return False
if giveaway.time_created_in_minutes is None: if giveaway.time_created_in_minutes is None:
return False return False
if self._blacklist is not None and self._blacklist != ['']: if self.blacklist is not None and self.blacklist != ['']:
for keyword in self._blacklist: for keyword in self.blacklist:
if giveaway.game_name.lower().find(keyword.lower()) != -1: if giveaway.game_name.lower().find(keyword.lower()) != -1:
txt = f"〰️ Game {giveaway.game_name} contains the blacklisted keyword {keyword}" txt = f"〰️ Game {giveaway.game_name} contains the blacklisted keyword {keyword}"
logger.info(txt) logger.info(txt)
return False return False
if giveaway.contributor_level is None or self._contributor_level < giveaway.contributor_level: if giveaway.contributor_level is None or self.contributor_level < giveaway.contributor_level:
txt = f"〰️ Game {giveaway.game_name} requires at least level {giveaway.contributor_level} contributor " \ txt = f"〰️ Game {giveaway.game_name} requires at least level {giveaway.contributor_level} contributor " \
f"level to enter. Your level: {self._contributor_level}" f"level to enter. Your level: {self.contributor_level}"
logger.info(txt) logger.info(txt)
return False return False
if giveaway.time_remaining_in_minutes > self._max_time_left and self._points < self._max_points: if self.points - int(giveaway.cost) < 0:
txt = f"〰️ Game {giveaway.game_name} has {giveaway.time_remaining_in_minutes} minutes left and is " \
f"above your cutoff of {self._max_time_left} minutes."
logger.info(txt)
return False
if giveaway.game_entries / giveaway.copies > self._max_entries and self._points < self._max_points:
txt = f"〰️ Game {giveaway.game_name} has {giveaway.game_entries} entries and is above your cutoff " \
f"of {self._max_entries} entries."
logger.info(txt)
return False
if self._points - int(giveaway.cost) < 0:
txt = f"〰️ Not enough points to enter: {giveaway.game_name}" txt = f"〰️ Not enough points to enter: {giveaway.game_name}"
logger.info(txt) logger.info(txt)
return False return False
if giveaway.cost < self._minimum_game_points: if giveaway.cost < self.minimum_game_points:
txt = f"〰️ Game {giveaway.game_name} costs {giveaway.cost}P and is below your cutoff of " \ txt = f"〰️ Game {giveaway.game_name} costs {giveaway.cost}P and is below your cutoff of " \
f"{self._minimum_game_points}P." f"{self.minimum_game_points}P."
logger.info(txt)
return False
if giveaway.time_remaining_in_minutes > self.max_time_left:
txt = f"〰️ Game {giveaway.game_name} has {giveaway.time_remaining_in_minutes} minutes left and is " \
f"above your cutoff of {self.max_time_left} minutes."
logger.info(txt)
return False
if giveaway.game_entries / giveaway.copies > self.max_entries:
txt = f"〰️ Game {giveaway.game_name} has {giveaway.game_entries} entries and is above your cutoff " \
f"of {self.max_entries} entries."
logger.info(txt) logger.info(txt)
return False return False
return True return True
def _enter_giveaway(self, giveaway): def enter_giveaway(self, giveaway):
headers = { headers = {
'User-Agent': self._user_agent 'User-Agent': self.user_agent
} }
payload = {'xsrf_token': self._xsrf_token, 'do': 'entry_insert', 'code': giveaway.giveaway_game_id} payload = {'xsrf_token': self.xsrf_token, 'do': 'entry_insert', 'code': giveaway.giveaway_game_id}
logger.debug(f"Sending enter giveaway payload: {payload}") logger.debug(f"Sending enter giveaway payload: {payload}")
entry = requests.post('https://www.steamgifts.com/ajax.php', data=payload, cookies=self._cookie, entry = requests.post('https://www.steamgifts.com/ajax.php', data=payload, cookies=self.cookie,
headers=headers) headers=headers)
json_data = json.loads(entry.text) json_data = json.loads(entry.text)
@ -177,17 +156,17 @@ class EnterGiveaways:
logger.error(f"❌ Failed entering giveaway {giveaway.giveaway_game_id}: {json_data}") logger.error(f"❌ Failed entering giveaway {giveaway.giveaway_game_id}: {json_data}")
return False return False
def _evaluate_giveaways(self, page=1): def evaluate_giveaways(self, page=1):
n = page n = page
run = True run = True
while run and n < 3: # hard stop safety net at page 3 as idk why we would ever get to this point while run and n < 3: # hard stop safety net at page 3 as idk why we would ever get to this point
txt = "〰️ Retrieving games from %d page." % n txt = "〰️ Retrieving games from %d page." % n
logger.info(txt) 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}"
soup = self._get_soup_from_page(paginated_url) soup = self.get_soup_from_page(paginated_url)
pinned_giveaway_count = len(soup.select('div.pinned-giveaways__outer-wrap div.giveaway__row-inner-wrap')) pinned_giveaway_count = len(soup.select('div.pinned-giveaways__outer-wrap div.giveaway__row-inner-wrap'))
all_games_list_count = len(soup.select('div.giveaway__row-inner-wrap')) all_games_list_count = len(soup.select('div.giveaway__row-inner-wrap'))
@ -202,17 +181,17 @@ class EnterGiveaways:
break break
for item in unentered_game_list: for item in unentered_game_list:
giveaway = GiveawayEntry(item) giveaway = Giveaway(item)
txt = f"{giveaway.game_name} - {giveaway.cost}P - {giveaway.game_entries} entries " \ txt = f"{giveaway.game_name} - {giveaway.cost}P - {giveaway.game_entries} entries " \
f"(w/ {giveaway.copies} copies) - Created {giveaway.time_created_string} ago " \ f"(w/ {giveaway.copies} copies) - Created {giveaway.time_created_string} ago " \
f"with {giveaway.time_remaining_string} remaining by {giveaway.user}." f"with {giveaway.time_remaining_string} remaining by {giveaway.user}."
logger.info(txt) logger.info(txt)
if giveaway.pinned and not self._pinned: if giveaway.pinned and not self.pinned:
logger.info(f"〰️ Giveaway {giveaway.game_name} is pinned. Ignoring.") logger.info(f"〰️ Giveaway {giveaway.game_name} is pinned. Ignoring.")
continue continue
if self._points == 0 or self._points < self._min_points: if self.points == 0 or self.points < self.min_points:
txt = f"🟡 We have {self._points} points, but we need {self._min_points} to start." txt = f"🟡 We have {self.points} points, but we need {self.min_points} to start."
logger.info(txt) logger.info(txt)
run = False run = False
break break
@ -220,27 +199,37 @@ class EnterGiveaways:
if not giveaway.cost: if not giveaway.cost:
logger.error(f"Cost could not be determined for '{giveaway.game_name}'") logger.error(f"Cost could not be determined for '{giveaway.game_name}'")
continue continue
if_enter_giveaway = self._should_we_enter_giveaway(giveaway) if_enter_giveaway = self.should_we_enter_giveaway(giveaway)
if if_enter_giveaway: if if_enter_giveaway:
res = self._enter_giveaway(giveaway) res = self.enter_giveaway(giveaway)
if res: if res:
GiveawayHelper.upsert_giveaway_with_details(giveaway, True, False) GiveawayHelper.upsert_giveaway(giveaway, True)
self._points -= int(giveaway.cost) self.points -= int(giveaway.cost)
txt = f"✅ Entered giveaway '{giveaway.game_name}'" txt = f"✅ Entered giveaway '{giveaway.game_name}'"
logger.info(txt) logger.info(txt)
sleep(randint(4, 15)) sleep(randint(4, 15))
else: else:
GiveawayHelper.upsert_giveaway_with_details(giveaway, False, False) GiveawayHelper.upsert_giveaway(giveaway, False)
else: else:
GiveawayHelper.upsert_giveaway(giveaway) GiveawayHelper.upsert_giveaway(giveaway, False)
# if we are on any filter type except New and we get to a giveaway that exceeds our # if we are on any filter type except New and we get to a giveaway that exceeds our
# max time left amount, then we don't need to continue to look at giveaways as any # max time left amount, then we don't need to continue to look at giveaways as any
# after this point will also exceed the max time left # after this point will also exceed the max time left
if self._gifts_type != "New" and not giveaway.pinned and \ if self.gifts_type != "New" and not giveaway.pinned and \
giveaway.time_remaining_in_minutes > self._max_time_left and \ giveaway.time_remaining_in_minutes > self.max_time_left:
self._points < self._max_points:
logger.info("🟡 We have run out of gifts to consider.") logger.info("🟡 We have run out of gifts to consider.")
run = False run = False
break break
n = n + 1 n = n + 1
def start(self):
self.update_info()
if self.points >= self.min_points:
txt = f"〰 You have {self.points} points. Evaluating '{self.gifts_type}' giveaways..."
logger.info(txt)
self.evaluate_giveaways()
else:
txt = f"🟡 You have {self.points} points which is below your minimum point threshold of " \
f"{self.min_points} points for '{self.gifts_type}' giveaways. Not evaluating right now."
logger.info(txt)

View file

@ -1,86 +0,0 @@
import requests
from bs4 import BeautifulSoup
from requests.adapters import HTTPAdapter
from urllib3.util import Retry
from .database import GiveawayHelper
from .log import get_logger
from .won_entry import WonEntry
logger = get_logger(__name__)
class SteamGiftsException(Exception):
pass
class EvaluateWonGiveaways:
def __init__(self, cookie, user_agent, notification):
self._contributor_level = None
self._xsrf_token = None
self._cookie = {
'PHPSESSID': cookie
}
self._user_agent = user_agent
self._notification = notification
self._base = "https://www.steamgifts.com/giveaways/won"
self._session = requests.Session()
def start(self):
self._evaluate_won_giveaways()
def _requests_retry_session(
self,
retries=5,
backoff_factor=0.3
):
session = self._session or requests.Session()
retry = Retry(
total=retries,
read=retries,
connect=retries,
backoff_factor=backoff_factor,
status_forcelist=(500, 502, 504),
)
adapter = HTTPAdapter(max_retries=retry)
session.mount('http://', adapter)
session.mount('https://', adapter)
return session
def _get_soup_from_page(self, url):
headers = {
'User-Agent': self._user_agent
}
self._requests_retry_session().get(url, headers=headers)
r = requests.get(url, cookies=self._cookie)
soup = BeautifulSoup(r.text, 'html.parser')
return soup
def _evaluate_won_giveaways(self, page=1):
soup = self._get_soup_from_page(self._base)
try:
self._xsrf_token = soup.find('input', {'name': 'xsrf_token'})['value']
self._points = int(soup.find('span', {'class': 'nav__points'}).text) # storage points
self._contributor_level = int(float(soup.select_one('nav a>span[title]')['title']))
except TypeError:
logger.error("⛔⛔⛔ Cookie is not valid. A new one must be added.⛔⛔⛔")
raise SteamGiftsException("Cookie is not valid. A new one must be added.")
won_game_list = soup.select('div[class=table__row-inner-wrap]')
if not len(won_game_list):
txt = f"🟡 No won games to evaluate"
logger.info(txt)
for item in won_game_list:
won_giveaway = WonEntry(item)
w = GiveawayHelper.get_by_giveaway_id(won_giveaway.giveaway_game_id)
logger.debug(f"Giveaway in db: {w}")
if w and not w.won and w.entered:
logger.info(f"Marking {won_giveaway.game_name} as won.")
logger.debug(f"Marking: {w}")
GiveawayHelper.mark_game_as_won(won_giveaway.giveaway_game_id)

View file

@ -5,7 +5,7 @@ import time
logger = get_logger(__name__) logger = get_logger(__name__)
class GiveawayEntry: class Giveaway:
def __init__(self, soup_item): def __init__(self, soup_item):
self.steam_app_id = None self.steam_app_id = None
@ -26,30 +26,29 @@ class GiveawayEntry:
self.time_created_string = None self.time_created_string = None
self.time_created_in_minutes = None self.time_created_in_minutes = None
logger.debug(f"Giveaway html: {soup_item}")
icons = soup_item.select('a.giveaway__icon') icons = soup_item.select('a.giveaway__icon')
self.steam_url = icons[0]['href'] self.steam_url = icons[0]['href']
self.steam_app_id = self._get_steam_app_id(self.steam_url) self.steam_app_id = self.get_steam_app_id(self.steam_url)
self.game_name = soup_item.find('a', {'class': 'giveaway__heading__name'}).text self.game_name = soup_item.find('a', {'class': 'giveaway__heading__name'}).text
self.giveaway_game_id = soup_item.find('a', {'class': 'giveaway__heading__name'})['href'].split('/')[2] self.giveaway_game_id = soup_item.find('a', {'class': 'giveaway__heading__name'})['href'].split('/')[2]
self.giveaway_uri = soup_item.select_one('a.giveaway__heading__name')['href'] self.giveaway_uri = soup_item.select_one('a.giveaway__heading__name')['href']
pin_class = soup_item.parent.parent.get("class") pin_class = soup_item.parent.parent.get("class")
self.pinned = pin_class is not None and len(pin_class) > 0 and pin_class[0].find('pinned') != -1 self.pinned = pin_class is not None and len(pin_class) > 0 and pin_class[0].find('pinned') != -1
self.cost, self.copies = self._determine_cost_and_copies(soup_item, self.game_name, self.giveaway_game_id) self.cost, self.copies = self.determine_cost_and_copies(soup_item, self.game_name, self.giveaway_game_id)
self.game_entries = int(soup_item.select('div.giveaway__links span')[0].text.split(' ')[0].replace(',', '')) self.game_entries = int(soup_item.select('div.giveaway__links span')[0].text.split(' ')[0].replace(',', ''))
contributor_level = soup_item.select_one('div[title="Contributor Level"]') contributor_level = soup_item.select_one('div[title="Contributor Level"]')
self.contributor_level = self._determine_contributor_level(contributor_level) self.contributor_level = self.determine_contributor_level(contributor_level)
self.user = soup_item.select_one('a.giveaway__username').text self.user = soup_item.select_one('a.giveaway__username').text
times = soup_item.select('div span[data-timestamp]') times = soup_item.select('div span[data-timestamp]')
self.time_remaining_timestamp = int(times[0]['data-timestamp']) self.time_remaining_timestamp = int(times[0]['data-timestamp'])
self.time_remaining_string = times[0].text self.time_remaining_string = times[0].text
self.time_remaining_in_minutes = self._determine_time_in_minutes(times[0]['data-timestamp']) self.time_remaining_in_minutes = self.determine_time_in_minutes(times[0]['data-timestamp'])
self.time_created_timestamp = int(times[1]['data-timestamp']) self.time_created_timestamp = int(times[1]['data-timestamp'])
self.time_created_string = times[1].text self.time_created_string = times[1].text
self.time_created_in_minutes = self._determine_time_in_minutes(times[1]['data-timestamp']) self.time_created_in_minutes = self.determine_time_in_minutes(times[1]['data-timestamp'])
logger.debug(f"Scraped Giveaway: {self}") logger.debug(f"Scraped Giveaway: {self}")
def _determine_contributor_level(self, contributor_level): def determine_contributor_level(self, contributor_level):
if contributor_level is None: if contributor_level is None:
return 0 return 0
match = re.search('^Level (?P<level>[0-9]+)\\+$', contributor_level.text, re.IGNORECASE) match = re.search('^Level (?P<level>[0-9]+)\\+$', contributor_level.text, re.IGNORECASE)
@ -58,14 +57,14 @@ class GiveawayEntry:
else: else:
return None return None
def _get_steam_app_id(self, steam_url): def get_steam_app_id(self, steam_url):
match = re.search('^.+/[a-z0-9]+/(?P<steam_app_id>[0-9]+)(/|\?)', steam_url, re.IGNORECASE) match = re.search('^.+/[a-z0-9]+/(?P<steam_app_id>[0-9]+)/$', steam_url, re.IGNORECASE)
if match: if match:
return match.group('steam_app_id') return match.group('steam_app_id')
else: else:
return None return None
def _determine_time_in_minutes(self, timestamp): def determine_time_in_minutes(self, timestamp):
if not timestamp or not re.search('^[0-9]+$', timestamp): if not timestamp or not re.search('^[0-9]+$', timestamp):
logger.error(f"Could not determine time from string {timestamp}") logger.error(f"Could not determine time from string {timestamp}")
return None return None
@ -73,7 +72,7 @@ class GiveawayEntry:
giveaway_endtime = time.localtime(int(timestamp)) giveaway_endtime = time.localtime(int(timestamp))
return int(abs((time.mktime(giveaway_endtime) - time.mktime(now)) / 60)) return int(abs((time.mktime(giveaway_endtime) - time.mktime(now)) / 60))
def _determine_cost_and_copies(self, item, game_name, game_id): def determine_cost_and_copies(self, item, game_name, game_id):
item_headers = item.find_all('span', {'class': 'giveaway__heading__thin'}) item_headers = item.find_all('span', {'class': 'giveaway__heading__thin'})
if len(item_headers) == 1: # then no multiple copies if len(item_headers) == 1: # then no multiple copies
game_cost = item_headers[0].getText().replace('(', '').replace(')', '').replace('P', '') game_cost = item_headers[0].getText().replace('(', '').replace(')', '').replace('P', '')

View file

@ -1,13 +1,14 @@
import datetime import datetime
import threading import threading
from datetime import timedelta, datetime from datetime import timedelta, datetime
from random import randint
from threading import Thread from threading import Thread
from time import sleep from time import sleep
from .enter_giveaways import EnterGiveaways from dateutil import tz
from .evaluate_won_giveaways import EvaluateWonGiveaways
from .log import get_logger from .log import get_logger
from .scheduler import Scheduler from .enter_giveaways import EnterGiveaways
logger = get_logger(__name__) logger = get_logger(__name__)
@ -19,87 +20,52 @@ class GiveawayThread(threading.Thread):
self.exc = None self.exc = None
self.config = config self.config = config
self.notification = notification self.notification = notification
logger.debug("Creating scheduler")
self._scheduler = Scheduler()
self.won_giveaway_job_id = 'eval_won_giveaways'
self.evaluate_giveaway_job_id = 'eval_giveaways'
self._all_page = None
self._wishlist_page = None
self._dlc_page = None
def run_steam_gifts(self, config, notification):
cookie = config['DEFAULT'].get('cookie') cookie = config['DEFAULT'].get('cookie')
user_agent = config['DEFAULT'].get('user_agent') user_agent = config['DEFAULT'].get('user_agent')
main_page_enabled = config['DEFAULT'].getboolean('enabled')
minimum_points = config['DEFAULT'].getint('minimum_points')
max_entries = config['DEFAULT'].getint('max_entries')
max_time_left = config['DEFAULT'].getint('max_time_left')
minimum_game_points = config['DEFAULT'].getint('minimum_game_points')
blacklist = config['DEFAULT'].get('blacklist_keywords')
if config['ALL'].getboolean('all.enabled'): all_page = EnterGiveaways(cookie, user_agent, 'All', False, minimum_points, max_entries,
all_minimum_points = config['ALL'].getint('all.minimum_points') max_time_left, minimum_game_points, blacklist, notification)
all_max_entries = config['ALL'].getint('all.max_entries')
all_max_time_left = config['ALL'].getint('all.max_time_left')
all_max_points = config['ALL'].getint('all.max_points')
all_minimum_game_points = config['DEFAULT'].getint('minimum_game_points')
all_blacklist = config['DEFAULT'].get('blacklist_keywords')
self._all_page = EnterGiveaways(cookie, user_agent, 'All', False, all_minimum_points, all_max_entries, wishlist_page_enabled = config['WISHLIST'].getboolean('wishlist.enabled')
all_max_time_left, all_max_points, all_minimum_game_points, all_blacklist, notification)
if config['WISHLIST'].getboolean('wishlist.enabled'):
wishlist_minimum_points = config['WISHLIST'].getint('wishlist.minimum_points') wishlist_minimum_points = config['WISHLIST'].getint('wishlist.minimum_points')
wishlist_max_entries = config['WISHLIST'].getint('wishlist.max_entries') wishlist_max_entries = config['WISHLIST'].getint('wishlist.max_entries')
wishlist_max_time_left = config['WISHLIST'].getint('wishlist.max_time_left') wishlist_max_time_left = config['WISHLIST'].getint('wishlist.max_time_left')
wishlist_max_points = config['WISHLIST'].getint('wishlist.max_points')
self._wishlist_page = EnterGiveaways(cookie, user_agent, 'Wishlist', False, wishlist_minimum_points, wishlist_page = EnterGiveaways(cookie, user_agent, 'Wishlist', False, wishlist_minimum_points,
wishlist_max_entries, wishlist_max_time_left, wishlist_max_points, 0, '', notification) wishlist_max_entries, wishlist_max_time_left, 0, '', notification)
if config['DLC'].getboolean('dlc.enabled'): if not main_page_enabled and not wishlist_page_enabled:
dlc_minimum_points = config['DLC'].getint('dlc.minimum_points') logger.error("⁉️ Both 'Default' and 'Wishlist' configurations are disabled. Nothing will run. Exiting...")
dlc_max_entries = config['DLC'].getint('dlc.max_entries')
dlc_max_time_left = config['DLC'].getint('dlc.max_time_left')
dlc_max_points = config['DLC'].getint('dlc.max_points')
self._dlc_page = EnterGiveaways(cookie, user_agent, 'DLC', False, dlc_minimum_points,
dlc_max_entries, dlc_max_time_left, dlc_max_points, 0, '', notification)
if not self._all_page and not self._wishlist_page and not self._dlc_page :
logger.error("⁉️ 'All', 'Wishlist' and 'DLC' configurations are disabled. Nothing will run. Exiting...")
sleep(10) sleep(10)
exit(-1) exit(-1)
won_giveaway_job = self._scheduler.get_job(job_id=self.won_giveaway_job_id) while True:
if won_giveaway_job: logger.info("🟢 Evaluating giveaways.")
logger.debug("Previous won giveaway evaluator job exists. Removing.") if wishlist_page_enabled:
won_giveaway_job.remove() wishlist_page.start()
won_runner = GiveawayThread.WonRunner(EvaluateWonGiveaways(cookie, user_agent, notification), if main_page_enabled:
self.won_giveaway_job_id) all_page.start()
self._scheduler.add_job(won_runner.run,
id=self.won_giveaway_job_id,
trigger='interval',
max_instances=1,
replace_existing=True,
hours=12,
next_run_time=datetime.now() + timedelta(minutes=5),
jitter=8000)
evaluate_giveaway_job = self._scheduler.get_job(job_id=self.evaluate_giveaway_job_id) logger.info("🔴 All giveaways evaluated.")
if evaluate_giveaway_job: random_seconds = randint(1740, 3540) # sometime between 29-59 minutes
logger.debug("Previous giveaway evaluator job exists. Removing.") when_to_start_again = datetime.now(tz=tz.tzlocal()) + timedelta(seconds=random_seconds)
evaluate_giveaway_job.remove() logger.info(f"🛋 Going to sleep for {random_seconds / 60} minutes. "
runner = GiveawayThread.GiveawayRunner(self._dlc_page, self._wishlist_page, self._all_page, f"Will start again at {when_to_start_again}")
self.evaluate_giveaway_job_id) sleep(random_seconds)
self._scheduler.add_job(runner.run,
id=self.evaluate_giveaway_job_id,
trigger='interval',
max_instances=1,
replace_existing=True,
minutes=44,
next_run_time=datetime.now(),
jitter=900)
def run(self): def run(self):
# Variable that stores the exception, if raised by someFunction # Variable that stores the exception, if raised by someFunction
self.exc = None self.exc = None
try: try:
self._scheduler.start() self.run_steam_gifts(self.config, self.notification)
except BaseException as e: except BaseException as e:
self.exc = e self.exc = e
@ -110,46 +76,3 @@ class GiveawayThread(threading.Thread):
# if any was caught # if any was caught
if self.exc: if self.exc:
raise self.exc raise self.exc
class WonRunner:
def __init__(self, won_page, job_id):
self._won_page = won_page
self._job_id = job_id
def run(self):
logger.info(" 💍 Evaluating won giveaways. 💍")
if self._won_page:
self._won_page.start()
logger.info(" 💍 All won giveaways evaluated. 💍")
scheduler = Scheduler()
evaluate_giveaway_job = scheduler.get_job(job_id=self._job_id)
if evaluate_giveaway_job:
when_to_start_again = evaluate_giveaway_job.next_run_time
logger.info(f"💍 Going to sleep. Will start again at {when_to_start_again}")
else:
logger.info("No set time to evaluate won giveaways again.")
class GiveawayRunner:
def __init__(self, dlc_page, wishlist_page, all_page, job_id):
self._dlc_page = dlc_page
self._wishlist_page = wishlist_page
self._all_page = all_page
self._job_id = job_id
def run(self):
logger.info("🟢 Evaluating giveaways.")
if self._dlc_page:
self._dlc_page.start()
if self._wishlist_page:
self._wishlist_page.start()
if self._all_page:
self._all_page.start()
logger.info("🔴 All giveaways evaluated.")
scheduler = Scheduler()
evaluate_giveaway_job = scheduler.get_job(job_id=self._job_id)
if evaluate_giveaway_job:
when_to_start_again = evaluate_giveaway_job.next_run_time
logger.info(f"🛋 Going to sleep. Will start again at {when_to_start_again}")
else:
logger.info("No set time to evaluate giveaways again.")

View file

@ -4,20 +4,20 @@ from logging.handlers import RotatingFileHandler
# log info level logs to stdout and debug to debug file # log info level logs to stdout and debug to debug file
debug_log_format = "%(levelname)s %(asctime)s - [%(filename)s:%(lineno)d] - %(message)s" log_format = "%(levelname)s %(asctime)s - %(message)s"
logging.basicConfig( logging.basicConfig(
handlers=[RotatingFileHandler(f"{os.getenv('BOT_CONFIG_DIR', './config')}/debug.log", maxBytes=500000, backupCount=10)], handlers=[RotatingFileHandler(f"{os.getenv('BOT_CONFIG_DIR', './config')}/debug.log", maxBytes=500000, backupCount=10)],
level=logging.DEBUG, level=logging.DEBUG,
format=debug_log_format) format=log_format)
console_output = logging.StreamHandler() console_output = logging.StreamHandler()
console_output.setLevel(logging.INFO) console_output.setLevel(logging.INFO)
console_format = logging.Formatter(debug_log_format) console_format = logging.Formatter(log_format)
console_output.setFormatter(console_format) console_output.setFormatter(console_format)
info_log_file = RotatingFileHandler(f"{os.getenv('BOT_CONFIG_DIR', './config')}/info.log", maxBytes=100000, backupCount=10) info_log_file = RotatingFileHandler(f"{os.getenv('BOT_CONFIG_DIR', './config')}/info.log", maxBytes=100000, backupCount=10)
info_log_file.setLevel(logging.INFO) info_log_file.setLevel(logging.INFO)
info_log_format = logging.Formatter(debug_log_format) info_log_format = logging.Formatter(log_format)
info_log_file.setFormatter(info_log_format) info_log_file.setFormatter(info_log_format)
logging.root.addHandler(console_output) logging.root.addHandler(console_output)

View file

@ -13,15 +13,11 @@ class TableNotification(Base):
message = Column(String(300), nullable=False) message = Column(String(300), nullable=False)
medium = Column(String(50), nullable=False) medium = Column(String(50), nullable=False)
success = Column(Boolean, nullable=False) success = Column(Boolean, nullable=False)
games_won = Column(Integer, nullable=True)
created_at = Column(DateTime(timezone=True), server_default=func.now()) created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
__mapper_args__ = {"eager_defaults": True} __mapper_args__ = {"eager_defaults": True}
def __str__(self):
return str(self.__class__) + ": " + str(self.__dict__)
class TableSteamItem(Base): class TableSteamItem(Base):
__tablename__ = 'steam_item' __tablename__ = 'steam_item'
@ -33,9 +29,6 @@ class TableSteamItem(Base):
giveaways = relationship("TableGiveaway", back_populates="steam_item") giveaways = relationship("TableGiveaway", back_populates="steam_item")
def __str__(self):
return str(self.__class__) + ": " + str(self.__dict__)
class TableGiveaway(Base): class TableGiveaway(Base):
__tablename__ = 'giveaway' __tablename__ = 'giveaway'
@ -57,6 +50,3 @@ class TableGiveaway(Base):
steam_item = relationship("TableSteamItem", back_populates="giveaways") steam_item = relationship("TableSteamItem", back_populates="giveaways")
__mapper_args__ = {"eager_defaults": True} __mapper_args__ = {"eager_defaults": True}
def __str__(self):
return str(self.__class__) + ": " + str(self.__dict__)

View file

@ -15,11 +15,11 @@ class Notification:
self.pushover_user_key = None self.pushover_user_key = None
self.message_prefix = f"{message_prefix}: " self.message_prefix = f"{message_prefix}: "
def send_won(self, message, number_won): def send_won(self, message):
self.__send('won', message, number_won) self.__send('won', message)
def send_error(self, message): def send_error(self, message):
self.__send('error', message, number_won=None) self.__send('error', message)
def enable_pushover(self, token, user_key): def enable_pushover(self, token, user_key):
logger.debug("Enabling pushover notifications.") logger.debug("Enabling pushover notifications.")
@ -27,13 +27,13 @@ class Notification:
self.pushover_token = token self.pushover_token = token
self.pushover_user_key = user_key self.pushover_user_key = user_key
def __send(self, type_of_error, message, number_won=None): def __send(self, type_of_error, message):
logger.debug(f"Attempting to notify: '{message}'. Won: {number_won}") logger.debug(f"Attempting to notify: {message}")
if self.pushover: if self.pushover:
logger.debug("Pushover enabled. Sending message.") logger.debug("Pushover enabled. Sending message.")
self.__pushover(type_of_error, message, number_won) self.__pushover(type_of_error, message)
def __pushover(self, type_of_error, message, number_won=None): def __pushover(self, type_of_error, message):
conn = http.client.HTTPSConnection("api.pushover.net:443") conn = http.client.HTTPSConnection("api.pushover.net:443")
conn.request("POST", "/1/messages.json", conn.request("POST", "/1/messages.json",
urllib.parse.urlencode({ urllib.parse.urlencode({
@ -48,4 +48,4 @@ class Notification:
else: else:
logger.error(f"Pushover notification failed. Code {response.getcode()}: {response.read().decode()}") logger.error(f"Pushover notification failed. Code {response.getcode()}: {response.read().decode()}")
success = False success = False
NotificationHelper.insert(type_of_error, f"{message}", 'pushover', success, number_won) NotificationHelper.insert(type_of_error, f"{message}", 'pushover', success)

View file

@ -1,13 +1,13 @@
import os import os
from time import sleep from time import sleep
from src.bot.log import get_logger from .log import get_logger
from src.bot.config_reader import ConfigReader, ConfigException from .config_reader import ConfigReader, ConfigException
from src.bot.enter_giveaways import SteamGiftsException from .enter_giveaways import SteamGiftsException
from src.bot.giveaway_thread import GiveawayThread from .giveaway_thread import GiveawayThread
from src.bot.notification import Notification from .notification import Notification
from src.bot.database import run_db_migrations from .database import run_migrations, create_engine
from src.web.webserver_thread import WebServerThread from .webserver_thread import WebServerThread
logger = get_logger(__name__) logger = get_logger(__name__)
config_file_name = f"{os.getenv('BOT_CONFIG_DIR', './config')}/config.ini" config_file_name = f"{os.getenv('BOT_CONFIG_DIR', './config')}/config.ini"
@ -73,9 +73,9 @@ def entry():
|___/ |___/
------------------------------------------------------------------------------------- -------------------------------------------------------------------------------------
""") """)
run_db_migrations(alembic_migration_files, db_url) run_migrations(alembic_migration_files, db_url)
create_engine(db_url)
run() run()
if __name__ == '__main__': if __name__ == '__main__':
entry() entry()

View file

@ -1,33 +0,0 @@
import os
from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore
from apscheduler.schedulers.blocking import BlockingScheduler
from .log import get_logger
logger = get_logger(__name__)
class Scheduler:
class __Scheduler:
def __init__(self):
jobstores = {
'default': SQLAlchemyJobStore(url=f"{os.getenv('BOT_DB_URL', 'sqlite:///./config/sqlite.db')}")
}
job_defaults = {
'coalesce': True,
'max_instances': 3
}
self.scheduler = BlockingScheduler(jobstores=jobstores, job_defaults=job_defaults)
def __getattr__(self, name):
return getattr(self.scheduler, name)
instance = None
def __init__(self):
if not Scheduler.instance:
Scheduler.instance = Scheduler.__Scheduler()
def __getattr__(self, name):
return getattr(self.instance, name)

View file

@ -74,33 +74,3 @@ h3 {
color: #444; color: #444;
text-decoration: none; text-decoration: none;
} }
.styled-table {
border-collapse: collapse;
margin: 25px 0;
font-size: 0.9em;
font-family: sans-serif;
min-width: 400px;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.15);
}
.styled-table thead tr {
background-color: #009879;
color: #ffffff;
text-align: left;
}
.styled-table th,
.styled-table td {
padding: 12px 15px;
}
.styled-table tbody tr {
border-bottom: 1px solid #dddddd;
}
.styled-table tbody tr:nth-of-type(even) {
background-color: #f3f3f3;
}
.styled-table tbody tr:last-of-type {
border-bottom: 2px solid #009879;
}
.styled-table tbody tr.active-row {
font-weight: bold;
color: #009879;
}

View file

@ -1,7 +1,7 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<title>{% block title %} {% endblock %}</title> <title>Steamgifts Bot Configuration</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/main.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/main.css') }}">
</head> </head>
<body> <body>
@ -11,17 +11,13 @@
<strong><nav> <strong><nav>
<ul class="menu"> <ul class="menu">
<li><a href="{{ url_for('config') }}">Config</a></li> <li><a href="{{ url_for('config') }}">Config</a></li>
<li><a href="{{ url_for('stats') }}">Stats</a></li> <li><a href="{{ url_for('logs') }}">Logs</a></li>
<li><a href="{{ url_for('log_info') }}">Info Logs</a></li>
<li><a href="{{ url_for('log_debug') }}">Debug Logs</a></li>
<li><a href="{{ url_for('db_giveaways') }}">Giveaways</a></li>
<li><a href="{{ url_for('db_notifications') }}">Notifications</a></li>
</ul> </ul>
</nav></strong> </nav></strong>
</div> </div>
</header> </header>
<div class="container"> <div class="container">
{% block content %} {% endblock %} <pre>{{content}}</pre>
</div> </div>
</body> </body>
</html> </html>

View file

@ -0,0 +1,23 @@
<!DOCTYPE html>
<html>
<head>
<title>Steamgifts Bot Logs</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/main.css') }}">
</head>
<body>
<header>
<div class="container">
<h1 class="logo">Steamgifts Bot</h1>
<strong><nav>
<ul class="menu">
<li><a href="{{ url_for('config') }}">Config</a></li>
<li><a href="{{ url_for('logs') }}">Logs</a></li>
</ul>
</nav></strong>
</div>
</header>
<div class="container">
<pre id="output">{{content}}</pre>
</div>
</body>
</html>

View file

@ -1,11 +1,9 @@
import os
import threading import threading
from threading import Thread from threading import Thread
from flask_basicauth import BasicAuth from flask_basicauth import BasicAuth
from src.bot.database import NotificationHelper, GiveawayHelper from .log import get_logger
from src.bot.log import get_logger
logger = get_logger(__name__) logger = get_logger(__name__)
@ -16,7 +14,6 @@ class WebServerThread(threading.Thread):
Thread.__init__(self) Thread.__init__(self)
self.exc = None self.exc = None
self.config = config self.config = config
self.prefix = config['NOTIFICATIONS'].get('notification.prefix')
self.host = config['WEB'].get('web.host') self.host = config['WEB'].get('web.host')
self.port = config['WEB'].getint('web.port') self.port = config['WEB'].getint('web.port')
self.ssl = config['WEB'].getboolean('web.ssl') self.ssl = config['WEB'].getboolean('web.ssl')
@ -41,42 +38,20 @@ class WebServerThread(threading.Thread):
@app.route(f"{self.app_root}") @app.route(f"{self.app_root}")
def config(): def config():
with open(f"{os.getenv('BOT_CONFIG_DIR', './config')}/config.ini", 'r') as f: with open('../config/config.ini', 'r') as f:
data = f.read() content = f.read()
return render_template('configuration.html', name=self.prefix, data=data) return render_template('configuration.html', content=content)
@app.route(f"{self.app_root}log_info") @app.route(f"{self.app_root}log")
def log_info(): def logs():
with open(f"{os.getenv('BOT_CONFIG_DIR', './config')}/info.log", 'r') as f: with open('../config/info.log', 'r') as f:
data = f.read() content = f.read()
return render_template('log.html', name=self.prefix, log_type='info', data=data) return render_template('log.html', content=content)
@app.route(f"{self.app_root}log_debug")
def log_debug():
with open(f"{os.getenv('BOT_CONFIG_DIR', './config')}/debug.log", 'r') as f:
data = f.read()
return render_template('log.html', name=self.prefix, log_type='debug', data=data)
@app.route(f"{self.app_root}alive") @app.route(f"{self.app_root}alive")
def alive(): def alive():
return 'OK' return 'OK'
@app.route(f"{self.app_root}notifications")
def db_notifications():
return render_template('notifications.html', name=self.prefix, data=NotificationHelper.get())
@app.route(f"{self.app_root}giveaways", methods=['GET'], defaults={"page": 1})
@app.route(f"{self.app_root}giveaways/<int:page>", methods=['GET'])
def db_giveaways(page):
return render_template('giveaways.html', name=self.prefix, data=GiveawayHelper.paginate(page=page))
@app.route(f"{self.app_root}stats")
def stats():
totals = GiveawayHelper.total_giveaways()
entered = GiveawayHelper.total_entered()
won = GiveawayHelper.total_won()
return render_template('stats.html', name=self.prefix, totals=totals, entered=entered, won=won)
if self.enabled: if self.enabled:
logger.info("Webserver Enabled. Running") logger.info("Webserver Enabled. Running")
if self.ssl: if self.ssl:

View file

@ -1,22 +0,0 @@
import re
from .log import get_logger
import time
logger = get_logger(__name__)
class WonEntry:
def __init__(self, soup_item):
self.game_name = None
self.giveaway_game_id = None
self.giveaway_uri = None
logger.debug(f"Won Giveaway html: {soup_item}")
self.game_name = soup_item.find('a', {'class': 'table__column__heading'}).text
self.giveaway_game_id = soup_item.find('a', {'class': 'table__column__heading'})['href'].split('/')[2]
self.giveaway_uri = soup_item.select_one('a.table__column__heading')['href']
logger.debug(f"Scraped Won Giveaway: {self}")
def __str__(self):
return str(self.__class__) + ": " + str(self.__dict__)

View file

@ -1,6 +0,0 @@
{% extends 'base.html' %}
{% block title %} {{name}} Steamgifts Bot Configuration {% endblock %}
{% block content %}
<pre>{{data}}</pre>
{% endblock %}

View file

@ -1,53 +0,0 @@
{% extends 'base.html' %}
{% block title %} {{name}} Steamgifts Bot Giveaways {% endblock %}
{% block content %}
<ul class="pagination">
{% if data.previous_page %}
<li class="page-item"> <a class="page-link" href="{{ url_for('db_giveaways', page=data.previous_page) }}">Previous</a></li>
{% else %}
<li class="page-item">Previous</li>
{% endif %}
{% if data.next_page %}
<li class="page-item"> <a class="page-link" href="{{ url_for('db_giveaways', page=data.next_page) }}">Next</a></li>
{% else %}
<li class="page-item">Next</li>
{% endif %}
</ul>
<table class="styled-table">
<thead>
<tr>
<th>Giveaway ID</th>
<th>Steam ID</th>
<th>Giveaway URI</th>
<th>Name</th>
<th>Steam URL</th>
<th>User</th>
<th>Giveaway Created At</th>
<th>Giveaway Ended At</th>
<th>Cost</th>
<th>Copies</th>
<th>Contributor Level</th>
<th>Entered</th>
</tr>
</thead>
<tbody>
{% for row in data.items %}
<tr>
<td>{{row.giveaway_id}}</td>
<td>{{row.steam_id}}</td>
<td>{{row.giveaway_uri}}</td>
<td>{{row.steam_item.game_name}}</td>
<td>{{row.steam_item.steam_url}}</td>
<td>{{row.user}}</td>
<td>{{row.giveaway_created_at}}</td>
<td>{{row.giveaway_ended_at}}</td>
<td>{{row.cost}}</td>
<td>{{row.copies}}</td>
<td>{{row.contributor_level}}</td>
<td>{{row.entered}}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}

View file

@ -1,6 +0,0 @@
{% extends 'base.html' %}
{% block title %} {{name}} Steamgifts Bot {{log_type}} Logs{% endblock %}
{% block content %}
<pre id="output">{{data}}</pre>
{% endblock %}

View file

@ -1,29 +0,0 @@
{% extends 'base.html' %}
{% block title %} {{name}} Steamgifts Bot Notifications {% endblock %}
{% block content %}
<table>
<thead>
<tr>
<th>ID</th>
<th>Medium</th>
<th>Type</th>
<th>Success</th>
<th>Created At</th>
<th>Message</th>
</tr>
</thead>
<tbody>
{% for row in data %}
<tr>
<td>{{row.id}}</td>
<td>{{row.medium}}</td>
<td>{{row.type}}</td>
<td>{{row.success}}</td>
<td>{{row.created_at}}</td>
<td>{{row.message}}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}

View file

@ -1,10 +0,0 @@
{% extends 'base.html' %}
{% block title %} {{name}} Steamgifts Bot Configuration {% endblock %}
{% block content %}
<ul>
<li>Total Giveaways Considered: {{totals}}</li>
<li>Giveaways Entered: {{entered}}</li>
<li>Giveaways Won (with the bot): {{won}}</li>
</ul>
{% endblock %}