Created
June 27, 2020 19:04
-
-
Save Xevion/a84b46e2413b200372d8bf54ff86b124 to your computer and use it in GitHub Desktop.
Searches perfectaim.io forums for open giveaways while spoofing the user's Firefox browser cookies and user-agent.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
main.py | |
Searches perfectaim.io using Firefox browser cookies and user-agents to spoof a real user. | |
This program looks for and checks for viable giveaways on the site. | |
It ignores completed (locked threads), unavailable (requirements too high) or already entered giveaways. | |
""" | |
import logging | |
import re | |
import time | |
from collections import namedtuple, defaultdict | |
from enum import Enum | |
from typing import List, Optional | |
import browser_cookie3 | |
import dateparser | |
import requests | |
from bs4 import BeautifulSoup | |
from fake_useragent import UserAgent | |
from requests_cache import install_cache | |
# Set all imported module logging level | |
logging.basicConfig(level=logging.ERROR) | |
# Setup logging level for this file alone | |
logger = logging.getLogger('perfectaim') | |
logger.setLevel(logging.DEBUG) | |
class Status(Enum): | |
""" | |
Giveaway statuses in relation to the user. | |
""" | |
AVAILABLE = 0 # You can still enter the giveaway at this moment. | |
ENTERED = 1 # You are already in the giveaway | |
UNAVAILABLE = 2 # The giveaway is not available to you (for whatever reason) | |
ENDED = 3 # The giveaway has ended and cannot be entered by anyone. | |
# Setup sqlite cache database | |
install_cache('perfectaim', expire_after=60 * 15) | |
# Fetch useragent catalog | |
ua = UserAgent() | |
# URL Constants | |
FORUM_PAGE = "https://perfectaim.io/forum/category/10/{page}" | |
THREAD = "https://perfectaim.io/forum/topic/{thread}" | |
# Get cookies | |
cj = browser_cookie3.firefox(domain_name='perfectaim.io') | |
ThreadStatus = namedtuple('ThreadStatus', ['id', 'name', 'locked']) | |
GiveawayStatus = namedtuple('GiveawayStatus', ['id', 'url', 'end_time', 'availability', 'min_posts', | |
'min_rep', 'require_vip', 'participants']) | |
def getThreads(page: int, nocache_wait: float = 0.5) -> List[ThreadStatus]: | |
""" | |
Get all threads from the giveaway forum category. | |
:param nocache_wait: | |
:param page: Page Number. Range of 1-241+ | |
:return: A list of integers representing thread IDs | |
""" | |
resp = requests.get(FORUM_PAGE.format(page=page), cookies=cj, headers={'User-Agent': ua.firefox}) | |
if not resp.from_cache: | |
time.sleep(nocache_wait) | |
soup = BeautifulSoup(resp.text, features='html.parser') | |
threads = [] | |
for threadTitle in soup.find_all(attrs={'class': 'forum-title'}): | |
threadURL = threadTitle.find('a')['href'] | |
threadID = int(re.search(r'perfectaim.io/forum/topic/(\d+)', threadURL)[1]) | |
if threadID == 2: | |
continue | |
threadName = threadTitle.find('h4') | |
threadLocked = threadName.find('i', attrs={'class': 'fa fa-lock'}) is not None | |
threadName = threadName.text.strip() | |
threads.append(ThreadStatus( | |
id=threadID, | |
name=threadName, | |
locked=threadLocked | |
)) | |
return threads | |
def getStatus(thread: int, nocache_wait: float = 0.5) -> Optional[GiveawayStatus]: | |
""" | |
Get giveaway status from a specific thread. | |
:param nocache_wait: If a request is made and the cache is not queried, wait this period of time (in seconds). | |
:param thread: Thread ID of thread to check. | |
:return: A simple dictionary outlining the giveaway's current status in relation to the user. | |
""" | |
logger.info('Getting Giveaway Status for Thread %d', thread) | |
resp = requests.get(THREAD.format(thread=thread), cookies=cj, headers={'User-Agent': ua.firefox}) | |
# Ratelimit operation | |
if not resp.from_cache: | |
logger.debug('Response is not cached, sleeping') | |
time.sleep(nocache_wait) | |
soup = BeautifulSoup(resp.text, features='html.parser') | |
disabledButton = soup.find('a', attrs={'class': 'btn btn-primary m-r-5 disabled'}) | |
enabledButton = soup.find('a', attrs={'class': 'btn btn-primary m-r-5'}) | |
panel = soup.find_all('div', attrs={'class': 'fCenterY'})[-1] | |
if disabledButton is not None: | |
if re.search(r"You're in", disabledButton.text) is not None: | |
status = Status.ENTERED | |
else: | |
if re.search(r'Giveaway ended', panel.text) is not None: | |
status = Status.ENDED | |
else: | |
status = Status.UNAVAILABLE | |
elif enabledButton is not None: | |
status = Status.AVAILABLE | |
else: | |
status = Status.UNAVAILABLE | |
print("Couldn't decide upon Giveaway Status.") | |
if status != Status.ENDED: | |
logger.debug('Looking for all giveaway details....') | |
# Get all specific giveaway details | |
details = soup.find('h6', text='Giveaway details') | |
# Extra check for | |
if not details: | |
logger.debug('Giveaway has not provided specific details but is still open.') | |
return GiveawayStatus( | |
id=thread, url=resp.url, availability=status, | |
end_time=None, min_posts=None, min_rep=None, require_vip=None, participants=None | |
) | |
details = details.next_sibling.get_text('\n') | |
closeEst = re.search(r'Closes automatically:\s+(.+)', details)[1] | |
minPosts = re.search(r'Minimum post count to enter:\s+(\d+)', details)[1] | |
minRep = re.search(r'Minimum reputation to enter:\s+(\d+)', details)[1] | |
reqVIP = re.search(r'Must have been a VIP member to participate:\s+(yes|no)', details)[1] | |
participants = re.search(r'Participants:\s+(\d+)', details)[1] | |
closeEst = dateparser.parse(closeEst) | |
minPosts = int(minPosts) | |
minRep = int(minRep) | |
reqVIP = {'no': False, 'yes': True}.get(reqVIP, None) | |
participants = int(participants) | |
return GiveawayStatus( | |
id=thread, | |
url=resp.url, | |
availability=status, | |
end_time=closeEst, | |
min_posts=minPosts, | |
min_rep=minRep, | |
require_vip=reqVIP, | |
participants=participants | |
) | |
else: | |
logger.debug('Giveaway has ended, getting limited giveaway details instead.') | |
# Limited giveaway details to look for | |
details = re.search(r'Giveaway ended (.+) with (\d+) participants', panel.text) | |
end_time = dateparser.parse(details[1]) | |
participants = int(details[2]) | |
return GiveawayStatus( | |
id=thread, | |
url=resp.url, | |
availability=status, | |
participants=participants, | |
end_time=end_time, | |
min_posts=None, | |
min_rep=None, | |
require_vip=None, | |
) | |
def collectThreads(locked: bool = False, empty_limit: int = 2) -> List[ThreadStatus]: | |
""" | |
Collects threads in a smart fashion, stopping once it reaches a page with only locked threads. | |
:param locked: If True, add locked threads to the final result. Does not effect empty_limit functionality. | |
:param empty_limit: The number of empty thread lists that can be found before collection will cease automatically. | |
:return: A list of threads. Locked threads are filtered out. | |
""" | |
unlocked_threads = [] | |
logger.info(f'Collecting threads, limit {empty_limit} pages of locked threads.') | |
for page in range(1, 241): | |
logger.debug(f'Getting Page {page} of Giveaway Threads') | |
new_threads = getThreads(page) | |
# Continue looking if any of the threads are still open | |
if any(not new_thread.locked for new_thread in new_threads): | |
if not locked: | |
unlocked_threads.extend(filter(lambda new_thread: not new_thread.locked, new_threads)) | |
else: | |
unlocked_threads.extend(new_threads) | |
else: | |
# Locked threads will still be added. | |
if locked: | |
unlocked_threads.extend(new_threads) | |
if empty_limit == 0: | |
logger.debug('Page of Locked Threads found. Threshold reached.') | |
break | |
else: | |
logger.debug(f'Page of Locked Threads found. {empty_limit - 1} left.') | |
empty_limit -= 1 | |
return unlocked_threads | |
def main() -> None: | |
""" | |
Main function. Made simply to prevent variable name shadowing warnings. | |
""" | |
# Get threads and then check relevant giveaway status | |
threads = collectThreads() | |
logger.info(f'Found {len(threads)} open giveaways.') | |
all_giveaways = [(thread, getStatus(thread.id)) for thread in threads] | |
all_giveaways = [(thread, giveaway) for thread, giveaway in all_giveaways if giveaway is not None] | |
# Sort giveaways by their status | |
giveaways = defaultdict(list) | |
for thread, giveaway in all_giveaways: | |
giveaways[giveaway.availability].append((thread, giveaway)) | |
# Count all giveaways | |
for ga_status in sorted(giveaways.keys(), key=lambda status: len(giveaways[status])): | |
logger.info(f'{len(giveaways[ga_status])} {ga_status.name.lower().capitalize()} Giveaways') | |
# Print all available giveaways | |
logger.info(f'Showing {len(sorted([Status.AVAILABLE]))} Available giveaways to participate in.') | |
for thread, giveaway in giveaways[Status.AVAILABLE]: | |
logger.info(f'[{giveaway.id}] {thread.name} ( {giveaway.url} )') | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment