diff --git a/richgo/client/client.go b/richgo/client/client.go new file mode 100644 index 0000000..546e9e7 --- /dev/null +++ b/richgo/client/client.go @@ -0,0 +1,77 @@ +package client + +import ( + "crypto/rand" + "encoding/json" + "fmt" + "os" + + "github.com/hugolgst/rich-go/ipc" +) + +var logged bool + +// Login sends a handshake in the socket and returns an error or nil +func Login(clientid string) error { + if !logged { + payload, err := json.Marshal(Handshake{"1", clientid}) + if err != nil { + return err + } + + err = ipc.OpenSocket() + if err != nil { + return err + } + + // TODO: Response should be parsed + ipc.Send(0, string(payload)) + } + logged = true + + return nil +} + +func Logout() { + logged = false + + err := ipc.CloseSocket() + if err != nil { + panic(err) + } +} + +func SetActivity(activity Activity) error { + if !logged { + return nil + } + + payload, err := json.Marshal(Frame{ + "SET_ACTIVITY", + Args{ + os.Getpid(), + mapActivity(&activity), + }, + getNonce(), + }) + + if err != nil { + return err + } + + // TODO: Response should be parsed + ipc.Send(1, string(payload)) + return nil +} + +func getNonce() string { + buf := make([]byte, 16) + _, err := rand.Read(buf) + if err != nil { + fmt.Println(err) + } + + buf[6] = (buf[6] & 0x0f) | 0x40 + + return fmt.Sprintf("%x-%x-%x-%x-%x", buf[0:4], buf[4:6], buf[6:8], buf[8:10], buf[10:]) +} diff --git a/richgo/client/inputMapper.go b/richgo/client/inputMapper.go new file mode 100644 index 0000000..2600067 --- /dev/null +++ b/richgo/client/inputMapper.go @@ -0,0 +1,132 @@ +package client + +import ( + "time" +) + +// ActivityType represents the type of Discord rich presence activity +type ActivityType int + +const ( + // ActivityTypePlaying represents "Playing ..." status + ActivityTypePlaying ActivityType = 0 + // ActivityTypeListening represents "Listening to ..." status + ActivityTypeListening ActivityType = 2 + // ActivityTypeWatching represents "Watching ..." status + ActivityTypeWatching ActivityType = 3 + // ActivityTypeCompeting represents "Competing in ..." status + ActivityTypeCompeting ActivityType = 5 +) + +// Activity holds the data for discord rich presence +type Activity struct { + // What the player is currently doing + Details string + // The user's current party status + State string + // The activity type, defaults to Playing if not specified + Type ActivityType + // The id for a large asset of the activity, usually a snowflake + LargeImage string + // Text displayed when hovering over the large image of the activity + LargeText string + // The id for a small asset of the activity, usually a snowflake + SmallImage string + // Text displayed when hovering over the small image of the activity + SmallText string + // Information for the current party of the player + Party *Party + // Unix timestamps for start and/or end of the game + Timestamps *Timestamps + // Secrets for Rich Presence joining and spectating + Secrets *Secrets + // Clickable buttons that open a URL in the browser + Buttons []*Button +} + +// Button holds a label and the corresponding URL that is opened on press +type Button struct { + // The label of the button + Label string + // The URL of the button + Url string +} + +// Party holds information for the current party of the player +type Party struct { + // The ID of the party + ID string + // Used to show the party's current size + Players int + // Used to show the party's maximum size + MaxPlayers int +} + +// Timestamps holds unix timestamps for start and/or end of the game +type Timestamps struct { + // unix time (in milliseconds) of when the activity started + Start *time.Time + // unix time (in milliseconds) of when the activity ends + End *time.Time +} + +// Secrets holds secrets for Rich Presence joining and spectating +type Secrets struct { + // The secret for a specific instanced match + Match string + // The secret for joining a party + Join string + // The secret for spectating a game + Spectate string +} + +func mapActivity(activity *Activity) *PayloadActivity { + final := &PayloadActivity{ + Details: activity.Details, + State: activity.State, + Type: activity.Type, + Assets: PayloadAssets{ + LargeImage: activity.LargeImage, + LargeText: activity.LargeText, + SmallImage: activity.SmallImage, + SmallText: activity.SmallText, + }, + } + + if activity.Timestamps != nil && activity.Timestamps.Start != nil { + start := uint64(activity.Timestamps.Start.UnixNano() / 1e6) + final.Timestamps = &PayloadTimestamps{ + Start: &start, + } + if activity.Timestamps.End != nil { + end := uint64(activity.Timestamps.End.UnixNano() / 1e6) + final.Timestamps.End = &end + } + } + + if activity.Party != nil { + final.Party = &PayloadParty{ + ID: activity.Party.ID, + Size: [2]int{activity.Party.Players, activity.Party.MaxPlayers}, + } + } + + if activity.Secrets != nil { + final.Secrets = &PayloadSecrets{ + Join: activity.Secrets.Join, + Match: activity.Secrets.Match, + Spectate: activity.Secrets.Spectate, + } + } + + if len(activity.Buttons) > 0 { + for _, btn := range activity.Buttons { + final.Buttons = append(final.Buttons, &PayloadButton{ + Label: btn.Label, + Url: btn.Url, + }) + } + } + + return final +} diff --git a/richgo/client/types.go b/richgo/client/types.go new file mode 100644 index 0000000..840bfe2 --- /dev/null +++ b/richgo/client/types.go @@ -0,0 +1,56 @@ +package client + +type Handshake struct { + V string `json:"v"` + ClientId string `json:"client_id"` +} + +type Frame struct { + Cmd string `json:"cmd"` + Args Args `json:"args"` + Nonce string `json:"nonce"` +} + +type Args struct { + Pid int `json:"pid"` + Activity *PayloadActivity `json:"activity"` +} + +type PayloadActivity struct { + Details string `json:"details,omitempty"` + State string `json:"state,omitempty"` + Type ActivityType `json:"type,omitempty"` + Assets PayloadAssets `json:"assets,omitempty"` + Party *PayloadParty `json:"party,omitempty"` + Timestamps *PayloadTimestamps `json:"timestamps,omitempty"` + Secrets *PayloadSecrets `json:"secrets,omitempty"` + Buttons []*PayloadButton `json:"buttons,omitempty"` +} + +type PayloadAssets struct { + LargeImage string `json:"large_image,omitempty"` + LargeText string `json:"large_text,omitempty"` + SmallImage string `json:"small_image,omitempty"` + SmallText string `json:"small_text,omitempty"` +} + +type PayloadParty struct { + ID string `json:"id,omitempty"` + Size [2]int `json:"size,omitempty"` +} + +type PayloadTimestamps struct { + Start *uint64 `json:"start,omitempty"` + End *uint64 `json:"end,omitempty"` +} + +type PayloadSecrets struct { + Match string `json:"match,omitempty"` + Join string `json:"join,omitempty"` + Spectate string `json:"spectate,omitempty"` +} + +type PayloadButton struct { + Label string `json:"label,omitempty"` + Url string `json:"url,omitempty"` +} diff --git a/richgo/ipc/ipc.go b/richgo/ipc/ipc.go new file mode 100644 index 0000000..c72a194 --- /dev/null +++ b/richgo/ipc/ipc.go @@ -0,0 +1,81 @@ +package ipc + +import ( + "bytes" + "encoding/binary" + "fmt" + "net" + "os" +) + +var socket net.Conn + +// Choose the right directory to the ipc socket and return it +func GetIpcPath() string { + variablesnames := []string{"XDG_RUNTIME_DIR", "TMPDIR", "TMP", "TEMP"} + + if _, err := os.Stat("/run/user/1000/snap.discord"); err == nil { + return "/run/user/1000/snap.discord" + } + + if _, err := os.Stat("/run/user/1000/.flatpak/com.discordapp.Discord/xdg-run"); err == nil { + return "/run/user/1000/.flatpak/com.discordapp.Discord/xdg-run" + } + + for _, variablename := range variablesnames { + path, exists := os.LookupEnv(variablename) + + if exists { + return path + } + } + + return "/tmp" +} + +func CloseSocket() error { + if socket != nil { + socket.Close() + socket = nil + } + return nil +} + +// Read the socket response +func Read() string { + buf := make([]byte, 512) + payloadlength, err := socket.Read(buf) + if err != nil { + //fmt.Println("Nothing to read") + } + + buffer := new(bytes.Buffer) + for i := 8; i < payloadlength; i++ { + buffer.WriteByte(buf[i]) + } + + return buffer.String() +} + +// Send opcode and payload to the unix socket +func Send(opcode int, payload string) string { + buf := new(bytes.Buffer) + + err := binary.Write(buf, binary.LittleEndian, int32(opcode)) + if err != nil { + fmt.Println(err) + } + + err = binary.Write(buf, binary.LittleEndian, int32(len(payload))) + if err != nil { + fmt.Println(err) + } + + buf.Write([]byte(payload)) + _, err = socket.Write(buf.Bytes()) + if err != nil { + fmt.Println(err) + } + + return Read() +} diff --git a/richgo/ipc/ipc_notwin.go b/richgo/ipc/ipc_notwin.go new file mode 100644 index 0000000..1f47687 --- /dev/null +++ b/richgo/ipc/ipc_notwin.go @@ -0,0 +1,20 @@ +//go:build !windows +// +build !windows + +package ipc + +import ( + "net" + "time" +) + +// OpenSocket opens the discord-ipc-0 unix socket +func OpenSocket() error { + sock, err := net.DialTimeout("unix", GetIpcPath()+"/discord-ipc-0", time.Second*2) + if err != nil { + return err + } + + socket = sock + return nil +} diff --git a/richgo/ipc/ipc_windows.go b/richgo/ipc/ipc_windows.go new file mode 100644 index 0000000..1ac0763 --- /dev/null +++ b/richgo/ipc/ipc_windows.go @@ -0,0 +1,23 @@ +//go:build windows +// +build windows + +package ipc + +import ( + npipe "gopkg.in/natefinch/npipe.v2" + "time" +) + +// OpenSocket opens the discord-ipc-0 named pipe +func OpenSocket() error { + // Connect to the Windows named pipe, this is a well known name + // We use DialTimeout since it will block forever (or very very long) on Windows + // if the pipe is not available (Discord not running) + sock, err := npipe.DialTimeout(`\\.\pipe\discord-ipc-0`, time.Second*2) + if err != nil { + return err + } + + socket = sock + return nil +} diff --git a/src/curdIntegration.go b/src/curdIntegration.go index f9c0024..c2d3872 100644 --- a/src/curdIntegration.go +++ b/src/curdIntegration.go @@ -320,7 +320,7 @@ func displayLocalProgress() { playButton.Text = fmt.Sprint("Play Ep", currentEp) fmt.Println("Current Ep:", currentEp) - setPlayButtonVisibility() + defer setPlayButtonVisibility() if localDbAnime != nil { if localDbAnime.Ep.Number == AnimeProgress { if localDbAnime.Ep.Player.PlaybackTime == 0 { @@ -339,6 +339,7 @@ func displayLocalProgress() { } func setPlayButtonVisibility() { + defer playButton.Refresh() if animeSelected.Media.NextAiringEpisode != nil { if *animeSelected.Progress+1 == animeSelected.Media.NextAiringEpisode.Episode { playButton.Hide() diff --git a/src/richPresence/richPresence.go b/src/richPresence/richPresence.go index 7de6b0d..7b01372 100644 --- a/src/richPresence/richPresence.go +++ b/src/richPresence/richPresence.go @@ -1,10 +1,10 @@ package richPresence import ( + "AnimeGUI/richgo/client" "AnimeGUI/src/config" "fmt" "github.com/charmbracelet/log" - "github.com/hugolgst/rich-go/client" "time" ) @@ -51,9 +51,10 @@ func InitDiscordRichPresence() { func SetMenuActivity() { log.Info("Main Menu Activity presence") err := client.SetActivity(client.Activity{ + Type: client.ActivityTypeWatching, Details: "In Main Menu", State: advert, - LargeImage: "https://apologize.fr/benri/icon.jpg", + LargeImage: "main-image", LargeText: advert, /*SmallImage: "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx170942-B77wUSM1jQTu.jpg", SmallText: "And this is the small image",*/ @@ -91,11 +92,12 @@ func SetAnimeActivity(anime *PresenceAnime) { } err := client.SetActivity(client.Activity{ + Type: client.ActivityTypeWatching, State: fmt.Sprintf("%s remaining", numberToTime(anime.Duration-anime.PlaybackTime)), Details: fmt.Sprintf("%s Episode %d/%d", anime.Name, anime.Ep, anime.TotalEp), LargeImage: anime.ImageLink, LargeText: anime.Name, - SmallImage: "https://apologize.fr/benri/icon.jpg", + SmallImage: "main-image", SmallText: advert, /*Party: &client.Party{ ID: "-1",