Skip to content

Commit

Permalink
Merge pull request #4 from UMLCloudComputing/stats-and-event-info
Browse files Browse the repository at this point in the history
Stats and event info
  • Loading branch information
cjcocokrisp authored Aug 23, 2024
2 parents 0fc5151 + 5ca61d2 commit 9cf87e3
Show file tree
Hide file tree
Showing 4 changed files with 171 additions and 50 deletions.
3 changes: 2 additions & 1 deletion attendance/attendance_stack.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
function_name=construct_id,
environment= {
"DISCORD_PUBLIC_KEY" : os.getenv('DISCORD_PUBLIC_KEY'),
"ID" : os.getenv('ID'),
"DISCORD_ID" : os.getenv('ID'),
"DISCORD_TOKEN" : os.getenv('TOKEN'),
"DYNAMO_USERTABLE" : user_table.table_name,
"DYNAMO_CODETABLE" : code_table.table_name
},
Expand Down
37 changes: 30 additions & 7 deletions commands/discord_commands.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,31 +9,54 @@
# required: true

- name: attend
description: Attend an event with the given code. You only get three attempts. You can only attend an event once.
description: attend an event with the given code, you can only attend an event once.
options:
- name: code
description: Event Code
description: event code
type: 3 # string
required: true
- name: type
description: how you attended the event
type: 3 # string
required: true
choices:
- name: Virtual
value: Virtual
- name: In Person
value: In Person

- name: generate
description: Generates an attendence code. Can only be used by Guild Administrators.
description: denerates an attendence code. can only be used by admins.
options:
- name: expire_in
description: Set when the code expires (in minutes from now)
description: set when the code expires (in minutes from now)
type: 4 #int
required: true
- name: event_name
description: set the event name
type: 3 # string
required: true

- name: validate
description: Check if an attendence code is valid. Can only be used by Guild Administrators.
description: check if an attendence code is valid. Can only be used by admins.
options:
- name: code
description: Event code
type: 3 # string
required: true

- name: reset
description: Reset a user's statistics
description: reset a user's statistics only can be used by admins.
options:
- name: user
description: mention a user to reset their stats
type: 9 # mentionable
required: true

- name: stats
description: Get a user's statistics
description: get a user's statistics
options:
- name: user
description: mention a user to get their stats (not required)
type: 9 # mentionable
required: false
39 changes: 26 additions & 13 deletions src/app/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,30 +6,28 @@
USER_TABLENAME = os.getenv("DYNAMO_USERTABLE")
CODE_TABLENAME = os.getenv("DYNAMO_CODETABLE")

# TODO: update user attendance stuff

def write_code(code, expiration):
def write_code(code, expiration, event_name):
dynamodb = boto3.resource("dynamodb", region_name='us-east-1')
table = dynamodb.Table(CODE_TABLENAME)

response = table.put_item(Item={"codeID": code, "expiration": expiration})
response = table.put_item(Item={"codeID": code, "expiration": expiration, "event_name": event_name})

if response['ResponseMetadata']['HTTPStatusCode'] == 200:
print(f"Item {code} added successfully.")
else:
print("Error adding item...")

def get_code_expiration(code):
def get_code(code):
dynamodb = boto3.resource("dynamodb", region_name='us-east-1')
table = dynamodb.Table(CODE_TABLENAME)

response = table.get_item(Key={"codeID": code})

expiration = None
code = None
if "Item" in response:
expiration = response["Item"]["expiration"]
code = response["Item"]

return expiration
return code

def get_user(userid):
dynamodb = boto3.resource("dynamodb", region_name='us-east-1')
Expand All @@ -43,32 +41,47 @@ def get_user(userid):

return data

def create_user(userid, attendance_num=0, codes_used=[]):
def create_user(userid, attendance_num=0, codes_used=[], events_attended=[]):
dynamodb = boto3.resource("dynamodb", region_name='us-east-1')
table = dynamodb.Table(USER_TABLENAME)

response = table.put_item(Item={
"userID": userid,
"attendance": attendance_num,
"codes_used": codes_used
"codes_used": codes_used,
"events_attended": events_attended
})

if response['ResponseMetadata']['HTTPStatusCode'] == 200:
print(f"User {userid} added successfully.")
else:
print(f"Error updating user {userid}...")

