diff --git a/backend/hoagiemail/api/mail_view.py b/backend/hoagiemail/api/mail_view.py index 8eb7478..05c0baa 100644 --- a/backend/hoagiemail/api/mail_view.py +++ b/backend/hoagiemail/api/mail_view.py @@ -7,6 +7,7 @@ from rest_framework.response import Response from rest_framework.views import APIView +from hoagiemail.email import get_listservs from hoagiemail.email.limiter import Visitor from hoagiemail.email.mailjet_client import get_mailjet_client from hoagiemail.email.sanitize import sanitize_html @@ -120,20 +121,6 @@ def delete(self, request) -> Response: return Response({"status": "OK", "message": "Scheduled mail deleted successfully"}, status=status.HTTP_200_OK) -def get_listservs(): - """Returns list of college listserv recipients""" - return [ - {"Email": "BUTLERBUZZ@PRINCETON.EDU", "Name": "Butler"}, - {"Email": "WHITMANWIRE@PRINCETON.EDU", "Name": "Whitman"}, - {"Email": "RockyWire@PRINCETON.EDU", "Name": "Rocky"}, - {"Email": "Re-INNformer@PRINCETON.EDU", "Name": "Forbes"}, - {"Email": "westwire@princeton.edu", "Name": "NCW"}, - {"Email": "matheymail@PRINCETON.EDU", "Name": "Mathey"}, - {"Email": "yehyellowpages@princeton.edu", "Name": "Yeh"}, - {"Email": "hoagiemailgradstudents@princeton.edu", "Name": "hoagiemailgradstudents"}, - ] - - def create_message(mail_data, sender_email, to_email): """ Creates mailjet message structure. diff --git a/backend/hoagiemail/email/__init__.py b/backend/hoagiemail/email/__init__.py new file mode 100644 index 0000000..bd3ac5e --- /dev/null +++ b/backend/hoagiemail/email/__init__.py @@ -0,0 +1,12 @@ +def get_listservs(): + """Returns list of college listserv recipients""" + return [ + {"Email": "BUTLERBUZZ@PRINCETON.EDU", "Name": "Butler"}, + {"Email": "WHITMANWIRE@PRINCETON.EDU", "Name": "Whitman"}, + {"Email": "RockyWire@PRINCETON.EDU", "Name": "Rocky"}, + {"Email": "Re-INNformer@PRINCETON.EDU", "Name": "Forbes"}, + {"Email": "westwire@princeton.edu", "Name": "NCW"}, + {"Email": "matheymail@PRINCETON.EDU", "Name": "Mathey"}, + {"Email": "yehyellowpages@princeton.edu", "Name": "Yeh"}, + {"Email": "hoagiemailgradstudents@princeton.edu", "Name": "hoagiemailgradstudents"}, + ] diff --git a/backend/hoagiemail/stuff/digest.py b/backend/hoagiemail/stuff/digest.py new file mode 100644 index 0000000..fa887b1 --- /dev/null +++ b/backend/hoagiemail/stuff/digest.py @@ -0,0 +1,209 @@ +import logging +from datetime import timedelta +from os import getenv +from typing import Final, Iterable, TypedDict + +from django.utils import timezone + +from hoagiemail.email import get_listservs +from hoagiemail.email.mailjet_client import get_mailjet_client +from hoagiemail.models import StuffPost + +REQUEST_TIMEOUT: Final[timedelta] = timedelta(seconds=10) +SUMMER: Final[bool] = False +HOAGIE_SANDWICH_LOGO: Final[str] = "" +LOGO: Final[str] = "Hoagie Digest" +IS_PRODUCTON: Final[bool] = getenv("HOAGIE_MODE") == "production" + +logger = logging.getLogger(__name__) + + +def link(text: str, link: str) -> str: + return f"{text}" + + +def link_mail(text: str) -> str: + return link(text, "mailto:" + text) + + +def format_tag(text: str) -> str: + return f'{text.title()}' + + +def add_tags(email: str, tags: Iterable[str]) -> str: + email += "
" + + for tag in tags: + email += format_tag(tag) + " " + + email += "
" + + return email + + +def format_message(message: StuffPost) -> str: + email = "" + name = str(message.user) + link_email = link_mail(message.user.email) + + match str(message.category): + case "sale": + tags = [str(t) for t in message.tags.all()] + + if not tags: + tags = message.title.split(", ") + # There's a TODO: remove here that I am not sure if I should remove + + email += f"
{message.description}
" + email += f"Contact: {name} ({link_email})
" + add_tags(email, tags) + case "lost": + if message.thumbnail: + email += 'See Picture
' + + email += "" + message.tags[0].upper() + ": " + message.title + "
" + email += "
" + message.description + "
" + email += f"Contact: {name} ({link_email})
" + case _: + email += "" + message.title + "
" + email += "
" + message.description + "
" + email += f"From: {name} ({link_email})
" + add_tags(email, [str(t) for t in message.tags.all()]) + + return email + + +MailJetData = TypedDict( + "MailJetData", + { + "From": dict[str, str], + "ReplyTo": dict[str, str], + "Cc": list[dict[str, str]], + "Subject": str, + "Text-part": str, + "Html-part": str, + "CustomID": str, + }, +) + + +def mail_request(header: str, sender: str, body: str, email: str) -> None: + data: MailJetData = { + "From": {"Email": "hoagie@princeton.edu", "Name": sender}, + "ReplyTo": {"Email": email, "Name": sender}, + "Cc": get_listservs(), + "Subject": header, + "Text-part": body, + "Html-part": body, + "CustomID": "HoagieStuffDigest", + } + + res = get_mailjet_client().send.create(data={"Messages": [data]}) + res.raise_for_status() + + if res.status_code != 201: + raise RuntimeError("did not receive HTTP 201") + + if res.json()[0]["Status"] != "success": + raise RuntimeError("message send not successful") + + +def run_digest_script() -> None: + posts = StuffPost.objects.filter(has_sent=False) + + if not posts: + logging.info("No messages found...Exiting...") + return + + digest: dict[str, list[StuffPost]] = {} + + for post in posts: + category_posts = digest.get(post.category.name, []) + category_posts.append(post) + digest[post.category.name] = category_posts + + if len(digest) < 5: + is_weekday = timezone.now().weekday() in {1, 3, 5} + + is_digest_day = is_weekday and not SUMMER + + if not is_digest_day: + logger.info("not a digest day...Exiting...") + return + + logger.info("5 or more digest posts...Running...") + + body = f""" +
+
{LOGO}
+ + { + '


Here is a digest of posts made to Hoagie Stuff

over past few days. It\'s Summer, so Hoagie is taking things slow.

' + if SUMMER + else '''


Here is a weekly digest of posts made to Hoagie Stuff, + from Sales to Lost & Found and more, sent every Tuesday, Thursday, and Saturday.

''' + } + +

+ Open Hoagie Stuff | + Add your message to next digest | + Give feedback +

+
+ """ + + if lost_posts := digest["lost"]: + body += """ +

🧭 Lost & Found

+
Access anytime through stuff.hoagie.io/lost
+ + """ + for i, post in enumerate(lost_posts): + body += format_message(post) + + if i == len(lost_posts) - 1: + body += "
" + + body += "
" + + if bulletin_posts := digest["bulletin"]: + body += """ +

✉️ Bulletins

+
Accessible anytime with stuff.hoagie.io/bulletins
+ """ + + for i, post in enumerate(bulletin_posts): + body += format_message(post) + + if i == len(bulletin_posts) - 1: + body += "
" + + body += "
" + + if len(digest) != 1: + body += f"

That's all! This could have been {len(digest)} emails in your inbox but instead it is just one!

" + + body += f""" +

You don't need to wait for the next digest to see what's new, check out the Hoagie Stuff + to keep up to date with the latest posts before others.

+
+ {HOAGIE_SANDWICH_LOGO}
+
+ Powered by HoagieMail
+ In the Hoagie world, hoagies digest you! +
+
+
+ """ + + if IS_PRODUCTON: + mail_request( + header=f"📬 DIGEST {timezone.now().strftime('%m/%d')}: Sales, Lost & Found, and more!", + sender="Hoagie Mail", + body=body, + email="hoagie@princeton.edu", + ) + + logger.info("Sucessfully sent via Hoagie Mail") + + posts.update(has_sent=True)