Compare commits

...
10 Commits
7 changed files with 376 additions and 126 deletions

No files matched your search

+1 -1
View File
@@ -2,7 +2,7 @@ package context
import ( import (
"context" "context"
"github.com/abelith/gomax/v1/state" "github.com/abelith/gomax/v1/fsm/state"
maxbot "github.com/max-messenger/max-bot-api-client-go/v2" maxbot "github.com/max-messenger/max-bot-api-client-go/v2"
"github.com/max-messenger/max-bot-api-client-go/v2/model" "github.com/max-messenger/max-bot-api-client-go/v2/model"
"time" "time"
+72 -22
View File
@@ -4,13 +4,14 @@ import (
"context" "context"
"errors" "errors"
mcontext "github.com/abelith/gomax/v1/context" mcontext "github.com/abelith/gomax/v1/context"
"github.com/abelith/gomax/v1/core/pollers"
"github.com/abelith/gomax/v1/fsm/memory"
"github.com/abelith/gomax/v1/fsm/state"
"github.com/abelith/gomax/v1/isolation" "github.com/abelith/gomax/v1/isolation"
"github.com/abelith/gomax/v1/state"
"github.com/abelith/gomax/v1/state/memory"
"github.com/google/uuid" "github.com/google/uuid"
maxbot "github.com/max-messenger/max-bot-api-client-go/v2" maxbot "github.com/max-messenger/max-bot-api-client-go/v2"
"github.com/max-messenger/max-bot-api-client-go/v2/model" "github.com/max-messenger/max-bot-api-client-go/v2/model"
"github.com/rs/zerolog/log" "github.com/rs/zerolog"
"sync" "sync"
"time" "time"
) )
@@ -27,9 +28,9 @@ type EventIsolation interface {
} }
type FSMStorage interface { type FSMStorage interface {
Get(ctx context.Context, userID int) (state.Snapshot, error) Get(ctx context.Context, scope isolation.Scope) (state.Snapshot, error)
Put(ctx context.Context, state state.Snapshot) error Put(ctx context.Context, state state.Snapshot) error
Delete(ctx context.Context, userID int) error Delete(ctx context.Context, scope isolation.Scope) error
} }
type UpdateHandler interface { type UpdateHandler interface {
@@ -39,6 +40,7 @@ type UpdateHandler interface {
type Poller interface { type Poller interface {
Start(ctx context.Context) (<-chan model.Update, error) Start(ctx context.Context) (<-chan model.Update, error)
Stop(ctx context.Context) error
} }
type DispatcherOption interface { type DispatcherOption interface {
@@ -50,7 +52,10 @@ type Dispatcher struct {
poller Poller poller Poller
fsmStorage FSMStorage fsmStorage FSMStorage
eventIsolation EventIsolation eventIsolation EventIsolation
onStart func(ctx context.Context) error
onStop func(ctx context.Context) error
rt *Router rt *Router
logger zerolog.Logger
wg sync.WaitGroup wg sync.WaitGroup
} }
@@ -58,6 +63,7 @@ type dispatcherOptions struct {
poller Poller poller Poller
fsmStorage FSMStorage fsmStorage FSMStorage
eventIsolation EventIsolation eventIsolation EventIsolation
logger zerolog.Logger
} }
type dispatcherOptionWrapper struct { type dispatcherOptionWrapper struct {
@@ -78,6 +84,7 @@ func NewDispatcher(api *maxbot.Api, options ...DispatcherOption) *Dispatcher {
d.poller = opts.poller d.poller = opts.poller
d.fsmStorage = opts.fsmStorage d.fsmStorage = opts.fsmStorage
d.eventIsolation = opts.eventIsolation d.eventIsolation = opts.eventIsolation
d.logger = opts.logger
return d return d
} }
@@ -104,11 +111,49 @@ func (d *Dispatcher) Start(ctx context.Context) error {
return err return err
} }
if d.onStart != nil {
if err := d.onStart(ctx); err != nil {
return err
}
}
d.handleUpdates(ch) d.handleUpdates(ch)
d.wg.Wait()
return nil return nil
} }
func (d *Dispatcher) Stop(ctx context.Context) error {
if err := d.poller.Stop(ctx); err != nil {
return err
}
done := make(chan struct{})
go func() {
d.wg.Wait()
done <- struct{}{}
}()
select {
case <-ctx.Done():
return ctx.Err()
case <-done:
}
if d.onStop != nil {
return d.onStop(ctx)
}
return nil
}
func (d *Dispatcher) OnStart(f func(ctx context.Context) error) {
d.onStart = f
}
func (d *Dispatcher) OnStop(f func(ctx context.Context) error) {
d.onStop = f
}
func (d *Dispatcher) handleUpdates(ch <-chan model.Update) { func (d *Dispatcher) handleUpdates(ch <-chan model.Update) {
for upd := range ch { for upd := range ch {
d.wg.Go(func() { d.wg.Go(func() {
@@ -117,7 +162,7 @@ func (d *Dispatcher) handleUpdates(ch <-chan model.Update) {
UserID: upd.UserID, UserID: upd.UserID,
ChatID: upd.ChatID, ChatID: upd.ChatID,
} }
l := log.With(). l := d.logger.With().
Str("update_id", key.UpdateID.String()). Str("update_id", key.UpdateID.String()).
Int64("user_id", key.UserID). Int64("user_id", key.UserID).
Int64("chat_id", key.ChatID). Int64("chat_id", key.ChatID).
@@ -127,7 +172,7 @@ func (d *Dispatcher) handleUpdates(ch <-chan model.Update) {
defer cancel() defer cancel()
if err := d.eventIsolation.Lock(lockCtx, key); err != nil { if err := d.eventIsolation.Lock(lockCtx, key); err != nil {
log.Error(). d.logger.Error().
Err(err). Err(err).
Int64("user_id", key.UserID). Int64("user_id", key.UserID).
Int64("chat_id", key.ChatID). Int64("chat_id", key.ChatID).
@@ -157,13 +202,13 @@ func (d *Dispatcher) handleUpdates(ch <-chan model.Update) {
func (d *Dispatcher) handleUpdate(ctx context.Context, upd model.Update) { func (d *Dispatcher) handleUpdate(ctx context.Context, upd model.Update) {
var st *state.Context var st *state.Context
snapshot, err := d.fsmStorage.Get(ctx, int(upd.UserID)) snapshot, err := d.fsmStorage.Get(ctx, isolation.Scope{UserID: upd.UserID, ChatID: upd.ChatID})
if err != nil && !errors.Is(err, state.ErrStateNotFound) { if err != nil && !errors.Is(err, state.ErrStateNotFound) {
log.Error().Err(err).Send() d.logger.Error().Err(err).Send()
return return
} }
if errors.Is(err, state.ErrStateNotFound) { if errors.Is(err, state.ErrStateNotFound) {
st = state.New(int(upd.UserID)) st = state.New(isolation.Scope{UserID: upd.UserID, ChatID: upd.ChatID})
} else { } else {
st = state.Restore(snapshot) st = state.Restore(snapshot)
} }
@@ -171,22 +216,20 @@ func (d *Dispatcher) handleUpdate(ctx context.Context, upd model.Update) {
c := mcontext.New(ctx, upd, st, d.api) c := mcontext.New(ctx, upd, st, d.api)
if err := d.rt.HandleUpdate(c); err != nil { if err := d.rt.HandleUpdate(c); err != nil {
log.Error().Err(err).Send() d.logger.Error().Err(err).Send()
return return
} }
st = c.State() st = c.State()
if st.State() == state.ClearState { if st.IsEmpty() {
if err := d.fsmStorage.Delete(ctx, int(upd.UserID)); err != nil { if err := d.fsmStorage.Delete(ctx, isolation.Scope{UserID: upd.UserID, ChatID: upd.ChatID}); err != nil {
log.Error().Err(err).Send() d.logger.Error().Err(err).Send()
return
} }
return
} }
if st.State() != "" {
if err := d.fsmStorage.Put(ctx, st.Snapshot()); err != nil { if err := d.fsmStorage.Put(ctx, st.Snapshot()); err != nil {
log.Error().Err(err).Send() d.logger.Error().Err(err).Send()
return
}
} }
} }
@@ -208,11 +251,18 @@ func WithEventIsolation(eventIsolation EventIsolation) DispatcherOption {
}) })
} }
func WithZerolog(logger zerolog.Logger) DispatcherOption {
return newOption(func(options *dispatcherOptions) {
options.logger = logger
})
}
func newDispatcherOptions(api *maxbot.Api) *dispatcherOptions { func newDispatcherOptions(api *maxbot.Api) *dispatcherOptions {
return &dispatcherOptions{ return &dispatcherOptions{
poller: NewLongPoller(api), poller: pollers.NewLongPoller(api),
fsmStorage: memory.NewFSMStorage(), fsmStorage: memory.NewFSMStorage(),
eventIsolation: memory.NewEventIsolation(defaultTTL), eventIsolation: memory.NewEventIsolation(defaultTTL),
logger: zerolog.Nop(),
} }
} }
+200 -53
View File
@@ -1,4 +1,4 @@
package core package pollers
import ( import (
"context" "context"
@@ -6,6 +6,7 @@ import (
"errors" "errors"
maxbot "github.com/max-messenger/max-bot-api-client-go/v2" maxbot "github.com/max-messenger/max-bot-api-client-go/v2"
"github.com/max-messenger/max-bot-api-client-go/v2/model" "github.com/max-messenger/max-bot-api-client-go/v2/model"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"net" "net"
"net/http" "net/http"
@@ -29,11 +30,14 @@ type LongPollerOption interface {
type Webhook struct { type Webhook struct {
srv *http.Server srv *http.Server
ch chan model.Update ch chan model.Update
wg *sync.WaitGroup
} }
type LongPoller struct { type LongPoller struct {
api *maxbot.Api api *maxbot.Api
pollInterval time.Duration pollInterval time.Duration
logger zerolog.Logger
wg sync.WaitGroup
} }
type TestPoller struct { type TestPoller struct {
@@ -41,6 +45,19 @@ type TestPoller struct {
mu sync.Mutex mu sync.Mutex
} }
type backoff struct {
base time.Duration
max time.Duration
window []bool
pos int
count int
errors int
sinceAdjust int
interval time.Duration
}
type webhookOptions struct { type webhookOptions struct {
addr string addr string
tlsConfig *tls.Config tlsConfig *tls.Config
@@ -55,6 +72,11 @@ type webhookOptions struct {
type longPollerOptions struct { type longPollerOptions struct {
pollInterval time.Duration pollInterval time.Duration
logger zerolog.Logger
}
type longPollerOptionWrapper struct {
fn func(opts *longPollerOptions)
} }
type webhookOptionWrapper struct { type webhookOptionWrapper struct {
@@ -98,38 +120,36 @@ func NewWebhookPoller(api *maxbot.Api, secret string, options ...WebhookOption)
return wh return wh
} }
func (w *Webhook) Start(_ context.Context) (<-chan model.Update, error) { func (w *Webhook) Start(ctx context.Context) (<-chan model.Update, error) {
errCh := make(chan error, 1) l, err := net.Listen("tcp", w.srv.Addr)
defer close(errCh) if err != nil {
return nil, err
}
go func() { go func() {
var err error var err error
defer func() {
errCh <- err
if err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatal().Err(err).Msg("webhook server error")
}
}()
if w.srv.TLSConfig == nil { if w.srv.TLSConfig == nil {
err = w.srv.ListenAndServe() err = w.srv.Serve(l)
return } else {
err = w.srv.ServeTLS(l, "", "")
} }
err = w.srv.ListenAndServeTLS("", "") if err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Ctx(ctx).Error().Err(err).Msg("failed to start webhook server")
}
}() }()
select { return w.ch, nil
case err := <-errCh:
return nil, err
case <-time.After(defaultWhStartTimeout):
return w.ch, nil
}
} }
func (w *Webhook) Stop(ctx context.Context) error { func (w *Webhook) Stop(ctx context.Context) error {
close(w.ch) defer close(w.ch)
return w.srv.Shutdown(ctx)
if err := w.srv.Shutdown(ctx); err != nil {
return err
}
return nil
} }
func NewLongPoller(api *maxbot.Api, options ...LongPollerOption) *LongPoller { func NewLongPoller(api *maxbot.Api, options ...LongPollerOption) *LongPoller {
@@ -143,50 +163,38 @@ func NewLongPoller(api *maxbot.Api, options ...LongPollerOption) *LongPoller {
} }
lp.pollInterval = opts.pollInterval lp.pollInterval = opts.pollInterval
lp.logger = opts.logger
return lp return lp
} }
func (lp *LongPoller) Start(ctx context.Context) (<-chan model.Update, error) { func (lp *LongPoller) Start(ctx context.Context) (<-chan model.Update, error) {
var updates []model.Update
var marker int64
var err error
ch := make(chan model.Update) ch := make(chan model.Update)
go func() { lp.wg.Go(func() {
defer close(ch) defer close(ch)
lp.poll(ctx, ch)
// todo: backoff on errors })
for {
select {
case <-ctx.Done():
return
case <-time.Tick(lp.pollInterval):
start := time.Now()
updates, marker, err = lp.api.Subscriptions.GetUpdates(ctx, marker)
latency := time.Since(start)
if err != nil && !errors.Is(err, context.Canceled) {
log.Error().Err(err).Send()
}
log.Ctx(ctx).Debug().
Int("received", len(updates)).
Dur("latency", latency).
Msg("received updates")
for _, update := range updates {
select {
case ch <- update:
}
}
}
}
}()
return ch, nil return ch, nil
} }
func (lp *LongPoller) Stop(ctx context.Context) error {
done := make(chan struct{})
go func() {
lp.wg.Wait()
done <- struct{}{}
}()
select {
case <-ctx.Done():
return ctx.Err()
case <-done:
return nil
}
}
func (tp *TestPoller) Inject(ctx context.Context, updates []model.Update) { func (tp *TestPoller) Inject(ctx context.Context, updates []model.Update) {
ch := make(chan model.Update, 1) ch := make(chan model.Update, 1)
@@ -210,6 +218,22 @@ func (tp *TestPoller) Start(_ context.Context) (<-chan model.Update, error) {
return tp.ch, nil return tp.ch, nil
} }
func (tp *TestPoller) Stop(_ context.Context) error {
return nil
}
func WithPollInterval(interval time.Duration) LongPollerOption {
return newLpOption(func(opts *longPollerOptions) {
opts.pollInterval = interval
})
}
func WithZerolog(logger zerolog.Logger) LongPollerOption {
return newLpOption(func(opts *longPollerOptions) {
opts.logger = logger
})
}
func WithAddress(addr string) WebhookOption { func WithAddress(addr string) WebhookOption {
return newWhOption(func(opts *webhookOptions) { return newWhOption(func(opts *webhookOptions) {
opts.addr = addr opts.addr = addr
@@ -264,9 +288,112 @@ func WithConnContext(fn func(ctx context.Context, c net.Conn) context.Context) W
}) })
} }
func newBackoff(
base, max time.Duration,
windowSize int,
) *backoff {
if base <= 0 || max < base || windowSize <= 0 {
panic("backoff: invalid configuration")
}
return &backoff{
base: base,
max: max,
window: make([]bool, windowSize),
interval: base,
}
}
func (b *backoff) observe(err error) time.Duration {
failed := err != nil
if b.count == len(b.window) && b.window[b.pos] {
b.errors--
} else {
b.count++
}
b.window[b.pos] = failed
if failed {
b.errors++
}
b.pos = (b.pos + 1) % len(b.window)
b.sinceAdjust++
if b.count < len(b.window) ||
b.sinceAdjust < len(b.window) {
return b.interval
}
b.sinceAdjust = 0
errorRate := float64(b.errors) / float64(b.count)
switch {
case errorRate >= 0.3:
b.interval = min(b.interval*2, b.max)
case errorRate <= 0.1:
b.interval = max(b.interval/2, b.base)
}
return b.interval
}
func (lp *LongPoller) poll(ctx context.Context, ch chan model.Update) {
b := newBackoff(
lp.pollInterval,
30*time.Second,
20,
)
var marker int64
for {
if err := wait(ctx, b.interval); err != nil {
return
}
start := time.Now()
updates, nextMarker, err := lp.api.Subscriptions.GetUpdates(ctx, marker)
latency := time.Since(start)
if err != nil {
if errors.Is(err, context.Canceled) ||
ctx.Err() != nil {
return
}
delay := b.observe(err)
lp.logger.Error().
Err(err).
Dur("latency", latency).
Dur("next_interval", delay).
Msg("failed to receive updates")
continue
}
b.observe(nil)
marker = nextMarker
lp.logger.Debug().
Int("received", len(updates)).
Dur("latency", latency).
Msg("received updates")
for _, update := range updates {
select {
case ch <- update:
}
}
}
}
func newLongPollerOptions() *longPollerOptions { func newLongPollerOptions() *longPollerOptions {
return &longPollerOptions{ return &longPollerOptions{
pollInterval: defaultInterval, pollInterval: defaultInterval,
logger: zerolog.Nop(),
} }
} }
@@ -276,10 +403,30 @@ func newWebhookOptions() *webhookOptions {
} }
} }
func newLpOption(fn func(opts *longPollerOptions)) LongPollerOption {
return longPollerOptionWrapper{fn: fn}
}
func newWhOption(fn func(options *webhookOptions)) WebhookOption { func newWhOption(fn func(options *webhookOptions)) WebhookOption {
return webhookOptionWrapper{fn: fn} return webhookOptionWrapper{fn: fn}
} }
func (low longPollerOptionWrapper) apply(opts *longPollerOptions) {
low.fn(opts)
}
func (wow webhookOptionWrapper) apply(opts *webhookOptions) { func (wow webhookOptionWrapper) apply(opts *webhookOptions) {
wow.fn(opts) wow.fn(opts)
} }
func wait(ctx context.Context, delay time.Duration) error {
timer := time.NewTimer(delay)
defer timer.Stop()
select {
case <-ctx.Done():
return ctx.Err()
case <-timer.C:
return nil
}
}
+1 -1
View File
@@ -2,7 +2,7 @@ package filters
import ( import (
mcontext "github.com/abelith/gomax/v1/context" mcontext "github.com/abelith/gomax/v1/context"
"github.com/abelith/gomax/v1/state" "github.com/abelith/gomax/v1/fsm/state"
"github.com/max-messenger/max-bot-api-client-go/v2/model" "github.com/max-messenger/max-bot-api-client-go/v2/model"
"regexp" "regexp"
"strings" "strings"
@@ -2,8 +2,8 @@ package memory
import ( import (
"context" "context"
"github.com/abelith/gomax/v1/fsm/state"
"github.com/abelith/gomax/v1/isolation" "github.com/abelith/gomax/v1/isolation"
"github.com/abelith/gomax/v1/state"
"github.com/google/uuid" "github.com/google/uuid"
"sync" "sync"
"time" "time"
@@ -19,6 +19,11 @@ type EventIsolation struct {
ttl time.Duration ttl time.Duration
} }
type fsmKey struct {
userID int64
chatID int64
}
type eventIsolationKey struct { type eventIsolationKey struct {
userID int userID int
chatID int chatID int
@@ -34,8 +39,11 @@ func NewFSMStorage() *FSMStorage {
return &FSMStorage{} return &FSMStorage{}
} }
func (f *FSMStorage) Get(_ context.Context, userID int) (state.Snapshot, error) { func (f *FSMStorage) Get(_ context.Context, scope isolation.Scope) (state.Snapshot, error) {
st, ok := f.m.Load(userID) st, ok := f.m.Load(fsmKey{
userID: scope.UserID,
chatID: scope.ChatID,
})
if !ok { if !ok {
return state.Snapshot{}, state.ErrStateNotFound return state.Snapshot{}, state.ErrStateNotFound
} }
@@ -44,12 +52,18 @@ func (f *FSMStorage) Get(_ context.Context, userID int) (state.Snapshot, error)
} }
func (f *FSMStorage) Put(_ context.Context, snapshot state.Snapshot) error { func (f *FSMStorage) Put(_ context.Context, snapshot state.Snapshot) error {
f.m.Store(snapshot.ID, snapshot) f.m.Store(fsmKey{
userID: snapshot.Scope.UserID,
chatID: snapshot.Scope.ChatID,
}, snapshot)
return nil return nil
} }
func (f *FSMStorage) Delete(_ context.Context, userID int) error { func (f *FSMStorage) Delete(_ context.Context, scope isolation.Scope) error {
f.m.Delete(userID) f.m.Delete(fsmKey{
userID: scope.UserID,
chatID: scope.ChatID,
})
return nil return nil
} }
+68 -18
View File
@@ -3,8 +3,8 @@ package state
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/abelith/gomax/v1/isolation"
"github.com/rs/zerolog/log" "strings"
) )
var ( var (
@@ -12,9 +12,15 @@ var (
ErrKeyNotFound = fmt.Errorf("state: key not found") ErrKeyNotFound = fmt.Errorf("state: key not found")
) )
const ClearState State = "clear" type State struct {
group string
name string
}
type State string type Group struct {
name string
states map[string]*State
}
type Codec interface { type Codec interface {
Marshal(v any) ([]byte, error) Marshal(v any) ([]byte, error)
@@ -32,25 +38,69 @@ func (JSONCodec) Unmarshal(d []byte, v any) error {
} }
type Snapshot struct { type Snapshot struct {
ID int `json:"id"` Scope isolation.Scope `json:"id"`
State State `json:"state"` State string `json:"state"`
Data map[string][]byte `json:"data"` Data map[string][]byte `json:"data"`
} }
type Context struct { type Context struct {
id int scope isolation.Scope
state State state State
data map[string][]byte data map[string][]byte
codec Codec codec Codec
clear bool
} }
type Option func(*Context) type Option func(*Context)
func NewStateGroup(name string) *Group {
return &Group{
name: name,
states: make(map[string]*State),
}
}
func (g *Group) State(name string) State {
state := g.states[name]
if state != nil {
return *state
}
if state = g.states[name]; state != nil {
return *state
}
state = &State{
group: g.name,
name: name,
}
g.states[name] = state
return *state
}
func (s *State) String() string {
return fmt.Sprintf("%s:%s", s.group, s.name)
}
func fromString(s string) State {
pieces := strings.Split(s, ":")
if len(pieces) < 2 {
return State{}
}
return State{
group: pieces[0],
name: pieces[1],
}
}
func WithCodec(c Codec) Option { return func(sc *Context) { sc.codec = c } } func WithCodec(c Codec) Option { return func(sc *Context) { sc.codec = c } }
func New(id int, opts ...Option) *Context { func New(scope isolation.Scope, opts ...Option) *Context {
sc := &Context{ sc := &Context{
id: id, scope: scope,
data: make(map[string][]byte), data: make(map[string][]byte),
codec: JSONCodec{}, codec: JSONCodec{},
} }
@@ -61,8 +111,8 @@ func New(id int, opts ...Option) *Context {
} }
func Restore(s Snapshot, opts ...Option) *Context { func Restore(s Snapshot, opts ...Option) *Context {
sc := New(s.ID, opts...) sc := New(s.Scope, opts...)
sc.state = s.State sc.state = fromString(s.State)
for k, v := range s.Data { for k, v := range s.Data {
sc.data[k] = v sc.data[k] = v
} }
@@ -74,25 +124,25 @@ func (sc *Context) Snapshot() Snapshot {
for k, v := range sc.data { for k, v := range sc.data {
data[k] = v data[k] = v
} }
return Snapshot{ID: sc.id, State: sc.state, Data: data} return Snapshot{Scope: sc.scope, State: sc.state.String(), Data: data}
} }
func (sc *Context) Set(state State) { func (sc *Context) Set(state State) {
log.Debug().
Int("user_id", sc.id).
Str("from", string(sc.state)).
Str("to", string(state)).
Msg("fsm state transition")
sc.state = state sc.state = state
} }
func (sc *Context) State() State { return sc.state } func (sc *Context) State() State { return sc.state }
func (sc *Context) Clear() { func (sc *Context) Clear() {
sc.state = ClearState sc.state = State{}
sc.clear = true
sc.data = make(map[string][]byte) sc.data = make(map[string][]byte)
} }
func (sc *Context) IsEmpty() bool {
return sc.state == State{} && sc.clear
}
func (sc *Context) Store(key string, val any) error { func (sc *Context) Store(key string, val any) error {
b, err := sc.codec.Marshal(val) b, err := sc.codec.Marshal(val)
if err != nil { if err != nil {
+14 -25
View File
@@ -2,20 +2,20 @@ package middleware
import ( import (
"errors" "errors"
"fmt"
mcontext "github.com/abelith/gomax/v1/context" mcontext "github.com/abelith/gomax/v1/context"
"github.com/abelith/gomax/v1/core" "github.com/abelith/gomax/v1/core"
merrors "github.com/abelith/gomax/v1/errors" merrors "github.com/abelith/gomax/v1/errors"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
) )
func MwPanicHandler() core.Middleware { func MwPanicHandler(onPanic func()) core.Middleware {
return func(next core.HandlerFunc) core.HandlerFunc { return func(next core.HandlerFunc) core.HandlerFunc {
return func(c *mcontext.Context) error { return func(c *mcontext.Context) (err error) {
defer func() { defer func() {
p := recover() p := recover()
if p != nil { if p != nil {
log.Fatal().Any("panic", p).Send() onPanic()
err = fmt.Errorf("panic: %v", p)
} }
}() }()
return next(c) return next(c)
@@ -30,45 +30,34 @@ func MwErrorResponder(defaultMsg string) core.Middleware {
var maxErr *merrors.GoMaxError var maxErr *merrors.GoMaxError
if errors.As(err, &maxErr) { if errors.As(err, &maxErr) {
return c.Respond(maxErr.Message) return errors.Join(c.Respond(maxErr.Message), maxErr)
} }
if err != nil { if err != nil {
return c.Respond(defaultMsg) return errors.Join(c.Respond(defaultMsg), err)
} }
return nil return nil
} }
} }
} }
func MwLoggerInjector(logger zerolog.Logger) core.Middleware { func MwCallback(callback func(c *mcontext.Context)) core.Middleware {
return func(next core.HandlerFunc) core.HandlerFunc { return func(next core.HandlerFunc) core.HandlerFunc {
return func(c *mcontext.Context) error { return func(c *mcontext.Context) error {
ctx := logger.With().Int64("user_id", c.Update().UserID). callback(c)
Logger().WithContext(c.Context())
c.SetContext(ctx)
return next(c) return next(c)
} }
} }
} }
func MwDebugState() core.Middleware { func MwErrorHandler(onError func(err error)) core.Middleware {
return func(next core.HandlerFunc) core.HandlerFunc { return func(next core.HandlerFunc) core.HandlerFunc {
return func(c *mcontext.Context) error { return func(c *mcontext.Context) error {
log.Debug().Any("state", c.State()).Any("update", c.Update()).Send() var err error
return next(c) if err = next(c); err != nil {
} onError(err)
}
}
func MwErrorLogger() core.Middleware {
return func(next core.HandlerFunc) core.HandlerFunc {
return func(c *mcontext.Context) error {
if err := next(c); err != nil {
log.Ctx(c).Err(err).Send()
return err
} }
return nil return err
} }
} }
} }