feat: add Chatmail channel for Delta Chat integration
- Add new channel 'chatmail' using rpc-client-go library - Implement Channel interface with Start, Stop, Send methods - Implement ReactionCapable interface for message reactions - Auto-mark messages as seen on receipt - Reject unauthorized users with helpful message containing their account ID - Ignore pending messages on startup to avoid responding to old messages - Add comprehensive documentation in docs/channels/chatmail/README.md Configuration: - enabled: Enable/disable the channel - account_path: Path to store Delta Chat account (default: ~/.accounts/chatmail) - allow_from: List of authorized account IDs - group_trigger: Trigger configuration for group chats - reasoning_channel_id: Channel ID for reasoning output
This commit is contained in:
parent
27f638e909
commit
4cc8e08515
8 changed files with 558 additions and 0 deletions
197
docs/channels/chatmail/README.md
Normal file
197
docs/channels/chatmail/README.md
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
> Back to [README](../../../README.md)
|
||||
|
||||
# Chatmail (Delta Chat)
|
||||
|
||||
The Chatmail channel enables PicoClaw to communicate via [Delta Chat](https://delta.chat/), a decentralized messaging platform based on email. It uses the [chatmail/rpc-client-go](https://github.com/chatmail/rpc-client-go) library to interact with Delta Chat through its RPC interface.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **deltachat-rpc-server**: The Delta Chat RPC server binary must be installed and available in your system's PATH.
|
||||
- **Compatible chatemail account**: Check compatibility in [https://providers.delta.chat/](https://providers.delta.chat/).
|
||||
|
||||
### Installing deltachat-rpc-server
|
||||
|
||||
#### Cargo
|
||||
|
||||
```bash
|
||||
# Server
|
||||
cargo install --git https://github.com/chatmail/core/ deltachat-rpc-server
|
||||
# Repl for configure account
|
||||
cargo install --git https://github.com/chatmail/core/ deltachat-repl
|
||||
```
|
||||
|
||||
#### From Source
|
||||
|
||||
```bash
|
||||
# Server
|
||||
git clone https://github.com/chatmail/core.git
|
||||
cd deltachat-rpc-server
|
||||
cargo build --release
|
||||
sudo cp target/release/deltachat-rpc-server /usr/local/bin/
|
||||
# Repl for configure account
|
||||
cd ../deltachat-repl
|
||||
cargo build --release
|
||||
sudo cp target/release/deltachat-repl /usr/local/bin/
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Add this to your `config.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"channels": {
|
||||
"chatmail": {
|
||||
"enabled": true,
|
||||
"account_path": "",
|
||||
"allow_from": [],
|
||||
"group_trigger": {
|
||||
"mention_only": false,
|
||||
"prefixes": []
|
||||
},
|
||||
"reasoning_channel_id": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Field Reference
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|----------------------|----------|----------|------------------------------------------------------------------------------------------------------|
|
||||
| enabled | bool | Yes | Whether to enable the Chatmail channel |
|
||||
| account_path | string | No | Path to store the Delta Chat account database. Default: `~/.accounts/chatmail` |
|
||||
| allow_from | []string | No | Allowlist of contact IDs; empty means all contacts are allowed |
|
||||
| group_trigger | object | No | Group trigger strategy (`mention_only` / `prefixes`) |
|
||||
| reasoning_channel_id | string | No | Target channel ID for reasoning output |
|
||||
|
||||
### Group Trigger Configuration
|
||||
|
||||
| Field | Type | Description |
|
||||
|--------------|----------|------------------------------------------------------------------------------------------------|
|
||||
| mention_only | bool | When `true`, the bot only responds when mentioned in group chats |
|
||||
| prefixes | []string | List of prefixes that trigger bot responses in groups (e.g., `["!", "/"]`) |
|
||||
|
||||
## Setup
|
||||
|
||||
### Step 1: Enable the Channel
|
||||
|
||||
Set `enabled: true` in the configuration file.
|
||||
|
||||
### Step 2: Start PicoClaw
|
||||
|
||||
When you start PicoClaw with the Chatmail channel enabled, you will see an invite link in the console:
|
||||
|
||||
```
|
||||
Chatmail invite link: https://i.delta.chat/#B2AE34...
|
||||
Scan this QR code with your Delta Chat app to start chatting.
|
||||
```
|
||||
|
||||
Open the url and scan the QR code.
|
||||
|
||||
### Step 3: Configure Your Bot Account
|
||||
|
||||
1. **First Run**: On the first startup, the channel creates a new Delta Chat account.
|
||||
2. **Scan the QR Code**: Use your Delta Chat app to scan the invite link QR code or manually enter the invite code.
|
||||
3. **Start Chatting**: Once connected, you can send messages to the bot from your Delta Chat app.
|
||||
|
||||
### Step 4: Send Messages
|
||||
|
||||
- **Direct Messages**: Send any message directly to the bot's chat.
|
||||
- **Group Chats**: Add the bot to a group chat. Configure `group_trigger` if you want the bot to only respond when mentioned.
|
||||
|
||||
## Account Storage
|
||||
|
||||
The Delta Chat account is stored at the path specified by `account_path` (default: `~/.accounts/chatmail`). This includes:
|
||||
|
||||
- Account database
|
||||
- Encryption keys
|
||||
- Message cache
|
||||
|
||||
**Important**: Keep this directory secure as it contains your private keys.
|
||||
|
||||
## Behavior
|
||||
|
||||
### Startup Behavior
|
||||
|
||||
When the channel starts:
|
||||
|
||||
1. Creates or loads the Delta Chat account
|
||||
2. Generates and displays an invite link for pairing
|
||||
3. **Ignores all pending messages** - only processes new messages received after startup
|
||||
4. Starts listening for incoming messages
|
||||
|
||||
### Message Handling
|
||||
|
||||
- **Direct messages**: All messages are processed
|
||||
- **Group messages**: Controlled by `group_trigger` configuration
|
||||
- **Bot replies**: Sent as regular chat messages with full markdown support
|
||||
|
||||
## Supported Features
|
||||
|
||||
| Feature | Status | Notes |
|
||||
|----------------------|--------|----------------------------------------------------------|
|
||||
| Text messages | ✅ | Send and receive |
|
||||
| Direct messages | ✅ | Full support |
|
||||
| Group chats | ✅ | With trigger configuration |
|
||||
| Markdown rendering | ✅ | Messages formatted with markdown |
|
||||
| Reactions | ✅ | 👀 reaction on incoming messages, removed after response |
|
||||
| Media attachments | ❌ | Not yet supported |
|
||||
| Typing indicators | ❌ | Not yet supported |
|
||||
| Message editing | ❌ | Not yet supported |
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **End-to-End Encryption**: Delta Chat uses Autocrypt for automatic E2E encryption. Messages are encrypted by default.
|
||||
2. **Account Keys**: Private keys are stored in `account_path`. Protect this directory.
|
||||
3. **Allowlist**: Use `allow_from` to restrict which contacts can interact with your bot.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "deltachat-rpc-server not found"
|
||||
|
||||
Ensure `deltachat-rpc-server` is installed and in your PATH:
|
||||
|
||||
```bash
|
||||
which deltachat-rpc-server
|
||||
```
|
||||
|
||||
### Bot not receiving messages
|
||||
|
||||
1. Verify the account is properly configured by checking the logs
|
||||
2. Ensure you've scanned the QR code or entered the invite code
|
||||
3. Check that the contact sending messages is in the `allow_from` list (if configured)
|
||||
|
||||
### Invite link not appearing
|
||||
|
||||
If no invite link appears in the logs:
|
||||
|
||||
1. Check that the channel is enabled in the configuration
|
||||
2. Verify write permissions for `account_path`
|
||||
3. Check for errors in the PicoClaw logs
|
||||
|
||||
## Example Configuration with Group Trigger
|
||||
|
||||
```json
|
||||
{
|
||||
"channels": {
|
||||
"chatmail": {
|
||||
"enabled": true,
|
||||
"account_path": "/var/lib/picoclaw/chatmail",
|
||||
"allow_from": [],
|
||||
"group_trigger": {
|
||||
"mention_only": true,
|
||||
"prefixes": ["!", "/"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
With this configuration, the bot will only respond in group chats when:
|
||||
- The bot is directly mentioned (`@botname your message`), OR
|
||||
- The message starts with one of the configured prefixes (`!command` or `/command`)
|
||||
|
||||
## Multiple Accounts
|
||||
|
||||
Each PicoClaw instance can have one Chatmail channel configured. To run multiple bot accounts, use separate configuration files with different `account_path` values.
|
||||
3
go.mod
3
go.mod
|
|
@ -12,6 +12,7 @@ require (
|
|||
github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.50.2
|
||||
github.com/bwmarrin/discordgo v0.29.0
|
||||
github.com/caarlos0/env/v11 v11.4.0
|
||||
github.com/chatmail/rpc-client-go/v2 v2.44.0
|
||||
github.com/creack/pty v1.1.24
|
||||
github.com/ergochat/irc-go v0.6.0
|
||||
github.com/ergochat/readline v0.1.3
|
||||
|
|
@ -61,6 +62,8 @@ require (
|
|||
github.com/aws/smithy-go v1.24.2 // indirect
|
||||
github.com/beeper/argo-go v1.1.2 // indirect
|
||||
github.com/coder/websocket v1.8.14 // indirect
|
||||
github.com/creachadair/jrpc2 v1.3.5 // indirect
|
||||
github.com/creachadair/mds v0.26.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/elliotchance/orderedmap/v3 v3.1.0 // indirect
|
||||
|
|
|
|||
6
go.sum
6
go.sum
|
|
@ -63,12 +63,18 @@ github.com/caarlos0/env/v11 v11.4.0 h1:Kcb6t5kIIr4XkoQC9AF2j+8E1Jsrl3Wz/hhm1LtoG
|
|||
github.com/caarlos0/env/v11 v11.4.0/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U=
|
||||
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chatmail/rpc-client-go/v2 v2.44.0 h1:DjuI2jZVdLLU5/jEp8QvaR0z1QpKrDOESOdne+s3nu8=
|
||||
github.com/chatmail/rpc-client-go/v2 v2.44.0/go.mod h1:FQq2gE3wIWj48/uunoDjaRjwdsSDAr4I8QJvYVcHGHU=
|
||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||
github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
|
||||
github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/creachadair/jrpc2 v1.3.5 h1:onJko+1u6xoiRph3xwWmfNISR91teCRhbJwSyS9Svzo=
|
||||
github.com/creachadair/jrpc2 v1.3.5/go.mod h1:YXDmS53AavsiytbAwskrczJPcVHvKC9GoyWzwfSQXoE=
|
||||
github.com/creachadair/mds v0.26.1 h1:CQG8f4cueHX/c20q5Sy/Ubk8Bvy+aRzVgbpxVieMBAs=
|
||||
github.com/creachadair/mds v0.26.1/go.mod h1:dMBTCSy3iS3dwh4Rb1zxeZz2d7K8+N24GCTsayWtQRI=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
||||
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||
|
|
|
|||
322
pkg/channels/chatmail/chatmail.go
Normal file
322
pkg/channels/chatmail/chatmail.go
Normal file
|
|
@ -0,0 +1,322 @@
|
|||
package chatmail
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"github.com/chatmail/rpc-client-go/v2/deltachat"
|
||||
|
||||
"github.com/sipeed/picoclaw/pkg/bus"
|
||||
"github.com/sipeed/picoclaw/pkg/channels"
|
||||
"github.com/sipeed/picoclaw/pkg/config"
|
||||
"github.com/sipeed/picoclaw/pkg/identity"
|
||||
"github.com/sipeed/picoclaw/pkg/logger"
|
||||
)
|
||||
|
||||
type ChatmailChannel struct {
|
||||
*channels.BaseChannel
|
||||
config config.ChatmailConfig
|
||||
rpc *deltachat.Rpc
|
||||
bot *deltachat.Bot
|
||||
transport *deltachat.IOTransport
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
accId uint32
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func NewChatmailChannel(cfg config.ChatmailConfig, messageBus *bus.MessageBus) (*ChatmailChannel, error) {
|
||||
base := channels.NewBaseChannel("chatmail", cfg, messageBus, cfg.AllowFrom,
|
||||
channels.WithGroupTrigger(cfg.GroupTrigger),
|
||||
channels.WithReasoningChannelID(cfg.ReasoningChannelID),
|
||||
)
|
||||
|
||||
return &ChatmailChannel{
|
||||
BaseChannel: base,
|
||||
config: cfg,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *ChatmailChannel) Start(ctx context.Context) error {
|
||||
logger.InfoC("chatmail", "Starting Chatmail channel")
|
||||
|
||||
c.ctx, c.cancel = context.WithCancel(ctx)
|
||||
|
||||
accountPath := c.config.AccountPath
|
||||
if accountPath == "" {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get home directory: %w", err)
|
||||
}
|
||||
accountPath = filepath.Join(homeDir, ".accounts", "chatmail")
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(accountPath, 0700); err != nil {
|
||||
return fmt.Errorf("failed to create account directory: %w", err)
|
||||
}
|
||||
|
||||
transport := deltachat.NewIOTransport()
|
||||
transport.AccountsDir = accountPath
|
||||
|
||||
if err := transport.Open(); err != nil {
|
||||
return fmt.Errorf("failed to open transport: %w", err)
|
||||
}
|
||||
c.transport = transport
|
||||
|
||||
c.rpc = &deltachat.Rpc{Context: c.ctx, Transport: transport}
|
||||
|
||||
accounts, err := c.rpc.GetAllAccountIds()
|
||||
if err != nil {
|
||||
transport.Close()
|
||||
return fmt.Errorf("failed to get accounts: %w", err)
|
||||
}
|
||||
|
||||
var accId uint32
|
||||
if len(accounts) == 0 {
|
||||
accId, err = c.rpc.AddAccount()
|
||||
if err != nil {
|
||||
transport.Close()
|
||||
return fmt.Errorf("failed to add account: %w", err)
|
||||
}
|
||||
logger.InfoCF("chatmail", "Created new account", map[string]any{"account_id": accId})
|
||||
} else {
|
||||
accId = accounts[0]
|
||||
logger.InfoCF("chatmail", "Using existing account", map[string]any{"account_id": accId})
|
||||
}
|
||||
c.accId = accId
|
||||
|
||||
isConfigured, err := c.rpc.IsConfigured(accId)
|
||||
if err != nil {
|
||||
transport.Close()
|
||||
return fmt.Errorf("failed to check account configuration: %w", err)
|
||||
}
|
||||
|
||||
if !isConfigured {
|
||||
botFlag := "1"
|
||||
if err := c.rpc.SetConfig(accId, "bot", &botFlag); err != nil {
|
||||
transport.Close()
|
||||
return fmt.Errorf("failed to set bot flag: %w", err)
|
||||
}
|
||||
logger.InfoC("chatmail", "Account configured as bot")
|
||||
}
|
||||
|
||||
inviteLink, err := c.rpc.GetChatSecurejoinQrCode(accId, nil)
|
||||
if err != nil {
|
||||
logger.WarnCF("chatmail", "Failed to get invite link", map[string]any{"error": err.Error()})
|
||||
} else {
|
||||
logger.InfoCF("chatmail", "Invite link", map[string]any{"link": inviteLink})
|
||||
fmt.Printf("\nChatmail invite link: %s\n", inviteLink)
|
||||
fmt.Println("Scan this QR code with your Delta Chat app to start chatting.")
|
||||
}
|
||||
|
||||
msgIds, err := c.rpc.GetNextMsgs(accId)
|
||||
if err != nil {
|
||||
logger.WarnCF("chatmail", "Failed to fetch pending messages", map[string]any{"error": err.Error()})
|
||||
} else if len(msgIds) > 0 {
|
||||
lastMsgId := fmt.Sprintf("%v", msgIds[len(msgIds)-1])
|
||||
if err := c.rpc.SetConfig(accId, "last_msg_id", &lastMsgId); err != nil {
|
||||
logger.WarnCF("chatmail", "Failed to set last_msg_id", map[string]any{"error": err.Error()})
|
||||
} else {
|
||||
// Mark all pending messages as seen
|
||||
if err := c.rpc.MarkseenMsgs(accId, msgIds); err != nil {
|
||||
logger.DebugCF("chatmail", "Failed to mark pending messages as seen", map[string]any{
|
||||
"count": len(msgIds),
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
logger.InfoCF("chatmail", "Ignored pending messages on startup", map[string]any{"count": len(msgIds)})
|
||||
}
|
||||
}
|
||||
|
||||
c.bot = deltachat.NewBot(c.rpc)
|
||||
c.bot.OnNewMsg(c.onNewMessage)
|
||||
|
||||
go func() {
|
||||
if err := c.bot.Run(); err != nil {
|
||||
logger.ErrorCF("chatmail", "Bot run error", map[string]any{"error": err.Error()})
|
||||
}
|
||||
}()
|
||||
|
||||
c.SetRunning(true)
|
||||
logger.InfoC("chatmail", "Chatmail channel started")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ChatmailChannel) Stop(ctx context.Context) error {
|
||||
logger.InfoC("chatmail", "Stopping Chatmail channel")
|
||||
c.SetRunning(false)
|
||||
|
||||
if c.bot != nil {
|
||||
c.bot.Stop()
|
||||
}
|
||||
|
||||
if c.cancel != nil {
|
||||
c.cancel()
|
||||
}
|
||||
|
||||
if c.transport != nil {
|
||||
c.transport.Close()
|
||||
}
|
||||
|
||||
logger.InfoC("chatmail", "Chatmail channel stopped")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ChatmailChannel) Send(ctx context.Context, msg bus.OutboundMessage) error {
|
||||
if !c.IsRunning() {
|
||||
return channels.ErrNotRunning
|
||||
}
|
||||
|
||||
chatId, err := c.parseChatID(msg.ChatID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid chat ID: %w", channels.ErrSendFailed)
|
||||
}
|
||||
|
||||
if msg.Content == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
text := msg.Content
|
||||
msgData := deltachat.MessageData{Text: &text}
|
||||
|
||||
_, err = c.rpc.SendMsg(c.accId, chatId, msgData)
|
||||
if err != nil {
|
||||
logger.ErrorCF("chatmail", "Failed to send message", map[string]any{
|
||||
"chat_id": chatId,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return fmt.Errorf("send failed: %w", channels.ErrSendFailed)
|
||||
}
|
||||
|
||||
logger.DebugCF("chatmail", "Message sent", map[string]any{"chat_id": chatId})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ChatmailChannel) parseChatID(chatID string) (uint32, error) {
|
||||
var chatId uint32
|
||||
_, err := fmt.Sscanf(chatID, "%d", &chatId)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return chatId, nil
|
||||
}
|
||||
|
||||
func (c *ChatmailChannel) parseMsgID(messageID string) (uint32, error) {
|
||||
var msgId uint32
|
||||
_, err := fmt.Sscanf(messageID, "%d", &msgId)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return msgId, nil
|
||||
}
|
||||
|
||||
func (c *ChatmailChannel) ReactToMessage(ctx context.Context, chatID, messageID string) (func(), error) {
|
||||
if !c.IsRunning() {
|
||||
return func() {}, nil
|
||||
}
|
||||
|
||||
msgId, err := c.parseMsgID(messageID)
|
||||
if err != nil {
|
||||
return func() {}, nil
|
||||
}
|
||||
|
||||
reactions := []string{"\U0001F440"}
|
||||
if _, err := c.rpc.SendReaction(c.accId, msgId, reactions); err != nil {
|
||||
logger.DebugCF("chatmail", "Failed to add reaction", map[string]any{"error": err.Error()})
|
||||
return func() {}, nil
|
||||
}
|
||||
|
||||
// Keep the reaction permanently - return no-op undo function
|
||||
return func() {}, nil
|
||||
}
|
||||
|
||||
func (c *ChatmailChannel) onNewMessage(bot *deltachat.Bot, accId uint32, msgId uint32) {
|
||||
msg, err := c.rpc.GetMessage(accId, msgId)
|
||||
if err != nil {
|
||||
logger.ErrorCF("chatmail", "Failed to get message", map[string]any{
|
||||
"msg_id": msgId,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if msg.FromId <= deltachat.ContactLastSpecial {
|
||||
return
|
||||
}
|
||||
|
||||
// Build sender info early for permission check
|
||||
senderID := fmt.Sprintf("%d", msg.FromId)
|
||||
sender := bus.SenderInfo{
|
||||
Platform: "chatmail",
|
||||
PlatformID: senderID,
|
||||
CanonicalID: identity.BuildCanonicalID("chatmail", senderID),
|
||||
}
|
||||
|
||||
contact, _ := c.rpc.GetContact(accId, msg.FromId)
|
||||
if contact.DisplayName != "" {
|
||||
sender.DisplayName = contact.DisplayName
|
||||
}
|
||||
|
||||
// Check authorization BEFORE processing
|
||||
if !c.IsAllowedSender(sender) {
|
||||
accountInfo := senderID
|
||||
|
||||
rejectionText := "⛔ Access denied. You are not authorized to use this bot.\n\n" +
|
||||
"To get access, ask the administrator to add this account to the configuration:\n\n" +
|
||||
"Account ID: " + accountInfo
|
||||
|
||||
text := rejectionText
|
||||
msgData := deltachat.MessageData{Text: &text}
|
||||
c.rpc.SendMsg(c.accId, msg.ChatId, msgData)
|
||||
c.rpc.MarkseenMsgs(c.accId, []uint32{msgId})
|
||||
|
||||
logger.InfoCF("chatmail", "Unauthorized user rejected", map[string]any{
|
||||
"sender_id": senderID,
|
||||
"display_name": sender.DisplayName,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Authorized - continue with normal flow
|
||||
chatIdStr := fmt.Sprintf("%d", msg.ChatId)
|
||||
chat, err := c.rpc.GetBasicChatInfo(accId, msg.ChatId)
|
||||
if err != nil {
|
||||
logger.ErrorCF("chatmail", "Failed to get chat info", map[string]any{
|
||||
"chat_id": msg.ChatId,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var peer bus.Peer
|
||||
isGroup := chat.ChatType == deltachat.ChatTypeGroup
|
||||
if isGroup {
|
||||
peer = bus.Peer{Kind: "group", ID: chatIdStr}
|
||||
} else {
|
||||
peer = bus.Peer{Kind: "direct", ID: chatIdStr}
|
||||
}
|
||||
|
||||
content := msg.Text
|
||||
metadata := map[string]string{
|
||||
"platform": "chatmail",
|
||||
"chat_type": string(chat.ChatType),
|
||||
}
|
||||
|
||||
c.mu.RLock()
|
||||
ctx := c.ctx
|
||||
c.mu.RUnlock()
|
||||
|
||||
c.HandleMessage(ctx, peer, fmt.Sprintf("%d", msgId), senderID, chatIdStr, content, nil, metadata, sender)
|
||||
|
||||
// Mark the message as seen after processing
|
||||
if err := c.rpc.MarkseenMsgs(accId, []uint32{msgId}); err != nil {
|
||||
logger.DebugCF("chatmail", "Failed to mark message as seen", map[string]any{
|
||||
"msg_id": msgId,
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
}
|
||||
16
pkg/channels/chatmail/init.go
Normal file
16
pkg/channels/chatmail/init.go
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
package chatmail
|
||||
|
||||
import (
|
||||
"github.com/sipeed/picoclaw/pkg/bus"
|
||||
"github.com/sipeed/picoclaw/pkg/channels"
|
||||
"github.com/sipeed/picoclaw/pkg/config"
|
||||
)
|
||||
|
||||
func init() {
|
||||
channels.RegisterFactory("chatmail", func(cfg *config.Config, b *bus.MessageBus) (channels.Channel, error) {
|
||||
if !cfg.Channels.Chatmail.Enabled {
|
||||
return nil, nil
|
||||
}
|
||||
return NewChatmailChannel(cfg.Channels.Chatmail, b)
|
||||
})
|
||||
}
|
||||
|
|
@ -425,6 +425,10 @@ func (m *Manager) initChannels(channels *config.ChannelsConfig) error {
|
|||
m.initChannel("irc", "IRC")
|
||||
}
|
||||
|
||||
if channels.Chatmail.Enabled {
|
||||
m.initChannel("chatmail", "Chatmail")
|
||||
}
|
||||
|
||||
logger.InfoCF("channels", "Channel initialization completed", map[string]any{
|
||||
"enabled_channels": len(m.channels),
|
||||
})
|
||||
|
|
|
|||
|
|
@ -374,6 +374,7 @@ type ChannelsConfig struct {
|
|||
Pico PicoConfig `json:"pico" yaml:"pico,omitempty"`
|
||||
PicoClient PicoClientConfig `json:"pico_client" yaml:"pico_client,omitempty"`
|
||||
IRC IRCConfig `json:"irc" yaml:"irc,omitempty"`
|
||||
Chatmail ChatmailConfig `json:"chatmail" yaml:"chatmail,omitempty"`
|
||||
}
|
||||
|
||||
// GroupTriggerConfig controls when the bot responds in group chats.
|
||||
|
|
@ -628,6 +629,14 @@ type IRCConfig struct {
|
|||
ReasoningChannelID string `json:"reasoning_channel_id" yaml:"-"`
|
||||
}
|
||||
|
||||
type ChatmailConfig struct {
|
||||
Enabled bool `json:"enabled" yaml:"-" env:"PICOCLAW_CHANNELS_CHATMAIL_ENABLED"`
|
||||
AccountPath string `json:"account_path" yaml:"-" env:"PICOCLAW_CHANNELS_CHATMAIL_ACCOUNT_PATH"`
|
||||
AllowFrom FlexibleStringSlice `json:"allow_from" yaml:"-" env:"PICOCLAW_CHANNELS_CHATMAIL_ALLOW_FROM"`
|
||||
GroupTrigger GroupTriggerConfig `json:"group_trigger,omitempty" yaml:"-"`
|
||||
ReasoningChannelID string `json:"reasoning_channel_id" yaml:"-" env:"PICOCLAW_CHANNELS_CHATMAIL_REASONING_CHANNEL_ID"`
|
||||
}
|
||||
|
||||
type HeartbeatConfig struct {
|
||||
Enabled bool `json:"enabled" env:"PICOCLAW_HEARTBEAT_ENABLED"`
|
||||
Interval int `json:"interval" env:"PICOCLAW_HEARTBEAT_INTERVAL"` // minutes, min 5
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import (
|
|||
"github.com/sipeed/picoclaw/pkg/agent"
|
||||
"github.com/sipeed/picoclaw/pkg/bus"
|
||||
"github.com/sipeed/picoclaw/pkg/channels"
|
||||
_ "github.com/sipeed/picoclaw/pkg/channels/chatmail"
|
||||
_ "github.com/sipeed/picoclaw/pkg/channels/dingtalk"
|
||||
_ "github.com/sipeed/picoclaw/pkg/channels/discord"
|
||||
_ "github.com/sipeed/picoclaw/pkg/channels/feishu"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue