mirror of
https://github.com/navidrome/navidrome.git
synced 2026-05-03 06:51:16 +00:00
refactor: Discord RPC struct to encapsulate WebSocket logic
Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
parent
e111c5832f
commit
a2ace6e84e
@ -7,4 +7,15 @@ require (
|
|||||||
github.com/navidrome/navidrome/plugins/pdk/go v0.0.0
|
github.com/navidrome/navidrome/plugins/pdk/go v0.0.0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/kr/pretty v0.3.1 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
github.com/rogpeppe/go-internal v1.13.1 // indirect
|
||||||
|
github.com/stretchr/objx v0.5.2 // indirect
|
||||||
|
github.com/stretchr/testify v1.11.1 // indirect
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
||||||
|
|
||||||
replace github.com/navidrome/navidrome/plugins/pdk/go => ../../pdk/go
|
replace github.com/navidrome/navidrome/plugins/pdk/go => ../../pdk/go
|
||||||
|
|||||||
@ -1,2 +1,24 @@
|
|||||||
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/extism/go-pdk v1.1.3 h1:hfViMPWrqjN6u67cIYRALZTZLk/enSPpNKa+rZ9X2SQ=
|
github.com/extism/go-pdk v1.1.3 h1:hfViMPWrqjN6u67cIYRALZTZLk/enSPpNKa+rZ9X2SQ=
|
||||||
github.com/extism/go-pdk v1.1.3/go.mod h1:Gz+LIU/YCKnKXhgge8yo5Yu1F/lbv7KtKFkiCSzW/P4=
|
github.com/extism/go-pdk v1.1.3/go.mod h1:Gz+LIU/YCKnKXhgge8yo5Yu1F/lbv7KtKFkiCSzW/P4=
|
||||||
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||||
|
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||||
|
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||||
|
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||||
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|||||||
@ -28,26 +28,19 @@ const (
|
|||||||
usersKey = "users"
|
usersKey = "users"
|
||||||
)
|
)
|
||||||
|
|
||||||
// discordPlugin implements the scrobbler, scheduler, and websocket interfaces.
|
// discordPlugin implements the scrobbler and scheduler interfaces.
|
||||||
type discordPlugin struct{}
|
type discordPlugin struct{}
|
||||||
|
|
||||||
|
// rpc handles Discord gateway communication (via websockets).
|
||||||
|
var rpc = &discordRPC{}
|
||||||
|
|
||||||
// init registers the plugin capabilities
|
// init registers the plugin capabilities
|
||||||
func init() {
|
func init() {
|
||||||
scrobbler.Register(&discordPlugin{})
|
scrobbler.Register(&discordPlugin{})
|
||||||
scheduler.Register(&discordPlugin{})
|
scheduler.Register(&discordPlugin{})
|
||||||
websocket.Register(&discordPlugin{})
|
websocket.Register(rpc)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure discordPlugin implements the required provider interfaces
|
|
||||||
var (
|
|
||||||
_ scrobbler.Scrobbler = (*discordPlugin)(nil)
|
|
||||||
_ scheduler.CallbackProvider = (*discordPlugin)(nil)
|
|
||||||
_ websocket.TextMessageProvider = (*discordPlugin)(nil)
|
|
||||||
_ websocket.BinaryMessageProvider = (*discordPlugin)(nil)
|
|
||||||
_ websocket.ErrorProvider = (*discordPlugin)(nil)
|
|
||||||
_ websocket.CloseProvider = (*discordPlugin)(nil)
|
|
||||||
)
|
|
||||||
|
|
||||||
// getConfig loads the plugin configuration.
|
// getConfig loads the plugin configuration.
|
||||||
func getConfig() (clientID string, users map[string]string, err error) {
|
func getConfig() (clientID string, users map[string]string, err error) {
|
||||||
clientID, ok := pdk.GetConfig(clientIDKey)
|
clientID, ok := pdk.GetConfig(clientIDKey)
|
||||||
@ -121,7 +114,7 @@ func (p *discordPlugin) NowPlaying(input scrobbler.NowPlayingRequest) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Connect to Discord
|
// Connect to Discord
|
||||||
if err := connect(input.Username, userToken); err != nil {
|
if err := rpc.connect(input.Username, userToken); err != nil {
|
||||||
return fmt.Errorf("%w: failed to connect to Discord: %v", scrobbler.ScrobblerErrorRetryLater, err)
|
return fmt.Errorf("%w: failed to connect to Discord: %v", scrobbler.ScrobblerErrorRetryLater, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -134,7 +127,7 @@ func (p *discordPlugin) NowPlaying(input scrobbler.NowPlayingRequest) error {
|
|||||||
endTime := startTime + int64(input.Track.Duration)*1000
|
endTime := startTime + int64(input.Track.Duration)*1000
|
||||||
|
|
||||||
// Send activity update
|
// Send activity update
|
||||||
if err := sendActivity(clientID, input.Username, userToken, activity{
|
if err := rpc.sendActivity(clientID, input.Username, userToken, activity{
|
||||||
Application: clientID,
|
Application: clientID,
|
||||||
Name: "Navidrome",
|
Name: "Navidrome",
|
||||||
Type: 2, // Listening
|
Type: 2, // Listening
|
||||||
@ -180,14 +173,14 @@ func (p *discordPlugin) OnCallback(input scheduler.SchedulerCallbackRequest) err
|
|||||||
switch input.Payload {
|
switch input.Payload {
|
||||||
case payloadHeartbeat:
|
case payloadHeartbeat:
|
||||||
// Heartbeat callback - scheduleId is the username
|
// Heartbeat callback - scheduleId is the username
|
||||||
if err := handleHeartbeatCallback(input.ScheduleID); err != nil {
|
if err := rpc.handleHeartbeatCallback(input.ScheduleID); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
case payloadClearActivity:
|
case payloadClearActivity:
|
||||||
// Clear activity callback - scheduleId is "username-clear"
|
// Clear activity callback - scheduleId is "username-clear"
|
||||||
username := strings.TrimSuffix(input.ScheduleID, "-clear")
|
username := strings.TrimSuffix(input.ScheduleID, "-clear")
|
||||||
if err := handleClearActivityCallback(username); err != nil {
|
if err := rpc.handleClearActivityCallback(username); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -198,31 +191,4 @@ func (p *discordPlugin) OnCallback(input scheduler.SchedulerCallbackRequest) err
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// WebSocket Callback Implementation
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
// OnTextMessage handles incoming WebSocket text messages.
|
|
||||||
func (p *discordPlugin) OnTextMessage(input websocket.OnTextMessageRequest) error {
|
|
||||||
return handleWebSocketMessage(input.ConnectionID, input.Message)
|
|
||||||
}
|
|
||||||
|
|
||||||
// OnBinaryMessage handles incoming WebSocket binary messages.
|
|
||||||
func (p *discordPlugin) OnBinaryMessage(input websocket.OnBinaryMessageRequest) error {
|
|
||||||
pdk.Log(pdk.LogDebug, fmt.Sprintf("Received unexpected binary message for connection '%s'", input.ConnectionID))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// OnError handles WebSocket errors.
|
|
||||||
func (p *discordPlugin) OnError(input websocket.OnErrorRequest) error {
|
|
||||||
pdk.Log(pdk.LogWarn, fmt.Sprintf("WebSocket error for connection '%s': %s", input.ConnectionID, input.Error))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// OnClose handles WebSocket connection closure.
|
|
||||||
func (p *discordPlugin) OnClose(input websocket.OnCloseRequest) error {
|
|
||||||
pdk.Log(pdk.LogInfo, fmt.Sprintf("WebSocket connection '%s' closed with code %d: %s", input.ConnectionID, input.Code, input.Reason))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {}
|
func main() {}
|
||||||
|
|||||||
@ -1,17 +1,18 @@
|
|||||||
// Discord Rich Presence Plugin - RPC Communication
|
// Discord Rich Presence Plugin - RPC Communication
|
||||||
//
|
//
|
||||||
// This file handles all Discord gateway communication including WebSocket connections,
|
// This file handles all Discord gateway communication including WebSocket connections,
|
||||||
// presence updates, and heartbeat management.
|
// presence updates, and heartbeat management. The discordRPC struct implements WebSocket
|
||||||
|
// callback interfaces and encapsulates all Discord communication logic.
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/extism/go-pdk"
|
"github.com/extism/go-pdk"
|
||||||
host "github.com/navidrome/navidrome/plugins/pdk/go/host"
|
host "github.com/navidrome/navidrome/plugins/pdk/go/host"
|
||||||
|
"github.com/navidrome/navidrome/plugins/pdk/go/websocket"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Discord WebSocket Gateway constants
|
// Discord WebSocket Gateway constants
|
||||||
@ -32,6 +33,36 @@ const (
|
|||||||
payloadClearActivity = "clear-activity"
|
payloadClearActivity = "clear-activity"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// discordRPC handles Discord gateway communication and implements WebSocket callbacks.
|
||||||
|
type discordRPC struct{}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// WebSocket Callback Implementation
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// OnTextMessage handles incoming WebSocket text messages.
|
||||||
|
func (r *discordRPC) OnTextMessage(input websocket.OnTextMessageRequest) error {
|
||||||
|
return r.handleWebSocketMessage(input.ConnectionID, input.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnBinaryMessage handles incoming WebSocket binary messages.
|
||||||
|
func (r *discordRPC) OnBinaryMessage(input websocket.OnBinaryMessageRequest) error {
|
||||||
|
pdk.Log(pdk.LogDebug, fmt.Sprintf("Received unexpected binary message for connection '%s'", input.ConnectionID))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnError handles WebSocket errors.
|
||||||
|
func (r *discordRPC) OnError(input websocket.OnErrorRequest) error {
|
||||||
|
pdk.Log(pdk.LogWarn, fmt.Sprintf("WebSocket error for connection '%s': %s", input.ConnectionID, input.Error))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnClose handles WebSocket connection closure.
|
||||||
|
func (r *discordRPC) OnClose(input websocket.OnCloseRequest) error {
|
||||||
|
pdk.Log(pdk.LogInfo, fmt.Sprintf("WebSocket connection '%s' closed with code %d: %s", input.ConnectionID, input.Code, input.Reason))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// activity represents a Discord activity.
|
// activity represents a Discord activity.
|
||||||
type activity struct {
|
type activity struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@ -74,13 +105,17 @@ type identifyProperties struct {
|
|||||||
Device string `json:"device"`
|
Device string `json:"device"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Image Processing
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
// processImage processes an image URL for Discord, with fallback to default image.
|
// processImage processes an image URL for Discord, with fallback to default image.
|
||||||
func processImage(imageURL, clientID, token string, isDefaultImage bool) (string, error) {
|
func (r *discordRPC) processImage(imageURL, clientID, token string, isDefaultImage bool) (string, error) {
|
||||||
if imageURL == "" {
|
if imageURL == "" {
|
||||||
if isDefaultImage {
|
if isDefaultImage {
|
||||||
return "", fmt.Errorf("default image URL is empty")
|
return "", fmt.Errorf("default image URL is empty")
|
||||||
}
|
}
|
||||||
return processImage(defaultImage, clientID, token, true)
|
return r.processImage(defaultImage, clientID, token, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.HasPrefix(imageURL, "mp:") {
|
if strings.HasPrefix(imageURL, "mp:") {
|
||||||
@ -107,7 +142,7 @@ func processImage(imageURL, clientID, token string, isDefaultImage bool) (string
|
|||||||
if isDefaultImage {
|
if isDefaultImage {
|
||||||
return "", fmt.Errorf("failed to process default image: HTTP %d", resp.Status())
|
return "", fmt.Errorf("failed to process default image: HTTP %d", resp.Status())
|
||||||
}
|
}
|
||||||
return processImage(defaultImage, clientID, token, true)
|
return r.processImage(defaultImage, clientID, token, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
var data []map[string]string
|
var data []map[string]string
|
||||||
@ -115,14 +150,14 @@ func processImage(imageURL, clientID, token string, isDefaultImage bool) (string
|
|||||||
if isDefaultImage {
|
if isDefaultImage {
|
||||||
return "", fmt.Errorf("failed to unmarshal default image response: %w", err)
|
return "", fmt.Errorf("failed to unmarshal default image response: %w", err)
|
||||||
}
|
}
|
||||||
return processImage(defaultImage, clientID, token, true)
|
return r.processImage(defaultImage, clientID, token, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(data) == 0 {
|
if len(data) == 0 {
|
||||||
if isDefaultImage {
|
if isDefaultImage {
|
||||||
return "", fmt.Errorf("no data returned for default image")
|
return "", fmt.Errorf("no data returned for default image")
|
||||||
}
|
}
|
||||||
return processImage(defaultImage, clientID, token, true)
|
return r.processImage(defaultImage, clientID, token, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
image := data[0]["external_asset_path"]
|
image := data[0]["external_asset_path"]
|
||||||
@ -130,7 +165,7 @@ func processImage(imageURL, clientID, token string, isDefaultImage bool) (string
|
|||||||
if isDefaultImage {
|
if isDefaultImage {
|
||||||
return "", fmt.Errorf("empty external_asset_path for default image")
|
return "", fmt.Errorf("empty external_asset_path for default image")
|
||||||
}
|
}
|
||||||
return processImage(defaultImage, clientID, token, true)
|
return r.processImage(defaultImage, clientID, token, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
processedImage := fmt.Sprintf("mp:%s", image)
|
processedImage := fmt.Sprintf("mp:%s", image)
|
||||||
@ -147,11 +182,15 @@ func processImage(imageURL, clientID, token string, isDefaultImage bool) (string
|
|||||||
return processedImage, nil
|
return processedImage, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Activity Management
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
// sendActivity sends an activity update to Discord.
|
// sendActivity sends an activity update to Discord.
|
||||||
func sendActivity(clientID, username, token string, data activity) error {
|
func (r *discordRPC) sendActivity(clientID, username, token string, data activity) error {
|
||||||
pdk.Log(pdk.LogInfo, fmt.Sprintf("Sending activity for user %s: %s - %s", username, data.Details, data.State))
|
pdk.Log(pdk.LogInfo, fmt.Sprintf("Sending activity for user %s: %s - %s", username, data.Details, data.State))
|
||||||
|
|
||||||
processedImage, err := processImage(data.Assets.LargeImage, clientID, token, false)
|
processedImage, err := r.processImage(data.Assets.LargeImage, clientID, token, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
pdk.Log(pdk.LogWarn, fmt.Sprintf("Failed to process image for user %s, continuing without image: %v", username, err))
|
pdk.Log(pdk.LogWarn, fmt.Sprintf("Failed to process image for user %s, continuing without image: %v", username, err))
|
||||||
data.Assets.LargeImage = ""
|
data.Assets.LargeImage = ""
|
||||||
@ -164,17 +203,21 @@ func sendActivity(clientID, username, token string, data activity) error {
|
|||||||
Status: "dnd",
|
Status: "dnd",
|
||||||
Afk: false,
|
Afk: false,
|
||||||
}
|
}
|
||||||
return sendMessage(username, presenceOpCode, presence)
|
return r.sendMessage(username, presenceOpCode, presence)
|
||||||
}
|
}
|
||||||
|
|
||||||
// clearActivity clears the Discord activity for a user.
|
// clearActivity clears the Discord activity for a user.
|
||||||
func clearActivity(username string) error {
|
func (r *discordRPC) clearActivity(username string) error {
|
||||||
pdk.Log(pdk.LogInfo, fmt.Sprintf("Clearing activity for user %s", username))
|
pdk.Log(pdk.LogInfo, fmt.Sprintf("Clearing activity for user %s", username))
|
||||||
return sendMessage(username, presenceOpCode, presencePayload{})
|
return r.sendMessage(username, presenceOpCode, presencePayload{})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Low-level Communication
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
// sendMessage sends a message over the WebSocket connection.
|
// sendMessage sends a message over the WebSocket connection.
|
||||||
func sendMessage(username string, opCode int, payload any) error {
|
func (r *discordRPC) sendMessage(username string, opCode int, payload any) error {
|
||||||
message := map[string]any{
|
message := map[string]any{
|
||||||
"op": opCode,
|
"op": opCode,
|
||||||
"d": payload,
|
"d": payload,
|
||||||
@ -192,7 +235,7 @@ func sendMessage(username string, opCode int, payload any) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// getDiscordGateway retrieves the Discord gateway URL.
|
// getDiscordGateway retrieves the Discord gateway URL.
|
||||||
func getDiscordGateway() (string, error) {
|
func (r *discordRPC) getDiscordGateway() (string, error) {
|
||||||
req := pdk.NewHTTPRequest(pdk.MethodGet, "https://discord.com/api/gateway")
|
req := pdk.NewHTTPRequest(pdk.MethodGet, "https://discord.com/api/gateway")
|
||||||
resp := req.Send()
|
resp := req.Send()
|
||||||
if resp.Status() != 200 {
|
if resp.Status() != 200 {
|
||||||
@ -207,18 +250,18 @@ func getDiscordGateway() (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// sendHeartbeat sends a heartbeat to Discord.
|
// sendHeartbeat sends a heartbeat to Discord.
|
||||||
func sendHeartbeat(username string) error {
|
func (r *discordRPC) sendHeartbeat(username string) error {
|
||||||
seqNum, _, err := host.CacheGetInt(fmt.Sprintf("discord.seq.%s", username))
|
seqNum, _, err := host.CacheGetInt(fmt.Sprintf("discord.seq.%s", username))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get sequence number: %w", err)
|
return fmt.Errorf("failed to get sequence number: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
pdk.Log(pdk.LogDebug, fmt.Sprintf("Sending heartbeat for user %s: %d", username, seqNum))
|
pdk.Log(pdk.LogDebug, fmt.Sprintf("Sending heartbeat for user %s: %d", username, seqNum))
|
||||||
return sendMessage(username, heartbeatOpCode, seqNum)
|
return r.sendMessage(username, heartbeatOpCode, seqNum)
|
||||||
}
|
}
|
||||||
|
|
||||||
// cleanupFailedConnection cleans up a failed Discord connection.
|
// cleanupFailedConnection cleans up a failed Discord connection.
|
||||||
func cleanupFailedConnection(username string) {
|
func (r *discordRPC) cleanupFailedConnection(username string) {
|
||||||
pdk.Log(pdk.LogInfo, fmt.Sprintf("Cleaning up failed connection for user %s", username))
|
pdk.Log(pdk.LogInfo, fmt.Sprintf("Cleaning up failed connection for user %s", username))
|
||||||
|
|
||||||
// Cancel the heartbeat schedule
|
// Cancel the heartbeat schedule
|
||||||
@ -238,8 +281,8 @@ func cleanupFailedConnection(username string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// isConnected checks if a user is connected to Discord by testing the heartbeat.
|
// isConnected checks if a user is connected to Discord by testing the heartbeat.
|
||||||
func isConnected(username string) bool {
|
func (r *discordRPC) isConnected(username string) bool {
|
||||||
err := sendHeartbeat(username)
|
err := r.sendHeartbeat(username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
pdk.Log(pdk.LogDebug, fmt.Sprintf("Heartbeat test failed for user %s: %v", username, err))
|
pdk.Log(pdk.LogDebug, fmt.Sprintf("Heartbeat test failed for user %s: %v", username, err))
|
||||||
return false
|
return false
|
||||||
@ -248,15 +291,15 @@ func isConnected(username string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// connect establishes a connection to Discord for a user.
|
// connect establishes a connection to Discord for a user.
|
||||||
func connect(username, token string) error {
|
func (r *discordRPC) connect(username, token string) error {
|
||||||
if isConnected(username) {
|
if r.isConnected(username) {
|
||||||
pdk.Log(pdk.LogInfo, fmt.Sprintf("Reusing existing connection for user %s", username))
|
pdk.Log(pdk.LogInfo, fmt.Sprintf("Reusing existing connection for user %s", username))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
pdk.Log(pdk.LogInfo, fmt.Sprintf("Creating new connection for user %s", username))
|
pdk.Log(pdk.LogInfo, fmt.Sprintf("Creating new connection for user %s", username))
|
||||||
|
|
||||||
// Get Discord Gateway URL
|
// Get Discord Gateway URL
|
||||||
gateway, err := getDiscordGateway()
|
gateway, err := r.getDiscordGateway()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get Discord gateway: %w", err)
|
return fmt.Errorf("failed to get Discord gateway: %w", err)
|
||||||
}
|
}
|
||||||
@ -278,7 +321,7 @@ func connect(username, token string) error {
|
|||||||
Device: "Discord Client",
|
Device: "Discord Client",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
if err := sendMessage(username, gateOpCode, payload); err != nil {
|
if err := r.sendMessage(username, gateOpCode, payload); err != nil {
|
||||||
return fmt.Errorf("failed to send identify payload: %w", err)
|
return fmt.Errorf("failed to send identify payload: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -295,7 +338,7 @@ func connect(username, token string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// disconnect closes the Discord connection for a user.
|
// disconnect closes the Discord connection for a user.
|
||||||
func disconnect(username string) error {
|
func (r *discordRPC) disconnect(username string) error {
|
||||||
if err := host.SchedulerCancelSchedule(username); err != nil {
|
if err := host.SchedulerCancelSchedule(username); err != nil {
|
||||||
return fmt.Errorf("failed to cancel schedule: %w", err)
|
return fmt.Errorf("failed to cancel schedule: %w", err)
|
||||||
}
|
}
|
||||||
@ -307,7 +350,7 @@ func disconnect(username string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// handleWebSocketMessage processes incoming WebSocket messages from Discord.
|
// handleWebSocketMessage processes incoming WebSocket messages from Discord.
|
||||||
func handleWebSocketMessage(connectionID, message string) error {
|
func (r *discordRPC) handleWebSocketMessage(connectionID, message string) error {
|
||||||
if len(message) < 1024 {
|
if len(message) < 1024 {
|
||||||
pdk.Log(pdk.LogTrace, fmt.Sprintf("Received WebSocket message for connection '%s': %s", connectionID, message))
|
pdk.Log(pdk.LogTrace, fmt.Sprintf("Received WebSocket message for connection '%s': %s", connectionID, message))
|
||||||
} else {
|
} else {
|
||||||
@ -332,31 +375,26 @@ func handleWebSocketMessage(connectionID, message string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// handleHeartbeatCallback processes heartbeat scheduler callbacks.
|
// handleHeartbeatCallback processes heartbeat scheduler callbacks.
|
||||||
func handleHeartbeatCallback(username string) error {
|
func (r *discordRPC) handleHeartbeatCallback(username string) error {
|
||||||
if err := sendHeartbeat(username); err != nil {
|
if err := r.sendHeartbeat(username); err != nil {
|
||||||
// On first heartbeat failure, immediately clean up the connection
|
// On first heartbeat failure, immediately clean up the connection
|
||||||
pdk.Log(pdk.LogWarn, fmt.Sprintf("Heartbeat failed for user %s, cleaning up connection: %v", username, err))
|
pdk.Log(pdk.LogWarn, fmt.Sprintf("Heartbeat failed for user %s, cleaning up connection: %v", username, err))
|
||||||
cleanupFailedConnection(username)
|
r.cleanupFailedConnection(username)
|
||||||
return fmt.Errorf("heartbeat failed, connection cleaned up: %w", err)
|
return fmt.Errorf("heartbeat failed, connection cleaned up: %w", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleClearActivityCallback processes clear activity scheduler callbacks.
|
// handleClearActivityCallback processes clear activity scheduler callbacks.
|
||||||
func handleClearActivityCallback(username string) error {
|
func (r *discordRPC) handleClearActivityCallback(username string) error {
|
||||||
pdk.Log(pdk.LogInfo, fmt.Sprintf("Removing presence for user %s", username))
|
pdk.Log(pdk.LogInfo, fmt.Sprintf("Removing presence for user %s", username))
|
||||||
if err := clearActivity(username); err != nil {
|
if err := r.clearActivity(username); err != nil {
|
||||||
return fmt.Errorf("failed to clear activity: %w", err)
|
return fmt.Errorf("failed to clear activity: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
pdk.Log(pdk.LogInfo, fmt.Sprintf("Disconnecting user %s", username))
|
pdk.Log(pdk.LogInfo, fmt.Sprintf("Disconnecting user %s", username))
|
||||||
if err := disconnect(username); err != nil {
|
if err := r.disconnect(username); err != nil {
|
||||||
return fmt.Errorf("failed to disconnect from Discord: %w", err)
|
return fmt.Errorf("failed to disconnect from Discord: %w", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// nowPlaying returns the current timestamp in milliseconds.
|
|
||||||
func nowMillis() int64 {
|
|
||||||
return time.Now().UnixMilli()
|
|
||||||
}
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user