Skip to content

Commit

Permalink
Merge pull request #266 from ItsCalebJones/notification_testing
Browse files Browse the repository at this point in the history
feat(bot): update notification service
  • Loading branch information
ItsCalebJones authored Sep 22, 2024
2 parents e79d2c0 + 36bd70f commit 987461f
Show file tree
Hide file tree
Showing 9 changed files with 549 additions and 132 deletions.
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ src/xmlrunner/**
report/
*secret*.yaml
dist/**
.vscode/**
docker/*.backup
*.log
.history/
.history/
**.json
13 changes: 13 additions & 0 deletions .vscode/SpaceLaunchNow-Server.code-workspace
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"folders": [
{
"path": "../."
},
{
"path": "../../LaunchLibrary-API"
}
],
"settings": {
"editor.defaultFormatter": "charliermarsh.ruff"
}
}
5 changes: 5 additions & 0 deletions k8s/helm/templates/notification-service/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,11 @@ spec:
secretKeyRef:
name: sln-sentry-key
key: sln-sentry-key
- name: WEBHOOK_URL
valueFrom:
secretKeyRef:
name: sln-auth-prod
key: discord-webhook-notification
- name: SERVER_ROLE
value: "worker"
envFrom:
Expand Down
350 changes: 253 additions & 97 deletions poetry.lock

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ pymemcache = "^4.0.0"
sentry-sdk = {extras = ["django"], version = "^2.8.0"}
django-silk = "^5.1.0"
toml = "^0.10.2"
google-api-python-client = "^2.146.0"
google-auth = "^2.34.0"
google-auth-httplib2 = "^0.2.0"
google-auth-oauthlib = "^1.2.1"


[tool.poetry.group.dev]
Expand Down
6 changes: 1 addition & 5 deletions src/app/models.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import datetime
import os

try:
from urllib import quote # Python 2.X
except ImportError:
from urllib.parse import quote # Python 3+
from urllib.parse import quote # Python 3+

from django.conf import settings
from django.core.files.storage import DefaultStorage, default_storage
Expand Down
220 changes: 194 additions & 26 deletions src/bot/app/notifications/notification_handler.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import logging
import uuid
from dataclasses import dataclass
from datetime import datetime
from typing import Dict, List

import pytz
from api.models import Article, Events, Launch
from discord_webhook import DiscordEmbed, DiscordWebhook
from django.core.cache import cache

from bot.app.notification_service import NotificationService
Expand All @@ -13,13 +17,21 @@
get_fcm_strict_topics_v3,
get_flutter_topics_v3,
)
from spacelaunchnow import settings

logger = logging.getLogger(__name__)


# TODO refactor to separate files/modules per version
@dataclass
class NotificationResult:
notification_type: str
topics: str
result: str | None
analytics_label: str
error: Exception | None


# TODO refactor to separate files/modules per version
class NotificationHandler(NotificationService):
def send_notification(self, launch: Launch, notification_type: str, notification: LaunchNotificationRecord):
current_time = datetime.now(tz=pytz.utc)
Expand All @@ -45,7 +57,7 @@ def send_notification(self, launch: Launch, notification_type: str, notification
elif launch.status.id == 2 or launch.status.id == 5 or launch.status.id == 8:
contents = "UPDATE: Launch has slipped, new launch date is unconfirmed."
else:
logger.warning("Invalid state for sending a notification - Launch: %s" % launch)
logger.warning(f"{notification_type} Invalid state for sending a notification - Launch: {launch}")
return
elif notification_type == "tenMinutes":
minutes = round(diff / 60)
Expand All @@ -54,13 +66,13 @@ def send_notification(self, launch: Launch, notification_type: str, notification
if launch.status.id == 1:
contents = f"Launch attempt from {launch.pad.location.name} in {minutes} minute(s)."
else:
logger.warning("Invalid state for sending a notification - Launch: %s" % launch)
logger.warning(f"{notification_type} Invalid state for sending a notification - Launch: {launch}")
return
elif notification_type == "oneMinute":
if launch.status.id == 1:
contents = "Launch attempt from %s in less than one minute." % launch.pad.location.name
else:
logger.warning("Invalid state for sending a notification - Launch: %s" % launch)
logger.warning(f"{notification_type} Invalid state for sending a notification - Launch: {launch}")
return
elif notification_type == "twentyFourHour":
hours = round(diff / 60 / 60)
Expand All @@ -71,15 +83,15 @@ def send_notification(self, launch: Launch, notification_type: str, notification
elif launch.status.id == 2 or launch.status.id == 5:
contents = f"Might be launching from {launch.pad.location.name} in {hours} hours."
else:
logger.warning("Invalid state for sending a notification - Launch: %s" % launch)
logger.warning(f"{notification_type} Invalid state for sending a notification - Launch: {launch}")
return
elif notification_type == "oneHour":
if launch.status.id == 1:
contents = "Launch attempt from %s in one hour." % launch.pad.location.name
elif launch.status.id == 2 or launch.status.id == 5:
contents = "Might be launching from %s in one hour." % launch.pad.location.name
else:
logger.error("Invalid state for sending a notification - Launch: %s" % launch)
logger.warning(f"{notification_type} Invalid state for sending a notification - Launch: {launch}")
return
elif notification_type == "success":
if (
Expand Down Expand Up @@ -164,22 +176,61 @@ def send_v3_notification(self, launch: Launch, notification_type: str, contents:
"launch_location": launch.pad.location.name,
}

all_topics = get_fcm_all_topics_v3(debug=self.DEBUG, notification_type=notification_type)
strict_topics = get_fcm_strict_topics_v3(launch, debug=self.DEBUG, notification_type=notification_type)
not_strict_topics = get_fcm_not_strict_topics_v3(launch, debug=self.DEBUG, notification_type=notification_type)
self.send_notif_v3(data, all_topics)
self.send_notif_v3(data, strict_topics)
self.send_notif_v3(data, not_strict_topics)
logger.info(f"Topics:\n\nALL: {all_topics}\nStrict: {strict_topics}\nNot Strict: {not_strict_topics}")
# Reusing topics from v2 - not doing strict topics
flutter_topics_v3 = get_flutter_topics_v3(
launch, notification_type=notification_type, debug=self.DEBUG, flutter=True
all_result = self.send_notif_v3(
data=data,
topics=get_fcm_all_topics_v3(debug=self.DEBUG, notification_type=notification_type),
analytics_label=f"notification_all_{data['launch_uuid']}",
)

strict_result = self.send_notif_v3(
data=data,
topics=get_fcm_strict_topics_v3(launch, debug=self.DEBUG, notification_type=notification_type),
analytics_label=f"notification_strict_{data['launch_uuid']}",
)

not_strict_result = self.send_notif_v3(
data=data,
topics=get_fcm_not_strict_topics_v3(launch, debug=self.DEBUG, notification_type=notification_type),
analytics_label=f"notification_not_strict_{data['launch_uuid']}",
)

flutter_result = self.send_notif_v3(
data=data,
topics=get_flutter_topics_v3(launch, notification_type=notification_type, debug=self.DEBUG, flutter=True),
message_title=launch.name,
message_body=contents,
analytics_label=f"notification_flutter_{data['launch_uuid']}",
)
self.send_notif_v3(data, flutter_topics_v3, message_title=launch.name, message_body=contents)

def send_notif_v3(self, data, topics, message_title=None, message_body=None):
# Send notifications to SLN Android > v3.7.0
# Catch any issue with sending notification.
self.notify_discord([all_result, strict_result, not_strict_result, flutter_result], data)

def send_debug_notif(self):
if self.DEBUG:
topics = "'debug_v3' in topics && 'newZealand' in topics" # noqa: E501
generated = str(uuid.uuid4())
analytics_label = "debug_test_topics_{generated}"

results = self.fcm.notify(
topic_condition=topics,
notification_title="Test Notification",
notification_body=f"{generated}\n{topics}",
android_config={"priority": "high", "collapse_key": generated, "ttl": "86400s"},
fcm_options={"analytics_label": analytics_label},
)

logger.info(f"NOTIF: {results} - {generated}")
self.notify_discord(
[
NotificationResult(
notification_type="Debug", topics=topics, analytics_label=analytics_label, result=results
)
],
data=None,
)

def send_notif_v3(
self, data, topics, message_title=None, message_body=None, analytics_label: str = None
) -> NotificationResult:
try:
logger.info("Notification v3 Data - %s" % data)
logger.info("Topic Data v3- %s" % topics)
Expand All @@ -188,13 +239,31 @@ def send_notif_v3(self, data, topics, message_title=None, message_body=None):
topic_condition=topics,
notification_title=message_title,
notification_body=message_body,
fcm_options={"analytics_label": analytics_label},
android_config={"priority": "high", "collapse_key": data["launch_uuid"], "ttl": "86400s"},
timeout=240,
)
logger.info(results)
return NotificationResult(
notification_type=data["notification_type"],
topics=topics,
result=results,
analytics_label=analytics_label,
error=None,
)
except Exception as e:
logger.error(e)
return NotificationResult(
notification_type=data["notification_type"],
topics=topics,
result=results,
analytics_label=analytics_label,
error=e,
)

def send_custom_ios_v3(self, pending):
def send_custom_ios_v3(self, pending) -> NotificationResult:
data = self.get_json_data(pending)
label = "notification_custom_ios"

if not self.DEBUG:
flutter_topics = "'flutter_production_v3' in topics && 'custom' in topics"
Expand All @@ -211,15 +280,32 @@ def send_custom_ios_v3(self, pending):
topic_condition=flutter_topics,
notification_title=pending.title,
notification_body=pending.message,
fcm_options={"analytics_label": label},
android_config={"priority": "high", "collapse_key": data["launch_uuid"], "ttl": "86400s"},
timeout=240,
)
logger.info(flutter_results)
return NotificationResult(
notification_type=data["notification_type"],
topics=flutter_topics,
result=flutter_results,
analytics_label=label,
error=None,
)
except Exception as e:
logger.error(e)
self.notify_discord(data=data, topics=flutter_topics, analytics_label=label, error=e)
return NotificationResult(
notification_type=data["notification_type"],
topics=flutter_topics,
result=None,
analytics_label=label,
error=e,
)

logger.info("----------------------------------------------------------")

def send_custom_android_v3(self, pending):
def send_custom_android_v3(self, pending) -> NotificationResult:
data = self.get_json_data(pending)
label = "notification_custom_android"

if not self.DEBUG:
topics = "'prod_v3' in topics && 'custom' in topics"
Expand All @@ -234,12 +320,27 @@ def send_custom_android_v3(self, pending):
android_result = self.fcm.notify(
data_payload=data,
topic_condition=topics,
fcm_options={"analytics_label": label},
android_config={"priority": "high", "collapse_key": data["launch_uuid"], "ttl": "86400s"},
timeout=240,
)
logger.info(android_result)
return NotificationResult(
notification_type=data["notification_type"],
topics=topics,
result=android_result,
analytics_label=label,
error=None,
)
except Exception as e:
logger.error(e)

logger.info("----------------------------------------------------------")
return NotificationResult(
notification_type=data["notification_type"],
topics=topics,
result=None,
analytics_label=label,
error=e,
)

def get_json_data(self, pending):
data = {
Expand Down Expand Up @@ -313,3 +414,70 @@ def get_json_data(self, pending):
}
)
return data

def notify_discord(
self,
notification_results: List[NotificationResult] = None,
data: Dict[str, str] = None,
) -> None:
launch_name = data.get("launch_name", "Unknown") if data else "Unknown"
launch_uuid = data.get("launch_uuid", "Unknown") if data else "Unknown"
launch_net = data.get("launch_net", "Unknown") if data else "Unknown"
launch_location = data.get("launch_location", "Unknown") if data else "Unknown"
launch_image = data.get("launch_image") if data else None

# Set up the webhook
webhook = DiscordWebhook(
url=settings.DISCORD_WEBHOOK,
username="Notification Tracker",
avatar_url="https://thespacedevs-prod.nyc3.digitaloceanspaces.com/static/home/img/launcher.png",
)

description = ""
for notification_result in notification_results:
fcm_result = {"title": None, "description": None}
if notification_result.error:
fcm_result["title"] = "Error"
fcm_result["description"] = f"`{notification_result.error}`"
if notification_result.result:
fcm_result["title"] = "Result"
fcm_result["description"] = f"`{notification_result.result}`"
if notification_result.result and notification_result.error:
fcm_result["title"] = "Result w/ Error"
fcm_result["description"] = f"`{notification_result.result}`\n`{notification_result.error}`"

description += (
f"**Notification Type:** `{notification_result.notification_type}`\n"
f"**Analytics Label:** `{notification_result.analytics_label}`\n"
f"**Topics:** `{notification_result.topics}`\n"
f"**{fcm_result["title"]}:** {fcm_result["description"]}\n"
f"{'-' * 50}\n"
)

# Create the Embed
embed = DiscordEmbed(
title=f"🚀 {launch_name} 🚀",
description=description,
color="03b2f8",
)

# Add fields for relevant data
embed.add_embed_field(name="Launch Name", value=launch_name, inline=False)
embed.add_embed_field(name="Launch UUID", value=launch_uuid, inline=False)
embed.add_embed_field(name="Launch NET", value=launch_net, inline=False)
embed.add_embed_field(name="Launch Location", value=launch_location, inline=False)

# Add an image for the launch if available
if launch_image is not None:
embed.set_thumbnail(url=launch_image)

# Add footer with timestamp
embed.set_footer(text="Space Launch Now - Notification Tracker")
embed.set_timestamp()

# Add the embed to the webhook
webhook.add_embed(embed)

# Execute the webhook (send the notification)
response = webhook.execute()
logger.info(f"Discord Notification Response: {response}")
Loading

0 comments on commit 987461f

Please sign in to comment.