DEV Community

Evan Lin
Evan Lin

Posted on • Originally published at evanlin.com on

Go + Gemini + GCP: A Smart Garbage Truck LINE Bot Solution

title: [Go] Build a Smart Garbage Truck LINE Bot with Go + Gemini + GCP: A Complete Solution from Query to Reminder
published: false
date: 2025-11-01 00:00:00 UTC
tags: 
canonical_url: https://www.evanlin.com/til-linebot-garbage-helper/
---

![image-20251102143236468](https://www.evanlin.com/images/image-20251102143236468.png)

# Preface

In Taiwan, the garbage truck schedule is always unpredictable. You remember it coming at seven o'clock last night, but you're still waiting today; or you just went out to take out the trash, and the garbage truck just passed by. I believe this is a common problem for many people.

With the development of smart cities, more and more cities are starting to provide real-time garbage truck information APIs, but this information is not easy for the general public to use.

At this time, I saw a [friend's post](https://www.facebook.com/yukaihuangtw/posts/pfbid02Hf5K28V7BmBcy9FzHBdu9r8zD5TjtK3MTKL4BpwMdX34Wc9SP1ktoZfvTGQTRix5l) on Facebook, where he described that he had created a website for tracking garbage trucks. ([Website](https://garbage.yukai.dev/), [github](https://github.com/Yukaii/garbage/))

![image-20251102143208176](https://www.evanlin.com/images/image-20251102143208176.png)

At this time, I was thinking, couldn't we combine a LINE Bot to create a tool that could quickly help others? Therefore, I decided to build a garbage truck LINE Bot so that everyone can easily query garbage truck information and even set up reminder notifications through the most familiar communication software. More importantly, this Bot is not just a simple command query, but integrates Google Gemini AI, which can understand natural language like "Where can I throw away garbage before 7 pm tonight?" to provide a truly intelligent service experience.

### Project Code:

[https://github.com/kkdai/linebot-garbage-helper](https://github.com/kkdai/linebot-garbage-helper)

(Through this code, you can quickly deploy to GCP Cloud Run and use Cloud Build to achieve automated CI/CD)

## πŸ—‘οΈ Project Feature Introduction

### Core Features

1.  **πŸ—‘οΈ Real-time Garbage Truck Query**
    *   Enter an address or share your location to query nearby garbage truck stops
    *   Displays estimated arrival time, route information, and Google Maps navigation links
2.  **⏰ Smart Reminder System**
    *   Can set a reminder N minutes before the garbage truck arrives
    *   Automatically push notifications, so you'll never miss the garbage truck again
    *   Supports multiple reminder status management (active, sent, expired, cancelled)
3.  **πŸ€– Natural Language Query**
    *   Integrates Google Gemini AI, supports natural language understanding
    *   For example: "Where can I throw away garbage in Da'an District, Taipei City before 7 pm?"
    *   Automatically extracts location, time range, and other query conditions
4.  **❀️ Favorite Location Feature**
    *   Saves frequently used locations (home, company, etc.)
    *   Quickly query garbage truck information for favorite locations

## πŸ—οΈ Technical Architecture Explanation

### Why Choose the Go Language?

1.  **Excellent Concurrency Handling**: Go's goroutines are very suitable for handling a large number of webhook requests
2.  **Fast Compilation and Deployment**: Especially suitable for containerized deployment on Cloud Run
3.  **Rich Ecosystem**: LINE Bot SDK and Google Cloud SDK both have official support
4.  **Excellent Performance**: Low memory usage, fast startup speed

### System Architecture Diagram

Enter fullscreen mode Exit fullscreen mode

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ LINE Client │───▢│ Cloud Run │───▢│ Firestore β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ (Go App) β”‚ β”‚ (Database) β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚
β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ External APIs β”‚
β”‚ β€’ Google Maps β”‚
β”‚ β€’ Gemini AI β”‚
β”‚ β€’ Garbage Truck Data Source β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚
β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Cloud Scheduler β”‚
β”‚ (Reminder Scheduling Trigger) β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜


### Main Technology Stack

-   **Language**: Go 1.24
-   **Cloud Platform**: Google Cloud Platform
-   **Database**: Firestore (NoSQL Document Database)
-   **External APIs**: LINE Bot SDK, Google Maps API, Gemini API
-   **Data Source**: [Yukaii/garbage](https://github.com/Yukaii/garbage)
-   **Deployment**: Cloud Run + Cloud Build

## πŸ’» Core Feature Implementation

### 1. LINE Webhook Handling

First, let's see how to handle LINE's webhook events:

Enter fullscreen mode Exit fullscreen mode

func (h *Handler) HandleWebhook(w http.ResponseWriter, r *http.Request) {
log.Printf("Webhook received from %s", r.RemoteAddr)

events, err := webhook.ParseRequest(h.channelSecret, r)
if err != nil {
    log.Printf("Error parsing webhook request: %v", err)
    http.Error(w, "Bad Request", http.StatusBadRequest)
    return
}

ctx := r.Context()
for _, event := range events {
    go h.handleEvent(ctx, event) // Use goroutine to handle events
}

w.WriteHeader(http.StatusOK)
Enter fullscreen mode Exit fullscreen mode

}


### 2. Gemini AI Natural Language Understanding

This is the most interesting part of the entire system, using Gemini to understand the user's natural language queries:

Enter fullscreen mode Exit fullscreen mode

func (gc *GeminiClient) AnalyzeIntent(ctx context.Context, userMessage string) (*IntentResult, error) {
model := gc.client.GenerativeModel(gc.model)

prompt := fmt.Sprintf(`You are a query intent analyzer, specializing in analyzing user queries about garbage trucks.
Enter fullscreen mode Exit fullscreen mode

User input may contain place names and times. Please analyze the input and output the results in JSON format.

Output format:
{
"district": "District Name (if any)",
"time_window": {
"from": "Start time (HH:MM format, if any)",
"to": "End time (HH:MM format, if any)"
},
"keywords": ["Keyword array"],
"query_type": "garbage_truck_eta"
}

Example:
Input: "Where can I throw away garbage in Da'an District, Taipei City before 7 pm tonight?"
Output:
{
"district": "Da'an District, Taipei City",
"time_window": {
"from": "",
"to": "19:00"
},
"keywords": ["Taipei City", "Da'an District", "throw away garbage", "tonight", "7 pm"],
"query_type": "garbage_truck_eta"
}

Please analyze the following user input:
"%s"

Please only return JSON, do not include any other explanatory text.`, userMessage)

resp, err := model.GenerateContent(ctx, genai.Text(prompt))
if err != nil {
    return nil, err
}

// Parse JSON response
var result IntentResult
if err := json.Unmarshal([]byte(responseText), &result); err != nil {
    // If Gemini cannot parse, fall back to simple keyword matching
    return &IntentResult{
        District: extractDistrict(userMessage),
        Keywords: []string{userMessage},
        QueryType: "garbage_truck_eta",
    }, nil
}

return &result, nil
Enter fullscreen mode Exit fullscreen mode

}


### 3. Smart Reminder Scheduling System

The reminder system is one of the core features of this project, and its design considers reliability and performance:

Enter fullscreen mode Exit fullscreen mode

func (s *Scheduler) ProcessReminders(ctx context.Context) error {
now := time.Now()

// Performance optimization: Check if there are active reminders first
count, err := s.store.CountActiveReminders(ctx)
if err != nil {
    log.Printf("Warning: failed to count active reminders: %v", err)
} else if count == 0 {
    log.Printf("No active reminders, skipping processing")
    return nil
}

reminders, err := s.store.GetActiveReminders(ctx, now)
if err != nil {
    return fmt.Errorf("failed to get active reminders: %w", err)
}

log.Printf("Found %d active reminders to process", len(reminders))

for _, reminder := range reminders {
    notificationTime := reminder.ETA.Add(-time.Duration(reminder.AdvanceMinutes) * time.Minute)

    // Check if it's time to send the reminder
    if now.Before(notificationTime) {
        continue // Not yet time to send
    }

    if now.After(reminder.ETA) {
        // ETA has expired, mark as expired
        s.store.UpdateReminderStatus(ctx, reminder.ID, "expired")
        continue
    }

    // Send reminder notification
    if err := s.sendReminderNotification(ctx, reminder); err != nil {
        log.Printf("Error sending reminder %s: %v", reminder.ID, err)
        continue
    }

    // Update status to sent
    s.store.UpdateReminderStatus(ctx, reminder.ID, "sent")
}

return nil
Enter fullscreen mode Exit fullscreen mode

}


### 4. Firestore Data Structure Design

We use Firestore to store user data and reminder information:

Enter fullscreen mode Exit fullscreen mode

type Reminder struct {
ID string firestore:"id"
UserID string firestore:"userId"
StopName string firestore:"stopName"
RouteID string firestore:"routeId"
ETA time.Time firestore:"eta"
AdvanceMinutes int firestore:"advanceMinutes"
Status string firestore:"status" // active, sent, expired, cancelled
CreatedAt time.Time firestore:"createdAt"
UpdatedAt time.Time firestore:"updatedAt"
}

type User struct {
ID string firestore:"id"
Favorites []Favorite firestore:"favorites"
CreatedAt time.Time firestore:"createdAt"
UpdatedAt time.Time firestore:"updatedAt"
}


### 5. Dual-Protection Reminder Mechanism

To ensure that reminders are not missed, the system is designed with a dual-protection mechanism:

1.  **Local Scheduler**: The background scheduling service starts automatically when the application starts
2.  **External Trigger**: Calls `/tasks/dispatch-reminders` periodically through Cloud Scheduler

Enter fullscreen mode Exit fullscreen mode

// Local scheduler
func (s *Scheduler) StartScheduler(ctx context.Context) {
ticker := time.NewTicker(1 * time.Minute)
defer ticker.Stop()

cleanupTicker := time.NewTicker(1 * time.Hour)
defer cleanupTicker.Stop()

for {
    select {
    case <-ctx.Done():
        return
    case <-ticker.C:
        s.ProcessReminders(ctx)
    case <-cleanupTicker.C:
        s.CleanupExpiredReminders(ctx)
    }
}
Enter fullscreen mode Exit fullscreen mode

}

// External trigger endpoint
r.HandleFunc("/tasks/dispatch-reminders", func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if token != "Bearer "+cfg.InternalTaskToken {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}

if err := reminderService.ProcessReminders(r.Context()); err != nil {
    log.Printf("Error processing reminders: %v", err)
    http.Error(w, "Internal Server Error", http.StatusInternalServerError)
    return
}

w.WriteHeader(http.StatusOK)
Enter fullscreen mode Exit fullscreen mode

})


## πŸš€ Cloud Build Automated Deployment

### Setting up Cloud Build Triggers

The deployment process is fully automated, and deployment is automatically triggered as long as the code is pushed to the main branch:

Enter fullscreen mode Exit fullscreen mode

cloudbuild.yaml

steps:
# Build the container image

  • name: 'gcr.io/cloud-builders/docker' args: ['build', '-t', 'gcr.io/$PROJECT_ID/garbage-linebot:$COMMIT_SHA', '.']

# Push the container image to Container Registry

  • name: 'gcr.io/cloud-builders/docker' args: ['push', 'gcr.io/$PROJECT_ID/garbage-linebot:$COMMIT_SHA']

# Deploy container image to Cloud Run

  • name: 'gcr.io/cloud-builders/gcloud' args:
    • 'run'
    • 'deploy'
    • 'garbage-linebot'
    • '--image'
    • 'gcr.io/$PROJECT_ID/garbage-linebot:$COMMIT_SHA'
    • '--region'
    • 'asia-east1'
    • '--platform'
    • 'managed'
    • '--allow-unauthenticated'
    • '--set-env-vars'
    • 'LINE_CHANNEL_SECRET=${_LINE_CHANNEL_SECRET},LINE_CHANNEL_ACCESS_TOKEN=${_LINE_CHANNEL_ACCESS_TOKEN},GOOGLE_MAPS_API_KEY=${_GOOGLE_MAPS_API_KEY},GEMINI_API_KEY=${_GEMINI_API_KEY},GCP_PROJECT_ID=$PROJECT_ID'

### Environment Variable Settings

Set substitution variables in the Cloud Build trigger:

Enter fullscreen mode Exit fullscreen mode

_LINE_CHANNEL_SECRET: your_line_channel_secret
_LINE_CHANNEL_ACCESS_TOKEN: your_line_channel_access_token
_GOOGLE_MAPS_API_KEY: your_google_maps_api_key
_GEMINI_API_KEY: your_gemini_api_key


In particular, `INTERNAL_TASK_TOKEN` will be automatically generated when the application starts, no manual configuration is required!

## πŸ”§ Challenges and Solutions Encountered

### 1. Gemini API Stability Handling

**Problem**: The Gemini API occasionally returns responses in a non-JSON format, causing parsing to fail.

**Solution**: Implement error handling and fallback mechanism:

Enter fullscreen mode Exit fullscreen mode

var result IntentResult
if err := json.Unmarshal([]byte(responseText), &result); err != nil {
// If Gemini cannot parse, fall back to simple keyword matching
return &IntentResult{
District: extractDistrict(userMessage),
Keywords: []string{userMessage},
QueryType: "garbage_truck_eta",
TimeWindow: TimeWindow{From: "", To: ""},
}, nil
}


### 2. Firestore Query Performance Optimization

**Problem**: Querying all active reminders every time causes unnecessary data reads.

**Solution**: Add a count query as an early return optimization:

Enter fullscreen mode Exit fullscreen mode

// Check if there are active reminders first
count, err := s.store.CountActiveReminders(ctx)
if count == 0 {
log.Printf("No active reminders, skipping processing")
return nil
}


### 3. Cloud Scheduler Region Configuration Issues

**Problem**: Cloud Scheduler may not be supported in some regions, causing automatic reminders to fail.

**Solution**: Design a dual-protection mechanism so that even if the external scheduler fails, the local scheduler can still operate normally.

### 4. LINE Bot Message Push Limits

**Problem**: LINE Bot has frequency limits for pushing messages.

**Solution**:

-   Implement reminder status management to avoid duplicate sending
-   Add appropriate error handling and retry mechanisms
-   Use goroutines to handle asynchronously to avoid blocking the main process

## πŸ“Š Performance Monitoring and Reliability

### Health Check Endpoint

Enter fullscreen mode Exit fullscreen mode

r.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
}).Methods("GET")


### Detailed Log Records

The system has detailed log records at key nodes for easy debugging and monitoring:

Enter fullscreen mode Exit fullscreen mode

log.Printf("Processing reminder %s: ETA=%s, NotificationTime=%s, AdvanceMinutes=%d",
reminder.ID, reminder.ETA.Format("2006-01-02 15:04:05"),
notificationTime.Format("2006-01-02 15:04:05"), reminder.AdvanceMinutes)


## 🎯 Summary and Future Improvements

This garbage truck LINE Bot project demonstrates how to combine modern technology stacks to solve real-life problems. Through the high performance of the Go language, the natural language understanding of Gemini AI, and the cloud services of GCP, we have created a solution that is both practical and intelligent.

### Project Highlights

1.  **Intelligent Query**: Understands natural language through Gemini AI, providing a more user-friendly experience
2.  **Reliable Reminder System**: Dual-protection mechanism ensures that important notifications are not missed
3.  **Modern Architecture**: Uses a microservices architecture, easy to scale and maintain
4.  **Automated Deployment**: Complete CI/CD process, reducing maintenance costs

### Future Improvement Directions

1.  **Performance Optimization**
    -   Create Firestore composite indexes to improve query performance
    -   Implement batch push to reduce API calls
2.  **Reliability Enhancement**
    -   Add distributed locks to avoid duplicate execution
    -   Implement exponential backoff retry mechanism
3.  **Feature Expansion**
    -   Support garbage truck data for more cities
    -   Add usage statistics and analysis functions
    -   Integrate more AI capabilities, such as image recognition
4.  **Monitoring Enhancement**
    -   Integrate Prometheus/OpenTelemetry
    -   Create a complete performance monitoring dashboard

Through this project, I have deeply realized the advantages of the Go language in cloud-native application development and how AI technology can make traditional applications more intelligent. I hope this experience sharing can help developers who are learning related technologies!

### Related Resources

-   [Project GitHub Repository](https://github.com/kkdai/linebot-garbage-helper)
-   [LINE Bot SDK for Go](https://github.com/line/line-bot-sdk-go)
-   [Google Gemini AI API](https://ai.google.dev/)
-   [GCP Cloud Run Documentation](https://cloud.google.com/run/docs)
-   [Firestore Documentation](https://cloud.google.com/firestore/docs)
Enter fullscreen mode Exit fullscreen mode

Top comments (0)