├── .gitignore ├── LICENSE ├── Procfile ├── README.md ├── app.json ├── clock.py ├── herokuapp.py ├── pollevbot ├── __init__.py ├── endpoints.py ├── main.py └── pollbot.py ├── requirements.txt └── runtime.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | .idea/ 107 | local.py 108 | test.py 109 | setup.py 110 | /build 111 | /dist 112 | /PollEvBot.egg-info 113 | /dev 114 | .pypirc -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 danielqiang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | clock: python clock.py 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pollevbot 2 | 3 | **pollevbot** is a bot that automatically responds to polls on [pollev.com](https://pollev.com/). 4 | It continually checks if a specified host has opened any polls. Once a poll has been opened, 5 | it submits a random response. 6 | 7 | Requires Python 3.7 or later. 8 | ## Dependencies 9 | 10 | [Requests](https://pypi.org/project/requests/), 11 | [BeautifulSoup](https://pypi.org/project/beautifulsoup4/). 12 | 13 | [APScheduler](https://pypi.org/project/APScheduler/) to deploy to Heroku. 14 | 15 | ## Usage 16 | 17 | Install `pollevbot`: 18 | ``` 19 | pip install pollevbot 20 | ``` 21 | 22 | Set your username, password, and desired poll host: 23 | ```python 24 | user = 'My Username' 25 | password = 'My Password' 26 | host = 'PollEverywhere URL Extension e.g. "uwpsych"' 27 | ``` 28 | 29 | And run the script. 30 | ```python 31 | from pollevbot import PollBot 32 | 33 | user = 'My Username' 34 | password = 'My Password' 35 | host = 'PollEverywhere URL Extension e.g. "uwpsych"' 36 | 37 | # If you're using a non-UW PollEv account, 38 | # add the argument "login_type='pollev'" 39 | with PollBot(user, password, host) as bot: 40 | bot.run() 41 | ``` 42 | Alternatively, you can clone this repo, set your login credentials in 43 | [main.py](pollevbot/main.py) and run it from there. 44 | 45 | ## Heroku 46 | 47 | **pollevbot** can be scheduled to run at specific dates/times (UTC timezone) using [Heroku](http://heroku.com/): 48 | 49 | [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/danielqiang/pollevbot) 50 | 51 | Required configuration variables: 52 | 53 | * `DAY_OF_WEEK`: [cron](https://apscheduler.readthedocs.io/en/stable/modules/triggers/cron.html) string 54 | specifying weekdays to run pollevbot (e.g. `mon,wed` is Monday and Wednesday). 55 | * `HOUR`: [cron](https://apscheduler.readthedocs.io/en/stable/modules/triggers/cron.html) string 56 | (UTC time) specifying which hours to run pollevbot. 57 | * `LIFETIME`: Time to run pollevbot before terminating (in seconds). Set to `inf` to run forever. 58 | * `LOGIN_TYPE`: Login protocol to use (either `uw` or `pollev`). 59 | * `MINUTE`: [cron](https://apscheduler.readthedocs.io/en/stable/modules/triggers/cron.html) string 60 | specifying what minutes to run pollevbot. 61 | * `PASSWORD`: PollEv account password. 62 | * `POLLHOST`: PollEv host name. 63 | * `USERNAME`: PollEv account username. 64 | 65 | **Example** 66 | 67 | Suppose you want to answer polls made by poll host `teacher123` every Monday and Wednesday 68 | from 11:30 AM to 12:30 PM PST (6:30 PM to 7:30 PM UTC) in your timezone on your UW account. To do this, set the config 69 | variables as follows: 70 | 71 | * `DAY_OF_WEEK`: `mon,wed` 72 | * `HOUR`: `18` 73 | * `LIFETIME`: `3600` 74 | * `LOGIN_TYPE`: `uw` 75 | * `MINUTE`: `30` 76 | * `PASSWORD`: `yourpassword` 77 | * `POLLHOST`: `teacher123` 78 | * `USERNAME`: `yourusername` 79 | 80 | Then click `Deploy App` and wait for the app to finish building. 81 | **pollevbot** is now deployed to Heroku! 82 | 83 | ## Disclaimer 84 | 85 | I do not promote or condone the usage of this script for any kind of academic misconduct 86 | or dishonesty. I wrote this script for the sole purpose of educating myself on cybersecurity 87 | and web protocol automation, and cannot be held liable for any indirect, incidental, consequential, 88 | special, or exemplary damages arising out of or in connection with the usage of this script. 89 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pollevbot", 3 | "description": "A response bot for multiple-choice polls on PollEverywhere", 4 | "repository": "https://github.com/danielqiang/pollevbot", 5 | "env": { 6 | "USERNAME": { 7 | "description": "PollEv account username" 8 | }, 9 | "PASSWORD": { 10 | "description": "PollEv account password" 11 | }, 12 | "POLLHOST": { 13 | "description": "PollEv host name" 14 | }, 15 | "DAY_OF_WEEK": { 16 | "description": "Cron string specifying weekdays to run pollevbot" 17 | }, 18 | "HOUR": { 19 | "description": "Cron string specifying which hours to run pollevbot" 20 | }, 21 | "MINUTE": { 22 | "description": "Cron string specifying what minutes to run pollevbot" 23 | }, 24 | "LOGIN_TYPE": { 25 | "description": "Login protocol to use (either uw or pollev)", 26 | "value": "uw" 27 | }, 28 | "LIFETIME": { 29 | "description": "Time to run pollevbot before terminating (in seconds). Set to inf to run forever.", 30 | "value": "3600" 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /clock.py: -------------------------------------------------------------------------------- 1 | """ 2 | Program scheduler to run on a Heroku clock dyno. 3 | 4 | Required config variables: 5 | - USERNAME 6 | - PASSWORD 7 | - POLLHOST 8 | - DAY_OF_WEEK (cron string) 9 | - HOUR (cron string) 10 | - MINUTE (cron string) 11 | - LOGIN_TYPE ('uw' or 'pollev') 12 | - LIFETIME 13 | 14 | clock.py is a standalone program that schedules and runs 15 | pollevbot. It uses APScheduler's Cron triggers to simulate 16 | cron (Unix util). 17 | 18 | See https://apscheduler.readthedocs.io/en/stable/modules/triggers/cron.html 19 | for more info. 20 | """ 21 | 22 | import os 23 | import logging 24 | import pytz 25 | from pollevbot import PollBot 26 | from apscheduler.schedulers.blocking import BlockingScheduler 27 | 28 | required_vars = {'USERNAME', 'PASSWORD', 'POLLHOST', 'DAY_OF_WEEK', 29 | 'HOUR', 'MINUTE', 'LOGIN_TYPE', 'LIFETIME'} 30 | missing_vars = sorted(required_vars - set(os.environ)) 31 | assert len(missing_vars) == 0, f"Missing required config variables: {missing_vars}" 32 | 33 | logger = logging.getLogger(__name__) 34 | 35 | 36 | def run(): 37 | # Heroku config vars 38 | user = os.environ['USERNAME'] 39 | password = os.environ['PASSWORD'] 40 | host = os.environ['POLLHOST'] 41 | login_type = os.environ['LOGIN_TYPE'] 42 | lifetime = float(os.environ['LIFETIME']) 43 | 44 | with PollBot(user, password, host, login_type=login_type, lifetime=lifetime) as bot: 45 | bot.run() 46 | 47 | 48 | def main(): 49 | logger.info("Starting blocking scheduler.") 50 | 51 | scheduler = BlockingScheduler(timezone=pytz.utc) 52 | scheduler.add_job(run, 'cron', 53 | day_of_week=os.environ['DAY_OF_WEEK'], 54 | hour=os.environ['HOUR'], 55 | minute=os.environ['MINUTE']) 56 | scheduler.start() 57 | 58 | 59 | if __name__ == '__main__': 60 | main() 61 | -------------------------------------------------------------------------------- /herokuapp.py: -------------------------------------------------------------------------------- 1 | """ 2 | Program to run with Heroku Scheduler add-on 3 | (command: python herokuapp.py) 4 | 5 | Required config variables: 6 | - USERNAME 7 | - PASSWORD 8 | - POLLHOST 9 | - LOGIN_TYPE ('uw' or 'pollev') 10 | - LIFETIME 11 | - DAY_OF_WEEK (cron string) 12 | 13 | Since Heroku Scheduler can only schedule programs 14 | to run at a certain time every day, this program 15 | will check if the current date matches the weekdays 16 | set in the config variable `DAY_OF_WEEK`. If it does, 17 | this program will run pollevbot; if not, the program 18 | will exit. 19 | """ 20 | 21 | import os 22 | import logging 23 | from datetime import date 24 | from pollevbot import PollBot 25 | 26 | required = {'USERNAME', 'PASSWORD', 'POLLHOST', 27 | 'DAY_OF_WEEK', 'LOGIN_TYPE', 'LIFETIME'} 28 | missing = sorted(required - set(os.environ)) 29 | assert len(missing) == 0, f"Missing required config variables: {missing}" 30 | 31 | logger = logging.getLogger(__name__) 32 | 33 | 34 | def check_day(): 35 | date_map = { 36 | 'mon': '0', 37 | 'tue': '1', 38 | 'wed': '2', 39 | 'thu': '3', 40 | 'fri': '4', 41 | 'sat': '5', 42 | 'sun': '6' 43 | } 44 | day_of_week = [s.strip() for s in os.environ['DAY_OF_WEEK'].split(',')] 45 | day_of_week = [date_map[s] if s in date_map else s for s in day_of_week] 46 | 47 | return str(date.today().weekday()) in day_of_week 48 | 49 | 50 | def main(): 51 | # Heroku config vars 52 | user = os.environ['USERNAME'] 53 | password = os.environ['PASSWORD'] 54 | host = os.environ['POLLHOST'] 55 | login_type = os.environ['LOGIN_TYPE'] 56 | lifetime = float(os.environ['LIFETIME']) 57 | 58 | if check_day(): 59 | with PollBot(user, password, host, 60 | login_type=login_type, lifetime=lifetime, 61 | max_option=3, open_wait=10) as bot: 62 | bot.run() 63 | else: 64 | logger.info("pollevbot is not configured to run today. Exiting.") 65 | 66 | 67 | if __name__ == '__main__': 68 | main() 69 | -------------------------------------------------------------------------------- /pollevbot/__init__.py: -------------------------------------------------------------------------------- 1 | from sys import version_info 2 | 3 | assert version_info >= (3, 7), "pollevbot requires python 3.7 or later" 4 | 5 | from .pollbot import PollBot 6 | import logging 7 | 8 | # Log all messages as white text 9 | WHITE = "\033[1m" 10 | logging.basicConfig(level=logging.INFO, 11 | format=WHITE + "%(asctime)s.%(msecs)03d [%(name)s] " 12 | "%(levelname)s: %(message)s", 13 | datefmt='%Y-%m-%d %H:%M:%S') 14 | -------------------------------------------------------------------------------- /pollevbot/endpoints.py: -------------------------------------------------------------------------------- 1 | endpoints = { 2 | 'home': 'https://pollev.com/{host}', 3 | 4 | # MyUW Login 5 | 'uw_login': 'https://idp.u.washington.edu/idp/profile/SAML2/' 6 | 'Redirect/SSO;jsessionid={id}.idp03?execution=e1s1', 7 | 'uw_saml': 'https://www.polleverywhere.com/auth/washington?' 8 | 'redirect=https%3A%2F%2Fpollev.com%2F&token_required=false', 9 | 'uw_callback': 'https://www.polleverywhere.com/auth/washington/callback', 10 | 'uw_auth_token': 'https://pollev.com/proxy/api/participant_auth_token', 11 | 12 | # General Login 13 | 'login': 'https://pollev.com/proxy/api/sessions', 14 | # CSRF authentication 15 | 'csrf': 'https://pollev.com/proxy/api/csrf_token?_={timestamp}', 16 | 17 | # Respond to a poll 18 | 'firehose_auth': 'https://pollev.com/proxy/api/users/{host}/registration_info?_={timestamp}', 19 | 'firehose_with_token': 'https://firehose-production.polleverywhere.com/users/{host}/activity/' 20 | 'current.json?firehose_token={token}&last_message_sequence=0&_={timestamp}', 21 | 'firehose_no_token': 'https://firehose-production.polleverywhere.com/users/{host}/activity/' 22 | 'current.json?last_message_sequence=0&_={timestamp}', 23 | 'poll_data': 'https://pollev.com/proxy/api/participant/multiple_choice_polls/{uid}?include=collection', 24 | 'respond_to_poll': 'https://pollev.com/proxy/api/participant/multiple_choice_polls/{uid}/results', 25 | 'clear_responses': 'https://pollev.com/proxy/api/results/{id}', 26 | 'check_responses': 'https://pollev.com/proxy/my/results?permalinks%5B%5D={uid}&' 27 | 'per_page=500&include_archived=false&_={timestamp}' 28 | } 29 | -------------------------------------------------------------------------------- /pollevbot/main.py: -------------------------------------------------------------------------------- 1 | from pollevbot import PollBot 2 | 3 | 4 | def main(): 5 | user = 'My Username' 6 | password = 'My Password' 7 | host = 'PollEverywhere URL Extension e.g. "uwpsych"' 8 | 9 | # If you're using a non-uw PollEv account, 10 | # add the argument "login_type='pollev'" 11 | with PollBot(user, password, host) as bot: 12 | bot.run() 13 | 14 | 15 | if __name__ == '__main__': 16 | main() 17 | -------------------------------------------------------------------------------- /pollevbot/pollbot.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import logging 3 | import time 4 | from typing import Optional 5 | from .endpoints import endpoints 6 | 7 | logger = logging.getLogger(__name__) 8 | __all__ = ['PollBot'] 9 | 10 | 11 | class LoginError(RuntimeError): 12 | """Error indicating that login failed.""" 13 | 14 | 15 | class PollBot: 16 | """Bot for answering polls on PollEverywhere. 17 | Responses are randomly selected. 18 | 19 | Usage: 20 | >>> bot = PollBot(user='username', password='password', 21 | ... host='host', login_type='uw') 22 | >>> bot.run() 23 | 24 | Can also be used as a context manager. 25 | """ 26 | 27 | def __init__(self, user: str, password: str, host: str, 28 | login_type: str = 'uw', min_option: int = 0, 29 | max_option: int = None, closed_wait: float = 5, 30 | open_wait: float = 5, lifetime: float = float('inf')): 31 | """ 32 | Constructor. Creates a PollBot that answers polls on pollev.com. 33 | 34 | :param user: PollEv account username. 35 | :param password: PollEv account password. 36 | :param host: PollEv host name, i.e. 'uwpsych' 37 | :param login_type: Login protocol to use (either 'uw' or 'pollev'). 38 | If 'uw', uses MyUW (SAML2 SSO) to authenticate. 39 | If 'pollev', uses pollev.com. 40 | :param min_option: Minimum index (0-indexed) of option to select (inclusive). 41 | :param max_option: Maximum index (0-indexed) of option to select (exclusive). 42 | :param closed_wait: Time to wait in seconds if no polls are open 43 | before checking again. 44 | :param open_wait: Time to wait in seconds if a poll is open 45 | before answering. 46 | :param lifetime: Lifetime of this PollBot (in seconds). 47 | If float('inf'), runs forever. 48 | :raises ValueError: if login_type is not 'uw' or 'pollev'. 49 | """ 50 | if login_type not in {'uw', 'pollev'}: 51 | raise ValueError(f"'{login_type}' is not a supported login type. " 52 | f"Use 'uw' or 'pollev'.") 53 | if login_type == 'pollev' and user.strip().lower().endswith('@uw.edu'): 54 | logger.warning(f"{user} looks like a UW email. " 55 | f"Use login_type='uw' to log in with MyUW.") 56 | 57 | self.user = user 58 | self.password = password 59 | self.host = host 60 | self.login_type = login_type 61 | # 0-indexed minimum and maximum option 62 | # indices to select on poll. 63 | self.min_option = min_option 64 | self.max_option = max_option 65 | # Wait time in seconds if poll is 66 | # closed or open, respectively 67 | self.closed_wait = closed_wait 68 | self.open_wait = open_wait 69 | 70 | self.lifetime = lifetime 71 | self.start_time = time.time() 72 | 73 | self.session = requests.Session() 74 | self.session.headers = { 75 | 'user-agent': "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " 76 | "(KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36" 77 | } 78 | # IDs of all polls we have answered already 79 | self.answered_polls = set() 80 | 81 | def __enter__(self): 82 | return self 83 | 84 | def __exit__(self, *args): 85 | self.session.close() 86 | 87 | @staticmethod 88 | def timestamp() -> float: 89 | return round(time.time() * 1000) 90 | 91 | def _get_csrf_token(self) -> str: 92 | url = endpoints['csrf'].format(timestamp=self.timestamp()) 93 | return self.session.get(url).json()['token'] 94 | 95 | def _pollev_login(self) -> bool: 96 | """ 97 | Logs into PollEv through pollev.com. 98 | Returns True on success, False otherwise. 99 | """ 100 | logger.info("Logging into PollEv through pollev.com.") 101 | 102 | r = self.session.post(endpoints['login'], 103 | headers={'x-csrf-token': self._get_csrf_token()}, 104 | data={'login': self.user, 'password': self.password}) 105 | # If login is successful, PollEv sends an empty HTTP response. 106 | return not r.text 107 | 108 | def _uw_login(self): 109 | """ 110 | Logs into PollEv through MyUW. 111 | Returns True on success, False otherwise. 112 | """ 113 | import bs4 as bs 114 | import re 115 | 116 | logger.info("Logging into PollEv through MyUW.") 117 | 118 | r = self.session.get(endpoints['uw_saml']) 119 | soup = bs.BeautifulSoup(r.text, "html.parser") 120 | data = soup.find('form', id='idplogindiv')['action'] 121 | session_id = re.findall(r'jsessionid=(.*)\.', data) 122 | 123 | r = self.session.post(endpoints['uw_login'].format(id=session_id), 124 | data={ 125 | 'j_username': self.user, 126 | 'j_password': self.password, 127 | '_eventId_proceed': 'Sign in' 128 | }) 129 | soup = bs.BeautifulSoup(r.text, "html.parser") 130 | saml_response = soup.find('input', type='hidden') 131 | 132 | # When user authentication fails, UW will send an empty SAML response. 133 | if not saml_response: 134 | return False 135 | 136 | r = self.session.post(endpoints['uw_callback'], 137 | data={'SAMLResponse': saml_response['value']}) 138 | auth_token = re.findall('pe_auth_token=(.*)', r.url)[0] 139 | self.session.post(endpoints['uw_auth_token'], 140 | headers={'x-csrf-token': self._get_csrf_token()}, 141 | data={'token': auth_token}) 142 | return True 143 | 144 | def login(self): 145 | """ 146 | Logs into PollEv. 147 | 148 | :raises LoginError: if login failed. 149 | """ 150 | if self.login_type.lower() == 'uw': 151 | success = self._uw_login() 152 | else: 153 | success = self._pollev_login() 154 | if not success: 155 | raise LoginError("Your username or password was incorrect.") 156 | logger.info("Login successful.") 157 | 158 | def get_firehose_token(self) -> str: 159 | """ 160 | Given that the user is logged in, retrieve an AWS firehose token. 161 | If the poll host is not affiliated with UW, PollEv will return 162 | a firehose token with a null value. 163 | 164 | :raises ValueError: if the specified poll host is not found. 165 | """ 166 | from uuid import uuid4 167 | # Before issuing a token, AWS checks for two visitor cookies that 168 | # PollEverywhere generates using js. They are random uuids. 169 | self.session.cookies['pollev_visitor'] = str(uuid4()) 170 | self.session.cookies['pollev_visit'] = str(uuid4()) 171 | url = endpoints['firehose_auth'].format( 172 | host=self.host, 173 | timestamp=self.timestamp 174 | ) 175 | r = self.session.get(url) 176 | 177 | if "presenter not found" in r.text.lower(): 178 | raise ValueError(f"'{self.host}' is not a valid poll host.") 179 | return r.json()['firehose_token'] 180 | 181 | def get_new_poll_id(self, firehose_token=None) -> Optional[str]: 182 | import json 183 | 184 | if firehose_token: 185 | url = endpoints['firehose_with_token'].format( 186 | host=self.host, 187 | token=firehose_token, 188 | timestamp=self.timestamp 189 | ) 190 | else: 191 | url = endpoints['firehose_no_token'].format( 192 | host=self.host, 193 | timestamp=self.timestamp 194 | ) 195 | try: 196 | r = self.session.get(url, timeout=0.3) 197 | # Unique id for poll 198 | poll_id = json.loads(r.json()['message'])['uid'] 199 | # Firehose either doesn't respond or responds with no data if no poll is open. 200 | except (requests.exceptions.ReadTimeout, KeyError): 201 | return None 202 | if poll_id in self.answered_polls: 203 | return None 204 | else: 205 | self.answered_polls.add(poll_id) 206 | return poll_id 207 | 208 | def answer_poll(self, poll_id) -> dict: 209 | import random 210 | 211 | url = endpoints['poll_data'].format(uid=poll_id) 212 | poll_data = self.session.get(url).json() 213 | options = poll_data['options'][self.min_option:self.max_option] 214 | try: 215 | option_id = random.choice(options)['id'] 216 | except IndexError: 217 | # `options` was empty 218 | logger.error(f'Could not answer poll: poll only has ' 219 | f'{len(poll_data["options"])} options but ' 220 | f'self.min_option was {self.min_option} and ' 221 | f'self.max_option: {self.max_option}') 222 | return {} 223 | r = self.session.post( 224 | endpoints['respond_to_poll'].format(uid=poll_id), 225 | headers={'x-csrf-token': self._get_csrf_token()}, 226 | data={'option_id': option_id, 'isPending': True, 'source': "pollev_page"} 227 | ) 228 | return r.json() 229 | 230 | def alive(self): 231 | return time.time() <= self.start_time + self.lifetime 232 | 233 | def run(self): 234 | """Runs the script.""" 235 | try: 236 | self.login() 237 | token = self.get_firehose_token() 238 | except (LoginError, ValueError) as e: 239 | logger.error(e) 240 | return 241 | 242 | while self.alive(): 243 | poll_id = self.get_new_poll_id(token) 244 | 245 | if poll_id is None: 246 | logger.info(f'`{self.host}` has not opened any new polls. ' 247 | f'Waiting {self.closed_wait} seconds before checking again.') 248 | time.sleep(self.closed_wait) 249 | else: 250 | logger.info(f"{self.host} has opened a new poll! " 251 | f"Waiting {self.open_wait} seconds before responding.") 252 | time.sleep(self.open_wait) 253 | r = self.answer_poll(poll_id) 254 | logger.info(f'Received response: {r}') 255 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | APScheduler==3.6.3 2 | beautifulsoup4==4.9.0 3 | bs4==0.0.1 4 | certifi==2020.4.5.1 5 | chardet==3.0.4 6 | idna==2.9 7 | pytz==2020.1 8 | requests==2.23.0 9 | six==1.14.0 10 | soupsieve==2.0 11 | tzlocal==2.0.0 12 | urllib3==1.25.9 13 | -------------------------------------------------------------------------------- /runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.8.2 --------------------------------------------------------------------------------