Building a Cleaning Reminder Bot with AWS Lambda + LINE

Overview

I built a cleaning reminder LINE Bot for family use. This article explains the technical implementation details.

See the completed Bot here: Cleaning Reminder Bot

Architecture

flowchart LR
    subgraph User
        A[LINE User]
    end

    subgraph AWS
        B[API Gateway]
        C[Lambda
process_user_message] D[(S3
JSON)] E[Lambda
push_message_periodically] F[EventBridge
hourly] end A -->|Send message| B B --> C C <--> D E <--> D F -->|Trigger| E E -->|Notify| A
γ‚―γƒͺックで拑倧

Services Used

ServicePurpose
API Gateway (HTTP API)Receive LINE Webhooks
LambdaMessage processing, scheduled notifications
S3Store per-user task data
EventBridgeScheduled execution

Why This Architecture

  • Serverless: Pay only for what you use, zero operational overhead
  • S3: Cheaper than RDS, simple JSON management
  • SAM: Infrastructure as Code for deployment management

Project Structure

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
clean-bot/
β”œβ”€β”€ lib/                      # Core modules
β”‚   β”œβ”€β”€ clean_task.py         # Task state management
β”‚   β”œβ”€β”€ message.py            # Command parser
β”‚   β”œβ”€β”€ line.py               # LINE API
β”‚   └── s3_client.py          # S3 operations
β”œβ”€β”€ test/                     # Tests
β”œβ”€β”€ line_clean_bot.py         # Lambda entry point
β”œβ”€β”€ template.yaml             # SAM template
└── Makefile                  # Development commands

Implementation Details

1. Lambda Entry Points

Two Lambda functions are defined.

 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
# line_clean_bot.py

def process_user_message(event, context):
    """LINE message webhook handler"""
    body = json.loads(event.get('body'))

    # Get user ID or group ID
    if body['events'][0]['source']['type'] == 'user':
        line_id = body['events'][0]['source']['userId']
    else:
        line_id = body['events'][0]['source']['groupId']

    message = body['events'][0]['message']['text']

    # Fetch user data from S3
    s3client = S3client(BUCKET_NAME)
    obj_key = line_id + '.json'
    if not s3client.check_exist_object(obj_key):
        s3client.update_object(obj_key, '{"tasks": []}')

    # Process message and reply
    # ...

def push_message_periodically(event, context):
    """Send reminder notifications on schedule"""
    current_time = datetime.now() + timedelta(hours=9)  # JST

    s3client = S3client(BUCKET_NAME)
    for obj_list in s3client.list_objects():
        clean_task = CleanTask(s3client.get_object_body(obj_list.key))

        # Check if notification conditions are met
        if clean_task.should_notify(current_time):
            # Send message
            # ...

2. Task Deadline Management

Tasks are evaluated based on “days since last completed.”

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# lib/clean_task.py

def __evaluate_cleanup_timing(self, task):
    """Determine if task is overdue"""
    task_time = datetime.strptime(task['updated_at'], self.date_format)
    return (self.now - task_time).days >= int(task['duration'])

def get_todo_tasks(self):
    """Get list of overdue tasks"""
    return [task for task in self.tasks
            if not task.get('paused', False)
            and self.__evaluate_cleanup_timing(task)]

3. Notification Logic

Notifications are sent only on user-specified days and times.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
def should_notify(self, current_time):
    """Determine if notification should be sent"""
    if not self.notification['enabled']:
        return False

    # Check day of week
    weekday_map = {0: 'Mon', 1: 'Tue', 2: 'Wed', 3: 'Thu', 4: 'Fri', 5: 'Sat', 6: 'Sun'}
    current_weekday = weekday_map[current_time.weekday()]
    if current_weekday not in self.notification['days']:
        return False

    # Check time
    if current_time.hour < self.notification['hour']:
        return False

    # Check if already notified today
    # ...

    return True

4. Command Parser

Commands are parsed using simple string splitting.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# lib/message.py

def get_return_message(self, message, s3client):
    task_operation = self.__get_task_operation_name(message)  # First word
    task_name = self.__get_task_name(message)                  # Second word

    if task_operation == 'done':
        for task_name in self.__get_all_task_name(message):
            self.clean_task.update_task_updated_at(task_name)
        s3client.update_object(self.object_keyname, self.clean_task.get_json())
        return "Completed"

    if task_operation == 'add':
        duration = self.__get_duration(message)  # Third word
        self.clean_task.add_task(task_name, duration)
        # ...

5. Data Structure (JSON)

One JSON file per user is stored in S3.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
{
  "tasks": [
    {
      "task_name": "vacuum",
      "updated_at": "2024-01-01 12:00:00",
      "duration": 7,
      "paused": false
    }
  ],
  "notification": {
    "enabled": true,
    "days": ["Mon", "Wed", "Fri"],
    "hour": 7,
    "last_notified_at": "2024-01-01 07:00:00"
  }
}

SAM Template

Infrastructure is defined using AWS SAM.

 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
# template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31

Parameters:
  ChannelAccessToken:
    Type: String
    NoEcho: true
  BucketName:
    Type: String
    Default: your-bucket-name

Globals:
  Function:
    Runtime: python3.11
    Timeout: 300
    Environment:
      Variables:
        CHANNEL_ACCESS_TOKEN: !Ref ChannelAccessToken
        BUCKET_NAME: !Ref BucketName

Resources:
  # Message processing Lambda
  ProcessUserMessageFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: line_clean_bot.process_user_message
      CodeUri: .
      Policies:
        - S3CrudPolicy:
            BucketName: !Ref BucketName
      Events:
        ApiEvent:
          Type: HttpApi
          Properties:
            Path: /process_user_message
            Method: ANY

  # Scheduled notification Lambda
  PushMessagePeriodicallyFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: line_clean_bot.push_message_periodically
      CodeUri: .
      Policies:
        - S3CrudPolicy:
            BucketName: !Ref BucketName
      Events:
        ScheduleEvent:
          Type: Schedule
          Properties:
            Schedule: cron(0 * * * ? *)  # Every hour at minute 0

Deployment

1. Prerequisites

  • AWS CLI configured
  • SAM CLI installed
  • LINE Messaging API channel access token obtained

2. Build & Deploy

1
2
3
4
5
# Build
sam build

# Deploy
sam deploy --parameter-overrides ChannelAccessToken=your_token

3. Configure LINE Webhook

Set the API endpoint URL output after deployment as the Webhook URL in the LINE Developers console.

Local Development

1
2
3
4
5
6
7
# Start local API server
sam local start-api --env-vars env.json

# Run tests
curl -X POST http://localhost:3000/process_user_message \
  -H "Content-Type: application/json" \
  -d @events/line_message.json

Cost

Estimated monthly cost (assuming 100 users):

ServiceCost
LambdaNearly free (within free tier)
API GatewayNearly free (within free tier)
S3A few cents/month
TotalA few cents to a few dollars/month

Summary

  • Serverless architecture minimizes operational costs
  • S3 + JSON for simple data management
  • SAM for infrastructure as code
  • “Days elapsed” deadline management suited for cleaning tasks

Source code is available on GitHub.

×