Skip to content

feat: Zalo messenger channel integration #100

Description

@aecs4u

Summary

Add Zalo integration to enable RustyClaw agents to communicate via Vietnam's leading messaging platform.

Background

Zalo is Vietnam's #1 messaging app with 75M+ users (78% of Vietnam's population). Key features:

  • Text and voice messaging
  • Group chats
  • Official Accounts (business accounts)
  • Mini apps ecosystem
  • Payment integration (ZaloPay)

Motivation

Zalo is critical for:

  • Vietnamese market reach
  • Southeast Asian business communication
  • Government/enterprise integration in Vietnam
  • Local language support (Vietnamese)

Proposed Design

  1. Configuration (config.toml):

    [[messengers]]
    name = "zalo"
    type = "zalo_official"  # Zalo Official Account
    enabled = true
    app_id = "1234567890"
    app_secret = "$ZALO_APP_SECRET"
    oa_id = "9876543210"  # Official Account ID
  2. Core Functionality:

    • Send text messages via Zalo OA API
    • Receive user messages via webhook
    • Handle message templates
    • Send rich media (images, attachments)
    • Broadcast messages to followers
  3. Implementation (src/messengers/zalo.rs):

    pub struct ZaloMessenger {
        app_id: String,
        app_secret: String,
        oa_id: String,
        access_token: Option<String>,
        client: reqwest::Client,
    }
    
    impl ZaloMessenger {
        pub async fn get_access_token(&mut self) -> Result<String> {
            let response = self.client
                .post("https://oauth.zaloapp.com/v4/oa/access_token")
                .json(&serde_json::json!({
                    "app_id": self.app_id,
                    "app_secret": self.app_secret,
                    "grant_type": "client_credentials"
                }))
                .send()
                .await?
                .json::<serde_json::Value>()
                .await?;
    
            let token = response["data"]["access_token"]
                .as_str()
                .ok_or("Missing access token")?
                .to_string();
    
            self.access_token = Some(token.clone());
            Ok(token)
        }
    
        pub async fn send_message(&self, user_id: &str, message: &str) -> Result<()> {
            let token = self.access_token.as_ref()
                .ok_or("No access token")?;
    
            self.client
                .post("https://openapi.zalo.me/v2.0/oa/message")
                .bearer_auth(token)
                .json(&serde_json::json!({
                    "recipient": {
                        "user_id": user_id
                    },
                    "message": {
                        "text": message
                    }
                }))
                .send()
                .await?;
    
            Ok(())
        }
    }

API Endpoints

Endpoint Method Purpose
/v4/oa/access_token POST Get access token
/v2.0/oa/message POST Send message
/v2.0/oa/message/template POST Send template message
/v2.0/oa/upload POST Upload media

Dependencies

[dependencies]
# Uses existing reqwest HTTP client

Acceptance Criteria

  • Zalo Official Account configuration
  • OAuth token management with refresh
  • Send text messages to users
  • Receive messages via webhook
  • Send template messages
  • Upload and send images/files
  • Handle message events (text, sticker, location)
  • Documentation in docs/MESSENGER_ZALO.md

Security Considerations

  • App secret stored in encrypted vault
  • OAuth token refresh mechanism
  • Webhook signature verification
  • HTTPS required for webhooks

Related Issues

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions