Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SCRUM-14-POST-attandces #26

Open
wants to merge 14 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/campaign/.aws-sam/build.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# This file is auto generated by SAM CLI build command

[function_build_definitions.1f0da280-026b-4834-b1be-2d9dee7a9c79]
packagetype = "Image"
functions = ["CampaignFunction"]

[function_build_definitions.1f0da280-026b-4834-b1be-2d9dee7a9c79.metadata]
Dockerfile = "Dockerfile"
DockerContext = "/Users/rich/Desktop/aws-educate-tpet-backend/src/campaign"
DockerTag = "python3.11"

[layer_build_definitions]
16 changes: 16 additions & 0 deletions src/campaign/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
FROM public.ecr.aws/lambda/python:3.11

# 安裝依賴項
COPY requirements.txt /var/task/
RUN pip install -r /var/task/requirements.txt

# 複製函數代碼
COPY lambda_function.py /var/task/
COPY s3.py /var/task/
COPY ses.py /var/task/
COPY sqs.py /var/task/
COPY dynamodb.py /var/task/
COPY .env /var/task/
Rich627 marked this conversation as resolved.
Show resolved Hide resolved
Rich627 marked this conversation as resolved.
Show resolved Hide resolved

# 設定運行 Lambda 函數的命令
CMD ["lambda_function.lambda_handler"]
29 changes: 29 additions & 0 deletions src/campaign/dynamodb.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import logging
import os

import boto3
from dotenv import load_dotenv

load_dotenv()

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

class DynamoDB:
"""
DynamoDB operations
"""
def __init__(self):
self.dynamodb_client = boto3.resource(
'dynamodb',
region_name=os.getenv('REGION_NAME')
)

def put_item(self, table_name: str, item: dict):
try:
table = self.dynamodb_client.Table(table_name)
table.put_item(Item=item)
logger.info("Saved email record to DynamoDB: %s", item.get("email_id"))
except Exception as e:
logger.error(f"Error saving to DynamoDB: {e}")
raise
124 changes: 124 additions & 0 deletions src/campaign/lambda_function.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import json
import logging
from ses import SES
from dynamodb import DynamoDB

logger = logging.getLogger()
logger.setLevel(logging.INFO)

def lambda_handler(event, context):
try:
code = event['queryStringParameters']['code']
is_attend = event['queryStringParameters']['is_attend']
logger.info(f"Received code: {code}, is_attend: {is_attend}")

# * 先寫死
# TODO 之後應該要換成從s3讀名單, 想辦法生成unique code
dummy_participants = [
{'code': '1', 'name': 'Richie', 'email': '[email protected]'},
{'code': '2', 'name': 'Shiun', 'email': '[email protected]'},
{'code': '3', 'name': 'Harry', 'email': '[email protected]'}
]

participant = next((p for p in dummy_participants if p['code'] == code), None)
logger.info(f"Participant found: {participant}")

if participant:
# * 這邊就把display_name寫死
send_confirmation_email("no-reply", participant['name'], participant['email'], is_attend)

save_to_dynamodb(participant['name'], 'c14274978654c2488923d7fee1eb61f', participant['code'], participant['email'], is_attend)

html_response = f"""
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<h1>登記出席成功</h1>
<p>恭喜您成功登記出席,稍後會收到一封系統信件以表示系統已收到回應。</p>
<p>若沒有收到該信件,務必聯繫我們:</p>
<p>Facebook: <a href="https://bit.ly/3pD9aCr">台灣 AWS Educate Cloud Ambassador Facebook</a></p>
<p>Instagram: <a href="https://bit.ly/3BBr7XQ">台灣 AWS Educate Cloud Ambassador Instagram</a></p>
</body>
</html>
"""
status = "success"
message = f"參與者 {participant['name']} 的出席狀態已更新為{is_attend}."

response = {
'statusCode': 200,
'body': html_response,
'headers': {
'Content-Type': 'text/html'
}
}

logger.info(f"Response status: {status}, message: {message}")
return response

except Exception as e:
logger.error(f"Error occurred: {str(e)}", exc_info=True)
return {
'statusCode': 500,
'body': json.dumps({
'status': "error",
'message': f"Internal server error: {str(e)}"
}),
'headers': {
'Content-Type': 'application/json'
}
}

def send_confirmation_email(display_name, name, email, is_attend):
try:
ses = SES()
source_email = "[email protected]"
formatted_source_email = f"{display_name} <{source_email}>"
email_title = "出席確認"

# 根據 is_attend 的值設定出席狀態文本
attendance_status = "會出席" if is_attend == "true" else "不會出席"

formatted_content = f"""
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<p>{name} 你好,</p>
<p>您的出席狀態已更新為 {attendance_status}。感謝您的回應!</p>
<p>若您未收到確認郵件,請聯繫我們:</p>
<p>Facebook: <a href="https://bit.ly/3pD9aCr">台灣 AWS Educate Cloud Ambassador Facebook</a></p>
<p>Instagram: <a href="https://bit.ly/3BBr7XQ">台灣 AWS Educate Cloud Ambassador Instagram</a></p>
</body>
</html>
"""
ses.send_email(
formatted_source_email=formatted_source_email,
receiver_email=email,
email_title=email_title,
formatted_content=formatted_content
)
logger.info(f"Confirmation email sent to {email}")

except Exception as e:
logger.error(f"Failed to send email: {str(e)}", exc_info=True)
raise

def save_to_dynamodb(name, campaign_id, participant_id, email, is_attend):
try:
dynamodb = DynamoDB()
item = {
'campaign_id': campaign_id,
'participant_id': participant_id,
'name': name,
'email': email,
'is_attend': is_attend
}
dynamodb.put_item('campaign', item)
logger.info(f"Record saved to DynamoDB: {item}")

