Compare commits

...

10 Commits

Author SHA1 Message Date
mcinj cc1cbc08f4 fix notifications page 2022-06-14 20:58:46 -04:00
mcinj 66a812ddd6 separate out won job function 2022-06-14 20:49:56 -04:00
mcinj 8dc2721efe scheduling of the main giveaway code 2022-06-10 15:48:27 -04:00
mcinj 113e111e9d woops 2022-06-08 22:03:04 -04:00
mcinj 48c030f445 win scraping
- will scape won games at start to update db for giveaways the bot has entered
- schedules a once a day run of scraping won giveaways
2022-06-08 21:55:17 -04:00
mcinj de2379587e won
- adding back ability to mark giveaway as won. no use just yet
2022-06-05 13:29:27 -04:00
mcinj 8010bbd750 stats 2022-06-01 07:58:06 -04:00
mcinj f3e6c165f4 clean up html template 2022-05-26 17:03:58 -04:00
mcinj 7a8d2fe793 stateful notifications 2022-05-24 09:15:25 -04:00
mcinj e7a4e90988 prettify (but still ugly) the giveaway table 2022-05-24 08:27:54 -04:00
20 changed files with 614 additions and 265 deletions

View File

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

View File

@ -0,0 +1,27 @@
"""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

@ -0,0 +1,31 @@
"""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

@ -1,11 +1,12 @@
import os
from datetime import datetime, timedelta
import paginate_sqlalchemy
import sqlalchemy
from alembic import command
from alembic.config import Config
from dateutil import tz
from sqlalchemy import func
from sqlalchemy import func, select
from sqlalchemy.orm import Session, joinedload
from sqlalchemy_utils import database_exists
@ -24,7 +25,7 @@ def run_db_migrations(script_location: str, db_url: str) -> None:
alembic_cfg.set_main_option('sqlalchemy.url', 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')
elif database_exists(db_url):
logger.debug(f"'{db_url}' exists.")
@ -50,9 +51,10 @@ class NotificationHelper:
return session.query(TableNotification).order_by(TableNotification.created_at.desc()).all()
@classmethod
def insert(cls, type_of_error, message, medium, success):
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)
n = TableNotification(type=type_of_error, message=message, medium=medium, success=success,
games_won=number_won)
session.add(n)
session.commit()
@ -64,7 +66,7 @@ class NotificationHelper:
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_by(type='won').all()
.filter_by(type='won').order_by(TableNotification.created_at.asc()).all()
actual = []
for r in within_3_days:
if r.created_at.replace(tzinfo=tz.tzutc()).astimezone(tz.tzlocal()).date() == datetime.now(
@ -92,7 +94,37 @@ 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()
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
def unix_timestamp_to_utc_datetime(cls, timestamp):
@ -105,7 +137,18 @@ class GiveawayHelper:
steam_id=giveaway.steam_app_id).all()
@classmethod
def insert(cls, giveaway, entered):
def mark_game_as_won(cls, game_id):
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:
result = session.query(TableSteamItem).filter_by(steam_id=giveaway.steam_app_id).all()
if result:
@ -129,15 +172,16 @@ class GiveawayHelper:
copies=giveaway.copies,
contributor_level=giveaway.contributor_level,
entered=entered,
won=won,
game_entries=giveaway.game_entries)
session.add(g)
session.commit()
@classmethod
def upsert_giveaway(cls, giveaway, entered):
def upsert_giveaway_with_details(cls, giveaway, entered, won):
result = GiveawayHelper.get_by_ids(giveaway)
if not result:
GiveawayHelper.insert(giveaway, entered)
GiveawayHelper.insert(giveaway, entered, won)
else:
with Session(engine) as session:
g = TableGiveaway(
@ -151,6 +195,28 @@ class GiveawayHelper:
copies=giveaway.copies,
contributor_level=giveaway.contributor_level,
entered=entered,
won=won,
game_entries=giveaway.game_entries)
session.merge(g)
session.commit()
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)
session.merge(g)
session.commit()

View File

