Skip to content

Commit e9f7a06

Browse files
authored
Merge pull request #6 from UMLCloudComputing/specify-semesters
Specify semesters
2 parents 2e7f71b + 70e27b7 commit e9f7a06

7 files changed

Lines changed: 155 additions & 6 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ __pycache__
55
.venv
66
*.egg-info
77
*.env
8+
cdk.context.json
89
# CDK asset staging directory
910
.cdk.staging
1011
cdk.out

attendance/attendance_stack.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
Duration,
55
aws_dynamodb as dynamodb,
66
aws_lambda as _lambda,
7-
aws_apigateway as apigateway
7+
aws_apigateway as apigateway,
8+
aws_ssm as ssm,
89
)
910
import os
1011
from constructs import Construct
@@ -28,7 +29,8 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
2829
"DISCORD_ID" : os.getenv('ID'),
2930
"DISCORD_TOKEN" : os.getenv('TOKEN'),
3031
"DYNAMO_USERTABLE" : user_table.table_name,
31-
"DYNAMO_CODETABLE" : code_table.table_name
32+
"DYNAMO_CODETABLE" : code_table.table_name,
33+
"SSM_PARAMETER_NAME" : os.getenv('SSM_PARAMETER_NAME')
3234
},
3335
code=_lambda.DockerImageCode.from_image_asset(
3436
directory="src"
@@ -40,6 +42,15 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
4042
handler=dockerFunc,
4143
proxy=True,
4244
)
45+
46+
parameter = ssm.StringParameter.from_string_parameter_name(
47+
self,
48+
f'SSMParameter{construct_id}',
49+
string_parameter_name=os.getenv('SSM_PARAMETER_NAME')
50+
)
51+
52+
parameter.grant_read(dockerFunc.role)
53+
parameter.grant_write(dockerFunc.role)
4354

4455
user_table.grant_read_write_data(dockerFunc.role)
4556
code_table.grant_read_write_data(dockerFunc.role)

commands/discord_commands.yaml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,22 @@
4545
type: 3 # string
4646
required: true
4747

48+
- name: set_semester
49+
description: set the semester that it currently is. (admins only)
50+
options:
51+
- name: semester
52+
description: name of the semester
53+
type: 3 # string
54+
required: true
55+
56+
- name: generate_report
57+
description: generate a report for a semester (admins only)
58+
options:
59+
- name: semester
60+
description: semester to query
61+
type: 3 # string
62+
required: true
63+
4864
- name: reset
4965
description: reset a user's statistics only can be used by admins.
5066
options:

src/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ RUN yum -y install git
99
RUN pip install --upgrade -r requirements.txt
1010

