Skip to content

Instantly share code, notes, and snippets.

@Xevion
Created June 27, 2020 19:04
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Xevion/a84b46e2413b200372d8bf54ff86b124 to your computer and use it in GitHub Desktop.
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.
"""
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