@ -9,7 +9,7 @@ from urllib3.util import Retry
from .log import get_logger
from .database import NotificationHelper, GiveawayHelper
from .giveaway import Giveaway
from .giveaway_entry import GiveawayEntry
logger = get_logger(__name__)
@ -19,28 +19,29 @@ class SteamGiftsException(Exception):
class EnterGiveaways:
def __init__(self, cookie, user_agent, gifts_type, pinned, min_points, max_entries,
max_time_left, minimum_game_points, blacklist, notification):
self.contributor_level = None
self.xsrf_token = None
self.points = None
self.cookie = {
self._contributor_level = None
self._xsrf_token = None
self._points = None
self._cookie = {
'PHPSESSID': cookie
}
self.user_agent = user_agent
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.minimum_game_points = int(minimum_game_points)
self.blacklist = blacklist.split(',')
self.notification = notification
self._user_agent = user_agent
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._minimum_game_points = int(minimum_game_points)
self._blacklist = blacklist.split(',')
self._notification = notification
self.base = "https://www.steamgifts.com"
self.session = requests.Session()
self._base = "https://www.steamgifts.com"
self._session = requests.Session()
self.filter_url = {
self._filter_url = {
'All': "search?page=%d",
'Wishlist': "search?page=%d&type=wishlist",
'Recommended': "search?page=%d&type=recommended",
@ -49,12 +50,23 @@ class EnterGiveaways:
'New': "search?page=%d&type=new"
}
def requests_retry_session(
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)
def _requests_retry_session(
self,
retries=5,
backoff_factor=0.3
):
session = self.session or requests.Session()
session = self._session or requests.Session()
retry = Retry(
total=retries,
read=retries,
@ -67,85 +79,93 @@ class EnterGiveaways:
session.mount('https://', adapter)
return session
def get_soup_from_page(self, url):
def _get_soup_from_page(self, url):
headers = {
'User-Agent': self.user_agent
'User-Agent': self._user_agent
}
self.requests_retry_session().get(url, headers=headers)
r = requests.get(url, cookies=self.cookie)
self._requests_retry_session().get(url, headers=headers)
r = requests.get(url, cookies=self._cookie)
soup = BeautifulSoup(r.text, 'html.parser')
return soup
def update_info(self):
soup = self.get_soup_from_page(self.base)
def _update_info(self):
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']))
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 = soup.select("a[title='Giveaways Won'] div")
if won:
number_won = soup.select_one("a[title='Giveaways Won'] div").text
number_won = int(soup.select_one("a[title='Giveaways Won'] div").text)
won_notifications = NotificationHelper.get_won_notifications_today()
if won_notifications and len(won_notifications) >= 1:
logger.info("🆒️ Win(s) detected, but we have already notified that there are won games waiting "
"to be received. Doing nothing.")
if number_won == won_notifications[-1].games_won:
logger.info("🆒️ Win(s) detected, but we have already notified that there are won games waiting "
"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:
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.")
self._notification.send_won(f"WINNER! You have {number_won} game(s) waiting to be claimed.", number_won)
else:
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:
return False
if giveaway.time_created_in_minutes is None:
return False
if self.blacklist is not None and self.blacklist != ['']:
for keyword in self.blacklist:
if self._blacklist is not None and self._blacklist != ['']:
for keyword in self._blacklist:
if giveaway.game_name.lower().find(keyword.lower()) != -1:
txt = f"〰️ Game {giveaway.game_name} contains the blacklisted keyword {keyword}"
logger.info(txt)
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 " \
f"level to enter. Your level: {self.contributor_level}"
f"level to enter. Your level: {self._contributor_level}"
logger.info(txt)
return False
if self.points - int(giveaway.cost) < 0:
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)
return False
if self._points - int(giveaway.cost) < 0:
txt = f"〰️ Not enough points to enter: {giveaway.game_name}"
logger.info(txt)
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 " \
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."
f"{self._minimum_game_points}P."
logger.info(txt)
return False
return True
def enter_giveaway(self, giveaway):
def _enter_giveaway(self, giveaway):
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}")
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)
json_data = json.loads(entry.text)
@ -156,17 +176,17 @@ class EnterGiveaways:
logger.error(f"❌ Failed entering giveaway {giveaway.giveaway_game_id}: {json_data}")
return False
def evaluate_giveaways(self, page=1):
def _evaluate_giveaways(self, page=1):
n = page
run = True
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
logger.info(txt)
filtered_url = self.filter_url[self.gifts_type] % n
paginated_url = f"{self.base}/giveaways/{filtered_url}"
filtered_url = self._filter_url[self._gifts_type] % n
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'))
all_games_list_count = len(soup.select('div.giveaway__row-inner-wrap'))
@ -181,17 +201,17 @@ class EnterGiveaways:
break
for item in unentered_game_list:
giveaway = Giveaway(item)
giveaway = GiveawayEntry(item)
txt = f"{giveaway.game_name} - {giveaway.cost}P - {giveaway.game_entries} entries " \
f"(w/ {giveaway.copies} copies) - Created {giveaway.time_created_string} ago " \
f"with {giveaway.time_remaining_string} remaining by {giveaway.user}."
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.")
continue
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."
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."
logger.info(txt)
run = False
break
@ -199,37 +219,26 @@ class EnterGiveaways:
if not giveaway.cost:
logger.error(f"Cost could not be determined for '{giveaway.game_name}'")
continue
if_enter_giveaway = self.should_we_enter_giveaway(giveaway)
if_enter_giveaway = self._should_we_enter_giveaway(giveaway)
if if_enter_giveaway:
res = self.enter_giveaway(giveaway)
res = self._enter_giveaway(giveaway)
if res:
GiveawayHelper.upsert_giveaway(giveaway, True)
self.points -= int(giveaway.cost)
GiveawayHelper.upsert_giveaway_with_details(giveaway, True, False)
self._points -= int(giveaway.cost)
txt = f"✅ Entered giveaway '{giveaway.game_name}'"
logger.info(txt)
sleep(randint(4, 15))
else:
GiveawayHelper.upsert_giveaway(giveaway, False)
GiveawayHelper.upsert_giveaway_with_details(giveaway, False, False)
else:
GiveawayHelper.upsert_giveaway(giveaway, False)
GiveawayHelper.upsert_giveaway(giveaway)
# 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
# after this point will also exceed the max time left
if self.gifts_type != "New" and not giveaway.pinned and \
giveaway.time_remaining_in_minutes > self.max_time_left:
if self._gifts_type != "New" and not giveaway.pinned and \
giveaway.time_remaining_in_minutes > self._max_time_left:
logger.info("🟡 We have run out of gifts to consider.")
run = False
break
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

@ -0,0 +1,86 @@
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__)
class Giveaway:
class GiveawayEntry:
def __init__(self, soup_item):
self.steam_app_id = None
@ -29,27 +29,27 @@ class Giveaway:
logger.debug(f"Giveaway html: {soup_item}")
icons = soup_item.select('a.giveaway__icon')
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.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']
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.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(',', ''))
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
times = soup_item.select('div span[data-timestamp]')
self.time_remaining_timestamp = int(times[0]['data-timestamp'])
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_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}")
def determine_contributor_level(self, contributor_level):
def _determine_contributor_level(self, contributor_level):
if contributor_level is None:
return 0
match = re.search('^Level (?P<level>[0-9]+)\\+$', contributor_level.text, re.IGNORECASE)
@ -58,14 +58,14 @@ class Giveaway:
else:
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)
if match:
return match.group('steam_app_id')
else:
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):
logger.error(f"Could not determine time from string {timestamp}")
return None
@ -73,7 +73,7 @@ class Giveaway:
giveaway_endtime = time.localtime(int(timestamp))
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'})
if len(item_headers) == 1: # then no multiple copies
game_cost = item_headers[0].getText().replace('(', '').replace(')', '').replace('P', '')

