Discord client being absent/disconnecting is not fatal to the RPC sender, in cases like that we return ErrAgain and in the case of broken pipe (Discord client going away) also reset the Client state so the caller can choose to retry the operation and therefore initiate a new connection attempt. Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
120 lines
2.4 KiB
Go
120 lines
2.4 KiB
Go
package rpcfetch
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"encoding/json"
|
|
"errors"
|
|
"net"
|
|
"os"
|
|
"strconv"
|
|
"syscall"
|
|
)
|
|
|
|
type Client struct {
|
|
id string
|
|
|
|
config *Config
|
|
user *User
|
|
|
|
dialed bool
|
|
active bool
|
|
conn net.Conn
|
|
}
|
|
|
|
// Raw wraps around the Raw method to provide generic json parsing
|
|
func Raw[T any](d *Client, opcode uint32, payload any) (uint32, T, error) {
|
|
var p T
|
|
|
|
opcodeResp, payloadResp, err := d.Raw(opcode, payload)
|
|
if err != nil {
|
|
return opcodeResp, p, err
|
|
}
|
|
|
|
return opcodeResp, p, json.Unmarshal(payloadResp, &p)
|
|
}
|
|
|
|
// Raw writes a raw payload to Discord and returns the response opcode and payload
|
|
func (d *Client) Raw(opcode uint32, payload any) (uint32, []byte, error) {
|
|
opcodeResp, payloadResp, err := d.raw(opcode, payload)
|
|
if errors.Is(err, syscall.EPIPE) {
|
|
// clean up as much as possible
|
|
_ = d.Close()
|
|
// advise retry
|
|
err = ErrAgain
|
|
}
|
|
return opcodeResp, payloadResp, err
|
|
}
|
|
|
|
func (d *Client) raw(opcode uint32, payload any) (uint32, []byte, error) {
|
|
if err := binary.Write(d.conn, binary.LittleEndian, opcode); err != nil {
|
|
return 0, nil, err
|
|
}
|
|
|
|
if p, err := json.Marshal(payload); err != nil {
|
|
return 0, nil, err
|
|
} else {
|
|
if err = binary.Write(d.conn, binary.LittleEndian, uint32(len(p))); err != nil {
|
|
return 0, nil, err
|
|
}
|
|
|
|
if _, err = d.conn.Write(p); err != nil {
|
|
return 0, nil, err
|
|
}
|
|
}
|
|
|
|
var (
|
|
opcodeResp uint32
|
|
lengthResp uint32
|
|
)
|
|
|
|
if err := binary.Read(d.conn, binary.LittleEndian, &opcodeResp); err != nil {
|
|
return 0, nil, err
|
|
}
|
|
|
|
if err := binary.Read(d.conn, binary.LittleEndian, &lengthResp); err != nil {
|
|
return 0, nil, err
|
|
}
|
|
|
|
payloadResp := make([]byte, lengthResp)
|
|
_, err := d.conn.Read(payloadResp)
|
|
return opcodeResp, payloadResp, err
|
|
}
|
|
|
|
// Close the client, this is required before exit
|
|
func (d *Client) Close() error {
|
|
if !d.dialed {
|
|
// silently succeed because client activation is implicit
|
|
return nil
|
|
}
|
|
|
|
d.active = false
|
|
d.dialed = false
|
|
return d.conn.Close()
|
|
}
|
|
|
|
// New sets up and returns the reference to a new Client
|
|
func New(id string) *Client {
|
|
d := &Client{
|
|
id: id,
|
|
}
|
|
|
|
return d
|
|
}
|
|
|
|
func sockPath() string {
|
|
snap := "/run/user/" + strconv.Itoa(os.Getuid()) + "/snap.discord"
|
|
|
|
if _, err := os.Stat(snap); err == nil {
|
|
return snap
|
|
}
|
|
|
|
for _, env := range []string{"XDG_RUNTIME_DIR", "TMPDIR", "TMP", "TEMP"} {
|
|
if val, ok := os.LookupEnv(env); ok {
|
|
return val
|
|
}
|
|
}
|
|
|
|
// fallback
|
|
return "/tmp"
|
|
}
|