This repository has been archived by the owner on Feb 25, 2024. It is now read-only.
/
instance.go
252 lines (233 loc) · 6.7 KB
/
instance.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
// Copyright (C) 2021 The Dank Grinder authors.
//
// This source code has been released under the GNU Affero General Public
// License v3.0. A copy of this license is available at
// https://www.gnu.org/licenses/agpl-3.0.en.html
package instance
import (
"fmt"
"math"
"math/rand"
"runtime"
"strconv"
"sync"
"time"
"github.com/dankgrinder/dankgrinder/config"
"github.com/dankgrinder/dankgrinder/discord"
"github.com/dankgrinder/dankgrinder/instance/scheduler"
"github.com/gorilla/websocket"
"github.com/sirupsen/logrus"
)
const fundReqInterval = time.Minute * 10
type Instance struct {
Client *discord.Client
Logger *logrus.Logger
ChannelID string
WG *sync.WaitGroup
Master *Instance
Cluster []*Instance
Features config.Features
SuspicionAvoidance config.SuspicionAvoidance
Compat config.Compat
Shifts []config.Shift
sdlr *scheduler.Scheduler
ws *discord.WSConn
initialBalance int
balance int
startingTime time.Time
lastState string
lastBalanceUpdate time.Time
fatal chan error
isClosed bool
}
func (in *Instance) Start() error {
if in.Client == nil {
return fmt.Errorf("no client")
}
if in.ChannelID == "" {
return fmt.Errorf("no channel id")
}
if len(in.Shifts) == 0 {
return fmt.Errorf("no shifts")
}
if in.WG == nil {
return fmt.Errorf("no waitgroup")
}
if in.Logger == nil {
return fmt.Errorf("no logger")
}
if in.Master == nil {
if in.Features.AutoGift.Enable {
in.Logger.Warnf("nobody to auto-gift to, no master instance available")
}
if in.Features.AutoShare.Enable {
in.Logger.Warnf("nobody to auto-share to, no master instance available")
}
}
// For now, we assume that in.SuspicionAvoidance, in.Compat and in.Features
// are correct. They are currently validated in the main function. Ideally,
// this needs to change in the future.
in.fatal = make(chan error)
in.WG.Add(1)
go func() {
defer in.WG.Done()
defer func() {
in.isClosed = true
}()
for {
for i, shift := range in.Shifts {
dur := shiftDur(shift)
in.Logger.WithFields(map[string]interface{}{
"state": shift.State,
"duration": dur,
}).Infof("starting shift %v", i+1)
if shift.State == in.lastState {
in.sleep(dur)
continue
}
in.lastState = shift.State
if shift.State == config.ShiftStateDormant {
if in.ws != nil {
if err := in.ws.Close(); err != nil {
in.Logger.Errorf("error while closing websocket: %v", err)
}
}
if in.sdlr != nil {
if err := in.sdlr.Close(); err != nil {
in.Logger.Errorf("error while closing scheduler: %v", err)
}
}
in.sleep(dur)
continue
}
if err := in.startWS(); err != nil {
in.Logger.Errorf("instance fatal: error while starting websocket: %v", err)
return
}
if err := in.startSdlr(); err != nil {
in.Logger.Errorf("instance fatal: error while starting scheduler: %v", err)
return
}
cmds := in.newCmds()
if in.Features.AutoSell.Enable {
cmds = append(cmds, in.newAutoSellChain())
}
if in.Features.AutoGift.Enable &&
in.Master != nil &&
in != in.Master {
cmds = append(cmds, in.newAutoGiftChain())
}
for _, cmd := range cmds {
in.sdlr.Schedule(cmd)
}
in.sleep(dur)
}
}
}()
if in.Features.AutoShare.Enable && in.Features.AutoShare.Fund && in == in.Master {
go func() {
t := time.NewTicker(time.Minute*5 + time.Duration(len(in.Cluster)*in.Compat.Cooldown.Share)*time.Second)
defer t.Stop()
for {
<-t.C
var totalFunding int
var fundingCmds []*scheduler.Command
for _, clusterInstance := range in.Cluster {
if in == clusterInstance {
continue
}
if !clusterInstance.Features.AutoShare.Enable ||
clusterInstance.LastBalanceUpdate().IsZero() ||
clusterInstance.IsClosed() {
continue
}
balance := clusterInstance.Balance()
if balance >= clusterInstance.Features.AutoShare.MinimumBalance {
continue
}
deficit := clusterInstance.Features.AutoShare.MinimumBalance - balance
deficit = int(math.Round(float64(deficit) / 0.92)) // Account for 8% tax.
if totalFunding+deficit > in.balance {
break
}
totalFunding += deficit
fundingCmds = append(fundingCmds, &scheduler.Command{
Value: shareCmdValue(strconv.Itoa(deficit), clusterInstance.Client.User.ID),
Log: "funding",
Interval: time.Duration(in.Compat.Cooldown.Share) * time.Second,
RescheduleAsPriority: true,
})
}
if len(fundingCmds) > 0 {
in.sdlr.PrioritySchedule(in.newCmdChain(fundingCmds, 0))
}
}
}()
}
return nil
}
func (in *Instance) sleep(dur time.Duration) {
select {
case err := <-in.fatal:
in.Logger.Errorf("instance fatal: %v", err)
runtime.Goexit()
case <-time.After(dur):
}
}
func (in *Instance) startSdlr() error {
in.sdlr = &scheduler.Scheduler{
Client: in.Client,
ChannelID: in.ChannelID,
Typing: &in.SuspicionAvoidance.Typing,
MessageDelay: &in.SuspicionAvoidance.MessageDelay,
Logger: in.Logger,
AwaitResumeTimeout: time.Duration(in.Compat.AwaitResponseTimeout) * time.Second,
FatalHandler: func(ferr error) {
in.fatal <- fmt.Errorf("scheduler fatal: %v", ferr)
},
}
if err := in.sdlr.Start(); err != nil {
return fmt.Errorf("error while starting scheduler: %v", err)
}
return nil
}
func (in *Instance) startWS() error {
ws, err := in.Client.NewWSConn(in.router(), in.wsFatalHandler)
if err != nil {
return fmt.Errorf("error while starting websocket: %v", err)
}
in.ws = ws
return nil
}
func shiftDur(shift config.Shift) time.Duration {
if shift.Duration.Base <= 0 {
return time.Duration(math.MaxInt64)
}
d := time.Duration(shift.Duration.Base) * time.Second
if shift.Duration.Variation > 0 {
d += time.Duration(rand.Intn(shift.Duration.Variation)) * time.Second
}
return d
}
func (in *Instance) wsFatalHandler(err error) {
if closeErr, ok := err.(*websocket.CloseError); ok && closeErr.Code == 4004 {
in.fatal <- fmt.Errorf("websocket closed: authentication failed, try using a new token")
return
}
in.Logger.Errorf("websocket closed: %v", err)
in.ws, err = in.Client.NewWSConn(in.router(), in.wsFatalHandler)
if err != nil {
in.fatal <- fmt.Errorf("error while connecting to websocket: %v", err)
return
}
in.Logger.Infof("reconnected to websocket")
}
func (in *Instance) IsClosed() bool {
return in.isClosed
}
func (in *Instance) LastBalanceUpdate() time.Time {
return in.lastBalanceUpdate
}
func (in *Instance) Balance() int {
return in.balance
}