Skip to content

Choose between FrugalFeeds and Couponese (issue #204) #205

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jul 5, 2024
Merged
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
217 changes: 173 additions & 44 deletions uqcsbot/dominos_coupons.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from datetime import datetime
from typing import List
from typing import List, Dict, Literal
import logging
import requests
from requests.exceptions import RequestException
from bs4 import BeautifulSoup
import random

import discord
from discord import app_commands
Expand All @@ -12,19 +13,21 @@
from uqcsbot.bot import UQCSBot
from uqcsbot.yelling import yelling_exemptor


MAX_COUPONS = 10 # Prevents abuse
NUMBER_WEBSITES = 2
COUPONESE_DOMINOS_URL = "https://www.couponese.com/store/dominos.com.au/"
FRUGAL_FEEDS_DOMINOS_URL = "https://www.frugalfeeds.com.au/dominos/"


class HTTPResponseException(Exception):
"""
An exception for when a HTTP response is not requests.codes.ok
"""

def __init__(self, http_code: int, *args: object) -> None:
def __init__(self, http_code: int, url: str, *args: object) -> None:
super().__init__(*args)
self.http_code = http_code
self.url = url


class DominosCoupons(commands.Cog):
Expand All @@ -36,6 +39,7 @@ def __init__(self, bot: UQCSBot):
number_of_coupons="The number of coupons to return. Defaults to 5 with max 10.",
ignore_expiry="Indicates to include coupons that have expired. Defaults to True.",
keywords="Words to search for within the coupon. All coupons descriptions will mention at least one keyword.",
source="Website to source coupons from (couponese or frugalfeeds). Defaults to both.",
)
@yelling_exemptor(input_args=["keywords"])
async def dominoscoupons(
Expand All @@ -44,50 +48,105 @@ async def dominoscoupons(
number_of_coupons: int = 5,
ignore_expiry: bool = True,
keywords: str = "",
source: Literal["both", "couponese", "frugalfeeds"] = "both",
):
"""
Returns a list of dominos coupons
"""

if number_of_coupons < 1 or number_of_coupons > MAX_COUPONS:
await interaction.response.send_message(
content=f"You can't have that many coupons. Try a number between 1 and {MAX_COUPONS}.",
ephemeral=True,
)
return

if source != "both":
if source != "couponese" and source != "frugalfeeds":
await interaction.response.send_message(
content=f"That website isn't recognised. Try couponese or frugalfeeds.",
ephemeral=True,
)
return

await interaction.response.defer(thinking=True)

try:
coupons = _get_coupons(number_of_coupons, ignore_expiry, keywords.split())
except RequestException as error:
resp_content = (
error.response.content if error.response else "No response error given."
)
logging.warning(
f"Could not connect to dominos coupon site ({COUPONESE_DOMINOS_URL}): {resp_content}"
)
await interaction.edit_original_response(
content=f"Sadly could not reach the coupon website (<{COUPONESE_DOMINOS_URL}>)..."
)
return
except HTTPResponseException as error:
logging.warning(
f"Received a HTTP response code {error.http_code}. Error information: {error}"
)
await interaction.edit_original_response(
content=f"Could not find the coupons on the coupon website (<{COUPONESE_DOMINOS_URL}>)..."
)
return
coupons: List[Coupon] = []
stored_url: str = " "

for tries in range(0, NUMBER_WEBSITES):
try:
coupons = _get_coupons(
number_of_coupons, ignore_expiry, keywords.split(), source
)
break
except (RequestException, HTTPResponseException) as error:
if isinstance(error, RequestException):
request_url: str = (
error.request.url
if error.request and error.request.url
else "Unknown site."
)
resp_content = (
error.response.content
if error.response
else "No response error given."
)
logging.warning(
f"Could not connect to dominos coupon site ({request_url}): {resp_content}"
)
stored_url = request_url

if isinstance(error, HTTPResponseException):
logging.warning(
f"Received a HTTP response code {error.http_code}. Error information: {error}"
)
stored_url = error.url

if source != "both" and tries == 0:
await interaction.edit_original_response(
content=f"Looks like that website ({stored_url}) isn't working. Try changing the website through the `source` command."
)
return

elif source == "both" or tries > 0:
if tries == 0:
await interaction.edit_original_response(
content=f"Looks like that website ({stored_url}) didn't work.. trying another."
)
if stored_url == COUPONESE_DOMINOS_URL:
source = "frugalfeeds"
elif stored_url == FRUGAL_FEEDS_DOMINOS_URL:
source = "couponese"
else:
await interaction.edit_original_response(
content=f"Unfortunately, it looks like both coupon websites are down right now."
)
return

if not coupons:
await interaction.edit_original_response(
content=f"Could not find any coupons matching the given arguments from the coupon website (<{COUPONESE_DOMINOS_URL}>)."
)
if source == "both":
content_str = "Could not find any coupons matching the given arguments from both websites."
else:
content_str = f"Could not find any coupons matching the given arguments from {source}. You can try changing the website through the `source` command."
await interaction.edit_original_response(content=content_str)
return

if source == "both":
description_string = f"Sourced from [FrugalFeeds]({COUPONESE_DOMINOS_URL}) and [Couponese]({FRUGAL_FEEDS_DOMINOS_URL})"
elif source == "couponese":
description_string = f"Sourced from [Couponese]({COUPONESE_DOMINOS_URL})"
else:
description_string = (
f"Sourced from [FrugalFeeds]({FRUGAL_FEEDS_DOMINOS_URL})"
)

if keywords:
description_string += f"\nKeywords: *{keywords}*"

embed = discord.Embed(
title="Domino's Coupons",
url=COUPONESE_DOMINOS_URL,
description=f"Keywords: *{keywords}*" if keywords else None,
description=description_string,
timestamp=datetime.now(),
)
for coupon in coupons:
Expand Down Expand Up @@ -123,12 +182,14 @@ def keyword_matches(self, keyword: str) -> bool:
return keyword.lower() in self.description.lower()


def _get_coupons(n: int, ignore_expiry: bool, keywords: List[str]) -> List[Coupon]:
def _get_coupons(
n: int, ignore_expiry: bool, keywords: List[str], source: str
) -> List[Coupon]:
"""
Returns a list of n Coupons
"""

coupons = _get_coupons_from_page()
coupons: List[Coupon] = _get_coupons_from_page(source)

if not ignore_expiry:
coupons = [coupon for coupon in coupons if coupon.is_valid()]
Expand All @@ -139,27 +200,95 @@ def _get_coupons(n: int, ignore_expiry: bool, keywords: List[str]) -> List[Coupo
for coupon in coupons
if any(coupon.keyword_matches(keyword) for keyword in keywords)
]

random.shuffle(coupons)

return coupons[:n]


def _get_coupons_from_page() -> List[Coupon]:
def _get_coupons_from_page(source: str) -> List[Coupon]:
"""
Strips results from html page and returns a list of Coupon(s)
"""
http_response = requests.get(COUPONESE_DOMINOS_URL)
if http_response.status_code != requests.codes.ok:
raise HTTPResponseException(http_response.status_code)
soup = BeautifulSoup(http_response.content, "html.parser")
soup_coupons = soup.find_all(class_="ov-coupon")

urls: List[str] = []
coupons: List[Coupon] = []

for soup_coupon in soup_coupons:
expiry_date_str = soup_coupon.find(class_="ov-expiry").get_text(strip=True)
description = soup_coupon.find(class_="ov-desc").get_text(strip=True)
code = soup_coupon.find(class_="ov-code").get_text(strip=True)
coupon = Coupon(code, expiry_date_str, description)
coupons.append(coupon)
website_coupon_classes: Dict[str, Dict[str, str]] = {
COUPONESE_DOMINOS_URL: {
"expiry": "ov-expiry",
"description": "ov-desc",
"code": "ov-code",
},
FRUGAL_FEEDS_DOMINOS_URL: {
"expiry": "column-3",
"description": "column-2",
"code": "column-1",
},
}

if source == "couponese":
urls.append(COUPONESE_DOMINOS_URL)
elif source == "frugalfeeds":
urls.append(FRUGAL_FEEDS_DOMINOS_URL)
else:
urls = [FRUGAL_FEEDS_DOMINOS_URL, COUPONESE_DOMINOS_URL]

for url in urls:
http_response: requests.Response = requests.get(url)
if http_response.status_code != requests.codes.ok:
raise HTTPResponseException(http_response.status_code, url)

soup = BeautifulSoup(http_response.content, "html.parser")
soup_coupons: List[BeautifulSoup] = []

if url == COUPONESE_DOMINOS_URL:
soup_coupons = soup.find_all(class_="ov-coupon")
elif url == FRUGAL_FEEDS_DOMINOS_URL:
tables = soup.select('[class^="tablepress"]')
for table in tables:
# Headers have stuff we don't want
rows = table.find_all("tr")[1:]
soup_coupons.extend(rows)

siteclass: Dict[str, str] = website_coupon_classes.get(url, {})

for soup_coupon in soup_coupons:
expiry_date_container = soup_coupon.find(class_=siteclass.get("expiry"))
description_container = soup_coupon.find(
class_=siteclass.get("description")
)
code_container = soup_coupon.find(class_=siteclass.get("code"))

if (
not expiry_date_container
or not description_container
or not code_container
):
continue

expiry_date_str: str = expiry_date_container.get_text(strip=True)
description: str = description_container.get_text(strip=True)
code: str = code_container.get_text(strip=True)

temp_code: str = code.replace(",", "")
if not temp_code.isdigit():
continue

code = code.replace(",", " ")

if url == FRUGAL_FEEDS_DOMINOS_URL:
date_values: List[str] = expiry_date_str.split()
try:
# Convert shortened month to numerical value
month: int = datetime.strptime(date_values[1], "%b").month
expiry_date_str = "{year}-{month}-{day}".format(
year=int(date_values[2]), month=month, day=int(date_values[0])
)
except (ValueError, IndexError):
pass

coupon = Coupon(code, expiry_date_str, description)
coupons.append(coupon)

return coupons

Expand Down