1
+ import logging
1
2
import random
3
+ import secrets
2
4
from datetime import datetime
3
5
from hashlib import sha256
4
6
from io import StringIO
7
9
from typing import Dict
8
10
from typing import List
9
11
12
+ import requests
13
+ from apscheduler .jobstores .base import ConflictingIdError
10
14
from apscheduler .schedulers .background import BackgroundScheduler
11
15
from apscheduler .triggers .cron import CronTrigger
12
16
from decouple import config as get_config
13
17
from errbot import arg_botcmd
14
18
from errbot import botcmd
15
19
from errbot import BotPlugin
20
+ from errbot import webhook
16
21
from errbot .backends .base import Message as ErrbotMessage
22
+ from flask import abort
17
23
from wrapt import synchronized # https://stackoverflow.com/a/29403915
18
24
19
25
TOPICS_LOCK = RLock ()
20
26
21
27
28
+ def do_webhook_post (url , headers = {}, data = {}):
29
+ logger = logging .getLogger ()
30
+ logger .debug ("Starting request for %s with headers: %s" , url , headers )
31
+ try :
32
+ response = requests .post (url , headers = headers , data = data )
33
+ except Exception as err :
34
+ logger .error ("Error while doing request: %s" , err )
35
+ return
36
+ logger .debug ("Webhook status code: %s" , response .status_code )
37
+ logger .debug ("Webhook response: %s" , response .text )
38
+
39
+
22
40
def get_config_item (
23
41
key : str , config : Dict , overwrite : bool = False , ** decouple_kwargs
24
42
) -> Any :
@@ -61,7 +79,7 @@ def add(self, topic: str) -> None:
61
79
"id" : self .hash_topic (topic ),
62
80
"topic" : topic ,
63
81
"used" : False ,
64
- "date_used " : None ,
82
+ "used_date " : None ,
65
83
}
66
84
)
67
85
self .bot_plugin ["TOPICS" ] = topics
@@ -98,10 +116,10 @@ def set_used(self, topic_id: str) -> None:
98
116
if topic ["id" ] == topic_id :
99
117
topic ["used" ] = True
100
118
topic ["used_date" ] = datetime .now ()
101
- self .bot_plugin ["TOPICS" ] = topics
102
119
found = True
103
120
if not found :
104
121
raise KeyError (f"{ topic_id } not found in topic list" )
122
+ self .bot_plugin ["TOPICS" ] = topics
105
123
106
124
@synchronized (TOPICS_LOCK )
107
125
def delete (self , topic_id : str ) -> None :
@@ -122,6 +140,25 @@ def delete(self, topic_id: str) -> None:
122
140
topics .pop (to_pop )
123
141
self .bot_plugin ["TOPICS" ] = topics
124
142
143
+ @synchronized (TOPICS_LOCK )
144
+ def reset (self , topic_id : str ) -> None :
145
+ """
146
+ resets the topic at topic_id
147
+
148
+ topic_id should be the 8 character topic hash from id in the topic
149
+ """
150
+ found = False
151
+ topics = self .bot_plugin ["TOPICS" ]
152
+ for index , topic in enumerate (topics ):
153
+ if topic ["id" ] == topic_id :
154
+ found = True
155
+ topic ["used" ] = False
156
+ topic ["used_date" ] = None
157
+ break
158
+ if not found :
159
+ raise KeyError (f"{ topic_id } not found in topic list" )
160
+ self .bot_plugin ["TOPICS" ] = topics
161
+
125
162
@staticmethod
126
163
def hash_topic (topic : str ) -> str :
127
164
"""
@@ -151,14 +188,43 @@ def activate(self) -> None:
151
188
super ().activate ()
152
189
self .topics = Topics (self )
153
190
# schedule our daily jobs
154
- self .sched = BackgroundScheduler (
155
- {"apscheduler.timezome" : self .config ["TOPIC_TZ" ]}
156
- )
157
- self .sched .add_job (
158
- self .post_topic , CronTrigger .from_crontab (self .config ["TOPIC_SCHEDULE" ])
159
- )
191
+ self .sched = BackgroundScheduler (self .config ["TAD_APSCHEDULER_CONFIG" ])
192
+ try :
193
+ if self .config ["TAD_ENABLE_WEBHOOK" ]:
194
+ request_args = {"url" : self .config ["TAD_WEBHOOK_URL" ]}
195
+ if self .config ["AUTH_POST_WEBHOOK" ]:
196
+ request_args ["headers" ] = {
197
+ "x-auth-token" : self .config ["AUTH_POST_WEBHOOK_TOKEN" ]
198
+ }
199
+ self .sched .add_job (
200
+ do_webhook_post ,
201
+ CronTrigger .from_crontab (self .config ["TAD_SCHEDULE" ]),
202
+ kwargs = request_args ,
203
+ name = "topic-a-day" ,
204
+ id = "topic-a-day" ,
205
+ replace_existing = True ,
206
+ )
207
+ else :
208
+ self .sched .add_job (
209
+ self .post_topic ,
210
+ CronTrigger .from_crontab (self .config ["TAD_SCHEDULE" ]),
211
+ name = "topic-a-day" ,
212
+ id = "topic-a-day" ,
213
+ replace_existing = True ,
214
+ )
215
+ except ConflictingIdError as err :
216
+ self .log .debug ("Hit error when adding job: %s" , err )
217
+ except Exception as err :
218
+ self .log .error ("Hit error while adding job: %s" , err )
160
219
self .sched .start ()
161
220
221
+ def deactivate (self ) -> None :
222
+ """
223
+ Shutsdown the scheduler and calls super deactivate
224
+ """
225
+ self .sched .shutdown ()
226
+ super ().deactivate ()
227
+
162
228
def configure (self , configuration : Dict ) -> None :
163
229
"""
164
230
Configures the plugin
@@ -168,15 +234,53 @@ def configure(self, configuration: Dict) -> None:
168
234
configuration = dict ()
169
235
170
236
# name of the channel to post in
171
- get_config_item ("TOPIC_CHANNEL " , configuration )
237
+ get_config_item ("TAD_CHANNEL " , configuration )
172
238
if getattr (self ._bot , "channelname_to_channelid" , None ) is not None :
173
239
configuration ["TOPIC_CHANNEL_ID" ] = self ._bot .channelname_to_channelid (
174
- configuration ["TOPIC_CHANNEL " ]
240
+ configuration ["TAD_CHANNEL " ]
175
241
)
176
- get_config_item ("TOPIC_SCHEDULE" , configuration , default = "0 9 * * 1,3,5" )
177
- get_config_item ("TOPIC_TZ" , configuration , default = "UTC" )
242
+ get_config_item ("TAD_SCHEDULE" , configuration , default = "0 9 * * 1,3,5" )
243
+
244
+ # apscheduler config
245
+ get_config_item ("TAD_APSCHEDULER_CONFIG_FILE" , configuration , default = "" )
246
+ if configuration ["TAD_APSCHEDULER_CONFIG_FILE" ] != "" :
247
+ configuration ["TAD_APSCHEDULER_CONFIG" ] = self ._load_config_file (
248
+ configuration ["TAD_APSCHEDULER_CONFIG_FILE" ]
249
+ )
250
+ else :
251
+ get_config_item ("TOPIC_TZ" , configuration , default = "UTC" )
252
+ configuration ["TAD_APSCHEDULER_CONFIG" ] = {
253
+ "apscheduler.timezone" : configuration ["TOPIC_TZ" ]
254
+ }
255
+
256
+ # Webhook options
257
+ get_config_item ("TAD_ENABLE_WEBHOOK" , configuration , default = "False" , cast = bool )
258
+ if configuration ["TAD_ENABLE_WEBHOOK" ]:
259
+ get_config_item (
260
+ "TAD_WEBHOOK_URL" ,
261
+ configuration ,
262
+ default = "http://localhost:3142/post_topic_rpc" ,
263
+ )
264
+ get_config_item (
265
+ "AUTH_POST_WEBHOOK" , configuration , default = "True" , cast = bool
266
+ )
267
+ if configuration ["AUTH_POST_WEBHOOK" ]:
268
+ get_config_item (
269
+ "AUTH_POST_WEBHOOK_TOKEN" ,
270
+ configuration ,
271
+ default = secrets .token_urlsafe (),
272
+ )
178
273
super ().configure (configuration )
179
274
275
+ @staticmethod
276
+ def _load_config_file (filepath : str ) -> Dict :
277
+ """"""
278
+ import json
279
+
280
+ with open (filepath , "r" ) as config_file :
281
+ data = json .load (config_file )
282
+ return data
283
+
180
284
@botcmd
181
285
@arg_botcmd ("topic" , nargs = "*" , type = str , help = "Topic to add to our topic list" )
182
286
def add_topic (self , msg : ErrbotMessage , topic : List [str ]) -> None :
@@ -191,6 +295,24 @@ def add_topic(self, msg: ErrbotMessage, topic: List[str]) -> None:
191
295
msg .frm , f"Topic added to the list: ```{ topic_sentence } ```" , in_reply_to = msg
192
296
)
193
297
298
+ @botcmd (admin_only = True )
299
+ @arg_botcmd (
300
+ "topic_id" , type = str , help = "Hash of the topic to remove from list topics"
301
+ )
302
+ def reset_topic (self , msg : ErrbotMessage , topic_id : str ) -> str :
303
+ """
304
+ Resets a topic from the topic list so it can be posted again
305
+ """
306
+ if len (topic_id ) != 8 :
307
+ return "Invalid Topic ID"
308
+
309
+ try :
310
+ self .topics .reset (topic_id )
311
+ except KeyError :
312
+ return "Invalid Topic ID"
313
+
314
+ return "Topic Reset"
315
+
194
316
@botcmd (admin_only = True )
195
317
@arg_botcmd (
196
318
"topic_id" , type = str , help = "Hash of the topic to remove from list topics"
@@ -201,17 +323,14 @@ def delete_topic(self, msg: ErrbotMessage, topic_id: str) -> str:
201
323
202
324
"""
203
325
if len (topic_id ) != 8 :
204
- self .send (msg .frm , f"Invalid Topic ID" , in_reply_to = msg )
205
- return
326
+ return "Invalid Topic ID"
206
327
207
328
try :
208
329
self .topics .delete (topic_id )
209
330
except KeyError :
210
- self .send (msg .frm , f"Invalid Topic ID" , in_reply_to = msg )
211
- return
331
+ return "Invalid Topic ID"
212
332
213
- self .send (msg .frm , f"Topic Deleted" , in_reply_to = msg )
214
- return
333
+ return "Topic Deleted"
215
334
216
335
@botcmd
217
336
def list_topics (self , msg : ErrbotMessage , _ : List ) -> None :
@@ -224,7 +343,7 @@ def list_topics(self, msg: ErrbotMessage, _: List) -> None:
224
343
for topic in topics :
225
344
if topic ["used" ]:
226
345
used_topics .append (
227
- f"{ topic ['id' ]} : { topic ['topic' ]} -- Posted on { topic ['date_used' ] } "
346
+ f"{ topic ['id' ]} : { topic ['topic' ]} -- Posted on { topic ['used_date' ]. strftime ( '%Y-%m-%d %H:%M' ) } "
228
347
)
229
348
else :
230
349
free_topics .append (f"{ topic ['id' ]} : { topic ['topic' ]} " )
@@ -249,6 +368,20 @@ def list_topic_jobs(self, msg: ErrbotMessage, _: List) -> None:
249
368
self .sched .print_jobs (out = pjobs_out )
250
369
self .send (msg .frm , pjobs_out .getvalue (), in_reply_to = msg )
251
370
371
+ @webhook (methods = ["POST" ], raw = True )
372
+ def post_topic_rpc (self , request ):
373
+ if not self .config ["TAD_ENABLE_WEBHOOK" ]:
374
+ abort (500 )
375
+ if self .config ["AUTH_POST_WEBHOOK" ]:
376
+ if (
377
+ request .headers .get ("x-auth-token" , "" )
378
+ != self .config ["AUTH_POST_WEBHOOK_TOKEN" ]
379
+ ):
380
+ abort (403 , "Endpoint auth turned on and your auth token did not match" )
381
+
382
+ self .post_topic ()
383
+ return "Ok"
384
+
252
385
def post_topic (self ) -> None :
253
386
"""
254
387
Called by our scheduled jobs to post the topic message for the day. Also calls any backend specific
@@ -272,7 +405,7 @@ def post_topic(self) -> None:
272
405
except AttributeError :
273
406
self .log .debug ("%s has no backend specific tasks" , self ._bot .mode )
274
407
self .log .debug ("Sending message to channel" )
275
- self .send (self .build_identifier (self .config ["TOPIC_CHANNEL " ]), topic_template )
408
+ self .send (self .build_identifier (self .config ["TAD_CHANNEL " ]), topic_template )
276
409
self .log .debug ("Setting topic to used" )
277
410
self .topics .set_used (new_topic ["id" ])
278
411
@@ -286,9 +419,7 @@ def slack_pre_post_topic(self, topic: str) -> None:
286
419
self ._bot .api_call (
287
420
"channels.setTopic" ,
288
421
{
289
- "channel" : self ._bot .channelname_to_channelid (
290
- self .config ["TOPIC_CHANNEL" ]
291
- ),
422
+ "channel" : self .config ["TOPIC_CHANNEL_ID" ],
292
423
"topic" : topic ,
293
424
},
294
425
)
0 commit comments