View File

@ -1,14 +1,13 @@
import datetime
import threading
from datetime import timedelta, datetime
from random import randint
from threading import Thread
from time import sleep
from dateutil import tz
from .log import get_logger
from .enter_giveaways import EnterGiveaways
from .evaluate_won_giveaways import EvaluateWonGiveaways
from .log import get_logger
from .scheduler import Scheduler
logger = get_logger(__name__)
@ -20,52 +19,72 @@ class GiveawayThread(threading.Thread):
self.exc = None
self.config = config
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'
def run_steam_gifts(self, config, notification):
cookie = config['DEFAULT'].get('cookie')
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['DEFAULT'].getboolean('enabled'):
cookie = config['DEFAULT'].get('cookie')
user_agent = config['DEFAULT'].get('user_agent')
all_page = EnterGiveaways(cookie, user_agent, 'All', False, minimum_points, max_entries,
max_time_left, minimum_game_points, blacklist, notification)
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')
wishlist_page_enabled = config['WISHLIST'].getboolean('wishlist.enabled')
wishlist_minimum_points = config['WISHLIST'].getint('wishlist.minimum_points')
wishlist_max_entries = config['WISHLIST'].getint('wishlist.max_entries')
wishlist_max_time_left = config['WISHLIST'].getint('wishlist.max_time_left')
self._all_page = EnterGiveaways(cookie, user_agent, 'All', False, minimum_points, max_entries,
max_time_left, minimum_game_points, blacklist, notification)
wishlist_page = EnterGiveaways(cookie, user_agent, 'Wishlist', False, wishlist_minimum_points,
wishlist_max_entries, wishlist_max_time_left, 0, '', notification)
if config['WISHLIST'].getboolean('wishlist.enabled'):
wishlist_minimum_points = config['WISHLIST'].getint('wishlist.minimum_points')
wishlist_max_entries = config['WISHLIST'].getint('wishlist.max_entries')
wishlist_max_time_left = config['WISHLIST'].getint('wishlist.max_time_left')
if not main_page_enabled and not wishlist_page_enabled:
self._wishlist_page = EnterGiveaways(cookie, user_agent, 'Wishlist', False, wishlist_minimum_points,
wishlist_max_entries, wishlist_max_time_left, 0, '', notification)
if not self._all_page and not self._wishlist_page:
logger.error("⁉️ Both 'Default' and 'Wishlist' configurations are disabled. Nothing will run. Exiting...")
sleep(10)
exit(-1)
while True:
logger.info("🟢 Evaluating giveaways.")
if wishlist_page_enabled:
wishlist_page.start()
if main_page_enabled:
all_page.start()
won_giveaway_job = self._scheduler.get_job(job_id=self.won_giveaway_job_id)
if won_giveaway_job:
logger.debug("Previous won giveaway evaluator job exists. Removing.")
won_giveaway_job.remove()
won_runner = GiveawayThread.WonRunner(EvaluateWonGiveaways(cookie, user_agent, notification),
self.won_giveaway_job_id)
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)
logger.info("🔴 All giveaways evaluated.")
random_seconds = randint(1740, 3540) # sometime between 29-59 minutes
when_to_start_again = datetime.now(tz=tz.tzlocal()) + timedelta(seconds=random_seconds)
logger.info(f"🛋 Going to sleep for {random_seconds / 60} minutes. "
f"Will start again at {when_to_start_again}")
sleep(random_seconds)
evaluate_giveaway_job = self._scheduler.get_job(job_id=self.evaluate_giveaway_job_id)
if evaluate_giveaway_job:
logger.debug("Previous giveaway evaluator job exists. Removing.")
evaluate_giveaway_job.remove()
runner = GiveawayThread.GiveawayRunner(self._wishlist_page, self._all_page,
self.evaluate_giveaway_job_id)
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):
# Variable that stores the exception, if raised by someFunction
self.exc = None
try:
self.run_steam_gifts(self.config, self.notification)
self._scheduler.start()
except BaseException as e:
self.exc = e
@ -76,3 +95,43 @@ class GiveawayThread(threading.Thread):
# if any was caught
if 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, wishlist_page, all_page, job_id):
self._wishlist_page = wishlist_page
self._all_page = all_page
self._job_id = job_id
def run(self):
logger.info("🟢 Evaluating giveaways.")
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

