首次推送
This commit is contained in:
@@ -0,0 +1,362 @@
|
||||
package teamspeak
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/honeybbq/teamspeak-go/crypto"
|
||||
"github.com/honeybbq/teamspeak-go/discovery"
|
||||
"github.com/honeybbq/teamspeak-go/transport"
|
||||
)
|
||||
|
||||
var errAlreadyConnectingOrConnected = errors.New("already connecting or connected")
|
||||
|
||||
// ClientStatus represents the current connection state of the client.
|
||||
type ClientStatus int
|
||||
|
||||
const (
|
||||
StatusDisconnected ClientStatus = iota
|
||||
StatusConnecting
|
||||
StatusConnected
|
||||
)
|
||||
|
||||
// AddrResolver resolves a TeamSpeak server address to host:port endpoints.
|
||||
// Implementations may replace the default chain (SRV, TSDNS, direct).
|
||||
type AddrResolver interface {
|
||||
Resolve(ctx context.Context, addr string) ([]discovery.ResolvedAddr, error)
|
||||
}
|
||||
|
||||
// CommandMiddleware wraps the final command sender; it may alter or drop commands.
|
||||
type CommandMiddleware func(next func(string) error) func(string) error
|
||||
|
||||
// EventMiddleware wraps event dispatch; it may observe or replace notifications.
|
||||
type EventMiddleware func(next func(any)) func(any)
|
||||
|
||||
type clientInitOptions struct {
|
||||
serverPassword string
|
||||
defaultChannel string
|
||||
defaultChannelPassword string
|
||||
}
|
||||
|
||||
// Client is the TeamSpeak 3 client.
|
||||
type Client struct {
|
||||
resolver AddrResolver
|
||||
finalCmdHandler func(string) error
|
||||
crypt *crypto.Crypt
|
||||
connectedChan chan struct{}
|
||||
connectedErr error // handshake error stored before closing connectedChan
|
||||
evtQueue []any
|
||||
evtQueueMu sync.Mutex
|
||||
evtCond *sync.Cond
|
||||
evtDone chan struct{}
|
||||
ftTrack *fileTransferTracker
|
||||
logger *slog.Logger
|
||||
handler *transport.PacketHandler
|
||||
cmdTrack *commandTracker
|
||||
throttle *commandThrottle
|
||||
clients map[uint16]ClientInfo
|
||||
finalEvtHandler func(any)
|
||||
addr string
|
||||
nickname string
|
||||
clientInitOptions clientInitOptions
|
||||
textMsgHandlers []func(TextMessage)
|
||||
cmdMiddlewares []CommandMiddleware
|
||||
eventMiddlewares []EventMiddleware
|
||||
clientEnterHandlers []func(ClientInfo)
|
||||
clientLeaveHandlers []func(ClientLeftViewEvent)
|
||||
clientMoveHandlers []func(ClientMovedEvent)
|
||||
connectedHandlers []func()
|
||||
disconnectedHandlers []func(error)
|
||||
pokedHandlers []func(PokeEvent)
|
||||
kickedHandlers []func(string)
|
||||
voiceDataHandlers []func(VoiceDataEvent)
|
||||
status ClientStatus
|
||||
mu sync.Mutex
|
||||
clid uint16
|
||||
}
|
||||
|
||||
// NewClient creates a new TeamSpeak 3 client.
|
||||
func NewClient(identity *crypto.Identity, addr string, nickname string, options ...ClientOption) *Client {
|
||||
crypt := crypto.NewCrypt(identity)
|
||||
|
||||
c := &Client{
|
||||
crypt: crypt,
|
||||
status: StatusDisconnected,
|
||||
logger: slog.Default(),
|
||||
addr: addr,
|
||||
nickname: nickname,
|
||||
clients: make(map[uint16]ClientInfo),
|
||||
throttle: newCommandThrottle(),
|
||||
cmdTrack: newCommandTracker(),
|
||||
ftTrack: newFileTransferTracker(),
|
||||
connectedChan: make(chan struct{}),
|
||||
evtDone: make(chan struct{}),
|
||||
}
|
||||
|
||||
c.evtCond = sync.NewCond(&c.evtQueueMu)
|
||||
c.startEventLoop()
|
||||
|
||||
for _, opt := range options {
|
||||
opt(c)
|
||||
}
|
||||
|
||||
c.handler = transport.NewPacketHandler(c.crypt, c.logger)
|
||||
if c.resolver == nil {
|
||||
c.resolver = discovery.NewResolver(c.logger)
|
||||
}
|
||||
c.handler.OnPacket = c.handlePacket
|
||||
c.handler.OnClosed = c.handleConnectionClosed
|
||||
|
||||
c.rebuildMiddlewareChains()
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
type ClientOption func(*Client)
|
||||
|
||||
func WithLogger(logger *slog.Logger) ClientOption {
|
||||
return func(c *Client) {
|
||||
if logger != nil {
|
||||
c.logger = logger
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithResolver sets a custom address resolver used by Connect.
|
||||
func WithResolver(r AddrResolver) ClientOption {
|
||||
return func(c *Client) {
|
||||
c.resolver = r
|
||||
}
|
||||
}
|
||||
|
||||
func WithCommandMiddleware(mw ...CommandMiddleware) ClientOption {
|
||||
return func(c *Client) {
|
||||
c.cmdMiddlewares = append(c.cmdMiddlewares, mw...)
|
||||
}
|
||||
}
|
||||
|
||||
func WithEventMiddleware(mw ...EventMiddleware) ClientOption {
|
||||
return func(c *Client) {
|
||||
c.eventMiddlewares = append(c.eventMiddlewares, mw...)
|
||||
}
|
||||
}
|
||||
|
||||
// WithServerPassword configures the server password sent during clientinit.
|
||||
func WithServerPassword(password string) ClientOption {
|
||||
return func(c *Client) {
|
||||
c.clientInitOptions.serverPassword = password
|
||||
}
|
||||
}
|
||||
|
||||
// WithDefaultChannel configures the default channel requested during clientinit.
|
||||
func WithDefaultChannel(channel string) ClientOption {
|
||||
return func(c *Client) {
|
||||
c.clientInitOptions.defaultChannel = channel
|
||||
}
|
||||
}
|
||||
|
||||
// WithDefaultChannelPassword configures the password for the default channel.
|
||||
func WithDefaultChannelPassword(password string) ClientOption {
|
||||
return func(c *Client) {
|
||||
c.clientInitOptions.defaultChannelPassword = password
|
||||
}
|
||||
}
|
||||
|
||||
// Connect starts the UDP session and handshake to the server.
|
||||
func (c *Client) Connect() error {
|
||||
c.mu.Lock()
|
||||
if c.status != StatusDisconnected {
|
||||
c.mu.Unlock()
|
||||
|
||||
return errAlreadyConnectingOrConnected
|
||||
}
|
||||
|
||||
finalAddr := c.resetForConnectLocked()
|
||||
|
||||
c.status = StatusConnecting
|
||||
c.mu.Unlock()
|
||||
|
||||
targetAddr, source := c.resolveConnectTarget(finalAddr)
|
||||
c.logger.Info("connecting to server", slog.String("address", targetAddr), slog.String("source", source))
|
||||
|
||||
return c.handler.Connect(targetAddr)
|
||||
}
|
||||
|
||||
// Disconnect gracefully closes the connection.
|
||||
func (c *Client) Disconnect() error {
|
||||
c.mu.Lock()
|
||||
oldStatus := c.status
|
||||
if oldStatus == StatusDisconnected {
|
||||
c.mu.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
connectErr := c.connectedErr
|
||||
c.status = StatusDisconnected
|
||||
handlers := c.disconnectedHandlers
|
||||
c.mu.Unlock()
|
||||
|
||||
c.logger.Info("disconnecting from server")
|
||||
|
||||
if oldStatus == StatusConnected {
|
||||
_ = c.ExecCommand("clientdisconnect reasonmsg=Shutdown", 1*time.Second)
|
||||
}
|
||||
|
||||
err := c.handler.Close()
|
||||
|
||||
// When disconnecting during the handshake phase, pass the stored handshake
|
||||
// error (e.g. wrong password, banned) so callbacks receive a useful message.
|
||||
handlerErr := error(nil)
|
||||
if oldStatus == StatusConnecting && connectErr != nil {
|
||||
handlerErr = connectErr
|
||||
}
|
||||
// Invoke disconnected handlers here; handleConnectionClosed skips them once
|
||||
// status is already Disconnected to avoid duplicate callbacks.
|
||||
for _, h := range handlers {
|
||||
go h(handlerErr)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Client) resetForConnectLocked() string {
|
||||
if c.handler != nil {
|
||||
_ = c.handler.Close()
|
||||
}
|
||||
identity := c.crypt.Identity
|
||||
c.crypt = crypto.NewCrypt(identity)
|
||||
c.handler = transport.NewPacketHandler(c.crypt, c.logger)
|
||||
c.handler.OnPacket = c.handlePacket
|
||||
c.handler.OnClosed = c.handleConnectionClosed
|
||||
c.connectedChan = make(chan struct{})
|
||||
// Note: connectedErr is NOT reset here so that handleConnectionClosed
|
||||
// can still access the handshake error when the server closes the
|
||||
// connection after a failed clientinit (e.g. wrong password).
|
||||
c.cmdTrack.reset()
|
||||
c.ftTrack.reset()
|
||||
c.clients = make(map[uint16]ClientInfo)
|
||||
c.clid = 0
|
||||
|
||||
finalAddr := c.addr
|
||||
if !strings.Contains(finalAddr, ":") {
|
||||
c.logger.Debug("no port specified, using default port 9987")
|
||||
finalAddr += ":9987"
|
||||
}
|
||||
|
||||
return finalAddr
|
||||
}
|
||||
|
||||
func (c *Client) resolveConnectTarget(fallbackAddr string) (string, string) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
resolved, err := c.resolver.Resolve(ctx, c.addr)
|
||||
if err != nil {
|
||||
c.logger.Warn("address resolution failed, falling back to direct", slog.Any("error", err))
|
||||
|
||||
return fallbackAddr, "Fallback"
|
||||
}
|
||||
|
||||
return resolved[0].Addr, resolved[0].Source
|
||||
}
|
||||
|
||||
func (c *Client) handleConnectionClosed(err error) {
|
||||
c.mu.Lock()
|
||||
if c.status == StatusDisconnected {
|
||||
c.mu.Unlock()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
wasConnecting := c.status == StatusConnecting
|
||||
connectErr := c.connectedErr
|
||||
c.status = StatusDisconnected
|
||||
handlers := c.disconnectedHandlers
|
||||
c.mu.Unlock()
|
||||
|
||||
// If we were still in the handshake phase, unblock WaitConnected so the
|
||||
// caller can observe the failure instead of blocking forever.
|
||||
if wasConnecting {
|
||||
c.signalConnected(err)
|
||||
}
|
||||
|
||||
// When the connection drops after a handshake error (e.g. wrong password),
|
||||
// the transport-level err is typically nil. Use the stored handshake error
|
||||
// so the disconnected handlers receive a meaningful message.
|
||||
handlerErr := err
|
||||
if handlerErr == nil && connectErr != nil {
|
||||
handlerErr = connectErr
|
||||
}
|
||||
|
||||
for _, h := range handlers {
|
||||
go h(handlerErr)
|
||||
}
|
||||
}
|
||||
|
||||
// signalConnected closes connectedChan (if not already closed) and stores err.
|
||||
// Safe to call from any goroutine; acquires c.mu internally.
|
||||
// A non-nil error never overwrites an existing non-nil error, so the original
|
||||
// handshake error (e.g. wrong password) is preserved even if a later transport
|
||||
// close also calls signalConnected with a nil or different error.
|
||||
func (c *Client) signalConnected(err error) {
|
||||
c.mu.Lock()
|
||||
c.logger.Info("signalConnected called",
|
||||
slog.Any("error", err),
|
||||
slog.Any("existingErr", c.connectedErr),
|
||||
slog.Int("status", int(c.status)))
|
||||
if err != nil || c.connectedErr == nil {
|
||||
c.connectedErr = err
|
||||
}
|
||||
select {
|
||||
case <-c.connectedChan:
|
||||
c.logger.Info("signalConnected: channel already closed")
|
||||
default:
|
||||
close(c.connectedChan)
|
||||
c.logger.Info("signalConnected: channel closed successfully")
|
||||
}
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
// startEventLoop 启动事件消费协程,保证所有事件在同一个协程中按顺序处理。
|
||||
// 消费协程的生命周期与 Client 相同(NewClient 时启动),
|
||||
// 通过 c.evtDone channel 控制退出。
|
||||
func (c *Client) startEventLoop() {
|
||||
go func() {
|
||||
for {
|
||||
// 等待队列非空或 done 信号
|
||||
c.evtQueueMu.Lock()
|
||||
for len(c.evtQueue) == 0 {
|
||||
select {
|
||||
case <-c.evtDone:
|
||||
c.evtQueueMu.Unlock()
|
||||
return
|
||||
default:
|
||||
}
|
||||
c.evtCond.Wait()
|
||||
}
|
||||
// 取出所有待处理事件
|
||||
events := make([]any, len(c.evtQueue))
|
||||
copy(events, c.evtQueue)
|
||||
c.evtQueue = nil
|
||||
c.evtQueueMu.Unlock()
|
||||
|
||||
// 在无锁状态下逐个分发事件
|
||||
for _, evt := range events {
|
||||
c.dispatchEvent(evt)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// notifyEvent 将事件放入队列,由事件消费协程顺序处理。
|
||||
// 线程安全,可从任何 goroutine 调用。
|
||||
func (c *Client) notifyEvent(evt any) {
|
||||
c.evtQueueMu.Lock()
|
||||
c.evtQueue = append(c.evtQueue, evt)
|
||||
c.evtQueueMu.Unlock()
|
||||
c.evtCond.Signal()
|
||||
}
|
||||
Reference in New Issue
Block a user