rpcfetch/io.go

148 lines
2.8 KiB
Go
Raw Permalink Normal View History

package rpcfetch
import (
"encoding/binary"
"encoding/json"
"errors"
"net"
"os"
"strconv"
)
var (
ErrAgain = errors.New("operation not performed")
)
type Client struct {
id string
config *Config
user *User
dialed bool
active bool
conn net.Conn
}
// ID returns the Client Application ID nil-safely
func (d *Client) ID() string {
if d == nil {
return ""
}
return d.id
}
// User returns the Client User nil-safely
func (d *Client) User() (User, bool) {
if d == nil || d.user == nil {
return User{}, false
}
return *d.user, true
}
// Config returns the Client Config nil-safely
func (d *Client) Config() (Config, bool) {
if d == nil || d.config == nil {
return Config{}, false
}
return *d.config, true
}
// 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, errPipe) {
// 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 == nil || !d.dialed {
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"
}