college-bot-v2/src/cogs/election.py

143 lines
5.5 KiB
Python
Raw Normal View History

2024-05-03 19:02:23 +01:00
"""
This module is only meant to be loaded during election times.
"""
import asyncio
2024-05-03 20:27:02 +01:00
import datetime
2024-05-03 19:02:23 +01:00
import logging
import re
import discord
import httpx
from bs4 import BeautifulSoup
from discord.ext import commands
SPAN_REGEX = re.compile(
2024-05-03 20:35:33 +01:00
r"^(?P<party>\D+)(?P<councillors>[0-9,]+)\scouncillors\s(?P<net>[0-9,]+)\scouncillors\s(?P<net_change>(gained|lost))$"
2024-05-03 19:02:23 +01:00
)
MULTI: dict[str, int] = {
"gained": 1,
"lost": -1
}
class ElectionCog(commands.Cog):
SOURCE = "https://bbc.com/"
HEADERS = {
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,"
"image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
"Accept-Language": "en-GB,en-US;q=0.9,en;q=0.8",
"Priority": "u=0, i",
"Sec-Ch-Ua": "\"Chromium\";v=\"124\", \"Google Chrome\";v=\"124\", \"Not-A.Brand\";v=\"99\"",
"Sec-Ch-Ua-Mobile": "?0",
"Sec-Ch-Ua-Platform": "\"Linux\"",
"Sec-Fetch-Dest": "document",
"Sec-Fetch-Mode": "navigate",
"Sec-Fetch-Site": "none",
"Sec-Fetch-User": "?1",
"Upgrade-Insecure-Requests": "1",
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 "
"Safari/537.36",
}
def __init__(self, bot):
self.bot = bot
self.log = logging.getLogger("jimmy.cogs.election")
def process_soup(self, soup: BeautifulSoup) -> dict[str, list[int]] | None:
good_soup = soup.find(attrs={"data-testid": "election-banner-results-bar"})
if not good_soup:
return
2024-05-03 20:23:37 +01:00
css: str = "\n".join([x.get_text() for x in soup.find_all("style")])
2024-05-03 19:02:23 +01:00
2024-05-03 20:26:15 +01:00
def find_colour(style_name: str, want: str = "background-color") -> str | None:
2024-05-03 20:22:50 +01:00
self.log.info("Looking for style %r", style_name)
2024-05-03 19:02:23 +01:00
index = css.index(style_name) + len(style_name) + 1
value = ""
for char in css[index:]:
if char == "}":
break
value += char
2024-05-03 19:14:33 +01:00
attributes = filter(None, value.split(";"))
2024-05-03 19:02:23 +01:00
parsed = {}
for attr in attributes:
name, val = attr.split(":")
parsed[name] = val
2024-05-03 20:25:30 +01:00
self.log.info("Parsed the following attributes: %r", parsed)
2024-05-03 19:02:23 +01:00
if want in parsed:
2024-05-03 20:25:30 +01:00
self.log.info("Returning %r: %r", want, parsed[want])
2024-05-03 19:14:33 +01:00
return parsed[want]
2024-05-03 20:25:30 +01:00
self.log.warning("%r was not in attributes.", want)
2024-05-03 19:02:23 +01:00
results: dict[str, list[int]] = {}
for child_ul in good_soup.children:
child_ul: BeautifulSoup
2024-05-03 20:19:46 +01:00
span = child_ul.find("span", recursive=False)
2024-05-03 19:02:23 +01:00
if not span:
self.log.warning("%r did not have a 'span' element.", child_ul)
continue
2024-05-03 20:36:19 +01:00
text = span.get_text().replace(",", "")
2024-05-03 19:02:23 +01:00
groups = SPAN_REGEX.match(text)
if groups:
groups = groups.groupdict()
else:
self.log.warning(
"Found span element (%r), however resolved text (%r) did not match regex.",
span, text
)
continue
2024-05-03 20:34:56 +01:00
results[str(groups["party"]).strip()] = [
2024-05-03 20:36:19 +01:00
int(groups["councillors"].strip()),
int(groups["net"].strip()) * MULTI[groups["net_change"]],
2024-05-03 20:21:33 +01:00
int(find_colour(child_ul.next["class"][0])[1:], base=16)
2024-05-03 19:02:23 +01:00
]
return results
@commands.slash_command(name="election")
async def get_election_results(self, ctx: discord.ApplicationContext):
"""Gets the current election results"""
await ctx.defer()
async with httpx.AsyncClient(headers=self.HEADERS) as client:
2024-05-03 19:12:33 +01:00
response = await client.get(self.SOURCE, follow_redirects=True)
2024-05-03 19:02:23 +01:00
if response.status_code != 200:
return await ctx.respond(
"Sorry, I can't do that right now (HTTP %d while fetching results from BBC)" % response.status_code
)
# noinspection PyTypeChecker
2024-05-03 19:14:33 +01:00
soup = await asyncio.to_thread(BeautifulSoup, response.text, "html.parser")
2024-05-03 19:02:23 +01:00
results = await self.bot.loop.run_in_executor(None, self.process_soup, soup)
if results:
2024-05-03 20:37:08 +01:00
now = discord.utils.utcnow()
date = now.date().strftime("%B %Y")
2024-05-03 19:02:23 +01:00
colour_scores = {}
embed = discord.Embed(
title="Election results - " + date,
2024-05-03 20:37:08 +01:00
url="https://bbc.co.uk/",
timestamp=now
2024-05-03 19:02:23 +01:00
)
embed.set_footer(text="Source from bbc.co.uk.")
description_parts = []
for party_name, values in results.items():
councillors, net, colour = values
colour_scores[party_name] = councillors
2024-05-03 20:39:08 +01:00
symbol = "+" if net > 0 else ''
2024-05-03 19:02:23 +01:00
description_parts.append(
2024-05-03 20:39:08 +01:00
f"**{party_name}**: {symbol}{net:,} ({councillors:,} total)"
2024-05-03 19:02:23 +01:00
)
top_party = list(sorted(colour_scores.keys(), key=lambda k: colour_scores[k], reverse=True))[0]
embed.colour = discord.Colour(results[top_party][2])
2024-05-03 20:29:00 +01:00
embed.description = "\n".join(description_parts)
2024-05-03 19:02:23 +01:00
return await ctx.respond(embed=embed)
else:
return await ctx.respond("Unable to get election results at this time.")
def setup(bot):
bot.add_cog(ElectionCog(bot))