college-bot-v2/src/cogs/election.py
nexy7574 249d55c180
All checks were successful
Build and Publish Jimmy.2 / build_and_publish (push) Successful in 9s
Add mroe detail
2024-05-03 20:39:08 +01:00

142 lines
5.5 KiB
Python

"""
This module is only meant to be loaded during election times.
"""
import asyncio
import datetime
import logging
import re
import discord
import httpx
from bs4 import BeautifulSoup
from discord.ext import commands
SPAN_REGEX = re.compile(
r"^(?P<party>\D+)(?P<councillors>[0-9,]+)\scouncillors\s(?P<net>[0-9,]+)\scouncillors\s(?P<net_change>(gained|lost))$"
)
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
css: str = "\n".join([x.get_text() for x in soup.find_all("style")])
def find_colour(style_name: str, want: str = "background-color") -> str | None:
self.log.info("Looking for style %r", style_name)
index = css.index(style_name) + len(style_name) + 1
value = ""
for char in css[index:]:
if char == "}":
break
value += char
attributes = filter(None, value.split(";"))
parsed = {}
for attr in attributes:
name, val = attr.split(":")
parsed[name] = val
self.log.info("Parsed the following attributes: %r", parsed)
if want in parsed:
self.log.info("Returning %r: %r", want, parsed[want])
return parsed[want]
self.log.warning("%r was not in attributes.", want)
results: dict[str, list[int]] = {}
for child_ul in good_soup.children:
child_ul: BeautifulSoup
span = child_ul.find("span", recursive=False)
if not span:
self.log.warning("%r did not have a 'span' element.", child_ul)
continue
text = span.get_text().replace(",", "")
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
results[str(groups["party"]).strip()] = [
int(groups["councillors"].strip()),
int(groups["net"].strip()) * MULTI[groups["net_change"]],
int(find_colour(child_ul.next["class"][0])[1:], base=16)
]
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:
response = await client.get(self.SOURCE, follow_redirects=True)
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
soup = await asyncio.to_thread(BeautifulSoup, response.text, "html.parser")
results = await self.bot.loop.run_in_executor(None, self.process_soup, soup)
if results:
now = discord.utils.utcnow()
date = now.date().strftime("%B %Y")
colour_scores = {}
embed = discord.Embed(
title="Election results - " + date,
url="https://bbc.co.uk/",
timestamp=now
)
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
symbol = "+" if net > 0 else ''
description_parts.append(
f"**{party_name}**: {symbol}{net:,} ({councillors:,} total)"
)
top_party = list(sorted(colour_scores.keys(), key=lambda k: colour_scores[k], reverse=True))[0]
embed.colour = discord.Colour(results[top_party][2])
embed.description = "\n".join(description_parts)
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))