Rich presence update + fix play button

This commit is contained in:
Apologieze
2025-03-20 13:48:32 -04:00
parent 74a01e38a9
commit 69cd7c965d
8 changed files with 396 additions and 4 deletions

77
richgo/client/client.go Normal file
View File

@@ -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:])
}

View File

@@ -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
}

56
richgo/client/types.go Normal file
View File

@@ -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"`
}

81
richgo/ipc/ipc.go Normal file
View File

@@ -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()
}

20
richgo/ipc/ipc_notwin.go Normal file
View File

@@ -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
}

23
richgo/ipc/ipc_windows.go Normal file
View File

@@ -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
}

View File

@@ -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()

View File

@@ -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",