@ -13,11 +13,15 @@ class TableNotification(Base):
message = Column(String(300), nullable=False)
medium = Column(String(50), nullable=False)
success = Column(Boolean, nullable=False)
games_won = Column(Integer, nullable=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
__mapper_args__ = {"eager_defaults": True}
def __str__(self):
return str(self.__class__) + ": " + str(self.__dict__)
class TableSteamItem(Base):
__tablename__ = 'steam_item'
@ -29,6 +33,9 @@ class TableSteamItem(Base):
giveaways = relationship("TableGiveaway", back_populates="steam_item")
def __str__(self):
return str(self.__class__) + ": " + str(self.__dict__)
class TableGiveaway(Base):
__tablename__ = 'giveaway'
@ -42,6 +49,7 @@ class TableGiveaway(Base):
copies = Column(Integer(), nullable=False)
contributor_level = Column(Integer(), nullable=False)
entered = Column(Boolean(), nullable=False)
won = Column(Boolean(), nullable=False)
game_entries = Column(Integer(), nullable=False)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
@ -49,3 +57,6 @@ class TableGiveaway(Base):
steam_item = relationship("TableSteamItem", back_populates="giveaways")
__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.message_prefix = f"{message_prefix}: "
def send_won(self, message):
self.__send('won', message)
def send_won(self, message, number_won):
self.__send('won', message, number_won)
def send_error(self, message):
self.__send('error', message)
self.__send('error', message, number_won=None)
def enable_pushover(self, token, user_key):
logger.debug("Enabling pushover notifications.")
@ -27,13 +27,13 @@ class Notification:
self.pushover_token = token
self.pushover_user_key = user_key
def __send(self, type_of_error, message):
logger.debug(f"Attempting to notify: {message}")
def __send(self, type_of_error, message, number_won=None):
logger.debug(f"Attempting to notify: '{message}'. Won: {number_won}")
if self.pushover:
logger.debug("Pushover enabled. Sending message.")
self.__pushover(type_of_error, message)
self.__pushover(type_of_error, message, number_won)
def __pushover(self, type_of_error, message):
def __pushover(self, type_of_error, message, number_won=None):
conn = http.client.HTTPSConnection("api.pushover.net:443")
conn.request("POST", "/1/messages.json",
urllib.parse.urlencode({
@ -48,4 +48,4 @@ class Notification:
else:
logger.error(f"Pushover notification failed. Code {response.getcode()}: {response.read().decode()}")
success = False
NotificationHelper.insert(type_of_error, f"{message}", 'pushover', success)
NotificationHelper.insert(type_of_error, f"{message}", 'pushover', success, number_won)

33
src/bot/scheduler.py Normal file
View File

@ -0,0 +1,33 @@
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)

22
src/bot/won_entry.py Normal file
View File

@ -0,0 +1,22 @@
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

@ -73,4 +73,34 @@ h3 {
.menu li a {
color: #444;
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

@ -0,0 +1,27 @@
<!DOCTYPE html>
<html>
<head>
<title>{% block title %} {% endblock %}</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('stats') }}">Stats</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>
</nav></strong>
</div>
</header>
<div class="container">
{% block content %} {% endblock %}
</div>
</body>
</html>

View File

@ -1,26 +1,6 @@
<!DOCTYPE html>
<html>
<head>
<title>{{name}} Steamgifts Bot Configuration</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('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>
</nav></strong>
</div>
</header>
<div class="container">
<pre>{{content}}</pre>
</div>
</body>
</html>
{% extends 'base.html' %}
{% block title %} {{name}} Steamgifts Bot Configuration {% endblock %}
{% block content %}
<pre>{{data}}</pre>
{% endblock %}

View File

@ -1,28 +1,20 @@
<!DOCTYPE html>
<html>
<head>
<title>{{name}} Steamgifts Bot DB View</title>
<link href="{{ url_for('static', filename='css/main.css') }}" rel="stylesheet">
</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('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>
{% 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>
</nav>
</strong>
</div>
</header>
<div class="container">
<table>
<table class="styled-table">
<thead>
<tr>
<th>Giveaway ID</th>
@ -40,7 +32,7 @@
</tr>
</thead>
<tbody>
{% for row in content %}
{% for row in data.items %}
<tr>
<td>{{row.giveaway_id}}</td>
<td>{{row.steam_id}}</td>
@ -58,6 +50,4 @@
{% endfor %}
</tbody>
</table>
</div>
</body>
</html>
{% endblock %}

View File

@ -1,26 +1,6 @@
<!DOCTYPE html>
<html>
<head>
<title>{{name}} Steamgifts Bot {{log_type}} 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('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>
</nav></strong>
</div>
</header>
<div class="container">
<pre id="output">{{content}}</pre>
</div>
</body>
</html>
{% extends 'base.html' %}
{% block title %} {{name}} Steamgifts Bot {{log_type}} Logs{% endblock %}
{% block content %}
<pre id="output">{{data}}</pre>
{% endblock %}

View File

@ -1,28 +1,8 @@
<!DOCTYPE html>
<html>
<head>
<title>{{name}} Steamgifts Bot DB View</title>
<link href="{{ url_for('static', filename='css/main.css') }}" rel="stylesheet">
</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('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>
</nav>
</strong>
</div>
</header>
<div class="container">
<table>
{% extends 'base.html' %}
{% block title %} {{name}} Steamgifts Bot Notifications {% endblock %}
{% block content %}
<table>
<thead>
<tr>
<th>ID</th>
@ -34,7 +14,7 @@
</tr>
</thead>
<tbody>
{% for row in content %}
{% for row in data %}
<tr>
<td>{{row.id}}</td>
<td>{{row.medium}}</td>
@ -46,6 +26,4 @@
{% endfor %}
</tbody>
</table>
</div>
</body>
</html>
{% endblock %}

View File

@ -0,0 +1,10 @@
{% 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 %}

View File

@ -16,7 +16,7 @@ class WebServerThread(threading.Thread):
Thread.__init__(self)
self.exc = None
self.config = config
self.name = config['NOTIFICATIONS'].get('notification.prefix')
self.prefix = config['NOTIFICATIONS'].get('notification.prefix')
self.host = config['WEB'].get('web.host')
self.port = config['WEB'].getint('web.port')
self.ssl = config['WEB'].getboolean('web.ssl')
@ -42,20 +42,20 @@ class WebServerThread(threading.Thread):
@app.route(f"{self.app_root}")
def config():
with open(f"{os.getenv('BOT_CONFIG_DIR', './config')}/config.ini", 'r') as f:
content = f.read()
return render_template('configuration.html', name=self.name, content=content)
data = f.read()
return render_template('configuration.html', name=self.prefix, data=data)
@app.route(f"{self.app_root}log_info")
def log_info():
with open(f"{os.getenv('BOT_CONFIG_DIR', './config')}/info.log", 'r') as f:
content = f.read()
return render_template('log.html', name=self.name, log_type='info', content=content)
data = f.read()
return render_template('log.html', name=self.prefix, log_type='info', data=data)
@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:
content = f.read()
return render_template('log.html', name=self.name, log_type='debug', content=content)
data = f.read()
return render_template('log.html', name=self.prefix, log_type='debug', data=data)
@app.route(f"{self.app_root}alive")
def alive():
@ -63,11 +63,19 @@ class WebServerThread(threading.Thread):
@app.route(f"{self.app_root}notifications")
def db_notifications():
return render_template('notifications.html', content=NotificationHelper.get())
return render_template('notifications.html', name=self.prefix, data=NotificationHelper.get())
@app.route(f"{self.app_root}giveaways")
def db_giveaways():
return render_template('giveaways.html', content=GiveawayHelper.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:
logger.info("Webserver Enabled. Running")