├── .gitignore ├── requirements.txt ├── __pycache__ └── config.cpython-36.pyc ├── README.md ├── messageBox.py ├── main.py └── networking.py /.gitignore: -------------------------------------------------------------------------------- 1 | config.py 2 | __pycache__ -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp 2 | git+https://github.com/Rapptz/discord.py@rewrite#egg=discord.py[voice] -------------------------------------------------------------------------------- /__pycache__/config.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FastestMolasses/HQ-Answers/HEAD/__pycache__/config.cpython-36.pyc -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HQ Answers Bot 2 | ### Crowdsourced HQ Trivia Discord Bot 3 | 4 | Invite this discord bot to your server and it will automatically collect answers from users whenever an HQ game is live. It will display everybody's answers nicely in any channel you specify. 5 | 6 | ### Installation 7 | ``` 8 | $ git clone https://github.com/FastestMolasses/HQ-Answers.git 9 | $ cd HQ-Answers 10 | $ pip install -r requirements.txt 11 | $ touch config.py 12 | ``` 13 | 14 | Your `config.py` file should look like this. Enter the relevant information. 15 | 16 | ``` 17 | DISCORD_TOKEN = 'enter your discord bot token' 18 | CHANNEL_ID = 000000 # Should be the channel you want the bot to post in. Leave as int. 19 | ``` 20 | -------------------------------------------------------------------------------- /messageBox.py: -------------------------------------------------------------------------------- 1 | import discord 2 | 3 | 4 | class MessageBox: 5 | def __init__(self): 6 | self.embed = discord.Embed(title='HQ Crowdsourced Answers') 7 | self.discordMsg = None # Stores the discord message object, so we can edit the msg 8 | 9 | async def resetEmbed(self, channel: discord.TextChannel, question: str, answers: list): 10 | """ 11 | Recreates the discord embed with the updated 12 | information. Will also automatically send the 13 | embed to the channel. 14 | 15 | :param channel: The discord channel to send the embed to. 16 | :param question: The trivia question being asked 17 | :param answers: A list of all the potential answers to the question 18 | """ 19 | # If we have an existing message, delete it before posting the new one 20 | # if self.discordMsg: 21 | # await self.discordMsg.delete() 22 | 23 | self.embed = self.createEmbed(question, answers, [0, 0, 0]) 24 | self.question = question 25 | self.answers = answers 26 | self.discordMsg = await channel.send(embed=self.embed) 27 | 28 | async def updateEmbedCounters(self, counts: list): 29 | """ 30 | Updates the discord embed with the new data. 31 | 32 | :param counts: A list of all the answer counts to each question 33 | """ 34 | if not self.discordMsg: 35 | return 36 | 37 | self.embed = self.createEmbed(self.question, self.answers, counts) 38 | # Edit the discord message to the new embed 39 | await self.discordMsg.edit(embed=self.embed) 40 | 41 | def createEmbed(self, question: list, answers: list, counts: list): 42 | """ 43 | Creates a discord embed object. 44 | 45 | :param: The question to be displayed. 46 | :parma: The list of potential answers 47 | """ 48 | embed = discord.Embed(title=question, color=0x4286F4) 49 | # embed.add_field(name='Question', value=question, inline=False) 50 | 51 | for i in range(min((len(answers), len(counts)))): 52 | embed.add_field(name=answers[i], value=counts[i], inline=False) 53 | 54 | embed.set_footer(text='Made by FMolasses') 55 | return embed 56 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import config 2 | import discord 3 | import asyncio 4 | import networking 5 | import messageBox 6 | 7 | 8 | class MyClient(discord.Client): 9 | def __init__(self, *args, **kwargs): 10 | """ 11 | Constructor that initializes background tasks. 12 | """ 13 | super().__init__(*args, **kwargs) 14 | 15 | # Background tasks 16 | self.showCheckerBG = self.loop.create_task(self.showChecker()) 17 | 18 | # Variables 19 | self.isGameLive = False 20 | self.answersCounts = [0, 0, 0] 21 | self.msgBox = messageBox.MessageBox() 22 | 23 | async def on_ready(self): 24 | """ 25 | Called when the discord bot logs in. 26 | """ 27 | print(f'{self.user.name} Logged In!') 28 | print('--------------------\n') 29 | 30 | async def on_message(self, message: discord.Message): 31 | """ 32 | Handles all the discord commands. 33 | """ 34 | # Make sure we don't respond to ourself or messages outside the channel 35 | # or when there is no active game 36 | if message.author == self.user or \ 37 | message.channel.id != config.CHANNEL_ID or \ 38 | not self.isGameLive: 39 | return 40 | 41 | # Make sure to not respond to DM messages 42 | if isinstance(message.author, discord.User): 43 | return 44 | 45 | # Prepare relevant variables 46 | msg = message.content.lower() 47 | 48 | if '1' in msg: 49 | self.answersCounts[0] += 1 50 | await self.msgBox.updateEmbedCounters(self.answersCounts) 51 | elif '2' in msg: 52 | self.answersCounts[1] += 1 53 | await self.msgBox.updateEmbedCounters(self.answersCounts) 54 | elif '3' in msg: 55 | self.answersCounts[2] += 1 56 | await self.msgBox.updateEmbedCounters(self.answersCounts) 57 | else: 58 | return 59 | 60 | # If the user submitted an answer, delete that message 61 | await message.delete() 62 | 63 | def resetAnswerCounts(self): 64 | """ 65 | Resets the answer counters to 0. 66 | """ 67 | self.answersCounts = [0, 0, 0] 68 | 69 | async def showChecker(self): 70 | """ 71 | Checks when shows are live and connects to 72 | their websockets. 73 | """ 74 | await self.wait_until_ready() 75 | 76 | while not self.is_closed(): 77 | # Check for shows 78 | broadcast = await networking.getBroadcast() 79 | 80 | # If there is an active show 81 | if broadcast: 82 | self.isGameLive = True 83 | socketUrl = broadcast.get('socketURL') 84 | channel = self.get_channel(config.CHANNEL_ID) 85 | 86 | # Add the websocket handler to the event loop 87 | # This web socket will get the questions and answers from HQ 88 | asyncio.get_event_loop().run_until_complete( 89 | networking.websocketHandler(socketUrl, 90 | channel, 91 | self.resetAnswerCounts, 92 | messageBox)) 93 | else: 94 | self.isGameLive = False 95 | await asyncio.sleep(60) 96 | 97 | 98 | if __name__ == '__main__': 99 | client = MyClient() 100 | client.run(config.DISCORD_TOKEN) 101 | -------------------------------------------------------------------------------- /networking.py: -------------------------------------------------------------------------------- 1 | import re 2 | import json 3 | import aiohttp 4 | import discord 5 | import messageBox 6 | 7 | BEARER_TOKEN = '' 8 | URL = 'https://api-quiz.hype.space/shows/now?type=' 9 | HEADERS = { 10 | 'Authorization': f'Bearer {BEARER_TOKEN}', 11 | 'x-hq-client': 'Android/1.3.0' 12 | } 13 | 14 | 15 | async def request(method: str, url: str, headers: dict = None, 16 | data: dict = None, stringify: bool = True, 17 | username: str=None, password: str=None): 18 | """ 19 | Asynchronous request that supports authentication and all 20 | RESTful HTTP calls. Returns back a JSON dict. 21 | 22 | :param method: The HTTP method to use. 23 | :param url: The URL to send a request to. 24 | :param headers: The headers for the request. 25 | :param data: The data to send. 26 | :param stringify: Whether to stringify the data or not. 27 | :param username: Username used for authentication. 28 | :param password: Password usef for authentication. 29 | """ 30 | # Setup and format data 31 | auth = None 32 | if username and password: 33 | auth = aiohttp.BasicAuth( 34 | login=username, password=password, encoding='utf-8') 35 | if data and stringify: 36 | data = json.dumps(data, sort_keys=False, separators=(',', ':')) 37 | 38 | # Make the request 39 | async with aiohttp.ClientSession(headers=headers, auth=auth) as session: 40 | async with session.request(method=method, url=url, data=data, timeout=5) as response: 41 | try: 42 | # Return the response in JSON 43 | d = await response.read() 44 | return json.loads(d) 45 | except: 46 | return {} 47 | 48 | 49 | async def getBroadcast(): 50 | """ 51 | Returns the broadcast data from HQ. If a game is live, 52 | it will return a dict of the game show information, otherwise 53 | it will return None. 54 | """ 55 | r = await request(method='GET', url=URL) 56 | return r.get('broadcast') 57 | 58 | 59 | async def websocketHandler(url: str, channel: discord.TextChannel, 60 | resetFunc, messageBox: messageBox.MessageBox): 61 | """ 62 | Handles websocket connections to HQ. Will retrieve both questions 63 | and answers during a live HQ game. 64 | 65 | :param url: The url of the socket to connect to. 66 | :param channel: The discord channel to send messages to. 67 | :param resetFunc: A callback to reset the answer counters when a new question appears 68 | :param messageBox: Used to store the discord embed information to display on Discord 69 | """ 70 | async with aiohttp.ClientSession() as session: 71 | async with session.ws_connect(url, headers=HEADERS, heartbeat=5, timeout=30) as ws: 72 | await channel.send('HQ Trivia game is live!') 73 | 74 | lastQuestion = '' 75 | 76 | # For every message received from the websocket... 77 | async for msg in ws: 78 | if msg.type == aiohttp.WSMsgType.TEXT: 79 | # Parse data into a dict object 80 | message = re.sub(r'[\x00-\x1f\x7f-\x9f]', '', msg.data) 81 | messageData = json.loads(message) 82 | 83 | if messageData.get('error'): 84 | await channel.send('Connection settings invalid!') 85 | 86 | elif messageData.get('type') == 'question': 87 | question = messageData.get('question') 88 | answers = [i.get('text') 89 | for i in messageData.get('answers')] 90 | 91 | print('QUESTION: ' + question) 92 | print('ANSWERS: ' + str(answers)) 93 | 94 | # If the question has changed, then reset the answer counters 95 | if lastQuestion != question: 96 | lastQuestion = question 97 | resetFunc() 98 | # Reset the question embed 99 | await messageBox.resetEmbed(channel, question, answers) 100 | --------------------------------------------------------------------------------