-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathlambda_function.py
522 lines (411 loc) · 21.3 KB
/
lambda_function.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
import asyncio
import json
import os
import traceback
import logging
from datetime import datetime
from typing import Dict
from telegram import ReplyKeyboardMarkup, ReplyKeyboardRemove, Update
from telegram.ext import (
Application,
CommandHandler,
ContextTypes,
ConversationHandler,
MessageHandler,
filters,
)
from dynamodbhelperv4 import DynamoDBHelper
db = DynamoDBHelper()
################################### Enable logging ###################################
logging.basicConfig(
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO
)
# set higher logging level for httpx to avoid all GET and POST requests being logged
logging.getLogger("httpx").setLevel(logging.WARNING)
logger = logging.getLogger(__name__)
LOGIN_REPLY, CHOOSING_CELL, CHOOSING_EVENTTYPE, CHOOSING_MONTH, CHOOSING_DAY, CHOOSING_MEMBERS_ATTENDEES, REMOVING_MEMBERS_ATTENDEES, CHOOSING_MEMBERS_VALABSENTEES, REMOVING_MEMBERS_VALABSENTEES = range(9)
reply_keyboard = sorted([[item] for item in db.get_cell_groups()])
markup = ReplyKeyboardMarkup(reply_keyboard, one_time_keyboard=True)
################################### Helper Function ###################################
def facts_to_str(user_data: Dict[str, str]) -> str:
"""Helper function for formatting the gathered user info."""
facts = [f"{key}: {value}\n" for key, value in user_data.items() if key not in ['Attendees','Valid Absentees']]
if 'Attendees' in user_data.keys():
facts = facts + [f"{key} ({len(value)}):" for key, value in user_data.items() if key == 'Attendees']
for n, item in enumerate(user_data['Attendees']):
facts.append(f'{n+1}. {item}')
if 'Valid Absentees' in user_data.keys():
facts = facts + [f"\n{key} ({len(value)}):" for key, value in user_data.items() if key == 'Valid Absentees']
for n, item in enumerate(user_data['Valid Absentees']):
facts.append(f'{n+1}. {item}')
print(facts)
return "\n".join(facts).join(["\n", "\n"])
def get_relevant_cell_members(cell_group, event_type, date):
"""Helper function for gathering three sets of information:
1. all cell members in the cell group,
2. attendees on the given date
3. valid absentees on the given date"""
attended_cell_members, absentvalid_cell_members = [], []
clean_date = datetime.strptime(date, '%Y-%b-%d')
all_cell_members = db.get_cell_members(cell_group)
attended_cell_members = db.get_alr_attended_cell_members(cell_group, event_type, clean_date)
absentvalid_cell_members = db.get_alr_absentvalid_cell_members(cell_group, event_type, clean_date)
return all_cell_members, attended_cell_members, absentvalid_cell_members
################################### State Function ###################################
## /start
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Start the conversation and ask user for verification."""
await update.message.reply_text(
f"Hi! This is an attendance bot for PoD, the youth ministry of COSB. If you wish to exit the attendance taking at any point of this exercise, simple type '/exit'."
"\n\n<b>Before we begin, I have to verify you. Please kindly insert the verification code.</b>",
parse_mode = 'HTML'
)
return LOGIN_REPLY
## /select_cell
async def select_cell(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Ask user to select cell group"""
await update.message.reply_text(
f"Welcome {update.effective_user.first_name}!"
" What cell group are we taking attendance for?",
reply_markup=markup,
parse_mode = 'HTML'
)
return CHOOSING_CELL
## /select_type
async def select_eventtype(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Ask user to select attendance type"""
text = update.message.text
context.user_data["Cell"] = text
## prepare a keyboard for the number of months
reply_keyboard = [['Sunday Service'],['Cell Group'],['Others']]
markup = ReplyKeyboardMarkup(reply_keyboard, one_time_keyboard=True)
await update.message.reply_text(
f"You have selected {text}!"
" What type of event is this for?",
reply_markup=markup,
parse_mode = 'HTML'
)
return CHOOSING_EVENTTYPE
## selecting the month
async def select_month(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Ask user for the month selection."""
text = update.message.text
context.user_data["Event Type"] = text
## prepare a keyboard for the number of months
reply_keyboard = [['Jan','Feb','Mar'],['Apr','May','Jun'],['Jul','Aug','Sep'],['Oct','Nov','Dec']]
markup = ReplyKeyboardMarkup(reply_keyboard, one_time_keyboard=True)
await update.message.reply_text(
f"You are taking {context.user_data['Cell']}'s attendance for {text}!"
" What month are we taking attendance for?",
reply_markup=markup,
parse_mode = 'HTML'
)
return CHOOSING_MONTH
## selecting the day
async def select_day(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Ask user for the day selection."""
text = update.message.text
context.user_data["month"] = text
print(context.user_data["Cell"],db.get_cell_members(context.user_data["Cell"]))
## prepare a keyboard for the number of months
reply_keyboard = [['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']]
markup = ReplyKeyboardMarkup(reply_keyboard, one_time_keyboard=True)
await update.message.reply_text(
f"You are taking {context.user_data['Cell']}'s attendance for {context.user_data['Event Type']}, in the month of {text}!"
" What day are we taking attendance for?",
reply_markup=markup,
parse_mode = 'HTML'
)
return CHOOSING_DAY
## selecting cell members
async def regular_choice_attendees(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Ask the user for cell members who attended."""
text = update.message.text
context.user_data["day"] = text
## store into the context object the user's date
attendance_date = str(datetime.now().year) + f'-{context.user_data["month"]}-{context.user_data["day"]}'
context.user_data["Date"] = attendance_date
del context.user_data["month"]
del context.user_data["day"]
## prepare lists of the relevant cell members
all_cell_members, attended_cell_members, absentvalid_cell_members = get_relevant_cell_members(context.user_data["Cell"], context.user_data["Event Type"], context.user_data['Date'])
context.user_data['Attendees'] = sorted(attended_cell_members)
context.user_data['Valid Absentees'] = sorted(absentvalid_cell_members)
relevant_cell_members = list(set(all_cell_members) - set(context.user_data['Attendees']) - set(context.user_data['Valid Absentees']))
## prepare the keyboard object
reply_keyboard = sorted([[name] for name in relevant_cell_members]) + [['REMOVE','NONE']]
markup = ReplyKeyboardMarkup(reply_keyboard, one_time_keyboard=True)
## reply
await update.message.reply_text(
"<b>Neat! Let's begin with our attendees. Who was present?</b>\n"
f"{facts_to_str(context.user_data)}\n<i>Instructions: Select 'REMOVE' to remove attendees. Select 'NONE' if no attendees to add."
" If there are new friends, type in their name! Preferably their first and last name, e.g. Nehemiah Tan.</i>",
reply_markup=markup,
parse_mode = 'HTML'
)
return CHOOSING_MEMBERS_ATTENDEES
## Storing the information and asking for more cell members
async def received_information_attendees(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Store info provided by user and ask for any more members who attended"""
user_data = context.user_data
print(user_data)
text = update.message.text
if text != 'DONE':
if text not in user_data['Attendees']:
user_data['Attendees'].append(text)
## prepare lists of the relevant cell members
all_cell_members = db.get_cell_members(user_data["Cell"])
relevant_cell_members = list(set(all_cell_members) - set(context.user_data['Attendees']) - set(context.user_data['Valid Absentees']))
## prepare the keyboard object
reply_keyboard = sorted([[name] for name in relevant_cell_members]) + [['REMOVE','DONE']]
markup = ReplyKeyboardMarkup(reply_keyboard, one_time_keyboard=True)
## reply
await update.message.reply_text(
"<b>Got it! Any more attendees?</b>\n"
f"{facts_to_str(user_data)}\n<i>Instructions: Select 'REMOVE' to remove attendees. Select 'DONE' if no more attendees to add.</i>",
reply_markup=markup,
parse_mode = 'HTML'
)
return CHOOSING_MEMBERS_ATTENDEES
## removing cell members
async def remove_attendees(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Ask the user for the cell members they want to remove from the attendees list"""
user_data = context.user_data
## prepare lists of the relevant cell members
attendees = user_data['Attendees']
## prepare the keyboard object
reply_keyboard = [[name] for name in attendees] + [['DONE']]
markup = ReplyKeyboardMarkup(reply_keyboard, one_time_keyboard=True)
## reply
await update.message.reply_text(
"<b>Okay, you want to remove names from the list of attendees. Who would you like to remove?</b>\n"
f"{facts_to_str(user_data)}",
reply_markup=markup,
parse_mode = 'HTML'
)
return REMOVING_MEMBERS_ATTENDEES
## Storing the information and asking for more cell members to remove
async def remove_attendees_update(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Store info provided by user and ask for any more members they want to remove from the attendees list"""
user_data = context.user_data
text = update.message.text
user_data['Attendees'].remove(text)
## if the member to be removed is already in the database, then we must delete it.
if text in db.get_alr_attended_cell_members(user_data['Cell'], user_data['Event Type'], datetime.strptime(user_data['Date'], '%Y-%b-%d')):
db.del_alr_attended_cell_members(text, user_data['Cell'], user_data['Event Type'], datetime.strptime(user_data['Date'], '%Y-%b-%d'))
## prepare lists of the relevant cell members
attendees = user_data['Attendees']
## prepare the keyboard object
reply_keyboard = [[name] for name in attendees] + [['DONE']]
markup = ReplyKeyboardMarkup(reply_keyboard, one_time_keyboard=True)
## reply
await update.message.reply_text(
"<b>Okay, I've removed the member. Who else would you like to remove?</b>\n"
f"{facts_to_str(user_data)}\n<i>Instructions: If you have finished removing, press 'DONE'.</i>",
reply_markup=markup,
parse_mode = 'HTML'
)
return REMOVING_MEMBERS_ATTENDEES
## selecting cell members
async def regular_choice_valabsentees(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Ask the user for cell members who were valid absentees"""
user_data = context.user_data
## prepare lists of the relevant cell members
all_cell_members = db.get_cell_members(user_data["Cell"])
relevant_cell_members = list(set(all_cell_members) - set(context.user_data['Attendees']) - set(context.user_data['Valid Absentees']))
## prepare the keyboard object
reply_keyboard = sorted([[name] for name in relevant_cell_members]) + [['REMOVE','NONE']]
markup = ReplyKeyboardMarkup(reply_keyboard, one_time_keyboard=True)
## reply
await update.message.reply_text(
f"<b>Great, let's move to our valid absentees. Who was absent with valid reasons?</b>\n {facts_to_str(user_data)}\n<i>Instructions: Select 'REMOVE' to remove valid absentees. Select 'NONE' if no valid absentees to add.</i>",
reply_markup=markup,
parse_mode = 'HTML'
)
return CHOOSING_MEMBERS_VALABSENTEES
## Storing the information and asking for more cell members
async def received_information_valabsentees(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Store info provided by user and ask for any more members who were valid absentees"""
user_data = context.user_data
print(user_data)
text = update.message.text
if text != 'DONE':
if 'Valid Absentees' not in user_data.keys():
user_data['Valid Absentees'] = []
if text not in user_data['Valid Absentees']:
user_data['Valid Absentees'].append(text)
## prepare lists of the relevant cell members
all_cell_members = db.get_cell_members(user_data["Cell"])
relevant_cell_members = list(set(all_cell_members) - set(context.user_data['Attendees']) - set(context.user_data['Valid Absentees']))
## prepare the keyboard object
reply_keyboard = sorted([[name] for name in relevant_cell_members]) + [['REMOVE','DONE']]
markup = ReplyKeyboardMarkup(reply_keyboard, one_time_keyboard=True)
## reply
await update.message.reply_text(
"<b>Got it! Any more valid absentees?</b>\n"
f"{facts_to_str(user_data)}\n<i>Instructions: Select 'REMOVE' to remove valid absentees. Select 'DONE' if no more valid absentees to add.</i>",
reply_markup=markup,
parse_mode = 'HTML'
)
return CHOOSING_MEMBERS_VALABSENTEES
## removing cell members
async def remove_valabsentees(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Ask the user for the cell members they want to remove from their current selected list"""
user_data = context.user_data
## prepare lists of the relevant cell members
attendees = user_data['Valid Absentees']
## prepare the keyboard object
reply_keyboard = [[name] for name in attendees] + [['DONE']]
markup = ReplyKeyboardMarkup(reply_keyboard, one_time_keyboard=True)
## reply
await update.message.reply_text(
"<b>Okay, you want to remove names from the list of valid absentees. Who would you like to remove?</b>\n"
f"{facts_to_str(user_data)}",
reply_markup=markup,
parse_mode = 'HTML'
)
return REMOVING_MEMBERS_VALABSENTEES
## Storing the information and asking for more cell members to remove
async def remove_valabsentees_update(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Store info provided by user and ask for any more members they want to remove"""
user_data = context.user_data
text = update.message.text
user_data['Valid Absentees'].remove(text)
## if the member to be removed is already in the database, then we must delete it.
if text in db.get_alr_absentvalid_cell_members(user_data['Cell'], user_data['Event Type'], datetime.strptime(user_data['Date'], '%Y-%b-%d')):
db.del_alr_absentvalid_cell_members(text, user_data['Cell'], user_data['Event Type'], datetime.strptime(user_data['Date'], '%Y-%b-%d'))
## prepare lists of the relevant cell members
attendees = user_data['Valid Absentees']
## prepare the keyboard object
reply_keyboard = [[name] for name in attendees] + [['DONE']]
markup = ReplyKeyboardMarkup(reply_keyboard, one_time_keyboard=True)
## reply
await update.message.reply_text(
"<b>Okay, I've removed the member. Who else would you like to remove?</b>\n"
f"{facts_to_str(user_data)}\n<i>Instructions: If you have finished removing, press 'DONE'.</i>",
reply_markup=markup,
parse_mode = 'HTML'
)
return REMOVING_MEMBERS_VALABSENTEES
## done
async def done(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Display the gathered info and end the conversation."""
user_data = context.user_data
## prepare a clean attendance date
attendance_date = datetime.strptime(user_data['Date'], '%Y-%b-%d')
## add attendees into the database first
if 'Attendees' in user_data.keys():
not_yet_added_cell_members = list(set(user_data['Attendees']) - set(db.get_alr_entered_cell_members(user_data['Cell'], user_data['Event Type'], attendance_date)))
for attendee in not_yet_added_cell_members:
db.add_attendance(user_data['Cell'], user_data['Event Type'],attendance_date, attendee, "Present")
existing_cell_members = list(set(user_data['Attendees']).intersection(db.get_cell_members(user_data['Cell'])))
new_cell_members = list(set(user_data['Attendees']) - set(existing_cell_members))
for attendee in new_cell_members:
db.add_new_member(attendee, 'New Friend', user_data['Cell'], 'None', '01-01-2000')
## add valid absentees into the database next
if 'Valid Absentees' in user_data.keys():
not_yet_added_cell_members = list(set(user_data['Valid Absentees']) - set(db.get_alr_entered_cell_members(user_data['Cell'], user_data['Event Type'], attendance_date)))
for attendee in not_yet_added_cell_members:
db.add_attendance(user_data['Cell'], user_data['Event Type'], attendance_date, attendee, "Absent Valid")
## reply
await update.message.reply_text(
f"<b>Thank you {update.effective_user.first_name}. As a recap, I have collected these information:</b>\n {facts_to_str(user_data)}\n<b>I have proceeded to update their attendance. Type '/start' to begin a new attendance.</b>",
reply_markup=ReplyKeyboardRemove(),
parse_mode = 'HTML'
)
user_data.clear()
return ConversationHandler.END
## restart
async def exit_(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Display the gathered info and end the conversation."""
user_data = context.user_data
await update.message.reply_text(
"Type '/start' to begin a new attendance.",
)
user_data.clear()
return ConversationHandler.END
############################### MAIN() ###############################
# Create the Application and pass it your bot's token.
application = Application.builder().token(os.getenv('TELEGRAM_TOKEN')).build()
# Add conversation handler with the states CHOOSING, TYPING_CHOICE and TYPING_REPLY
conv_handler = ConversationHandler(
entry_points=[CommandHandler("start", start)],
states={
LOGIN_REPLY: [
MessageHandler(
filters.Regex(f"^({os.getenv('VERIFICATION_CODE')})$"), select_cell
),
# CommandHandler("exit", exit_),
],
CHOOSING_CELL: [
MessageHandler(
filters.Regex("^(ONE|Bouquet|Kadesh|Gilead)$"), select_eventtype
),
# CommandHandler("exit", exit_),
],
CHOOSING_EVENTTYPE: [
MessageHandler(
filters.Regex("^(Sunday Service|Cell Group|Others)$"), select_month
),
# CommandHandler("exit", exit_),
],
CHOOSING_MONTH: [
MessageHandler(
filters.Regex("^(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)$"), select_day
),
# CommandHandler("exit", exit_),
],
CHOOSING_DAY: [
MessageHandler(
filters.Regex("^(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)$"), regular_choice_attendees
),
# CommandHandler("exit", exit_),
],
CHOOSING_MEMBERS_ATTENDEES: [
MessageHandler(
filters.TEXT & ~(filters.COMMAND | filters.Regex("^DONE$") | filters.Regex("^REMOVE$") | filters.Regex("^NONE$")), received_information_attendees
),
MessageHandler(filters.Regex("^REMOVE$"), remove_attendees),
MessageHandler(filters.Regex("^(DONE|NONE)$"), regular_choice_valabsentees),
# CommandHandler("exit", exit_),
],
REMOVING_MEMBERS_ATTENDEES: [
MessageHandler(
filters.TEXT & ~(filters.COMMAND | filters.Regex("^DONE$")), remove_attendees_update
),
MessageHandler(filters.Regex("^DONE$"), received_information_attendees),
# CommandHandler("exit", exit_),
],
CHOOSING_MEMBERS_VALABSENTEES: [
MessageHandler(
filters.TEXT & ~(filters.COMMAND | filters.Regex("^DONE$") | filters.Regex("^REMOVE$") | filters.Regex("^NONE$")), received_information_valabsentees
),
MessageHandler(filters.Regex("^REMOVE$"), remove_valabsentees),
MessageHandler(filters.Regex("^(DONE|NONE)$"), done),
# CommandHandler("exit", exit_),
],
REMOVING_MEMBERS_VALABSENTEES: [
MessageHandler(
filters.TEXT & ~(filters.COMMAND | filters.Regex("^DONE$")), remove_valabsentees_update
),
MessageHandler(filters.Regex("^DONE$"), received_information_valabsentees),
# CommandHandler("exit", exit_),
],
},
fallbacks=[CommandHandler("exit", exit_)],
)
application.add_handler(conv_handler)
#######################################################################
async def tg_bot_main(application, event):
async with application:
await application.process_update(
Update.de_json(json.loads(event["body"]), application.bot)
)
def lambda_handler(event, context):
try:
asyncio.run(tg_bot_main(application, event))
except Exception as e:
traceback.print_exc()
print(e)
return {"statusCode": 500}
return {"statusCode": 200}