-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathreminder.py
executable file
·173 lines (152 loc) · 6.42 KB
/
reminder.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
#! /usr/bin/env python2.7
import datetime
import re
import time
import cronline
import slacker
import token
class Reminder(object):
def __init__(self, config, log, delete, slacker):
self.slacker = slacker
self.config_channel_name = config
self.log_channel_name = log
self.delete_done_after = delete
self.cache_ids()
self.config_channel_id = self.get_channel_id(self.config_channel_name)
self.log_channel_id = self.get_channel_id(self.log_channel_name)
self.log("Reminder.__init__ completed")
self.loop()
def log(self, message):
self.slacker.channel_post(message, self.log_channel_id, link_names=False)
def execute_cline(self, cline):
id = self.find_id(cline.destination)
if not id:
self.log("Could not find ID for {}".format(destination))
return
if '#' in cline.destination:
self.slacker.channel_post(cline.message, id)
else:
self.slacker.user_post(cline.message, id)
if cline.onetime:
self.update_message(cline.text, cline.ts)
def update_message(self, message, ts):
now_friendly = time.ctime()
now_computer = time.time()
new_text = "EXECUTED on {} {}: \n{}".format(now_computer, now_friendly, message)
self.slacker.channel_post(new_text, self.config_channel_id, ts=ts)
def find_id(self, destination):
# print "Finding id in {}".format(destination)
if re.match("<#.+\|.+>", destination):
id, name = destination.split("|")
return id[2:]
if re.match("<@.*>", destination):
return destination[2:-1]
subdestination = destination[1:]
self.log("Trying to find ID for {}".format(subdestination))
if destination[0] == '@':
return self.cache['members'].get(subdestination)
if destination[0] == '#':
if subdestination in self.cache['channels']:
return self.cache['channels'][subdestination]
elif subdestination in self.cache['groups']:
return self.cache['groups'][subdestination]
return None
def run_once(self):
cronlines = self.get_cronlines()
for cline in cronlines:
execute = False
if cline.execute_now():
self.execute_cline(cline)
def log_heartbeat(self):
"""
Every 10 minutes, emit a "I'm still alive" Slack message to log channel
"""
now = datetime.datetime.now()
if now.minute % 10 == 0:
self.log("It's now {} and I'm still running".format(time.ctime()))
def loop(self):
now = datetime.datetime.now()
while True:
print "Looping at {}".format(time.ctime())
self.run_once()
self.log_heartbeat()
self.sleep_to_next_minute(now)
now = datetime.datetime.now()
def sleep_to_next_minute(self, dt_of_previous_minute):
"""
sleep until the beginning of the minute following dt_of_previous_minute
"""
later = datetime.datetime.now()
diff = later - dt_of_previous_minute
if later.minute == dt_of_previous_minute.minute:
sleep_for = 60 - later.second
time.sleep(sleep_for)
def potential_delete(self, message):
"""
figure out when we executed and, if delete_done_after is set
delete the message it's been at least that many seconds
"""
if not self.delete_done_after:
return
text = message['text']
ts = message['ts']
done = re.sub("EXECUTED on (\d+).*", r"\1", text, re.M)
# that keeps the next line in done, and I'm too tired to figure out how to much
# with re to have it do the multiline match, so just split and discard
done = done.split()[0]
done = int(done)
now = time.time()
diff = now - done
if diff > self.delete_done_after:
m = "Deleted config message '{}' because it was too old".format(text)
self.log(m)
self.delete_config_message(ts)
def delete_config_message(self, ts):
self.slacker.api_call("chat.delete?ts={}&channel={}".format(ts, self.config_channel_id))
def get_cronlines(self):
"""
returns a list of cronlines; each cronline is a
[datetime_to_next execution, destination, message, ts, onetime]
spec
(onetime is binary)
"""
messages = self.get_messages()
cronlines = []
for message in messages:
cline = cronline.Cronline(message)
if cline.valid:
cronlines.append(cline)
continue
# if we got here, the cline is not valid
if message['text'].find("EXECUTED on") == 0:
self.potential_delete(message)
else:
self.log("Skipping message {} because it's neither HHMM or cron".format(message))
continue
# print "cronlines: {}".format(cronlines)
return cronlines
def get_messages(self):
api = self.message_api + "?channel={}".format(self.config_channel_id)
payload = self.slacker.api_call(api)
return [message for message in payload["messages"] if message.get("subtype") is None]
def cache_ids(self):
"""
cache name:id mapping for channels, groups, and users
"""
self.cache = {}
self.cache['groups'] = {}
private_groups = self.slacker.paginated_lister("groups.list", "groups")
self.cache['groups'] = {group['name_normalized']: group['id'] for group in private_groups}
channels = self.slacker.paginated_lister("channels.list?exclude_members=true", 'channels')
self.cache['channels'] = {channel['name_normalized']: channel['id'] for channel in channels}
users = self.slacker.paginated_lister("users.list?presence=false", 'members')
self.cache['users'] = {user['name']: user['id'] for user in users}
def get_channel_id(self, channel_name):
if channel_name in self.cache['groups']:
self.message_api = "groups.history"
return self.cache['groups'][channel_name]
elif channel_name in self.cache['channels']:
self.message_api = "channels.history"
return self.cache['channels'][channel_name]
else:
raise RuntimeError("Could not find an ID for channel {}".format(channel_name))