def update_users_attendance(userid, attendance_num, event_serialization):
def update_user(userid, attendance_num, serialized_code, serialized_event):
dynamodb = boto3.resource("dynamodb", region_name='us-east-1')
table = dynamodb.Table(USER_TABLENAME)

response = table.update_item(
Key={'userID': userid},
UpdateExpression="SET codes_used = list_append(codes_used, :val), attendance = :val2",
ExpressionAttributeValues={":val": [event_serialization], ":val2": str(attendance_num)}
UpdateExpression="SET attendance = :val, codes_used = list_append(codes_used, :val2), events_attended = list_append(events_attended, :val3)",
ExpressionAttributeValues={
":val": str(attendance_num),
":val2": [serialized_code],
":val3": [serialized_event] }
)

if response['ResponseMetadata']['HTTPStatusCode'] == 200:
print(f"User {userid} updated successfully.")
else:
print(f"Error updating user {userid}...")

def delete_user(userid):
dynamodb = boto3.resource("dynamodb")
table = dynamodb.Table(USER_TABLENAME)

response = table.delete_item(Key={'userID': userid})

if response['ResponseMetadata']['HTTPStatusCode'] == 200:
print(f"User {userid} deleted successfully.")
else:
print(f"Error updating user {userid}...")
142 changes: 113 additions & 29 deletions src/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,18 @@
from nacl.signing import VerifyKey
from nacl.exceptions import BadSignatureError
import secrets
from enum import Enum

DISCORD_PUBLIC_KEY = os.environ.get("DISCORD_PUBLIC_KEY")
DISCORD_TOKEN = os.environ.get("DISCORD_TOKEN")
DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S.%f"

class AttendanceStatus(Enum):
VALID = 0
EXPIRED = 1
USED = 2
NONEXISTENT = 3

def verify(event):
signature = event['headers']['x-signature-ed25519']
timestamp = event['headers']['x-signature-timestamp']
Expand Down Expand Up @@ -80,24 +88,44 @@ def interact(raw_request):
case "attend":
send("Submitting attendance...", id, token)
code = str(data["options"][0]["value"])
status = validate_attendance(userID, code)
type = str(data["options"][1]["value"])
status = validate_attendance(userID, code, type)
match status:
case 0: message = "You have checked in. We hope you enjoy the event and thanks for coming!"
case 1: message = "The attendance code you have entered is expired."
case 2: message = "The attendance code you have entered you have already used."
case AttendanceStatus.VALID:
message = "You have checked in. We hope you enjoy the event and thanks for coming!"
case AttendanceStatus.EXPIRED:
message = "The attendance code you have entered is expired."
case AttendanceStatus.USED:
message = "The attendance code you have entered you have already used."
case AttendanceStatus.NONEXISTENT:
message = "The attendance code you have entered does not exist."
update(f"{message}", token)
case "generate":
if admin:
send("Generating attendence code...", id, token)
minutes = int(data["options"][0]["value"])
code = generate_code(minutes)
update(f"Attendence Code is {code} and is valid for {minutes} minutes.", token)
event_name = str(data["options"][1]["value"])
code = generate_code(minutes, event_name)
update(f"Attendence Code for {event_name} is {code} and is valid for {minutes} minutes.", token)
else: send("Only administrators can generate attendance codes!", id, token)
case "validate":
code = str(data["options"][0]["value"])
status = "valid" if validate_code(code) else "invalid"
if admin: send(f"{code} is {status}.", id, token)
else: send("Only administrators can validate attendance codes!", id, token)
case "stats":
try:
user = get_mentioned_user(data["options"][0]["value"])
except:
user = raw_request["member"]["user"]
embeds = build_stats_embed(user)
send_embed(embeds, id, token)
case "reset":
if admin:
send(f"Deleting specified user...", id, token)
db.delete_user(data["options"][0]["value"])
update("Specified user has been reset.", token)
else: send("Only administrators can reset a user!", id, token)

# Send a new message
def send(message, id, token):
Expand All @@ -115,6 +143,24 @@ def send(message, id, token):
print("Response status code: ")
print(response.status_code)

