mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-05 15:26:36 +08:00
feat(message-gateway): add channel types, registry, and pairing codes
This commit is contained in:
@@ -0,0 +1,21 @@
|
|||||||
|
// Copyright 2026 Arctel.net
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package message_gateway
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
// Handler processes one inbound message.
|
||||||
|
type Handler func(ctx context.Context, msg InboundMessage) error
|
||||||
|
|
||||||
|
// Factory constructs a Channel from decrypted config.
|
||||||
|
type Factory func(cfg ChannelConfig, onInbound Handler) (Channel, error)
|
||||||
|
|
||||||
|
// Channel is one connected messaging adapter.
|
||||||
|
type Channel interface {
|
||||||
|
Type() string
|
||||||
|
Connect(ctx context.Context) error
|
||||||
|
Disconnect(ctx context.Context) error
|
||||||
|
Send(ctx context.Context, to Recipient, msg OutboundMessage) error
|
||||||
|
Capabilities() Capability
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
// Copyright 2026 Arctel.net
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package message_gateway
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CodeAlphabet excludes easily confused runes 0/O/1/I.
|
||||||
|
const CodeAlphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"
|
||||||
|
|
||||||
|
// CodeLength is the raw pairing code size.
|
||||||
|
const CodeLength = 8
|
||||||
|
|
||||||
|
// GenerateCode returns an 8-character pairing code.
|
||||||
|
func GenerateCode() (string, error) {
|
||||||
|
buf := make([]byte, CodeLength)
|
||||||
|
if _, err := rand.Read(buf); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
out := make([]byte, CodeLength)
|
||||||
|
for i, b := range buf {
|
||||||
|
out[i] = CodeAlphabet[int(b)%len(CodeAlphabet)]
|
||||||
|
}
|
||||||
|
return string(out), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NormalizeCode strips separators and uppercases.
|
||||||
|
func NormalizeCode(s string) string {
|
||||||
|
var b strings.Builder
|
||||||
|
for _, r := range s {
|
||||||
|
if r == '-' || unicode.IsSpace(r) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
b.WriteRune(unicode.ToUpper(r))
|
||||||
|
}
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// FormatCode renders ABCD-EFGH.
|
||||||
|
func FormatCode(s string) string {
|
||||||
|
s = NormalizeCode(s)
|
||||||
|
if len(s) != CodeLength {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return s[:4] + "-" + s[4:]
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
// Copyright 2026 Arctel.net
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package message_gateway
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGenerateCode_AlphabetAndLength(t *testing.T) {
|
||||||
|
code, err := GenerateCode()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(code) != 8 {
|
||||||
|
t.Fatalf("len=%d", len(code))
|
||||||
|
}
|
||||||
|
for _, r := range code {
|
||||||
|
if !strings.ContainsRune(CodeAlphabet, r) {
|
||||||
|
t.Fatalf("bad rune %q", r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeAndFormat(t *testing.T) {
|
||||||
|
if got := NormalizeCode("ab-cd-ef-gh"); got != "ABCDEFGH" {
|
||||||
|
t.Fatalf("got %q", got)
|
||||||
|
}
|
||||||
|
if got := FormatCode("ABCDEFGH"); got != "ABCD-EFGH" {
|
||||||
|
t.Fatalf("got %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
// Copyright 2026 Arctel.net
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package message_gateway
|
||||||
|
|
||||||
|
import "sync"
|
||||||
|
|
||||||
|
var (
|
||||||
|
factoriesMu sync.RWMutex
|
||||||
|
factories = map[string]Factory{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Register stores a channel factory under typ.
|
||||||
|
func Register(typ string, fn Factory) {
|
||||||
|
factoriesMu.Lock()
|
||||||
|
defer factoriesMu.Unlock()
|
||||||
|
factories[typ] = fn
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lookup returns a previously registered factory.
|
||||||
|
func Lookup(typ string) (Factory, bool) {
|
||||||
|
factoriesMu.RLock()
|
||||||
|
defer factoriesMu.RUnlock()
|
||||||
|
fn, ok := factories[typ]
|
||||||
|
return fn, ok
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
// Copyright 2026 Arctel.net
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package message_gateway
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type stubChannel struct{}
|
||||||
|
|
||||||
|
func (stubChannel) Type() string { return "stub" }
|
||||||
|
func (stubChannel) Connect(context.Context) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (stubChannel) Disconnect(context.Context) error { return nil }
|
||||||
|
func (stubChannel) Send(context.Context, Recipient, OutboundMessage) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (stubChannel) Capabilities() Capability { return Capability{Text: true} }
|
||||||
|
|
||||||
|
func TestRegisterLookup(t *testing.T) {
|
||||||
|
Register("stub", func(ChannelConfig, Handler) (Channel, error) {
|
||||||
|
return stubChannel{}, nil
|
||||||
|
})
|
||||||
|
fn, ok := Lookup("stub")
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("expected factory")
|
||||||
|
}
|
||||||
|
ch, err := fn(ChannelConfig{}, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if ch.Type() != "stub" {
|
||||||
|
t.Fatalf("type=%s", ch.Type())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
// Copyright 2026 Arctel.net
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package message_gateway
|
||||||
|
|
||||||
|
// ChannelTypeTelegram is the Telegram private-chat adapter type.
|
||||||
|
const ChannelTypeTelegram = "telegram"
|
||||||
|
|
||||||
|
// ChannelTypeQQ is the official QQ Bot C2C adapter type.
|
||||||
|
const ChannelTypeQQ = "qq"
|
||||||
|
|
||||||
|
// Capability describes what an adapter can send and receive.
|
||||||
|
type Capability struct {
|
||||||
|
Text bool
|
||||||
|
Image bool
|
||||||
|
File bool
|
||||||
|
Reply bool
|
||||||
|
Group bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChannelConfig is the decrypted runtime config passed to a factory.
|
||||||
|
type ChannelConfig struct {
|
||||||
|
ID uint64
|
||||||
|
Type string
|
||||||
|
Name string
|
||||||
|
Credentials map[string]string
|
||||||
|
Extra map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recipient is the outbound destination on a platform.
|
||||||
|
type Recipient struct {
|
||||||
|
ChatID string
|
||||||
|
PlatformUserID string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attachment is a downloaded inbound file sitting on local disk.
|
||||||
|
type Attachment struct {
|
||||||
|
Path string
|
||||||
|
FileName string
|
||||||
|
MIME string
|
||||||
|
Error string
|
||||||
|
}
|
||||||
|
|
||||||
|
// InboundMessage is a normalized private-chat message.
|
||||||
|
type InboundMessage struct {
|
||||||
|
ChannelID uint64
|
||||||
|
ChannelType string
|
||||||
|
PlatformUserID string
|
||||||
|
ChatID string
|
||||||
|
MessageID string
|
||||||
|
Text string
|
||||||
|
Attachments []Attachment
|
||||||
|
BindingUserID *uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
// OutboundMessage is a reply or probe send.
|
||||||
|
type OutboundMessage struct {
|
||||||
|
Text string
|
||||||
|
ReplyToID string
|
||||||
|
Attachments []Attachment
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user