-
Notifications
You must be signed in to change notification settings - Fork 37
/
Copy pathapp.py
1349 lines (971 loc) · 59.5 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
#|||||||||||||||||||||||||||||||||||||||||||||||||||||||#
#
#--- Notes ---#
#
#
#====================================#
# Database close and open statements #
#====================================#
# The Database closed and opened comments are for easy identification of when we can
# access the database and to ensure we're properly closing connections
# after opening them. Sometimes they are at the far left, sometimes they are indented
# when they are indented it is because they have been closed or opened within an "if" meaning
# that code in other if statements is not affected. At the points where the database is closed
# or opened for every eventuality, the open or closed statement is pushed to the far left
# messages about whether the database is open or closed are also included within the functions
# but they rely on those connections being set up before the functions are called so if you change
# something and that breaks, that could be the cause
#====================================#
# App code and location code #
#====================================#
# Wherever you see this, even if it is in a function, it is only needed to help us keep track
# of what is going on in print statements. I've added it in as a variable so that each time
# we start a new process we change the location code. If two different parts of code call exactly
# the fame function we'll know if one of them is causing an error or whether it is the function itself
# because the print statement will help us keep track of where the information is coming from
#====================================#
# Functions #
#====================================#
# Anything of the format "bunch_of_words(thing_1, thing_2, thing_3, etc.)" is a function
# to execute them, you just write them out and replace thing_1 etc. with the information you want
# them to use, to create them you write "def" then write out the function using placeholders for the data
# wherever you find a function in this code and don't know what it does, scroll to the synchronous functions
# section at the bottom where it will be explained
#====================================#
# return #
#====================================#
# When we write "return" that's the end of the process, if you haven't got that, the code will keep
# going down the list, if it is within an "if" condition it'll jump out of the if and keep running unless
# there is no more code that it meets the conditions for
#====================================#
# Contexts #
#====================================#
# Don't confuse the contexts we RECEIVE from API.AI with the contexts we SEND to API.AI
# Slack token and challenge management resource: https://github.com/slackapi/Slack-Python-Onboarding-Tutorial
#====================================#
# Deduplication section #
#====================================#
# Slack is quite prone to sending event notifications more than once, particularly
# if our application takes a little while longer to respond. One solution is to
# pay money for faster processing, another is to move the response up as high as possible
# in the code so we respond as quickly as possible, a third is to run through some Deduplication
# when we get these messages and discount the repeated calls.
# The risk here is that the user might legitimately send us two identical responses
# within a short time frame, perhaps, for example, if they are responding to a few messages with "yes"
# . The solution here is to carefully manage the amount of time we're ignoring (a minute should hopefully
# cover Slack resent responses without discounting many repeated messages) and to control the times when users might send a
# duplicate response by using buttons in Slack. Some chat bots use buttons to help control the full conversational
# flow but relying purely on buttons does devolve the conversation into essentially a pretty linear website journey
#====================================#
# Tokens #
#====================================#
# This program uses a combination of user tokens and bot tokens, user tokens give us permission to post wherever that user can
# bot tokens give us permission to post wherever that bot has been allowed. In this application that difference in freedom
# to roam doesn't particularly change the functionality but it does allow us to show how we'd deal with each.
#====================================#
# Celery #
#====================================#
# Celery on Heroku resources (the former is a good resource for understanding but clashes with our database so this
# program implements the latter): https://blog.miguelgrinberg.com/post/using-celery-with-flask
# and https://devcenter.heroku.com/articles/celery-heroku
#
#
#--- End of notes ---#
#
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
#|||||||||||||||||||||||||||||||||||||||||||||||||||||||#
#
#--- Steps to take before implementing this code ---#
# 1 Create an API.AI account and build your conversation
# The important parts here are the intents which need to include phrases and actions
# food:
# User says: I want [food] (etc.)
# In context: None
# Out context: food-followup
# Values: parameter: food, entity: sys.any, value:$food
# Action: food
# Event: None
# Response (this is overwritten in our code but could also be created through API.AI): Ok [food] for [username], got it, I'll add that and let you know when we're done
# food-fallback (we've called this food fallback because it would usually be food however we don't process it as food to be safe)
# User says: whatever the heck they want
# In context: None
# Out context: None
# Action: food-fallback
# Event: None
# Response: I'm sorry, I don't have a huge number of functions so I can't understand very many inputs, try saying "I want" and then whatever food you'd like to order
# no-name
# User says: nothing, we trigger this by sending an Event
# In context: None
# Out context: food-followup, no-name
# Action: None
# Event: food-no-name
# Response: It looks as though you haven't ordered with Nambot before, what name should I use?
# name-is
# User says: "my name is [name]"
# In context: no-name, food-followup
# Out context: food-followup, name-is
# Action: name-is
# Event: None
# Response: (this is overwritten in our code but could also be created through API.AI): Ok [food] for [username], got it, I'll add that and let you know when we're done
# name-fallback
# User says: whatever the heck they want
# In context: no-name, food-followup
# Out context: name-fallback, food-followup
# Action: name-fallback
# Event: None
# Response: (this is overwritten in our code) I think you're asking for me to set your name as [whatever the heck they want], is that right?
#================================================================================
# Add the following by clicking on the name-fallback intent and selecting "yes"
#================================================================================
# name-confirmation
# User says: yes (or similar, this is generated by API.AI)
# In context: name-fallback, food-followup, potential-name (we set this last one through a POST request to API.AI)
# Out context: food-followup
# Action: name-confirmation
# Event: None
# Response: (this is overwritten in our code but could also be created through API.AI): Ok [food] for [username], got it, I'll add that and let you know when we're done
# more-details
# User says: give me more info
# In context: order-list
# Out context:
# Action: give-deets
# Event: None
# Response: (this is overwritten in our code but could also be created through API.AI): Ok I'll get those for you
#================================================================================
# Add the following by clicking on the name-fallback intent and selecting "no"
#================================================================================
# name-incorrect
# User says: no (or similar)
# In context: name-fallback, food-followup, potential-name (we set this last one through a POST request to API.AI)
# Out context: food-followup, no-name
# Action: name-incorrect
# Event: None
# Response: Oh, sorry about getting your name wrong there, could you let me know your name by saying "My name is" and then writing your name
# no-details
# User says: no more info
# In context: order-list
# Out context:
# Action: no-deets
# Event: None
# Response: (this is overwritten in our code but could also be created through API.AI): Ok you can order at https://caphehouse.orderswift.com/ and the sheet at [sheet location]
#================================================================================
# Add the following as normal
#================================================================================
# messing-about
# User says: " 'My name is' and then writing your name"
# In context: no-name, food-followup
# Out context: no-name, food-followup
# Action: messing-about
# Event: None
# Response: Haha, very funny, do just tell me your name and we'll continue
# help:
# User says: "Help" (etc. you'll need to write this out yourself)
# In context: None
# Out context: None
# Action: Help
# Event: None
# Response: Hi, I'm Vietnam bot, I can place your Vietnamese order in the shared Google sheet. Just say "I want " and then the food you want
# change-name:
# User says: I want to be called [name]
# In context: None
# Out context: new-name
# Action: change-name
# Values: parameter: name, entity: sys-any, value:$name
# Event: None
# Response: (overwritten by our code) Ok I'll do that
# 2 Create Heroku app: https://devcenter.heroku.com/articles/git
# The above will take you through the steps needed to create a Heroku application
# it looks complicated but the instructions are really easy to follow, it's one of
# the first things I did
#--- End of steps to take before implementing this code ---#
#
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
#|||||||||||||||||||||||||||||||||||||||||||||||||||||||#
#
#--- Requirements for process in chronological order ---#
#
#
# 1 Receive Slack token and username and record in database for future identification and retrieval, use that initial information to send private message to user which gives basic instruction
# 2 Receive Slack challenge, respond (this allows the bot to receive notifications from Slack)
# 3 Receive message posted in either private Slack channel or a public channel which the bot has been added to, check to ensure the message is not a repeated message by checking that it is not exactly the same as the last message while being within a minute of the last message
# (this is to account for the fact that users might send the same message time after time if they want the same order)
# 4 Check that message doesn't have a bot id - suggesting that it was sent by a bot (perhaps vietnambot) this is to avoid the program responding to itself
# 5 Whether the message is genuine, a repeat or a bot message, respond semi-immediately to prevent Slack resending the event notifications
# 6 Take all genuine messages and initially check database using user_id as unique key to determine whether we have a user name
# 7 Send message to API.AI to update API.AI contexts and to have message categorised by API.AI system
# 8 Receive response from API.AI and, depending on whether we have username, either send follow-up message to API.AI to create username request contexts
# 9 Depending on whether username is present, either start process_food process or ask for username, set username, and start process_food process
# 10 Connect to Google Sheet with GSpread library and select the correct sheet
# 11 Find the first empty row in the sheet by counting all the cells in the first column and subtracting those with None values
# 12 Add values of date, user name, and order food to the first empty row
# 13 Retrieve the current "top nommer" record for the team concerned (which is saved in the database from previous operations for speed of retrieval)
# 14 Calculate the number of orders that have been placed today by filtering recent results to only those which match the current date
# 15 Send appropriate follow-up message based on number of returned results matching minimum threshold and whether the current "top nommer" matches the one retrieved
# 16 Calulate the "top nommer" and update the database for quick retrieval in the next process
#
#
#--- End of process requirements ---#
#
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
#|||||||||||||||||||||||||||||||||||||||||||||||||||||||#
#
#--- API integrations and limitations ---#
#
#
# 1 API.AI - natural language processing (https://api.ai/)
# Key points of interaction are:
# 1 Initial message which can be formatted to include information or trigger an event (either POST or GET request)
# 2 Optional slot filling portion, where it sends a request and waits five seconds for a response (POST request)
# 3 Point as which it responds to the initial message with key information about categorisation and actions set up in the API.AI interface accessible at API.AI
# Limitations are:
# 1 If using its built-in integrations with existing chat services, you must use the optional slot filling to do your processing
# 2 Using the optional slot filling means you must respond within 5 seconds, if your app needs to pass information back it must be done in that time (should be no problem unless using a very slow API)
# 3 When it receives a POST or GET request, it will only offer a response to that, it won't offer information by new POST or GET (meaning you can't send and forget, pretty standard operation of these requests)
# 4 If you are using the optional slot filling, API.AI can NOT receive both the initial request AND the response to its slot filling request from the same place. This isn't a problem most of the time, if you are manually sending a request to API.AI you should do the processing before or after and avoid the slot filling option if possible)
# 5 If your process is slower than the five second window you cannot use API.AI with platforms like Google Home because you HAVE to use the slot filling option in that case. This tight window and inability to push messages to spoken platforms is quite standard to avoid a confusing or invasive user experience (also applies to Alexa)
# 6 Excellent platform but support team are slow to respond/ sometimes don't respond at all
# 2 Slack - messaging platform (https://slack.com/)
# Key points of interaction are:
# 1 Slack sending "challenge" request to our service to make sure it is set up to receive events from Slack
# 2 Slack sending user name and user token to our service, for us to save, as part of authentication
# 3 Slack pushing events to our application whenever someone posts a message that our bot sees which meets our criteria
# Limitations are:
# 1 Slack requires a quick response to the messages it sends, and can send them multiple times which can result in repeated action from our app if it isn't deduped (particularly problematic as on Heroku free the app will take a while to respond, and all in all this process takes quite a while so we can't wait until we've finished to respond)
# 2 Posting to Slack channels requires an appropriate user token which must be stored securely
# 3 GSpread - Google sheet interaction library
# Key points of interaction are:
# 1 Retrieving first empty row
# 2 Putting order in first empty row
# 3 Checking number of orders that match today's date
# 4 Checking the most frequently occurring user in recent orders
# Limitations are:
# 1 Reading blocks of data from this are very slow (often longer than five seconds) meaning we cannot do this within API.AI's optional five second slot filling window, meaning we have to process either before or after API.AI has done it's process, so we have to manage Slack integrations and auth
# 2 Empty cells don't return None value, but rather "" meaning we have to check for empty cells in a slightly different way
#--- End of APIs integrations and limitations ---#
#
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
#|||||||||||||||||||||||||||||||||||||||||||||||||||||||#
#
#--- Platforms used ---#
# 1 Heroku - free plan https://www.heroku.com/
# Heroku Redis :: Redis (free)
# Heroku Postgres :: Database (free)
# LogDNA (paid)
#--- End of platforms used ---#
#
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
#|||||||||||||||||||||||||||||||||||||||||||||||||||||||#
#
#--- Requirements for database ---#
# 1 Must contain: id serial PRIMARY KEY, source varchar, user_id varchar, user_name varchar, user_token varchar, team_id varchar, team_name varchar, bot_token varchar, most_recent_user_channel varchar, most_recent_user_session_id varchar, most_recent_action_for_user varchar, most_recent_user_food varchar, most_recent_user_query varchar, most_recent_query_time TIME
# 2 Must be quick
# 3 readable from web and worker processes
# 3 Must allow read and overwrite
#Some of the columns in that database list are as-yet unused. For instance most_recent_user_food, however that will allow more easy rollout of features like easy repeat ordering
#--- End of database requirements ---#
#
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
#|||||||||||||||||||||||||||||||||||||||||||||||||||||||#
#
#--- Imports ---#
# Throughout this program we call the below imports for various actions
# failing to import one of these libraries, or importing the wrong one
# may be one reason for an app just failing mid-process
# All of these libraries need to be reflected in our requirements file
# which we upload as part of our app
import os
from flask import Flask, make_response, request
from slackclient import SlackClient
import gspread
import oauth2client
import datetime
from datetime import datetime, time, timedelta
import time
from oauth2client.service_account import ServiceAccountCredentials
import json
import os.path
import sys
import requests
try:
import apiai
except ImportError:
sys.path.append(
os.path.join(os.path.dirname(os.path.realpath(__file__)), os.pardir))
import apiai
from collections import Counter
import psycopg2
from flask.ext.sqlalchemy import SQLAlchemy
#Thanks to http://blog.y3xz.com/blog/2012/08/16/flask-and-postgresql-on-heroku
import urllib.parse
#Thanks to https://stackoverflow.com/questions/45133831/heroku-cant-launch-python-flask-app-attributeerror-function-object-has-no
import tasks
app = Flask(__name__)
#--- End of imports ---#
#
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
#===========================================================================================================================#
#---------------------------------------------------------------------------------------------------------------------------#
#===========================================================================================================================#
#|||||||||||||||||||||||||||||||||||||||||||||||||||||||#
#
#--- Confirming asynchronous process is working ---#
# This is run when we are first starting up our application.
# It allows us to make sure that our asynchronous worker process
# is receiving commands correctly
tasks.add("startup", " testing task ",1, 2)
#--- End of confirming asynchronous process is working ---#
#
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
#|||||||||||||||||||||||||||||||||||||||||||||||||||||||#
#
#--- Environment variables ---#
# These are pieces of information that any app in our program
# should be able to call. One reason to set them as environmental
# variables is that by sharing the code we aren't sharing confidential
# login information.
#Slack variables
#
# We set these variables based on information from our Slack bot
# instructions for how to create environment variables in Heroku are at (https://devcenter.heroku.com/articles/config-vars)
client_id = os.environ["SLACK_CLIENT_ID"]
client_secret = os.environ["SLACK_CLIENT_SECRET"]
# Slack verification token is the unique token that Slack provides in the
# Basic Information section in the Slack app management portal (https://api.slack.com/apps/A6GN5QC8G/general)
# You use it for interactive messages to confirm that the message is coming from Slack
slack_verification_token=os.environ["SLACK_VERIFICATION_TOKEN"]
#
#-#
#Database variables
#
# These variables are instructions to allow our program to connect to the
# database we set up
# instructions for how to create environment variables in Heroku are at (https://devcenter.heroku.com/articles/config-vars)
# setting database variables: http://blog.y3xz.com/blog/2012/08/16/flask-and-postgresql-on-heroku
app.config['SQLALCHEMY_DATABASE_URI'] = os.environ['DATABASE_URL']
db = SQLAlchemy(app)
#
#-#
#--- End of environment variables ---#
#
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
#|||||||||||||||||||||||||||||||||||||||||||||||||||||||#
#
#--- 1 Start application ---#
# I put this big block here because it allows us to easily identify the time when we last uploaded our code. Quite often when
# something isn't working Slack sends multiple requests so it can take a while to find the actual start, this speeds up
# that process
print ("""
#===========================================================================================================================#
#---------------------------------------------------------------------------------------------------------------------------#
#===========================================================================================================================#
""")
@app.route('/', methods=["GET", "POST"])
def webhook():
#------------------------------------------------------------#
# Setting location codes, these will be used in print commands
# so we can easily keep track of where things are. The only
# purpose of these is to be included in the print commands
# however if they are removed the app will break at the first
# print
#------------------------------------------------------------#
app_code="web "
location_code="1 (startup)"
#------------------------------------------------------------#
# End of setting location codes
#------------------------------------------------------------#
#------------------------------------------------------------#
# Printing initial startup information, sharing the request
# that activated the app_code
#------------------------------------------------------------#
print (app_code,location_code, "starting at ", datetime.utcnow())
print(app_code,location_code, "request", request)
print(app_code,location_code, "request method", request.method)
#------------------------------------------------------------#
# End of printing initial startup info
#------------------------------------------------------------#
#--- End of start application ---#
#
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
#===========================================================================================================================#
#---------------------------------------------------------------------------------------------------------------------------#
#===========================================================================================================================#
#|||||||||||||||||||||||||||||||||||||||||||||||||||||||#
#
#--- 2 Receive Slack token and record in database ---#
#
# Thanks to https://github.com/slackapi/Slack-Python-Onboarding-Tutorial
if request.method == 'GET':
location_code="2 (slack token)"
print (app_code,location_code, "method is GET at ", datetime.utcnow())
#Only 'GET' requests come from Slack when user is authorising app and giving user token
print (app_code,location_code, "received get request, starting get process")
open_db_connection(app_code,location_code)
# In order to read or make any changes to the
# database we need to open the connetion and
# set up the relevant variables. We want to
# minimise the number of concurrent connections
# so we use our open and close connection functions
# the notice below is for easy reading of whether it's
# accessible
#------------------------------------------------------#
#//////////////////////////////////////////////////////#
#============ Database connection open ==============#
#//////////////////////////////////////////////////////#
#------------------------------------------------------#
# Calling action from python onboarding tutorial which retrieves
# the appropriate tokens and authentication
auth_complete=get_token(app_code,location_code)
# Closing the connection with the database
close_db_connection(app_code,location_code)
#------------------------------------------------------#
#//////////////////////////////////////////////////////#
#============ Database connection closed =============#
#//////////////////////////////////////////////////////#
#------------------------------------------------------#
print (app_code,location_code,"successfully got token, finishing process and shutting down")
# This is the message we send to the Slack user, this is very plain
# but instead we could send HTML for a richer page
return auth_complete
#----- Process ended because responded to auth -----#
#--- End of 2 receive Slack token and record in database ---#
#
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
#===========================================================================================================================#
#---------------------------------------------------------------------------------------------------------------------------#
#===========================================================================================================================#
#|||||||||||||||||||||||||||||||||||||||||||||||||||||||#
#
#--- 3 Respond to Slack Challenge request ---#
#
# Thanks to https://github.com/slackapi/Slack-Python-Onboarding-Tutorial
if request.method == 'POST':
# If the request isn't 'Get' we need to start working out what it IS
# The first test is whether it contains "challenge", if so it's part of the Slack authorisation setup
location_code="3 (received POST)"
print (app_code,location_code," method is POST at ", datetime.utcnow()," post process started")
# Getting the information from the 'Post' request, whatever it may be
post_request = request.get_json(silent=True, force=True)
if not post_request:
# If we can't unpack the post request like this then there's a good chance it is
# an interactive message from Slack so we just need to handle it differently
button_message(app_code,location_code,request)
return make_response("", 200)
#Printing the value of the post request for debugging
print (app_code,location_code," got json")
print (app_code,location_code,"post_request: ", post_request)
# As mentioned above, if it contains 'challenge' that's an easy way to see it's part of Slack auth so this is
# dealing with that eventuality first so we can cater to other scenarios further down
if "challenge" in post_request:
# We are using the presence of a challenge value
# as a way of identifying a challenge request
# from Slack
location_code="3.1 (Slack challenge)"
print (app_code,location_code," challenge detected")
# Calling function from Slack-Python-Onboarding-Tutorial which creates an
# appropriate response to the challenge request (https://github.com/slackapi/Slack-Python-Onboarding-Tutorial)
response_to_challenge=challenge_response(app_code,location_code,post_request)
print (app_code,location_code," response_to_challenge: ",response_to_challenge )
print (app_code,location_code,"success, sending challenge response")
# Sending the response to Slack, it's important that this is
# right otherwise Slack won't accept it and send us notifications
# when people post
return response_to_challenge
#----- Process ended because responded to challenge -----#
#--- End of 3 respond to Slack Challenge request ---#
#
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
#===========================================================================================================================#
#---------------------------------------------------------------------------------------------------------------------------#
#===========================================================================================================================#
#|||||||||||||||||||||||||||||||||||||||||||||||||||||||#
#
#--- 4 Receive Slack message - send response immediately and have worker send message on to API.AI ---#
else:
# Now that we know the request ISN'T either the challenge or token Slack authorisation we can manage the
# scenario where it is an actual message passed to our bot from Slack
# Slack needs a quick response otherwise it will keep sending the
# message so we have to use our asynchronous background process
# to do most of the work while this main process just triages and
# sends a quick 200 "I've got it" type response
location_code="4.1 (Slack user message)"
print (app_code,location_code," know it isn't challange at ", datetime.utcnow())
if "event" in post_request:
# When our Slack integration is activated by a user the
# Post request includes an 'event' field
print (app_code,location_code," slack event")
# Peeling out information from Slack event
post_request_data=post_request.get("event")
print (app_code,location_code," have got event")
# Printing out the data for debugging
print (app_code,location_code," post_request_data", post_request_data)
#|||||||||||||||||||||||||||||||||||||||||||||||||||||||#
#
#--- 4.1.1 check to make sure bot doesn't respond to messages from bots ---#
# Asking for bot_id from the message, if it doesn't exist the value will be
# None so we'll skip over the next "if bot_id" step
bot_id=post_request_data.get("bot_id")
if bot_id:
location_code="4.1.1 (bot message)"
print (app_code,location_code," picking up bot message, id: ", bot_id)
# If a bot has posted the message (including us)
# the message will have a bot_id, we want to ignore those
print (app_code, location_code, "shutting down")
return make_response("Bot message", 200)
#----- Process ended because bot message -----#
#--- End of 4.1.1 check to make sure bot doesn't respond to messages from bots ---#
#
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
#|||||||||||||||||||||||||||||||||||||||||||||||||||||||#
#
#--- 4.2 getting information to direct response ---#
location_code="4.2 (getting info)"
# Getting the unique Slack user ID, we also use this
# unique number in our database to identify and update
# user records
user_id=post_request_data.get("user")
print (app_code,location_code," user_id: ", user_id)
# Getting what user has said in the message
query=post_request_data.get("text")
print (app_code,location_code," query: ", query)
#Getting event_id from function argument
event_id=post_request.get("event_id")
print (app_code,location_code," event id: ", event_id)
#getting source that user is messaging from (to direct message back)
channel=post_request_data.get("channel")
print (app_code,location_code," channel: ", channel)
#--- End of 4.2 getting location information to direct response ---#
#
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
#|||||||||||||||||||||||||||||||||||||||||||||||||||||||#
#
#--- 4.3 Sending message to asynchronous process (async will then check if it's duplicat) ---#
#Sending to async thanks https://devcenter.heroku.com/articles/celery-heroku
# Unfortunately sending Slack messages to the API.AI process, then processing the response
# and calculating information using the Google Sheets integration takes too long
# we are using asynchronous processing to manage our longer process so we can
# send Slack the "quick and confident" 200 response mentioned here: https://api.slack.com/events-api
location_code="4.3 (send async)"
print (app_code,location_code," sending to worker")
tasks.send_to_api(event_id, user_id, channel, query)
#--- End of 4.3 Sending genuine message to asynchronous process ---#
#
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
#|||||||||||||||||||||||||||||||||||||||||||||||||||||||#
#
#--- 4.4 make quick 200 response to Slack to confirm we've received the message to stop it sending again ---#
#Sending quick response to Slack to confirm we have received the message
return make_response("Passed to API", 200)
#--- End of 4.4 make quick 200 response to Slack to confirm we've received the message to stop it sending again ---#
#
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
#--- End of 4 Receive Slack message - send response immediately and have worker send message on to API.AI ---#
#
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
#===========================================================================================================================#
#---------------------------------------------------------------------------------------------------------------------------#
#============================================= Synchronous functions =======================================================#
#---------------------------------------------------------------------------------------------------------------------------#
#===========================================================================================================================#
#|||||||||||||||||||||||||||||||||||||||||||||||||||||||#
#
#--- (A) getting Slack token ---#
def get_token(app_code,location_code):
#|||||||||||||||||||||||||||||||||||||||||||||||||||||||#
#
#--- Sending information to Slack API and getting authorisation token in response---#
# Thanks to (https://github.com/slackapi/Slack-Python-Onboarding-Tutorial/blob/master/app.py)
sublocation="A (get_token) - "
print(app_code,location_code,sublocation,"token received")
print(app_code,location_code,sublocation," request referrer: ",request.headers.get("Referer"))
print(app_code,location_code,sublocation," starting post install")
# Retrieve the auth code from the request params
auth_code = request.args['code']
print (app_code,location_code,sublocation," auth code: ", auth_code)
# An empty string is a valid token for this request
sc = SlackClient("")
# Request the auth tokens from Slack
auth_response = sc.api_call(
"oauth.access",
client_id=client_id,
client_secret=client_secret,
code=auth_code
)
# Printing off the authorisation response from Slack
# for debugging
print(app_code,location_code,sublocation,"auth response: ",auth_response)
print(app_code,location_code,sublocation,"app access token",auth_response['access_token'])
print(app_code,location_code,sublocation,"bot access token",auth_response['bot']['bot_access_token'])
#--- End of Sending information to Slack API and getting authorisation token in response ---#
#
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
#|||||||||||||||||||||||||||||||||||||||||||||||||||||||#
#
#--- Adding information to database ---#
# If you haven't got your database set up yet or you're testing this process regardless of the database
# then comment out the lines from here until "End of adding information to database" and remove the comments
# from the "Adding information to environmental variables" block
print(app_code,location_code,sublocation," printing authorisation response: ", auth_response)
source="Slack" #This is hard coded for now but could be changed based on where the authorisation request comes from
user_id=auth_response['user_id']
user_token= auth_response['access_token']
bot_token= auth_response['bot']['bot_access_token']
team_id= auth_response['team_id']
team_name= auth_response['team_name']
channel=auth_response['incoming_webhook']['channel_id']
# user_creator process takes information and puts it in the database
# we use the user_id as our unique identifier when calling and updating
# information, we also use the user_token as authorisation to post to Slack
user_creator(app_code,location_code,source, user_id, user_token, bot_token, team_id, team_name)
#--- End of Adding information to database ---#
#
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
#|||||||||||||||||||||||||||||||||||||||||||||||||||||||#
#
#--- Adding information to environmental variables ---#
#
#os.environ["SLACK_USER_TOKEN"] = auth_response['access_token']
#os.environ["SLACK_BOT_TOKEN"] = auth_response['bot']['bot_access_token']
#
#--- End of Adding information to environmental variables ---#
#
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
print (app_code,location_code,sublocation," done user_creator process")
speech_to_send='Hi, thanks for adding Vietnambot! If you ever want to add an order to the Vietnamese sheet just write that food in this channel or say "I want [food]" and I\'ll add it. For reference the sheet is located at: '+google_sheet_url
#Sending message to slack which includes the defined speech from API.AI (thanks to https://api.slack.com/methods/chat.postMessage)
params = (
('token', user_token),
('channel', channel),
('text', speech_to_send),
('username', 'vietnambot'),
('icon_emoji', ':ramen:'),
('pretty', '1'),
)
requests.get('https://slack.com/api/chat.postMessage', params=params)
print (app_code,location_code,sublocation,"sent to Slack, finishing process")
#----------#
# Don't forget to let the user know that auth has succeeded!
return "Auth complete!"
#--- End of (A) Getting slack token ---#
#
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
#===========================================================================================================================#
#---------------------------------------------------------------------------------------------------------------------------#
#===========================================================================================================================#
#|||||||||||||||||||||||||||||||||||||||||||||||||||||||#
#
#--- (B) responding to challenge request ---#
def challenge_response(app_code,location_code,post_request):
#Taken from https://github.com/slackapi/Slack-Python-Onboarding-Tutorial/blob/master/app.py
#This route listens for incoming events from Slack and uses the event
sublocation="B (challenge_response) - "
print (app_code,location_code,sublocation,"post_request is", post_request)
# ============= Slack URL Verification ============ #
# In order to verify the url of our endpoint, Slack will send a challenge
# token in a request and check for this token in the response our endpoint
# sends back.
# For more info: https://api.slack.com/events/url_verification
print(app_code,location_code,sublocation," challenge in slack event, creating response")
return make_response(post_request["challenge"], 200, {"content_type":"application/json"})
#--- End of Web responding to challenge request ---#
#
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
#===========================================================================================================================#
#---------------------------------------------------------------------------------------------------------------------------#
#===========================================================================================================================#
#|||||||||||||||||||||||||||||||||||||||||||||||||||||||#
#
#--- C Setting up psycopg2 connection ---#
# Thanks to: https://devcenter.heroku.com/articles/heroku-postgresql#connecting-in-python
def open_db_connection(app_code,location_code):
sublocation="C (open_db_connection) - "
print (app_code,location_code,sublocation," starting setting up database connection")
#Thanks to https://stackoverflow.com/questions/45133831/heroku-cant-launch-python-flask-app-attributeerror-function-object-has-no
urllib.parse.uses_netloc.append("postgres")
url = urllib.parse.urlparse(os.environ["DATABASE_URL"])
#}
print (app_code,location_code,sublocation," url: ", url)
#|||||||||||||||||||||||||||||||||||||||||||||||||||||||#
#
#--- Setting conn as a global variable ---#
# We are setting conn as a global variable which means it doesn't
# have to be included when we call a new function, we will be referencing
# it in a few functions so it's important to be able to access it
# without causing errors by forgetting to include it from one function to the next
global conn
conn = psycopg2.connect(
database=url.path[1:],
user=url.username,
password=url.password,
host=url.hostname,
port=url.port
)
#--- End of Setting conn as a global variable ---#
#
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
#|||||||||||||||||||||||||||||||||||||||||||||||||||||||#
#
#--- Setting cur as a global variable ---#
# See reasoning for including conn as global variable above
global cur
cur = conn.cursor()
#--- End of Setting cur as a global variable ---#
#
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
print (app_code,location_code,sublocation," finishing setting up database connection")
return
#--- End of Web Setting up psycopg2 connection ---#
#
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
#===========================================================================================================================#
#---------------------------------------------------------------------------------------------------------------------------#
#===========================================================================================================================#
#|||||||||||||||||||||||||||||||||||||||||||||||||||||||#
#
#--- D Web closing psycopg2 connection ---#
def close_db_connection(app_code,location_code):
sublocation="D (close_db_connection) - "