399 lines
8.6 KiB
Go
399 lines
8.6 KiB
Go
package nixmsg
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"time"
|
|
)
|
|
|
|
func (c *Client) handleDown(payload []byte) {
|
|
var head struct {
|
|
Type string `json:"type"`
|
|
RID string `json:"rid"`
|
|
}
|
|
if err := unmarshalJSON(payload, &head); err != nil {
|
|
return
|
|
}
|
|
if head.Type == "resp" {
|
|
var rf respFrame
|
|
if err := unmarshalJSON(payload, &rf); err != nil {
|
|
return
|
|
}
|
|
c.mu.Lock()
|
|
p := c.pending[head.RID]
|
|
if p != nil {
|
|
delete(c.pending, head.RID)
|
|
}
|
|
c.mu.Unlock()
|
|
if p != nil {
|
|
select {
|
|
case p.ch <- rf:
|
|
default:
|
|
}
|
|
}
|
|
return
|
|
}
|
|
if head.Type == "fatal" {
|
|
var f struct {
|
|
Reason string `json:"reason"`
|
|
}
|
|
_ = unmarshalJSON(payload, &f)
|
|
c.handleFatal(f.Reason)
|
|
return
|
|
}
|
|
kind := head.Type
|
|
cp := append([]byte(nil), payload...)
|
|
c.enqueueDown(queuedFrame{payload: cp, kind: kind})
|
|
}
|
|
|
|
func (c *Client) enqueueDown(fr queuedFrame) {
|
|
c.mu.Lock()
|
|
ch := c.incoming
|
|
ctx := c.ctx
|
|
c.mu.Unlock()
|
|
if ch == nil || ctx == nil {
|
|
return
|
|
}
|
|
if fr.kind == "presence" || fr.kind == "group_event" {
|
|
select {
|
|
case ch <- fr:
|
|
default:
|
|
// 超阈值丢最旧:缓冲满则丢弃本条事件
|
|
}
|
|
return
|
|
}
|
|
select {
|
|
case ch <- fr:
|
|
case <-ctx.Done():
|
|
default:
|
|
go func() {
|
|
select {
|
|
case ch <- fr:
|
|
case <-ctx.Done():
|
|
}
|
|
}()
|
|
}
|
|
}
|
|
|
|
func (c *Client) downLoop(ctx context.Context) {
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case fr := <-c.incoming:
|
|
c.handleDownApp(fr.payload)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *Client) handleDownApp(payload []byte) {
|
|
var head struct {
|
|
Type string `json:"type"`
|
|
}
|
|
if err := unmarshalJSON(payload, &head); err != nil {
|
|
return
|
|
}
|
|
switch head.Type {
|
|
case "msg":
|
|
c.handleMsg(payload)
|
|
case "receipt":
|
|
c.handleReceipt(payload)
|
|
case "revoked":
|
|
c.handleRevoked(payload)
|
|
case "presence":
|
|
c.handlePresence(payload)
|
|
case "group_event":
|
|
c.handleGroupEvent(payload)
|
|
}
|
|
}
|
|
|
|
func msgKey(from, id string) string {
|
|
return "m\x00" + from + "\x00" + id
|
|
}
|
|
|
|
func receiptKey(id string) string {
|
|
return "r\x00" + id
|
|
}
|
|
|
|
func (c *Client) handleMsg(payload []byte) {
|
|
var m struct {
|
|
ID string `json:"id"`
|
|
From string `json:"from"`
|
|
To Target `json:"to"`
|
|
Body Body `json:"body"`
|
|
Meta map[string]any `json:"meta"`
|
|
SendAtMs int64 `json:"send_at_ms"`
|
|
}
|
|
if err := unmarshalJSON(payload, &m); err != nil {
|
|
return
|
|
}
|
|
key := msgKey(m.From, m.ID)
|
|
c.mu.Lock()
|
|
manual := c.opts.ManualAck
|
|
raw, ok := c.store.Get(key)
|
|
if ok {
|
|
ent := raw.(*dedupEntry)
|
|
if ent.state == dedupAcked {
|
|
c.mu.Unlock()
|
|
go func() { _ = c.sendAckFrame(m.From, m.ID) }()
|
|
return
|
|
}
|
|
if ent.state == dedupDelivered || ent.state == dedupRevoked {
|
|
c.mu.Unlock()
|
|
return
|
|
}
|
|
}
|
|
c.store.Put(key, &dedupEntry{state: dedupDelivered, from: m.From, id: m.ID})
|
|
c.mu.Unlock()
|
|
|
|
msg := Message{ID: m.ID, From: m.From, To: m.To, Body: m.Body, Meta: m.Meta, SendAtMs: m.SendAtMs}
|
|
done := make(chan struct{})
|
|
c.dispatch(func() {
|
|
defer close(done)
|
|
var cbErr error
|
|
if c.onMessage != nil {
|
|
cbErr = c.onMessage(msg)
|
|
}
|
|
if manual {
|
|
return
|
|
}
|
|
if cbErr != nil {
|
|
c.mu.Lock()
|
|
c.store.Delete(key)
|
|
c.mu.Unlock()
|
|
return
|
|
}
|
|
go func() {
|
|
_ = c.sendAckFrame(m.From, m.ID)
|
|
c.mu.Lock()
|
|
if raw, ok := c.store.Get(key); ok {
|
|
if ent, ok := raw.(*dedupEntry); ok {
|
|
ent.state = dedupAcked
|
|
}
|
|
}
|
|
c.mu.Unlock()
|
|
}()
|
|
})
|
|
select {
|
|
case <-done:
|
|
case <-c.ctx.Done():
|
|
}
|
|
}
|
|
|
|
// Ack 手动确认。
|
|
func (c *Client) Ack(msg Message) error {
|
|
if err := c.sendAckFrame(msg.From, msg.ID); err != nil {
|
|
return err
|
|
}
|
|
key := msgKey(msg.From, msg.ID)
|
|
c.mu.Lock()
|
|
c.store.Put(key, &dedupEntry{state: dedupAcked, from: msg.From, id: msg.ID})
|
|
c.mu.Unlock()
|
|
return nil
|
|
}
|
|
|
|
func (c *Client) sendAckFrame(from, id string) error {
|
|
req := map[string]any{
|
|
"v": 1, "type": "ack", "rid": c.nextRID(), "from": from, "id": id,
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
data, err := c.request(ctx, req, true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(data) > 0 {
|
|
var d struct {
|
|
Result string `json:"result"`
|
|
}
|
|
if json.Unmarshal(data, &d) == nil && d.Result != "" && d.Result != "accepted" {
|
|
c.emitRevoked(RevokedEvent{ID: id, From: from, Reason: d.Result})
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *Client) handleReceipt(payload []byte) {
|
|
var r struct {
|
|
ReceiptID string `json:"receipt_id"`
|
|
ID string `json:"id"`
|
|
EndpointID string `json:"endpoint_id"`
|
|
State string `json:"state"`
|
|
Reason string `json:"reason"`
|
|
AtMs int64 `json:"at_ms"`
|
|
}
|
|
if err := unmarshalJSON(payload, &r); err != nil {
|
|
return
|
|
}
|
|
key := receiptKey(r.ReceiptID)
|
|
c.mu.Lock()
|
|
if c.store.Has(key) {
|
|
c.mu.Unlock()
|
|
go func() { _ = c.sendReceiptAck(r.ReceiptID) }()
|
|
return
|
|
}
|
|
c.store.Put(key, struct{}{})
|
|
c.mu.Unlock()
|
|
ev := Receipt{ReceiptID: r.ReceiptID, ID: r.ID, EndpointID: r.EndpointID, State: r.State, Reason: r.Reason, AtMs: r.AtMs}
|
|
c.dispatch(func() {
|
|
if c.onReceipt != nil {
|
|
c.onReceipt(ev)
|
|
}
|
|
})
|
|
go func() { _ = c.sendReceiptAck(r.ReceiptID) }()
|
|
}
|
|
|
|
func (c *Client) sendReceiptAck(receiptID string) error {
|
|
req := map[string]any{"v": 1, "type": "receipt_ack", "rid": c.nextRID(), "receipt_id": receiptID}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
_, err := c.request(ctx, req, true)
|
|
return err
|
|
}
|
|
|
|
func (c *Client) handleRevoked(payload []byte) {
|
|
var r struct {
|
|
ID string `json:"id"`
|
|
From string `json:"from"`
|
|
Reason string `json:"reason"`
|
|
}
|
|
if err := unmarshalJSON(payload, &r); err != nil {
|
|
return
|
|
}
|
|
key := msgKey(r.From, r.ID)
|
|
c.mu.Lock()
|
|
raw, ok := c.store.Get(key)
|
|
if ok {
|
|
ent := raw.(*dedupEntry)
|
|
if ent.state == dedupAcked || ent.state == dedupRevoked {
|
|
c.mu.Unlock()
|
|
return
|
|
}
|
|
}
|
|
c.store.Put(key, &dedupEntry{state: dedupRevoked, from: r.From, id: r.ID})
|
|
c.mu.Unlock()
|
|
c.emitRevoked(RevokedEvent{ID: r.ID, From: r.From, Reason: r.Reason})
|
|
}
|
|
|
|
func (c *Client) emitRevoked(e RevokedEvent) {
|
|
c.dispatch(func() {
|
|
if c.onRevoked != nil {
|
|
c.onRevoked(e)
|
|
}
|
|
})
|
|
}
|
|
|
|
func (c *Client) handlePresence(payload []byte) {
|
|
var p struct {
|
|
ID string `json:"id"`
|
|
Online bool `json:"online"`
|
|
AtMs int64 `json:"at_ms"`
|
|
}
|
|
if err := unmarshalJSON(payload, &p); err != nil {
|
|
return
|
|
}
|
|
c.dispatch(func() {
|
|
if c.onPresence != nil {
|
|
c.onPresence(PresenceEvent{ID: p.ID, Online: p.Online, AtMs: p.AtMs})
|
|
}
|
|
})
|
|
}
|
|
|
|
func (c *Client) handleGroupEvent(payload []byte) {
|
|
var g struct {
|
|
GroupID string `json:"group_id"`
|
|
Event string `json:"event"`
|
|
EndpointID string `json:"endpoint_id"`
|
|
AtMs int64 `json:"at_ms"`
|
|
}
|
|
if err := unmarshalJSON(payload, &g); err != nil {
|
|
return
|
|
}
|
|
c.dispatch(func() {
|
|
if c.onGroupEvent != nil {
|
|
c.onGroupEvent(GroupEvent{GroupID: g.GroupID, Event: g.Event, EndpointID: g.EndpointID, AtMs: g.AtMs})
|
|
}
|
|
})
|
|
}
|
|
|
|
func (c *Client) request(ctx context.Context, frame map[string]any, allowUnready bool) (json.RawMessage, error) {
|
|
if _, ok := ctx.Deadline(); !ok {
|
|
var cancel context.CancelFunc
|
|
ctx, cancel = context.WithTimeout(ctx, 60*time.Second)
|
|
defer cancel()
|
|
}
|
|
c.mu.Lock()
|
|
if c.closed {
|
|
c.mu.Unlock()
|
|
return nil, apiErr(CodeClosed, "已关闭")
|
|
}
|
|
if !allowUnready && !c.handshook {
|
|
c.mu.Unlock()
|
|
return nil, apiErr(CodeNotConnected, "未握手")
|
|
}
|
|
tr := c.transport
|
|
clientCtx := c.ctx
|
|
c.mu.Unlock()
|
|
if tr == nil {
|
|
return nil, apiErr(CodeNotConnected, "未连接")
|
|
}
|
|
rid, _ := frame["rid"].(string)
|
|
if rid == "" {
|
|
rid = c.nextRID()
|
|
frame["rid"] = rid
|
|
}
|
|
payload, err := marshalJSON(frame)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ch := make(chan respFrame, 1)
|
|
c.mu.Lock()
|
|
c.pending[rid] = &pendingReq{rid: rid, ch: ch, isSend: false}
|
|
c.mu.Unlock()
|
|
if err := tr.PublishUp(payload); err != nil {
|
|
c.mu.Lock()
|
|
delete(c.pending, rid)
|
|
c.mu.Unlock()
|
|
return nil, err
|
|
}
|
|
var rf respFrame
|
|
select {
|
|
case <-ctx.Done():
|
|
c.mu.Lock()
|
|
delete(c.pending, rid)
|
|
c.mu.Unlock()
|
|
return nil, ctx.Err()
|
|
case <-clientCtx.Done():
|
|
c.mu.Lock()
|
|
delete(c.pending, rid)
|
|
c.mu.Unlock()
|
|
return nil, apiErr(CodeNotConnected, "未连接")
|
|
case rf = <-ch:
|
|
}
|
|
if !rf.OK {
|
|
code, msg := CodeBadRequest, "请求失败"
|
|
if rf.Error != nil {
|
|
code, msg = rf.Error.Code, rf.Error.Message
|
|
}
|
|
return nil, apiErr(code, msg)
|
|
}
|
|
return rf.Data, nil
|
|
}
|
|
|
|
// bodyDecodedLen 按解码后字节计正文大小。
|
|
func bodyDecodedLen(b Body) (int, error) {
|
|
switch b.Enc {
|
|
case "utf8", "":
|
|
return len([]byte(b.Data)), nil
|
|
case "base64":
|
|
raw, err := base64.StdEncoding.DecodeString(b.Data)
|
|
if err != nil {
|
|
return 0, apiErr(CodeBadRequest, "base64 正文无效")
|
|
}
|
|
return len(raw), nil
|
|
default:
|
|
return 0, apiErr(CodeBadRequest, "未知 enc")
|
|
}
|
|
}
|