Skip to content

Instantly share code, notes, and snippets.

@hugoShaka
Last active September 25, 2018 09:09
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save hugoShaka/2d43a8c8756a2f82b4ef4cb8ec30aeef to your computer and use it in GitHub Desktop.
Save hugoShaka/2d43a8c8756a2f82b4ef4cb8ec30aeef to your computer and use it in GitHub Desktop.
"""
This is a bot monitoring WebDAV calendar and issuing slack alerts.
"""
import os
import time
import ics
import requests
from slackclient import SlackClient
class Notifier:
"""Handles notifications for slack."""
def __init__(self, token, channel):
"""Constructor. Creates the connection."""
self.slack_token = token
self.channel = channel
self.slack_client = SlackClient(self.slack_token)
def notify(self, payload):
"""Send a notification to the given channel."""
result = self.slack_client.api_call(
"chat.postMessage", channel=self.channel, text=payload
)
return result["ts"]
def notify_created(self, event):
"""Send a notification about a created event."""
state = ":heavy_check_mark: Added"
payload = self.craft_text_message(event)
thread_state = self.notify("%s %s" % (payload, state))
self.add_absolute_date(event, thread_state)
def notify_deleted(self, event):
"""Send a notification about a deleted event."""
state = ":x: Deleted"
payload = self.craft_text_message(event)
thread_state = self.notify("%s %s" % (payload, state))
self.add_absolute_date(event, thread_state)
def notify_edited(self, event_new, event_old):
"""Send a notification to the given channel concatenating the event
string and the state."""
state = self.compute_editions(event_new, event_old)
payload = self.craft_text_message(event_new)
thread_state = self.notify("%s %s" % (payload, state))
self.add_absolute_date(event_new, thread_state)
def add_absolute_date(self, event, thread_state):
"""Adds details about the event on the thread"""
payload = "Absolute date : %s" % event.begin.format(
fmt="DD MMM YYYY - HH:mm", locale="en_us"
)
self.slack_client.api_call(
"chat.postMessage", channel=self.channel, text=payload, thread_ts=thread_state
)
def craft_text_message(self, evt):
"""Takes an event and return a textual representation of it."""
if evt.location is not None:
loc = "( %s )" % evt.location
else:
loc = ""
payload = "%s - %s %s" % (evt.name, evt.begin.humanize(), loc)
return payload
def compute_editions(self, new, old):
"""Takes the new version of an event and the old one.
Returns a string describing the chages"""
assert new.uid == old.uid
description = ""
if new.name != old.name:
description += " :pencil2: renamed"
if new.begin != old.begin:
description += " :calendar: recheduled"
if new.location != old.location:
description += " :door: changed location"
return description
def monkey_patch(self, other):
"""Dirty monkey patching of ics module.
This function will replace the Events __eq__() relation in order to be able
to compare events with the same UID but different data.
Two events are identical if :
- They have the same name
- They start at the same date
- They are at the same place
"""
if isinstance(other, ics.Event):
return (
(self.name == other.name)
and (self.begin == other.begin)
and (self.location == other.location)
)
raise NotImplementedError("Cannot compare Event and {}".format(type(other)))
ics.Event.__eq__ = monkey_patch
def get_calendar(url, auth):
"""Takes a WebDav URL and auth objects, returns the calendar."""
answer = requests.get(url, auth=auth).text
return ics.Calendar(answer)
def compare_events(new, old):
"""Takes two lists of events and returns two lists representing the
differences between them. The first list are elements that are added, and
the second elements removed.
"""
created = list(set(new) - set(old))
deleted = list(set(old) - set(new))
return (created, deleted)
def compute_changes(new, old):
"""Takes the lists of old and new events and returns 3 lists of events:
events created, deleted and edited. Edited events are detected when a
created event and a deleted event share the same uid.
"""
created, deleted = compare_events(new, old)
edited = []
for i, evt_created in enumerate(created):
for j, evt_deleted in enumerate(deleted):
if evt_created.uid == evt_deleted.uid:
edited.append((evt_created, evt_deleted))
created.pop(i)
deleted.pop(j)
return (created, deleted, edited)
def main():
"""Main loop"""
version = "2.0"
url = os.getenv("DAV_URL")
password = os.getenv("DAV_PASSWORD")
user = os.getenv("DAV_USER")
channel = os.getenv("SLACK_CHANNEL")
slack_token = os.getenv("SLACK_TOKEN")
slack = Notifier(slack_token, channel)
slack.notify("Bot started (v%s) :tada:" % version)
errcount = 0
auth = requests.auth.HTTPBasicAuth(user, password)
old_calendar = get_calendar(url, auth)
while True:
time.sleep(20)
try:
new_calendar = get_calendar(url, auth)
(created, deleted, edited) = compute_changes(
new_calendar.events, old_calendar.events
)
for evt in created:
slack.notify_created(evt)
for evt in deleted:
slack.notify_deleted(evt)
for evt in edited:
slack.notify_edited(evt[0], evt[1])
old_calendar = new_calendar
errcount = 0
except Exception:
print("Supelec failing")
errcount += 1
if errcount >= 15:
raise
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment