rpcfetch/io.go
Ophestra Umiker b3938173ae
library: io: expose ID, User and Config via methods
These fields can be copied safely and is useful information outside the library, so they are exposed via methods that copy their value.

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
2024-06-29 17:48:32 +09:00

145 lines
2.8 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
}
// 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, 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 == 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"
}