except Exception as e:
logger.error(f"Failed to save to DynamoDB: {str(e)}")
raise
1 change: 1 addition & 0 deletions src/campaign/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
python-dotenv
97 changes: 97 additions & 0 deletions src/campaign/s3.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import logging
import os

import boto3
from dotenv import load_dotenv

load_dotenv()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

你有使用到 .env,這樣你會把機敏資料放在 Docker Image 內唷
我認為更安全且更方便共享 configuration 的方式:

  • 建議放在 Lambda Environment Variables
  • 若有機敏資料不能 hard code 在 configuration 上,則放在 Secret Manager 或是 Parameter Store 加密,在程式碼裡面用 SDK 呼叫去拿

除此之外,若有使用 .env,要記得寫一個 .env.example 用來告知團隊其他成員你配置了什麼


logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
Rich627 marked this conversation as resolved.
Show resolved Hide resolved


class S3:
"""
S3 operations
"""
def __init__(self):
self.s3_client = boto3.client(
's3',
region_name=os.getenv('REGION_NAME')
)

def create_bucket(self, bucket_name: str):
"""
Create an S3 bucket
"""
try:
self.s3_client.create_bucket(
Bucket=bucket_name,
CreateBucketConfiguration={'LocationConstraint': os.getenv('REGION_NAME')}
)
logger.info(f"Bucket {bucket_name} created successfully.")
except Exception as e:
logger.error(f"Error creating bucket {bucket_name}: {e}")

def upload_file(self, file_name: str, bucket_name: str, object_name: str = None):
"""
Upload a file to an S3 bucket
"""
if object_name is None:
object_name = file_name

try:
self.s3_client.upload_file(file_name, bucket_name, object_name)
logger.info(f"File {file_name} uploaded to {bucket_name}/{object_name} successfully.")
except Exception as e:
logger.error(f"Error uploading file {file_name} to {bucket_name}/{object_name}: {e}")

def list_files(self, bucket_name: str):
"""
List files in an S3 bucket
"""
try:
response = self.s3_client.list_objects_v2(Bucket=bucket_name)
if 'Contents' in response:
for obj in response['Contents']:
logger.info(f"Found file: {obj['Key']}")
else:
logger.info(f"No files found in bucket {bucket_name}.")
except Exception as e:
logger.error(f"Error listing files in bucket {bucket_name}: {e}")

def download_file(self, bucket_name: str, file_key: str, local_file_path: str):
"""
Download a single file from an S3 bucket to a local path
"""
try:
self.s3_client.download_file(bucket_name, file_key, local_file_path)
logger.info(f"Downloaded {file_key} to {local_file_path}")
except Exception as e:
logger.error(f"Error downloading file {file_key} from bucket {bucket_name}: {e}")

def list_files(self, bucket_name: str):
"""
List all files in an S3 bucket
"""
try:
response = self.s3_client.list_objects_v2(Bucket=bucket_name)
if 'Contents' in response:
return [obj['Key'] for obj in response['Contents']]
else:
logger.info(f"No files found in bucket {bucket_name}.")
return []
except Exception as e:
logger.error(f"Error listing files in bucket {bucket_name}: {e}")
return []

def get_object(self, bucket_name: str, key: str):
"""
Get object from an S3 bucket
"""
try:
response = self.s3_client.get_object(Bucket=bucket_name, Key=key)
return response
except Exception as e:
logger.error(f"Error getting object from bucket {bucket_name}: {e}")
raise
9 changes: 9 additions & 0 deletions src/campaign/samconfig.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
version = 0.1
[default.deploy.parameters]
stack_name = "confirm-attendence"
resolve_s3 = true
s3_prefix = "confirm-attendence"
region = "us-east-1"
capabilities = "CAPABILITY_NAMED_IAM"
parameter_overrides = "AwsRegion=\"us-east-1\""
image_repositories = ["CampaignFunction=070576557102.dkr.ecr.us-east-1.amazonaws.com/confirmattendence01cecb19/campaignfunction708cd7aerepo"]
36 changes: 36 additions & 0 deletions src/campaign/ses.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import logging
import os

import boto3
from dotenv import load_dotenv

load_dotenv()

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)


class SES:
"""
SES operations
"""
def __init__(self):
self.ses_client = boto3.client(
'ses',
region_name='ap-northeast-1'
)

def send_email(self, formatted_source_email: str, receiver_email: str, email_title: str, formatted_content: str):
try:
response = self.ses_client.send_email(
Source=formatted_source_email,
Destination={"ToAddresses": [receiver_email]},
Message={
"Subject": {"Data": email_title},
"Body": {"Html": {"Data": formatted_content}},
},
)
return response
except Exception as e:
logger.error(f"Error sending email: {e}")
Rich627 marked this conversation as resolved.
Show resolved Hide resolved
raise
32 changes: 32 additions & 0 deletions src/campaign/sqs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import logging
import os

import boto3
from dotenv import load_dotenv

load_dotenv()

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)


class SQS:
"""
SQS operations
"""
def __init__(self):
self.sqs_client = boto3.client(
'sqs',
region_name=os.getenv('REGION_NAME')
)

def delete_message(self, queue_url: str, receipt_handle: str):
try:
self.sqs_client.delete_message(
QueueUrl=queue_url,
ReceiptHandle=receipt_handle
)
logger.info("Deleted message from SQS: %s", receipt_handle)
except Exception as e:
logger.error(f"Error deleting message from SQS: {e}")
raise
Loading