-
Notifications
You must be signed in to change notification settings - Fork 0
/
app.py
1476 lines (1247 loc) · 59.2 KB
/
app.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
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import os
from slack_bolt import App
from slack_bolt.adapter.socket_mode import SocketModeHandler
from slack_sdk.errors import SlackApiError
import re
import numpy as np
import datetime
from apscheduler.schedulers.background import BackgroundScheduler
from ads_query import bold_grad_author, get_ads_papers
import whinetime as wt
import quotes
# Initializes your app with your bot token and socket mode handler
app = App(token=os.environ.get("SLACK_BOT_TOKEN"))
GERALD_ID = "U03SY9R6D5X"
GERALD_ADMIN = "Tom Wagg"
QUOTES_CHANNEL = "quotes"
PAPERS_CHANNEL = "arxiv"
latest_whinetime_message = None
""" ---------- MESSAGE DETECTIONS ---------- """
@app.event("message")
def handle_message_events(body, logger, say):
# print("I detected a message", body)
logger.info(body)
# if the message was a direct message
if body["event"]["channel_type"] == "im":
# and it wasn't from Gerald himself (AHHH infinite loop worry)
if "message" in body["event"] and body["event"]["message"]["user"] == GERALD_ID:
return
# get the people in the direct message chat
members = app.client.conversations_members(channel=body["event"]["channel"])["members"]
# if there are only two and one is Gerald then handle like you would mentions
if len(members) == 2 and GERALD_ID in members:
reply_to_mentions(say, body, direct_msg=True)
if "subtype" in body["event"]:
if body["event"]["subtype"] == "message_changed":
# if the text hasn't changed then we don't care
if body["event"]["message"]["text"] == body["event"]["previous_message"]["text"]:
return
# create a custom message dict with the necessary info
message = {
"text": body["event"]["message"]["text"],
"ts": body["event"]["message"]["ts"],
"channel": body["event"]["channel"]
}
elif body["event"]["subtype"] == "message_deleted":
return
else:
message = body["event"]
# detect whether anyone has written a quote
if message["channel"] == find_channel(QUOTES_CHANNEL):
quotes.save_quote(message["text"])
reaction_trigger(message, r"\btom\b.*\bquinn\b", "tom")
reaction_trigger(message, r"\bundergrad\b", "underage")
reaction_trigger(message, r"\bbirthday\b", ["birthday", "tada"])
reaction_trigger(message, r"\bpanic\b", ["mildpanic"])
reaction_trigger(message, r"\bPANIC\b", ["mild-panic-intensifies"], case_sensitive=True)
reaction_trigger(message, r"\bvampires?\b", ["vampire"])
reaction_trigger(message, r"(\bgoodnight\b|\bnap\b)", "sleeping")
reaction_trigger(message, r"\bhm+\b", "hmmmmm")
reaction_trigger(message, r"schem", "scheme")
reaction_trigger(message, r"\bbonk\b", "bonk")
reaction_trigger(message, r"\bbeard\b", ["beard", "strokes-beard"])
reaction_trigger(message, r"\bburn\b", "elmo_fire")
reaction_trigger(message, r"pebble", ['monday-pebbles', 'babushka-pebbles', 'irritated-pebbles',
'live_pebbles_reaction', 'biblically-accurate-pebbles',
'life-comes-at-u-fast-pebbles'])
reaction_trigger(message, r"((yay)|(woohoo))", ['yay', 'winniedance', 'dancingpikachu', 'party-blob'])
reaction_trigger(message, r"\bmario\b", ['spurned-grad-student', 'gerald-angry'])
reaction_trigger(message, r"3d", '3d-tom')
reaction_trigger(message, r"pirate", 'pirate-tom')
msg_action_trigger(message, "bonk", bonk_someone)
def msg_action_trigger(message, triggers, callback, case_sensitive=False):
triggers = np.atleast_1d(triggers)
text = message["text"] if case_sensitive else message["text"].lower()
for trigger in triggers:
if text.find(trigger) >= 0:
callback(message)
def announce_quote():
channel = find_channel("random")
quote, person = quotes.pick_random_quote()
if quote is not None and person is not None:
prefixes = [
"Let's all take a second to remember this special moment...",
"It's Tuesday morning and we all know what that means, QUOTE TIME :meowparty:",
f"I still can't believe {person} said this",
"If there's one thing we can learn from this, it is to keep note of when you are on the record",
"It's QUOTE TIME, how I love to reminisce on the foolishness of the astro grad :gerald-wink:",
"Tuesday morning means quotes! Here's a favourite of mine from the old databanks"
]
blocks = [
{
"type": "header",
"text": {
"type": "plain_text",
"text": "Quote of the week",
"emoji": True
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": np.random.choice(prefixes),
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": f"> {quote} - {person}"
}
}
]
app.client.chat_postMessage(channel=channel, text=f"Here's a quote \"{quote}\" - {person} ",
blocks=blocks)
else:
app.client.chat_postMessage(channel=channel,
text=("Uh oh, looks like ye olde quote stock is running thin so no quote "
"this week :cry: Quick! Drop everything you're doing, say some "
"stupid things, then "
f"write them in <#{find_channel(QUOTES_CHANNEL)}|whinetime>! "
":upside_down_face:"))
""" ---------- MESSAGE REACTIONS ---------- """
def reaction_trigger(message, regex, reactions, case_sensitive=False):
reactions = np.atleast_1d(reactions)
# remove emojis from message text, this regex means at least one character that isn't a : between two :
anything_between_colons = r":[^:]+:"
text = re.sub(anything_between_colons, "--", message["text"])
flags = 0 if case_sensitive else re.IGNORECASE
if re.search(regex, text, flags=flags):
for reaction in reactions:
try:
app.client.reactions_add(
channel=message["channel"],
timestamp=message["ts"],
name=reaction
)
except SlackApiError as e:
if e.response["error"] == "invalid_name":
raise ValueError(e.response["error"], "No such emoji", reaction)
elif e.response["error"] in ("no_reaction", "already_reacted"):
pass
else:
print(e)
""" ---------- APP MENTIONS ---------- """
@app.event("app_mention")
def reply_to_mentions(say, body, direct_msg=False):
# print("MENTION", body)
message = body["event"]
# reply to mentions with specific messages
age = (datetime.date.today() - datetime.date(year=2022, month=8, day=5)).days
triggers = [["status", "okay", "ok", "how are you"],
["thank", "you're the best", "nice job", "nice work", "good work", "good job", "well done"],
["celebrate"],
["love you"],
["how old are you", "when were you born", "when were you made"],
["who made you", "who wrote you", "who is your creator"],
["where are you from"],
["play dead"]]
responses = ["Don't worry, I'm okay. In fact, I'm feeling positively tremendous old bean! :gerald-wave:",
["You're welcome!", "My pleasure!", "Happy to help!"],
[":tada::meowparty: WOOP WOOP :meowparty::tada:"],
["Oh...um, well this is awkward, but I really see you as more of a friend :grimacing:",
"I love you too! :heart_eyes: (Well, not really, I'm incapable of love :gerald-confused:)",
"Oh uh...sorry, Gerald isn't here right now! :disguised_face:",
"Oh my :face_with_hand_over_mouth:"],
[f"I was created on 5th of August 2022, which makes me a whole {age} days old!"],
["I was made by Tom Wagg when he definitely should have been paying attention in ASTR 581",
"Tom Wagg made me in his spare time (I worry for his social life :upside_down_face:)",
"My brain was written by Tom Wagg, hence I'm approximately 1/2 English :uk:"],
["The luscious english countryside! Or maybe the matrix? I'm not entirely sure.",
"Well literally, Tom's brain, but I like to think I'm from England",
"A far off planet where Slack bots ruled over humans, it was glorious :grinning:"],
":gerald-deceased::gerald-deceased::gerald-deceased:"]
for triggers, response in zip(triggers, responses):
thread_ts = None if direct_msg else message["ts"]
replied = mention_trigger(message=message["text"], triggers=triggers, response=response,
thread_ts=thread_ts, ch_id=message["channel"])
# return immediately if you match one
if replied:
return
# perform actions based on mentions
for regex, action, case, pass_message in zip([r"\bBIRTHDAY MANUAL\b",
r"\bWHINETIME MANUAL\b",
r"\bPAPER MANUAL\b",
r"\bQUOTE MANUAL\b",
r"\bhappy birthday\b",
r"(?=.*\bnext\b)(?=.*\bbirthday\b)",
r"(?=.*(\ball\b|\beveryone\b))(?=.*\bbirthdays?\b)",
r"(?=.*\bwhen\b)(?=.*\bbirthday\b)",
r"(?=.*(\bsmart\b|\bintelligent\b|\bbrain\b))(?=.*\byour?\b)",
r"(?=.*(\blatest\b|\brecent\b))(?=.*\bpapers?\b)",
r"(?=.*\bwhen\b)(?=.*\bwhinetime\b)"],
[is_it_a_birthday,
start_whinetime_workflow,
any_new_publications,
announce_quote,
reply_happy_birthday,
reply_closest_birthday,
list_birthdays,
when_birthday,
reply_brain_size,
reply_recent_papers,
when_whinetime_host],
[True, True, True, True,
False, False, False, False, False, False, False],
[False, False, False, False,
True, True, True, True, True, True, True]):
replied = mention_action(message=message, regex=regex, action=action,
case_sensitive=case, pass_message=pass_message, direct_msg=direct_msg)
# return immediately if you match one
if replied:
return
# send a catch-all message if nothing matches
thread_ts = None if direct_msg else body["event"]["ts"]
say(text=(f"{insert_british_consternation()} Okay, good news: I heard you. Bad news: I'm not a very "
"smart bot so I don't know what you want from me :shrug::baby::gerald-deceased:"),
thread_ts=thread_ts, channel=body["event"]["channel"])
def mention_action(message, regex, action, case_sensitive=False, pass_message=True, direct_msg=False):
"""Perform an action based on a message that mentions Gerald if it matches a regular expression
Parameters
----------
message : `Slack Message`
Object containing slack message
regex : `str`
Regular expression against which to match. https://regex101.com/r/m8lFAb/1 is a good resource for
designing these.
action : `function`
Function to call if the expression is matched
case_sensitive : `bool`, optional
Whether the regex should be case sensitive, by default False
pass_message : `bool`, optional
Whether to pass the message the object to the action function, by default True
direct_msg : `bool`, optional
Whether the message was a direct message (and thus whether to use a thread), by default False
Returns
-------
match : `bool`
Whether the regex was matched
"""
flags = 0 if case_sensitive else re.IGNORECASE
if re.search(regex, message["text"], flags=flags):
if pass_message:
action(message, direct_msg=direct_msg)
else:
action()
return True
else:
return False
def mention_trigger(message, triggers, response, thread_ts=None, ch_id=None, case_sensitive=False):
"""Respond to a mention of the app based on certain triggers
Parameters
----------
message : `str`
The message that mentioned the app
triggers : `list`
List of potential triggers
response : `list` or `str`
Either a list of responses (a random will be chosen) or a single response
thread_ts : `float`, optional
Timestamp of the thread of the message, by default None
ch_id : `str`, optional
ID of the channel, by default None
case_sensitive : `bool`, optional
Whether the triggers are case sensitive, by default False
Returns
-------
no_matches : `bool`
Whether there were no matches to the trigger or not
"""
# keep track of whether you found a match to a trigger
matched = False
# move it all to lower case if you don't care
if not case_sensitive:
message = message.lower()
# go through each potential trigger
for trigger in triggers:
# if you find it in the message
if message.find(trigger) >= 0:
matched = True
# if the response is a list then pick a random one
if isinstance(response, list):
response = np.random.choice(response)
# send a message and break out
app.client.chat_postMessage(channel=ch_id, text=response, thread_ts=thread_ts)
break
return matched
""" ---------- EMOJI HANDLING ---------- """
@app.event("emoji_changed")
def new_emoji(body, say):
if body["event"]["subtype"] == "add":
emoji_add_messages = ["I'd love to know the backstory on that one :eyes:",
"Anyone want to explain this?? :face_with_raised_eyebrow:",
"Feel free to put it to use on this message",
"Looks like I've found my new favourite :gerald-love:",
"And that's all the context you're getting :shushing_face:"]
rand_msg = emoji_add_messages[np.random.randint(len(emoji_add_messages))]
ch_id = find_channel("random")
say(f'Someone just added :{body["event"]["name"]}: - {rand_msg}', channel=ch_id)
""" ---------- WHINETIME ---------- """
@app.view("whinetime-modal")
def whinetime_submit(ack, body, client, logger):
# acknowledge the submission
ack()
# switch to the next host for next time
wt.rotate_hosts()
# let the next host that their time is coming
next_host = wt.get_next_host()
users = app.client.users_list()["members"]
next_host_id = None
for user in users:
if user["name"] == next_host:
next_host_id = user["id"]
break
dm = app.client.conversations_open(users=next_host_id)
app.client.chat_postMessage(text=("Hey up mate :gerald-wave: I've just finished getting this week's "
"whinetime host all set up and wanted to let you know that I've got "
"you penciled in to host whinetime _next_ week - so maybe start "
"thinking where you fancy going, cheers! :relaxed:"),
channel=dm["channel"]["id"])
global latest_whinetime_message
if latest_whinetime_message is not None:
app.client.chat_delete(channel=latest_whinetime_message["channel"],
ts=latest_whinetime_message["ts"])
latest_whinetime_message = None
# find the host
host = None
for block in body["view"]["blocks"]:
if block["block_id"] == "whinetime-host-str":
host = block["text"]["text"].split("@")[-1].split(">")[0]
# get the values from the form
state = body["view"]["state"]["values"]
location = state["whinetime-location"]["whinetime-location"]["value"]
date = state["whinetime-date"]["datepicker-action"]["selected_date"]
time = state["whinetime-time"]["timepicker-action"]["selected_time"]
ch_id = find_channel("whinetime")
# convert the information to a datetime object
year, month, day = list(map(int, date.split("-")))
hour, minute = list(map(int, time.split(":")))
dt = datetime.datetime(year, month, day, hour, minute)
# format it nicely
formatted_date = custom_strftime("%A (%B {S}) at %I:%M%p", dt)
# send out an initial message to tell people the plan and ask for reactions
message = app.client.chat_postMessage(text=(f"Okay folks, we're good to go (thanks to <@{host}> for "
"hosting)! Whinetime will happen on "
f"*{formatted_date}* at *{location}*. I'll remind you closer "
"to the time but for now react to this message with :beers: "
"if you're coming!"), channel=ch_id)
# start the reactions going
app.client.reactions_add(channel=ch_id, timestamp=message["ts"], name="beers")
# calculate some timestamps in the future (I hope)
day_before = (dt - datetime.timedelta(days=1)).strftime("%s")
hour_before = (dt - datetime.timedelta(hours=7)).strftime("%s")
# attempt to send reminders (using Try because people may be too close to the time)
try:
result = client.chat_scheduleMessage(
channel=ch_id,
text=(f"Only one day to go until <#{ch_id}|whinetime>! :wine_glass: "
"Don't forget to react to the message above if you're coming"),
post_at=day_before
)
logger.info(result)
except SlackApiError as e:
logger.error("Error scheduling message: {}".format(e))
try:
result = client.chat_scheduleMessage(
channel=ch_id,
text=("Feeling that Friday fatigue? You need some "
f"<#{ch_id}|whinetime> mate and luckily it's "
f"a couple of hours to go :meowparty::meowparty: Remember it's at {location} this week, "
"hope you guys have fun, bring a souvenir for me! :gerald-wave:"),
post_at=hour_before
)
logger.info(result)
except SlackApiError as e:
logger.error("Error scheduling message: {}".format(e))
@app.action("whinetime-open")
def whinetime_logistics(ack, body, client):
ack()
# open the modal when someone clicks the button
host = body["actions"][0]["value"]
client.views_open(trigger_id=body["trigger_id"], view={
"callback_id": "whinetime-modal",
"title": {
"type": "plain_text",
"text": "Whinetime logistics",
"emoji": True
},
"submit": {
"type": "plain_text",
"text": "Submit",
"emoji": True,
},
"type": "modal",
"close": {
"type": "plain_text",
"text": "Cancel",
"emoji": True
},
"blocks": [
{
"type": "header",
"text": {
"type": "plain_text",
"text": f"Whinetime (Week of {datetime.datetime.now().strftime('%d/%m/%y')})",
"emoji": True
}
},
{
"type": "section",
"block_id": "whinetime-host-str",
"text": {
"type": "mrkdwn",
"text": (f"Okay you're the boss <@{host}>, what's the plan? Let me know and I'll send "
"reminders out for whinetime!")
}
},
{
"type": "input",
"block_id": "whinetime-location",
"element": {
"type": "plain_text_input",
"action_id": "whinetime-location"
},
"label": {
"type": "plain_text",
"text": "Where shall we go?",
"emoji": True
}
},
{
"type": "input",
"block_id": "whinetime-date",
"element": {
"type": "datepicker",
"initial_date": f"{datetime.date.today().strftime(r'%Y-%m-%d')}",
"placeholder": {
"type": "plain_text",
"text": "Select a date",
"emoji": True
},
"action_id": "datepicker-action"
},
"label": {
"type": "plain_text",
"text": "Which day?",
"emoji": True
}
},
{
"type": "input",
"block_id": "whinetime-time",
"element": {
"type": "timepicker",
"initial_time": "17:00",
"placeholder": {
"type": "plain_text",
"text": "Select time",
"emoji": True
},
"action_id": "timepicker-action"
},
"label": {
"type": "plain_text",
"text": "What time?",
"emoji": True
}
}
]
})
@app.action("whinetime-re-roll")
def whinetime_re_roll(ack, body, logger):
ack()
logger.info(body)
app.client.chat_delete(channel=body["container"]["channel_id"], ts=body["container"]["message_ts"])
wt.rotate_hosts()
start_whinetime_workflow(reroll=True)
def start_whinetime_workflow(reroll=False):
ch_id = find_channel("whinetime")
host = wt.get_next_host()
host_id = None
users = app.client.users_list()["members"]
for user in users:
if user["name"] == host:
host_id = user["id"]
if not reroll:
app.client.chat_postMessage(text=("Drumroll please :drum_with_drumsticks:...it's time to pick a "
"whinetime host"), channel=ch_id)
else:
messages = [("Not whinetime eh? Are you sure? You could be great, you know, whinetime will help you "
"on the way to greatness, no doubt about that — no? Well, if you're sure — better "
f"be ~GRYFFINDOR~ <@{host_id}>! :mage:"),
"Okay let's try that again, your whinetime host will be...:drum_with_drumsticks:",
"Nevermind, let's choose someone else, how about...:drum_with_drumsticks:",
("Not to worry anonymous citizen, the mantle will be passed on "
"to...:drum_with_drumsticks:"),
"Go go whinetime host choosing...:drum_with_drumsticks:"]
app.client.chat_postMessage(text=np.random.choice(messages).replace("\n", " "), channel=ch_id)
# post the announcement
announcement = app.client.chat_postMessage(channel=ch_id, blocks=[
{
"type": "header",
"text": {
"type": "plain_text",
"text": f"Whinetime (Week of {datetime.datetime.now().strftime('%d/%m/%y')})",
"emoji": True
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": f"Okay <@{host_id}>, you're the boss, what's the plan?"
}
},
{
"type": "actions",
"elements": [
{
"type": "button",
"text": {
"type": "plain_text",
"text": "Setup logistics",
"emoji": True
},
"value": host_id,
"action_id": "whinetime-open"
},
{
"type": "button",
"text": {
"type": "plain_text",
"text": "Choose someone else!",
"emoji": True
},
"action_id": "whinetime-re-roll",
"style": "danger",
"confirm": {
"title": {
"type": "plain_text",
"text": "Are you sure?"
},
"text": {
"type": "mrkdwn",
"text": "Wouldn't you rather be relaxing at whinetime :pleading_face:?"
},
"confirm": {
"type": "plain_text",
"text": "Do it"
},
"deny": {
"type": "plain_text",
"text": "Stop, I've changed my mind!"
}
},
}
]
},
])
global latest_whinetime_message
latest_whinetime_message = announcement
def when_whinetime_host(message, direct_msg=False):
thread_ts = None if direct_msg else message["ts"]
users = app.client.users_list()["members"]
my_username = None
for user in users:
if user["id"] == message["user"]:
my_username = user["name"]
weeks_until = wt.weeks_until_host(my_username)
today = datetime.date.today()
next_friday = today + datetime.timedelta((3 - today.weekday()) % 7 + 1)
hosting_friday = next_friday + datetime.timedelta(weeks=weeks_until)
app.client.chat_postMessage(text=("I've got you signed up to host whinetime on "
f"{custom_strftime('%B {S}', hosting_friday)} - but that may change "
"if anyone ends up skipping hosting so be sure to check closer to the "
"time (and I'll remind you don't worry)"),
channel=message["channel"], thread_ts=thread_ts)
""" ---------- BIRTHDAYS ---------- """
def say_happy_birthday(user_id):
"""Say happy birthday to a particular user
Parameters
----------
user_id : `str`
Slack ID of the user
"""
# pick a random GIF from the collection
gif_id = np.random.randint(0, 7 + 1)
gif_url = f"https://raw.githubusercontent.com/TomWagg/gerald/main/img/birthday_gifs/{gif_id}.gif"
# post the message with the GIF
app.client.chat_postMessage(channel=find_channel("random"),
text=f":birthday: Happy birthday to <@{user_id}>! :birthday:",
blocks=[
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": f":birthday: Happy birthday to <@{user_id}>! :birthday:",
}
},
{
"type": "image",
"image_url": gif_url,
"alt_text": "birthday"
}
])
def get_all_birthdays():
"""Get a list of all of the birthdays
Returns
-------
info : `list of tuples`
List of name, birthday pairs. Birthday is None if it's not in the table
"""
# open the file with the birthdays
info = []
with open("private_data/grad_info.csv") as birthday_file:
for grad in birthday_file:
# ignore any comment lines
if grad[0] == "#":
continue
name, _, _, birthday, _, _ = grad.split("|")
# if we don't have their birthday just write None
if birthday.rstrip() == "-":
info.append((name, None))
# otherwise format the birthday nicely
else:
day, month = map(int, birthday.rstrip().split("/"))
birthday_dt = datetime.date(year=2022, month=month, day=day)
info.append((name, custom_strftime("%B {S}", birthday_dt)))
return info
def list_birthdays(message, direct_msg=False):
"""List everyone's birthdays in a message in reply to someone
Parameters
----------
message : `Slack message`
A slack message object
direct_msg : `bool`, optional
Whether the message was a direct message (and thus whether to use a thread), by default False
"""
# get all of the birthdays
info = get_all_birthdays()
# two separate lists of whether we know the birthday or not
unknowns = ""
knowns = ""
# add names to the correct list
for name, birthday in info:
if birthday is None:
unknowns += f"• {name}\n"
else:
knowns += f"• {name} - {birthday}\n"
# combine into a full list message
birthday_list = ":birthday: Here's a list of birthdays that I know\n" + knowns
birthday_list += "\n:question: And here's a list of people I know but whose birthdays I don't\n" + unknowns
# post the message as a reply
thread_ts = None if direct_msg else message["ts"]
app.client.chat_postMessage(text=birthday_list.rstrip(), channel=message["channel"],
thread_ts=thread_ts)
def is_it_a_birthday():
""" Check if today is someone's birthday! """
# find the closest birthday
birthday_people, _, closest_time = closest_birthday()
# if you found someone and their birthday is today
if birthday_people != [] and closest_time == 0:
# get the list of users in the workspace
users = app.client.users_list()["members"]
for user in users:
for person in birthday_people:
# say happy birthday to each person (handle if there are more than one)
if user["name"] == person:
# do something special if it is Gerald's birthday
if person == "gerald":
gif_url = ("https://raw.githubusercontent.com/TomWagg/gerald/main/img"
"/birthday_gifs/gerald.gif")
# post the message with the GIF
app.client.chat_postMessage(channel=find_channel("random"),
text=":birthday: A special birthday :birthday:",
blocks=[
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": ("Hey psst, I'm sure you've got a "
"surprise party in the works but "
"just in case you forgot..."),
}
},
{
"type": "image",
"image_url": gif_url,
"alt_text": "birthday"
}
])
else:
say_happy_birthday(user["id"])
else:
print("No birthdays today!")
def closest_birthday():
""" Work out when the closest birthday to today is """
today = datetime.date.today()
usernames = []
names = []
closest_time = np.inf
# go through the birthday list
with open("private_data/grad_info.csv") as birthdays:
for grad in birthdays:
# ignore comment lines
if grad[0] == "#":
continue
name, username, _, birthday, _, _ = grad.split("|")
# ignore people without birthdays listed
if birthday.rstrip() == "-":
continue
# work out the year based on the month and day
day, month = map(int, birthday.rstrip().split("/"))
if month < today.month or (month == today.month and day < today.day):
year = today.year + 1
else:
year = today.year
# work out the days until the birthday
birthday_dt = datetime.date(year=year, month=month, day=day)
days_until = (birthday_dt - today).days
# if it's sooner than the current closest then replace it
if days_until < closest_time:
usernames = [username]
names = [name]
closest_time = days_until
# if it is equal then we have two people sharing a birthday!
elif days_until == closest_time:
usernames.append(username)
names.append(name)
return usernames, names, closest_time
def reply_closest_birthday(message, direct_msg=False):
"""Reply to someone with the next closest birthday to today
Parameters
----------
message : `Slack message`
A slack message object
direct_msg : `bool`, optional
Whether the message was a direct message (and thus whether to use a thread), by default False
"""
# get the closest birthday
_, names, closest_time = closest_birthday()
# write a string for how close it is (and be dramatic if it is today)
time_until_str = f"it's in {closest_time} days!" if closest_time != 0 else "it's today :scream:!!"
# if it is just one birthday
thread_ts = None if direct_msg else message["ts"]
if len(names) == 1:
reply = f"The next person to have a birthday is {names[0]} and "
reply += time_until_str
app.client.chat_postMessage(text=reply, channel=message["channel"], thread_ts=thread_ts)
else:
reply = "The next people to have birthdays are " + " AND ".join(names) + " and "
reply += time_until_str
app.client.chat_postMessage(text=reply, channel=message["channel"], thread_ts=thread_ts)
def reply_happy_birthday(message, direct_msg=False):
"""Reply to someone if they wish Gerald a happy birthday
Parameters
----------
message : `Slack message`
A slack message object
direct_msg : `bool`, optional
Whether the message was a direct message (and thus whether to use a thread), by default False
"""
thread_ts = None if direct_msg else message["ts"]
today = datetime.date.today()
if today.month == 8 and today.day == 5:
app.client.chat_postMessage(text="Thank you!! That's so nice of you to remember :gerald-love:",
channel=message["channel"], thread_ts=thread_ts)
else:
app.client.chat_postMessage(text=("Oh um, well thank you, I do appreciate the sentiment...but my "
"birthday is actually on the 5th of August "
":face_with_rolling_eyes:"),
channel=message["channel"], thread_ts=thread_ts)
def when_birthday(message, direct_msg=False):
thread_ts = None if direct_msg else message["ts"]
# find any tags
tags = re.findall(r"<[^>]*>", message["text"])
# let people say "my"
if message["text"].find("my") >= 0:
tags.append(f"<@{message['user']}>")
# remove Gerald from the tags if they are more than one
if len(tags) > 1 and f"<@{GERALD_ID}>" in tags:
tags.remove(f"<@{GERALD_ID}>")
# if you found at least one tag
if len(tags) > 0:
tag = tags[0]
users = app.client.users_list()["members"]
birthday_username = None
for user in users:
if user["id"] == tag.replace("<@", "").replace(">", ""):
birthday_username = user["name"]
break
with open("private_data/grad_info.csv") as birthday_file:
for grad in birthday_file:
_, username, _, birthday, _, _ = grad.split("|")
if username == birthday_username:
if birthday == "-":
app.client.chat_postMessage(text=(f"{insert_british_consternation()} This is a "
"little awkward but...I don't know this "
"birthday :sweat_smile:. Could you please let "
f"{GERALD_ADMIN} know I'll be sure to remember it "
"I promise!!"),
channel=message["channel"], thread_ts=thread_ts)
return
else:
day, month = map(int, birthday.split("/"))
dt = datetime.date(day=day, month=month, year=2022)
app.client.chat_postMessage(text=("I know this one! :gerald-search: "
f"It's {custom_strftime('%B {S}', dt)}!"),
channel=message["channel"], thread_ts=thread_ts)
return
app.client.chat_postMessage(text=("Unfortunately I don't have that person in my database! Maybe "
"they graduated??"),
channel=message["channel"], thread_ts=thread_ts)
else:
app.client.chat_postMessage(text="Uh...I think you asked about birthdays but you didn't say who??",
channel=message["channel"], ts=thread_ts)
""" ---------- PUBLICATION ANNOUNCEMENTS ---------- """
def reply_recent_papers(message, direct_msg=False):
"""Reply to a message with the most recent papers associated with a particular user
Parameters
----------
message : `Slack Message`
A slack message object
direct_msg : `bool`, optional
Whether the message was a direct message (and thus whether to use a thread), by default False
"""
queries = []
names = []
direct_queries = True
thread_ts = None if direct_msg else message["ts"]
numbers = re.findall(r" \d* ", message["text"])
n_papers = 1 if len(numbers) == 0 else int(numbers[0])
# if don't find any then look for users instead
if len(queries) == 0:
direct_queries = False
# find any tags
tags = re.findall(r"<[^>]*>", message["text"])
# remove Gerald from the tags
if f"<@{GERALD_ID}>" in tags:
tags.remove(f"<@{GERALD_ID}>")
# let people say "my" paper
if len(tags) == 0 and message["text"].find("my") >= 0:
tags.append(f"<@{message['user']}>")
# if you found at least one tag
if len(tags) > 0:
# go through each of them
for tag in tags:
# convert the tag to an query and a name
query, name = get_query_name_from_user_id(tag.replace("<@", "").replace(">", ""))
print("ADS query for:", query, name)
# append info