完善私聊

This commit is contained in:
2026-06-14 19:26:43 +08:00
parent a2d838d556
commit 5d4aced3e0
5 changed files with 650 additions and 34 deletions
+111 -9
View File
@@ -3,7 +3,10 @@ package main
import (
"context"
"crypto/rand"
"encoding/base64"
"encoding/binary"
"encoding/hex"
"errors"
"fmt"
"strings"
"time"
@@ -12,6 +15,7 @@ import (
"meshtastic_mqtt_server/mqtpp"
mqtt "github.com/mochi-mqtt/server/v2"
"gorm.io/gorm"
)
const botMaxTextBytes = 200
@@ -73,13 +77,6 @@ func (s *botService) SendText(_ context.Context, req botSendTextRequest) (*botMe
if len([]byte(text)) > botMaxTextBytes {
return nil, fmt.Errorf("text is too long, max %d bytes", botMaxTextBytes)
}
channelID := strings.TrimSpace(req.ChannelID)
if channelID == "" {
channelID = bot.DefaultChannelID
}
if channelID == "" {
return nil, fmt.Errorf("channel id is required")
}
toNodeNum, toNodeID, err := botMessageTarget(messageType, req)
if err != nil {
return nil, err
@@ -88,6 +85,20 @@ func (s *botService) SendText(_ context.Context, req botSendTextRequest) (*botMe
if err != nil {
return nil, err
}
fromNodeNum := uint32(bot.NodeNum)
// direct 私聊走 PKIchannel 群聊保留旧的 AES-CTR + PSK 路径
if messageType == botMessageTypeDirect {
return s.sendPKIDirect(bot, fromNodeNum, uint32(toNodeNum), toNodeID, packetID, text, req.CreatedBy)
}
channelID := strings.TrimSpace(req.ChannelID)
if channelID == "" {
channelID = bot.DefaultChannelID
}
if channelID == "" {
return nil, fmt.Errorf("channel id is required")
}
psk := strings.TrimSpace(bot.PSK)
if psk == "" {
psk = botDefaultPSK
@@ -96,7 +107,6 @@ func (s *botService) SendText(_ context.Context, req botSendTextRequest) (*botMe
if err != nil {
return nil, err
}
fromNodeNum := uint32(bot.NodeNum)
raw, err := mqtpp.BuildTextMessageServiceEnvelope(mqtpp.TextMessageBuildOptions{
PacketBuildOptions: mqtpp.PacketBuildOptions{
FromNodeNum: fromNodeNum,
@@ -121,7 +131,7 @@ func (s *botService) SendText(_ context.Context, req botSendTextRequest) (*botMe
MessageType: messageType,
ChannelID: channelID,
ToNodeID: toNodeID,
ToNodeNum: int64PtrOrNil(toNodeNum, messageType == botMessageTypeDirect),
ToNodeNum: int64PtrOrNil(toNodeNum, false),
Topic: topic,
PacketID: int64(packetID),
Text: text,
@@ -130,6 +140,98 @@ func (s *botService) SendText(_ context.Context, req botSendTextRequest) (*botMe
Status: botMessageStatusPending,
CreatedBy: strings.TrimSpace(req.CreatedBy),
}
return s.persistAndPublish(row, topic, raw)
}
// sendPKIDirect 按固件 PKI 流程发送私聊:
// - 从 nodeinfo 中查目标节点的 X25519 公钥
// - 用 bot 自身私钥与对端公钥派生共享密钥,AES-CCM(M=8,L=2) 加密
// - ServiceEnvelope.channel_id = "PKI"topic 也用 "PKI"
func (s *botService) sendPKIDirect(bot *botNodeRecord, fromNodeNum, toNodeNum uint32, toNodeID *string, packetID uint32, text, createdBy string) (*botMessageRecord, error) {
if toNodeID == nil {
return nil, fmt.Errorf("target node id is required for pki direct message")
}
privateKeyB64 := strings.TrimSpace(bot.PrivateKey)
if privateKeyB64 == "" {
return nil, fmt.Errorf("bot has no private key, regenerate keys first")
}
privateKey, err := base64.StdEncoding.DecodeString(privateKeyB64)
if err != nil {
return nil, fmt.Errorf("invalid bot private key: %w", err)
}
senderPublic, err := decodeBotPublicKey(*bot)
if err != nil {
return nil, err
}
recipientPublic, err := s.lookupRecipientPublicKey(*toNodeID)
if err != nil {
return nil, err
}
raw, err := mqtpp.BuildPKITextMessageServiceEnvelope(mqtpp.PKITextMessageBuildOptions{
FromNodeNum: fromNodeNum,
ToNodeNum: toNodeNum,
PacketID: packetID,
GatewayID: bot.NodeID,
ViaMQTT: true,
SenderPrivate: privateKey,
RecipientPub: recipientPublic,
SenderPublic: senderPublic,
Text: text,
})
if err != nil {
return nil, err
}
topic := botMQTTTopic(bot.TopicPrefix, mqtpp.PKIChannelID, bot.NodeID)
row := &botMessageRecord{
BotID: bot.ID,
BotNodeID: bot.NodeID,
BotNodeNum: bot.NodeNum,
MessageType: botMessageTypeDirect,
ChannelID: mqtpp.PKIChannelID,
ToNodeID: toNodeID,
ToNodeNum: int64PtrOrNil(int64(toNodeNum), true),
Topic: topic,
PacketID: int64(packetID),
Text: text,
PayloadLen: int64(len(raw)),
Encrypted: true,
Status: botMessageStatusPending,
CreatedBy: strings.TrimSpace(createdBy),
}
return s.persistAndPublish(row, topic, raw)
}
// lookupRecipientPublicKey 从 nodeinfo 表中按 node_id 查询目标节点的 X25519 公钥(hex 编码)。
func (s *botService) lookupRecipientPublicKey(nodeID string) ([]byte, error) {
node, err := s.store.GetNodeInfo(nodeID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("recipient node %s not found in nodeinfo, cannot send PKI message", nodeID)
}
return nil, err
}
if node.PublicKey == nil || strings.TrimSpace(*node.PublicKey) == "" {
return nil, fmt.Errorf("recipient node %s has no public key on file", nodeID)
}
keyHex := strings.TrimSpace(*node.PublicKey)
keyBytes, err := hex.DecodeString(keyHex)
if err != nil {
// 兼容历史上可能存储为 base64 的情况
if alt, altErr := base64.StdEncoding.DecodeString(keyHex); altErr == nil {
keyBytes = alt
} else {
return nil, fmt.Errorf("invalid recipient public key for %s: %w", nodeID, err)
}
}
if len(keyBytes) != 32 {
return nil, fmt.Errorf("recipient public key for %s has unexpected length %d", nodeID, len(keyBytes))
}
return keyBytes, nil
}
// persistAndPublish 把消息记录入库后发布到 MQTT,统一处理失败状态写回。
func (s *botService) persistAndPublish(row *botMessageRecord, topic string, raw []byte) (*botMessageRecord, error) {
if err := s.store.InsertBotMessage(row); err != nil {
return nil, err
}