initial commit
This commit is contained in:
commit
1bbbda5385
1
README.md
Normal file
1
README.md
Normal file
@ -0,0 +1 @@
|
||||
# sati-go - golang api wrapper for [sati.ac](sati.ac)
|
80
events.go
Normal file
80
events.go
Normal file
@ -0,0 +1,80 @@
|
||||
package sati
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/mitchellh/mapstructure"
|
||||
)
|
||||
|
||||
type EventBus struct {
|
||||
mu *sync.Mutex
|
||||
idCounter uint32
|
||||
handlers map[string]map[uint32]func(data any)
|
||||
eventTypes map[string]any
|
||||
}
|
||||
|
||||
type EventHandler struct {
|
||||
id uint32
|
||||
type_ string
|
||||
bus *EventBus
|
||||
}
|
||||
|
||||
func (e *EventHandler) Off() {
|
||||
e.bus.mu.Lock()
|
||||
defer e.bus.mu.Unlock()
|
||||
|
||||
delete(e.bus.handlers[e.type_], e.id)
|
||||
}
|
||||
|
||||
func (e *EventBus) On(event string, handler func(data any)) (*EventHandler, error) {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
if _, ok := e.eventTypes[event]; !ok {
|
||||
return nil, fmt.Errorf("bad event type")
|
||||
}
|
||||
|
||||
e.idCounter++
|
||||
e.handlers[event][e.idCounter] = handler
|
||||
return &EventHandler{
|
||||
id: e.idCounter,
|
||||
type_: event,
|
||||
bus: e,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (e *EventBus) dispatch(event string, data any) error {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
formattedData, ok := e.eventTypes[event]
|
||||
if !ok {
|
||||
return fmt.Errorf("bad event type")
|
||||
}
|
||||
|
||||
if err := mapstructure.Decode(data, &formattedData); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, handler := range e.handlers[event] {
|
||||
handler(formattedData)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// eventTypes values must be structs, not pointers
|
||||
func newEventBus(eventTypes map[string]any) *EventBus {
|
||||
e := &EventBus{
|
||||
mu: &sync.Mutex{},
|
||||
handlers: make(map[string]map[uint32]func(data any)),
|
||||
eventTypes: eventTypes,
|
||||
}
|
||||
|
||||
for event := range eventTypes {
|
||||
e.handlers[event] = make(map[uint32]func(data any))
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
9
go.mod
Normal file
9
go.mod
Normal file
@ -0,0 +1,9 @@
|
||||
module git.sati.ac/sati.ac/sati-go
|
||||
|
||||
go 1.20
|
||||
|
||||
require (
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/mitchellh/mapstructure v1.5.0
|
||||
github.com/shopspring/decimal v1.3.1
|
||||
)
|
6
go.sum
Normal file
6
go.sum
Normal file
@ -0,0 +1,6 @@
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
|
||||
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
85
sati.go
Normal file
85
sati.go
Normal file
@ -0,0 +1,85 @@
|
||||
package sati
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
ReconnectionInterval time.Duration
|
||||
Endpoint string
|
||||
Debug bool
|
||||
Token string
|
||||
}
|
||||
|
||||
func NewConfig(token string) Config {
|
||||
return Config{
|
||||
ReconnectionInterval: time.Second,
|
||||
Debug: false,
|
||||
Endpoint: "wss://api.sati.ac/ws",
|
||||
Token: token,
|
||||
}
|
||||
}
|
||||
|
||||
type Api struct {
|
||||
socket *socket
|
||||
}
|
||||
|
||||
func (a *Api) Solve(task AnyTask, result any) (*TaskEntity, error) {
|
||||
createdTask := CreateTaskResult{}
|
||||
if err := a.socket.call("createTask", task.serialize(), &createdTask); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resultCh := make(chan *TaskUpdateEvent)
|
||||
handler, err := a.socket.events.On("taskUpdate", func(data any) {
|
||||
e := data.(TaskUpdateEvent)
|
||||
if createdTask.Id == e.Id && (e.State == "error" || e.State == "success") {
|
||||
resultCh <- &e
|
||||
}
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
solvedTask := <-resultCh
|
||||
handler.Off()
|
||||
|
||||
if solvedTask.State == "error" {
|
||||
return nil, fmt.Errorf("sati: failed to solve task")
|
||||
}
|
||||
|
||||
if err := mapstructure.Decode(solvedTask.Result, result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return (*TaskEntity)(solvedTask), nil
|
||||
}
|
||||
|
||||
func (a *Api) GetBalance() (*decimal.Decimal, error) {
|
||||
result := GetBalanceResult{}
|
||||
if err := a.socket.call("getBalance", &GetBalanceRequest{}, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
balance, err := decimal.NewFromString(result.Balance)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &balance, nil
|
||||
}
|
||||
|
||||
func (a *Api) Dispose() {
|
||||
a.socket.close()
|
||||
}
|
||||
|
||||
func NewApi(config Config) *Api {
|
||||
return &Api{
|
||||
socket: newSocket(config),
|
||||
}
|
||||
}
|
285
socket.go
Normal file
285
socket.go
Normal file
@ -0,0 +1,285 @@
|
||||
package sati
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
)
|
||||
|
||||
const (
|
||||
stReconnecting uint32 = iota
|
||||
stConnected
|
||||
stUnrecoverable
|
||||
)
|
||||
|
||||
type message struct {
|
||||
Type string
|
||||
Id uint32
|
||||
Data any
|
||||
To uint32
|
||||
}
|
||||
|
||||
type outgoingMessage struct {
|
||||
msg message
|
||||
result chan *incomingMessage
|
||||
}
|
||||
|
||||
type incomingMessage struct {
|
||||
msg *message
|
||||
err error
|
||||
}
|
||||
|
||||
type socket struct {
|
||||
idCounter uint32
|
||||
state uint32
|
||||
unrecoverableError error
|
||||
closeNotifier chan struct{}
|
||||
outgoing chan *outgoingMessage
|
||||
ws *websocket.Conn
|
||||
awaitedReplies map[uint32]chan *incomingMessage
|
||||
mu *sync.Mutex
|
||||
events *EventBus
|
||||
config Config
|
||||
}
|
||||
|
||||
func (s *socket) reciever() {
|
||||
for {
|
||||
var message message
|
||||
if err := s.ws.ReadJSON(&message); err != nil {
|
||||
if s.config.Debug {
|
||||
fmt.Println("sati: got error while reading socket", err.Error())
|
||||
}
|
||||
s.mu.Lock()
|
||||
for _, ch := range s.awaitedReplies {
|
||||
ch <- &incomingMessage{
|
||||
err: s.unrecoverableError,
|
||||
}
|
||||
}
|
||||
s.awaitedReplies = make(map[uint32]chan *incomingMessage)
|
||||
s.mu.Unlock()
|
||||
s.closeNotifier <- struct{}{}
|
||||
return
|
||||
}
|
||||
|
||||
if s.config.Debug {
|
||||
fmt.Println("sati: recieved message", &message)
|
||||
}
|
||||
|
||||
switch message.Type {
|
||||
case "auth":
|
||||
fallthrough
|
||||
case "call":
|
||||
if message.To == 0 {
|
||||
continue
|
||||
}
|
||||
s.mu.Lock()
|
||||
resultCh, ok := s.awaitedReplies[message.To]
|
||||
if ok {
|
||||
delete(s.awaitedReplies, message.To)
|
||||
}
|
||||
s.mu.Unlock()
|
||||
if resultCh != nil {
|
||||
resultCh <- &incomingMessage{msg: &message}
|
||||
}
|
||||
case "event":
|
||||
var event struct {
|
||||
Type string `json:"type"`
|
||||
Data any `json:"data"`
|
||||
}
|
||||
|
||||
mapstructure.Decode(message.Data, &event)
|
||||
if err := s.events.dispatch(event.Type, event.Data); err != nil && s.config.Debug {
|
||||
fmt.Println("sati: error while dispatching event:", err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *socket) send(msg *outgoingMessage) error {
|
||||
s.idCounter++
|
||||
msg.msg.Id = s.idCounter
|
||||
if msg.result != nil {
|
||||
s.mu.Lock()
|
||||
s.awaitedReplies[msg.msg.Id] = msg.result
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
if s.config.Debug {
|
||||
fmt.Println("sati: sending message", msg)
|
||||
}
|
||||
|
||||
err := s.ws.WriteJSON(msg.msg)
|
||||
if msg.result != nil && err != nil {
|
||||
s.mu.Lock()
|
||||
s.awaitedReplies[msg.msg.Id] <- &incomingMessage{
|
||||
err: err,
|
||||
}
|
||||
delete(s.awaitedReplies, msg.msg.Id)
|
||||
s.mu.Unlock()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *socket) sender() {
|
||||
for {
|
||||
select {
|
||||
case msg := <-s.outgoing:
|
||||
s.send(msg)
|
||||
case <-s.closeNotifier:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *socket) connect() error {
|
||||
if s.config.Debug {
|
||||
fmt.Println("sati: connecting")
|
||||
}
|
||||
ws, _, err := websocket.DefaultDialer.Dial(s.config.Endpoint, http.Header{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
s.state = stReconnecting
|
||||
s.ws = ws
|
||||
s.mu.Unlock()
|
||||
|
||||
resultChan := make(chan *incomingMessage)
|
||||
s.send(&outgoingMessage{
|
||||
message{
|
||||
Type: "auth",
|
||||
Data: struct {
|
||||
Token string `json:"token"`
|
||||
}{s.config.Token},
|
||||
}, resultChan,
|
||||
})
|
||||
|
||||
go s.reciever()
|
||||
|
||||
rawResult := <-resultChan
|
||||
if rawResult.err != nil {
|
||||
return rawResult.err
|
||||
}
|
||||
|
||||
var result struct {
|
||||
Success bool `json:"success"`
|
||||
}
|
||||
|
||||
if err := mapstructure.Decode(rawResult.msg.Data, &result); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !result.Success {
|
||||
s.setUnrecoverableState("invalid auth token")
|
||||
return s.unrecoverableError
|
||||
}
|
||||
|
||||
s.sender()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *socket) connector() {
|
||||
for {
|
||||
s.mu.Lock()
|
||||
state := s.state
|
||||
s.mu.Unlock()
|
||||
if state == stUnrecoverable {
|
||||
break
|
||||
}
|
||||
|
||||
err := s.connect() // will block until disconnect
|
||||
if s.config.Debug && err != nil {
|
||||
fmt.Println("sati: disconnected", err.Error())
|
||||
}
|
||||
time.Sleep(s.config.ReconnectionInterval)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *socket) setUnrecoverableState(err string) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.state == stUnrecoverable {
|
||||
return
|
||||
}
|
||||
s.unrecoverableError = fmt.Errorf("sati: %s", err)
|
||||
s.state = stUnrecoverable
|
||||
s.ws.Close()
|
||||
}
|
||||
|
||||
func newSocket(config Config) *socket {
|
||||
s := &socket{
|
||||
closeNotifier: make(chan struct{}),
|
||||
outgoing: make(chan *outgoingMessage),
|
||||
awaitedReplies: make(map[uint32]chan *incomingMessage),
|
||||
mu: &sync.Mutex{},
|
||||
events: newEventBus(map[string]any{
|
||||
"taskUpdate": TaskUpdateEvent{},
|
||||
"tokenReissue": TokenReissueEvent{},
|
||||
}),
|
||||
config: config,
|
||||
}
|
||||
|
||||
s.events.On("tokenReissue", func(any) {
|
||||
s.setUnrecoverableState("token was reissued")
|
||||
})
|
||||
|
||||
go s.connector()
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *socket) close() {
|
||||
s.setUnrecoverableState("socket closed")
|
||||
}
|
||||
|
||||
func (s *socket) call(method string, data any, result any) error {
|
||||
s.mu.Lock()
|
||||
if s.state == stUnrecoverable {
|
||||
err := s.unrecoverableError
|
||||
s.mu.Unlock()
|
||||
return err
|
||||
}
|
||||
s.mu.Unlock()
|
||||
|
||||
resultCh := make(chan *incomingMessage)
|
||||
s.outgoing <- &outgoingMessage{
|
||||
msg: message{
|
||||
Type: "call",
|
||||
Data: CallMessageOutgoing{
|
||||
Method: method,
|
||||
Data: data,
|
||||
},
|
||||
},
|
||||
result: resultCh,
|
||||
}
|
||||
|
||||
resultMsg := <-resultCh
|
||||
if resultMsg.err != nil {
|
||||
return resultMsg.err
|
||||
}
|
||||
|
||||
callResult := CallMessageIncoming{}
|
||||
if err := mapstructure.Decode(resultMsg.msg.Data, &callResult); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !callResult.Success {
|
||||
callErr := &CallError{}
|
||||
if err := mapstructure.Decode(callResult.Data, callErr); err != nil {
|
||||
return err
|
||||
}
|
||||
return callErr
|
||||
}
|
||||
|
||||
if err := mapstructure.Decode(callResult.Data, result); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
44
tasks.go
Normal file
44
tasks.go
Normal file
@ -0,0 +1,44 @@
|
||||
package sati
|
||||
|
||||
type AnyTask interface {
|
||||
serialize() task
|
||||
}
|
||||
|
||||
type task struct {
|
||||
Type string `json:"type"`
|
||||
Data any `json:"data"`
|
||||
}
|
||||
|
||||
type TurnstileTask struct {
|
||||
SiteKey string `json:"siteKey"`
|
||||
PageUrl string `json:"pageUrl"`
|
||||
CData *string `json:"cData,omitempty"`
|
||||
Action *string `json:"action,omitempty"`
|
||||
}
|
||||
|
||||
func (t *TurnstileTask) serialize() task {
|
||||
return task{
|
||||
Type: "Turnstile",
|
||||
Data: t,
|
||||
}
|
||||
}
|
||||
|
||||
type TurnstileResult struct {
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
type ReCaptcha2Task struct {
|
||||
SiteKey string `json:"siteKey"`
|
||||
PageUrl string `json:"pageUrl"`
|
||||
}
|
||||
|
||||
func (t *ReCaptcha2Task) serialize() task {
|
||||
return task{
|
||||
Type: "ReCaptcha2",
|
||||
Data: t,
|
||||
}
|
||||
}
|
||||
|
||||
type ReCaptcha2Result struct {
|
||||
Token string `json:"token"`
|
||||
}
|
42
types.go
Normal file
42
types.go
Normal file
@ -0,0 +1,42 @@
|
||||
package sati
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type TokenReissueEvent struct{}
|
||||
|
||||
type TaskUpdateEvent TaskEntity
|
||||
|
||||
type CallMessageOutgoing struct {
|
||||
Method string `json:"method"`
|
||||
Data any `json:"data"`
|
||||
}
|
||||
|
||||
type CallMessageIncoming struct {
|
||||
Success bool `json:"success"`
|
||||
Data any `json:"data"`
|
||||
}
|
||||
|
||||
type CallError struct {
|
||||
Description string `json:"description"`
|
||||
Code uint32 `json:"code"`
|
||||
}
|
||||
|
||||
func (c *CallError) Error() string {
|
||||
return fmt.Sprintf("sati: api: #%d %s", c.Code, c.Description)
|
||||
}
|
||||
|
||||
type TaskEntity struct {
|
||||
Id uint32 `json:"id"`
|
||||
Type string `json:"type"`
|
||||
State string `json:"state"`
|
||||
Cost string `json:"cost"`
|
||||
Result any `json:"result"`
|
||||
}
|
||||
|
||||
type CreateTaskResult TaskEntity
|
||||
type GetBalanceRequest struct{}
|
||||
type GetBalanceResult struct {
|
||||
Balance string `json:"balance"`
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user