Skip to content
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

Notifications #140

Open
wants to merge 10 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions config.example.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@

SPARKPOST_KEY = ""

class FIREBASE:
CREDENTIALS = {} # Firebase serviceAccountKey.json
TOPIC = "announcements"

class GOOGLE_CAL:
CAL_ID = ""
Expand Down
4 changes: 4 additions & 0 deletions config.travis.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
import json
from datetime import datetime, timezone, timedelta

# uri should contain auth and default database
Expand All @@ -19,6 +20,9 @@
'channel': os.getenv("TRAVIS_SLACK_CHANNEL_ID")
}

class FIREBASE:
CREDENTIALS = json.loads(os.getenv("FIREBASE_CREDENTIALS", "{}")) # Firebase serviceAccountKey.json
TOPIC = "announcements"

class GOOGLE_CAL:
CAL_ID = os.getenv("TRAVIS_GOOGLE_CAL_ID", "")
Expand Down
14 changes: 14 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,29 @@ bcrypt==3.1.5
beautifulsoup4==4.6.3
boto3==1.9.200
botocore==1.12.253
CacheControl==0.12.6
cachetools==3.0.0
certifi==2018.11.29
cffi==1.11.5
chardet==3.0.4
colorama==0.4.4
coverage==4.5.2
dnspython==1.16.0
docutils==0.15.2
firebase-admin==4.4.0
google-api-core==1.23.0
google-api-python-client==1.7.6
google-auth==1.6.2
google-auth-httplib2==0.0.3
google-auth-oauthlib==0.4.1
google-cloud-core==1.4.3
google-cloud-firestore==1.9.0
google-cloud-storage==1.32.0
google-crc32c==1.0.0
google-resumable-media==1.1.0
googleapis-common-protos==1.52.0
googlemaps==3.0.2
grpcio==1.33.2
httplib2==0.12.0
idna==2.8
isort==4.3.4
Expand All @@ -26,9 +37,11 @@ lazy-object-proxy==1.3.1
mccabe==0.6.1
mock==4.0.2
more-itertools==5.0.0
msgpack==1.0.0
oauth2client==4.1.3
oauthlib==3.1.0
pluggy==0.8.1
protobuf==3.13.0
py==1.7.0
pyasn1==0.4.5
pyasn1-modules==0.2.3
Expand All @@ -40,6 +53,7 @@ pytest==4.0.2
pytest-cov==2.6.0
pytest-ordering==0.6
python-dateutil==2.7.5
pytz==2020.4
requests==2.21.0
requests-oauthlib==1.3.0
rsa==4.0
Expand Down
4 changes: 4 additions & 0 deletions serverless.yml
Original file line number Diff line number Diff line change
Expand Up @@ -255,3 +255,7 @@ functions:
request:
template:
application/json: '$input.body'
notifications:
handler: src/notifications.update_notification_status
events:
- schedule: rate(10 minutes)
114 changes: 114 additions & 0 deletions src/notifications.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import datetime
import logging

import firebase_admin
from firebase_admin import credentials
from firebase_admin import messaging

from src import cal_announce
from config import FIREBASE

logger = logging.getLogger()
logger.setLevel(logging.INFO)

firebase_admin.initialize_app(credentials.Certificate(FIREBASE.CREDENTIALS))

def send_notification(message_title, message_body, message_topic):
"""Sends a notification to the appropriate topic in Firebase.

Sends a message consisting of a title and a body in the form of a push
notification to the Flutter mobile app.

Documentation:
https://pub.dev/packages/firebase_messaging
"""
message = messaging.Message(
notification=messaging.Notification(
title=message_title,
body=message_body,
),
topic=message_topic,
data={
"title": message_title,
"body": message_body,
"click_action": "FLUTTER_NOTIFICATION_CLICK"
}
)
return messaging.send(message)

def check_slack(n=10):
"""Retrieves latest message from slack. If the message timestamp is within n minutes of the
current time, returns the message body.
"""
slack_resp = cal_announce.slack_announce({"num_messages": 1}, None)
if slack_resp["statusCode"] != 200:
return {"error": f'Error retriving slack messages: {slack_resp["statusCode"]} {slack_resp["body"]}'}

if not slack_resp["body"]:
return {"body": None}

latest_msg = slack_resp["body"][0]
msg_time = datetime.datetime.utcfromtimestamp(float(latest_msg["ts"]))
body = None
if msg_time + datetime.timedelta(minutes=n) > datetime.datetime.utcnow():
body = latest_msg["text"]
return {"body": body}

def slack_notifications():
"""Cron job to send Firebase notifications whenever a new Slack message appears in #announcements."""
slack_msg = check_slack()

if "error" in slack_msg:
logging.error(slack_msg["error"])
return

if not slack_msg["body"]:
logging.info("No new slack messages")
return

try:
response = send_notification("New Slack Announcement!", slack_msg["body"], FIREBASE.TOPIC)
logging.info(f'Firebase message for Slack ({slack_msg["body"]}) sent successfully! Response: {response}')
except Exception as e:
logging.error(f"Firebase messaging error: {e}")

def check_google_calendar(n=10):
"""Retrieves latest event from Google Calendar. If the event timestamp is within n minutes of the
current time, returns the event body.
"""
cal_resp = cal_announce.google_cal({"num_events": 1}, None)
if cal_resp["statusCode"] != 200:
return {"error": f'Error retriving Google Calendar events: {cal_resp["statusCode"]} {cal_resp["body"]}'}

if not cal_resp["body"]:
return {"body": None}

latest_cal_event = cal_resp["body"][0]
event_time = datetime.datetime.strptime(latest_cal_event["start"]["dateTime"], "%Y-%m-%dT%H:%M:%S%z")
body = None
if datetime.datetime.now(event_time.tzinfo) + datetime.timedelta(minutes=n) > event_time:
body = latest_cal_event["summary"]
return {"body": body}

def google_calendar_notifications():
"""Cron job to send Firebase notifications whenever a new Google calendar event is approaching."""
google_cal_event = check_google_calendar()

if "error" in google_cal_event:
logging.error(google_cal_event["error"])
return

if not google_cal_event["body"]:
logging.info("No new Google Calendar events")
return

try:
response = send_notification("Calender Event Approaching!", google_cal_event["body"], FIREBASE.TOPIC)
logging.info(f'Firebase message for Google Calendar ({google_cal_event["body"]}) sent successfully! Response: {response}')
except Exception as e:
logging.error(f"Firebase messaging error: {e}")

def update_notification_status():
"""This function should be run as a cron job."""
slack_notifications()
google_calendar_notifications()
10 changes: 10 additions & 0 deletions tests/test_notifications.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# This is different from regular lambda functions because it does not have a response to a user
# In the event that this function fails, the only side effect is that Firebase notifications will not be made

from src import notifications

def test_notifications():
try:
notifications.update_notification_status()
except Exception as e:
print(f"Error: {e}")