1111
# Copy all files in ./app
12-
COPY app/* ${LAMBDA_TASK_ROOT}
12+
COPY app/ ${LAMBDA_TASK_ROOT}
1313

1414
# Set the CMD to your handler (could also be done as a parameter override outside of the Dockerfile)
1515
CMD [ "main.handler" ]

src/app/main.py

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import os
2+
import io
23
import requests
34
import json
45
import db
@@ -7,6 +8,8 @@
78
from nacl.exceptions import BadSignatureError
89
import secrets
910
from enum import Enum
11+
from parameter import get_ssm_parameter, set_ssm_parameter
12+
from report import generate_report
1013

1114
DISCORD_PUBLIC_KEY = os.environ.get("DISCORD_PUBLIC_KEY")
1215
DISCORD_TOKEN = os.environ.get("DISCORD_TOKEN")
@@ -62,6 +65,9 @@ def interact(raw_request):
6265
# The ID of the Guild in which the bot resides in
6366
guildID = raw_request["guild_id"]
6467

68+
# The ID of the channel in which the interaction happened
69+
channelID = raw_request["channel_id"]
70+
6571
# A boolean variable that determines if the user who executed the command is an administrator or not
6672
admin = (int(raw_request["member"]["permissions"]) & 0x8) == 0x8
6773

@@ -120,6 +126,18 @@ def interact(raw_request):
120126
user = raw_request["member"]["user"]
121127
embeds = build_stats_embed(user)
122128
send_embed(embeds, id, token)
129+
case "set_semester":
130+
if admin:
131+
semester = str(data["options"][0]["value"])
132+
set_ssm_parameter(semester)
133+
send(f"The semester has been set to {semester} successfully.", id, token)
134+
else: send("Only administrators can set the semester!", id, token)
135+
case "generate_report":
136+
if admin:
137+
semester = str(data["options"][0]["value"])
138+
filename = generate_report(semester)
139+
send_file(filename, id, token)
140+
else: send("Only administrators can generate reports!", id, token)
123141
case "reset":
124142
if admin:
125143
# send(f"Deleting specified user...", id, token)
@@ -214,7 +232,7 @@ def validate_attendance(userid: str, code: str, type: str) -> AttendanceStatus:
214232
if active:
215233
user = db.get_user(userid)
216234
serialized_code = code + '|' + code_response['expiration']
217-
serialized_event = code_response['event_name'] + '|' + type
235+
serialized_event = code_response['event_name'] + '|' + type + '|' + get_ssm_parameter()
218236
if user != None:
219237
if serialized_code not in user['codes_used']:
220238
status = AttendanceStatus.VALID
@@ -248,9 +266,14 @@ def build_stats_embed(user):
248266
i = len(user_stats["events_attended"]) - 1
249267
while len(embeds['fields']) < 25 and i >= 0:
250268
event_deserialized = user_stats['events_attended'][i].split('|')
269+
try:
270+
semester = event_deserialized[2]
271+
except:
272+
semester = "Fall 2024"
273+
251274
embeds['fields'].append({
252275
'name': event_deserialized[0],
253-
'value': event_deserialized[1],
276+
'value': event_deserialized[1] + f" ({semester})",
254277
'inline': True
255278
})
256279

@@ -266,4 +289,26 @@ def get_mentioned_user(userid):
266289
}
267290
response = requests.get(f"https://discord.com/api/users/{userid}", headers=headers)
268291

269-
return response.json()
292+
return response.json()
293+
294+
def send_file(filename: str, id, token):
295+
url = f"https://discord.com/api/interactions/{id}/{token}/callback"
296+
297+
with open(filename, 'rb') as fp:
298+
callback_data = {
299+
"type": 4,
300+
"data": {
301+
"content": "Here is the requested report.",
302+
}
303+
}
304+
305+
response = requests.post(
306+
url,
307+
data={"payload_json": json.dumps(callback_data)},
308+
files={
309+
"file": (filename, fp, 'text/csv')
310+
}
311+
)
312+
313+
print("Response status: code: ")
314+
print(response.status_code)

src/app/parameter.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import boto3
2+
import os
3+
4+
AWS_ID = os.getenv("AWS_ACCESS_KEY_ID")
5+
AWS_SECRET = os.getenv("AWS_SECRET_ACCESS_KEY")
6+
PARAMETER_NAME = os.getenv("SSM_PARAMETER_NAME")
7+
8+
def set_ssm_parameter(value: str) -> None:
9+
ssm = boto3.client('ssm')
10+
11+
ssm.put_parameter(
12+
Name=PARAMETER_NAME,
13+
Value=value,
14+
Overwrite=True
15+
)
16+
17+
def get_ssm_parameter() -> str:
18+
ssm = boto3.client('ssm')
19+
20+
response = ssm.get_parameter(Name='/attendance/semester')
21+
return response['Parameter']['Value']

src/app/report.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import boto3
2+
import csv
3+
import os
4+
5+
AWS_ID = os.getenv("AWS_ACCESS_KEY_ID")
6+
AWS_SECRET = os.getenv("AWS_SECRET_ACCESS_KEY")
7+
USER_TABLENAME = os.getenv("DYNAMO_USERTABLE")
8+
9+
def generate_report(report_semester: str) -> bytes:
10+
filename = '/tmp/attendance-report-' + report_semester.lower().replace(' ', '-') + '.csv'
11+
12+
dynamodb = boto3.client("dynamodb")
13+
14+
response = dynamodb.scan(
15+
TableName=USER_TABLENAME,
16+
Select="ALL_ATTRIBUTES"
17+
)
18+
19+
fp = open(filename, 'w')
20+
csvwriter = csv.DictWriter(fp, fieldnames=['meeting_name', 'total_attended', 'in_person', 'virtual'])
21+
csvwriter.writeheader()
22+
data = {}
23+
24+
for item in response['Items']:
25+
for event in item['events_attended']['L']:
26+
try:
27+
name, type, semester = event['S'].split('|')
28+
except ValueError:
29+
name, type = event['S'].split('|')
30+
semester = "Fall 2024"
31+
32+
33+
if semester == report_semester:
34+
if name not in data.keys():
35+
data[name] = {
36+
'total_attended': 0,
37+
'in_person': 0,
38+
'virtual': 0
39+
}
40+
41+
data[name][type.lower().replace(' ', '_')] += 1
42+
data[name]['total_attended'] += 1
43+
44+
rows = []
45+
for event in data.keys():
46+
row = {
47+
'meeting_name': event,
48+
'total_attended': data[event]['total_attended'],
49+
'in_person': data[event]['in_person'],
50+
'virtual': data[event]['virtual']
51+
}
52+
rows.append(row)
53+
csvwriter.writerows(rows)
54+
55+
return filename

0 commit comments

Comments
 (0)