# Send an embed message
# Documentation on embed objects: https://discord.com/developers/docs/resources/message#embed-object
def send_embed(embeds: dict, id, token):
url = f"https://discord.com/api/interactions/{id}/{token}/callback"

callback_data = {
"type": 4,
"data": {
"tts": False,
"embeds": [embeds]
}
}

response = requests.post(url, json=callback_data)

print("Response status code: ")
print(response.status_code)

# Updates an already sent message. The flags 1 << 6 means "ephemeral message" which means only the person who sent the command can see the result.
def update(message, token):
app_id = os.environ.get("ID")
Expand All @@ -132,52 +178,90 @@ def update(message, token):
print("Response status code: ")
print(response.status_code)

def generate_code(expiration_time: int) -> int:
def generate_code(expiration_time: int, event_name: str) -> int:
code = secrets.randbelow(900000) + 100000

expire = datetime.now()
expire_time = timedelta(minutes=expiration_time)
expire = expire + expire_time

db.write_code(str(code), expire.strftime(DATETIME_FORMAT))
db.write_code(str(code), expire.strftime(DATETIME_FORMAT), event_name)
return code

def validate_code(code: str, output_serialized=False) -> bool | tuple:
expiration = db.get_code_expiration(code)
def validate_code(code: str, expiration=None) -> bool | tuple:
if expiration != None:
code = db.get_code(code)

valid = False
if expiration != None:
expiration_datetime = datetime.strptime(expiration, DATETIME_FORMAT)
if code != None or expiration != None:
expiration_datetime = datetime.strptime(code['expiration'], DATETIME_FORMAT)
valid = datetime.now() < expiration_datetime
if output_serialized:
valid = (valid, code + '|' + expiration)

return valid

def validate_attendance(userid: str, code: str) -> bool:
def validate_attendance(userid: str, code: str, type: str) -> AttendanceStatus:
"""
Outputs a status code depending on the user's status with the code.\n
0 - Valid\n
1 - Code is expired\n
2 - Code has already been used\n
Outputs a status code enum depending on the user's status with the code.\n
"""
code_serialization = validate_code(code, output_serialized=True)
code_response = db.get_code(code)

status = 2
if code_serialization:
active, serialized_code = code_serialization
status = AttendanceStatus.NONEXISTENT
if code_response:
active = validate_code(code, expiration=code_response['expiration'])

if active:
user = db.get_user(userid)
serialized_code = code + '|' + code_response['expiration']
serialized_event = code_response['event_name'] + '|' + type
if user != None:
if serialized_code not in user['codes_used']:
status = 0
db.update_users_attendance(userid, int(user["attendance"]) + 1, serialized_code)
status = AttendanceStatus.VALID
db.update_user(userid, int(user['attendance']) + 1, serialized_code, serialized_event)
else:
status = AttendanceStatus.USED
else:
db.create_user(userid, 1, [serialized_code])
status = 0
db.create_user(userid, 1, [serialized_code], [serialized_event])
status = AttendanceStatus.VALID
else:
status = 1
status = AttendanceStatus.EXPIRED

return status

def build_stats_embed(user):
embeds = dict()

embeds['thumbnail'] = {
"url": f"https://cdn.discordapp.com/avatars/{user['id']}/{user['avatar']}.png"
}
embeds['color'] = 0x5494de
embeds['title'] = f"{user['username']}'s Attendance Stats"

# Add Information from Dynamo DB table to embed
user_stats = db.get_user(user['id'])
if user_stats != None:
embeds['description'] = f"Total Attendance: {user_stats['attendance']}"

# Deserialize event information
embeds['fields'] = []
i = len(user_stats["events_attended"]) - 1
while len(embeds['fields']) < 25 and i >= 0:
event_deserialized = user_stats['events_attended'][i].split('|')
embeds['fields'].append({
'name': event_deserialized[0],
'value': event_deserialized[1],
'inline': True
})

i -= 1
else:
embeds['description'] = f"{user['username']} has not attended any events."

return embeds

def get_mentioned_user(userid):
headers = {
"Authorization": f"Bot {DISCORD_TOKEN}"
}
response = requests.get(f"https://discord.com/api/users/{userid}", headers=headers)

return status
return response.json()

0 comments on commit 9cf87e3

Please sign in to comment.