-
Notifications
You must be signed in to change notification settings - Fork 0
Implement base telegram bot #9
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Olderestin
wants to merge
20
commits into
main
Choose a base branch
from
feature/yatgbot
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
227e1bb
feat(yatgbot): Implement `fsm`
Olderestin d72e1d8
feat(yatgbot): Implement `message_queue`
Olderestin fd94634
feat(yatgbot): Implement base tg bot
Olderestin ab9fdec
refactor(yatgbot): Partial refactor
Olderestin b1fa78f
refactor(yatgbot): Separate yafsm & improve API
Olderestin 883b995
chore(yatgbot): Add docstrings
Olderestin ea24f52
chore(yatgbot): Update docstrings
Olderestin bbff652
fix(yatgbot): Correct `stateDataKey` constant
Olderestin b702542
test(yatgbot): Add unit tests
Olderestin 22c3859
Merge branch 'main' into feature/yatgbot
YaCodesDevelopment 4605d2f
refactor(yatgbot): Add yalocales
Olderestin 8c2ee6a
chore(yatgbot): Improve code
Olderestin f9d979c
Merge branch 'main' into feature/yatgbot
Olderestin 62e6503
feat(yatgbot): Support new tg updates
Olderestin 3c6fa58
fix(yatgbot): `messagequeqe` sorting by timestamp
Olderestin 2d5549f
style(yatgbot): Improve code formatting and naming
Olderestin b778c10
chore(yatgbot): Update docstrings
Olderestin a8357d7
refactor(yatgbot): Remove unecessary check
Olderestin cc67fcc
chore(yatgbot): Ignore `unexported-return` for `fsm`
Olderestin c0fab37
fix(yatgbot): `messagequeqe` sorting by timestamp
Olderestin File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| package yafsm | ||
|
|
||
| const ( | ||
| stateKey = "state" | ||
| stateDataKey = "stateData" | ||
| ) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| package yafsm | ||
|
|
||
| import ( | ||
| "context" | ||
|
|
||
| "github.com/YaCodeDev/GoYaCodeDevUtils/yaerrors" | ||
| ) | ||
|
|
||
| // EntityFSMStorage is a wrapper over FSM to work with specific entity (user, chat, etc). | ||
| type EntityFSMStorage struct { | ||
| storage FSM | ||
| uid string | ||
| } | ||
|
|
||
| // NewUserFSMStorage creates a new EntityFSMStorage for a specific user ID. | ||
| // | ||
| // Example usage: | ||
| // | ||
| // userFSMStorage := NewUserFSMStorage(fsmStorage, "user123") | ||
| func NewUserFSMStorage( | ||
| storage FSM, | ||
| uid string, | ||
| ) *EntityFSMStorage { | ||
| return &EntityFSMStorage{ | ||
| storage: storage, | ||
| uid: uid, | ||
| } | ||
| } | ||
|
|
||
| // SetState sets the state for the entity. | ||
| // | ||
| // Example usage: | ||
| // | ||
| // err := userFSMStorage.SetState(ctx, &SomeState{Field: "value"}) | ||
| // | ||
| // if err != nil { | ||
| // // handle error | ||
| // } | ||
| func (b *EntityFSMStorage) SetState( | ||
| ctx context.Context, | ||
| stateData State, | ||
| ) yaerrors.Error { | ||
| return b.storage.SetState(ctx, b.uid, stateData) | ||
| } | ||
|
|
||
| // GetState retrieves the current state and its data for the entity. | ||
| // | ||
| // Example usage: | ||
| // | ||
| // stateName, stateData, err := userFSMStorage.GetState(ctx) | ||
| // | ||
| // if err != nil { | ||
| // // handle error | ||
| // } | ||
| func (b *EntityFSMStorage) GetState( | ||
| ctx context.Context, | ||
| ) (string, stateDataMarshalled, yaerrors.Error) { // nolint: revive | ||
| return b.storage.GetState(ctx, b.uid) | ||
| } | ||
|
|
||
| // GetStateData unmarshals the state data into the provided empty state struct. | ||
| // | ||
| // Example usage: | ||
| // | ||
| // var stateData SomeState | ||
| // | ||
| // err := userFSMStorage.GetStateData(marshalledData, &stateData) | ||
| // | ||
| // if err != nil { | ||
| // // handle error | ||
| // } | ||
| func (b *EntityFSMStorage) GetStateData( | ||
| stateData stateDataMarshalled, | ||
| emptyState State, | ||
| ) yaerrors.Error { | ||
| return b.storage.GetStateData(stateData, emptyState) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,196 @@ | ||
| package yafsm | ||
|
|
||
| import ( | ||
| "context" | ||
| "encoding/json" | ||
| "net/http" | ||
| "reflect" | ||
|
|
||
| "github.com/YaCodeDev/GoYaCodeDevUtils/yacache" | ||
| "github.com/YaCodeDev/GoYaCodeDevUtils/yaerrors" | ||
| ) | ||
|
|
||
| // State is an interface that all states must implement. | ||
| type State interface { | ||
| StateName() string | ||
| } | ||
|
|
||
| // BaseState provides a default implementation of the State interface. | ||
| type BaseState[T State] struct{} | ||
|
|
||
| // StateName returns the name of the state type. | ||
| func (BaseState[T]) StateName() string { | ||
Olderestin marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| var zero T | ||
|
|
||
| t := reflect.TypeOf(zero) | ||
| if t.Kind() == reflect.Pointer { | ||
| t = t.Elem() | ||
| } | ||
|
|
||
| return t.Name() | ||
| } | ||
|
|
||
| // Empty state is implementation of State interface with no data. | ||
| type EmptyState struct { | ||
| BaseState[EmptyState] | ||
| } | ||
|
|
||
| // stateDataMarshalled is a type alias for marshalled state data. | ||
| type stateDataMarshalled string | ||
|
|
||
| // StateAndData is a struct that holds the state name and its marshalled data. | ||
| type StateAndData struct { | ||
| State string `json:"state"` | ||
| StateData string `json:"stateData"` | ||
| } | ||
|
|
||
| // FSM is an interface for finite state machine storage.\ | ||
| type FSM interface { | ||
| SetState(ctx context.Context, uid string, state State) yaerrors.Error | ||
| GetState(ctx context.Context, uid string) (string, stateDataMarshalled, yaerrors.Error) | ||
| GetStateData(stateData stateDataMarshalled, emptyState State) yaerrors.Error | ||
| } | ||
|
|
||
| // DefaultFSMStorage is a default implementation of the FSM interface using yacache. | ||
| type DefaultFSMStorage[T yacache.Container] struct { | ||
| storage yacache.Cache[T] | ||
| defaultState State | ||
| } | ||
|
|
||
| // NewDefaultFSMStorage creates a new instance of DefaultFSMStorage. | ||
| // | ||
| // Example usage: | ||
| // | ||
| // cache := yacache.NewCache(redisClient) | ||
| // | ||
| // fsmStorage := fsm.NewDefaultFSMStorage(cache, fsm.EmptyState{}) | ||
| func NewDefaultFSMStorage[T yacache.Container]( | ||
| storage yacache.Cache[T], | ||
| defaultState State, | ||
| ) *DefaultFSMStorage[T] { | ||
| return &DefaultFSMStorage[T]{ | ||
| storage: storage, | ||
| defaultState: defaultState, | ||
| } | ||
| } | ||
|
|
||
| // SetState sets the state for a given user ID. | ||
| // The state data is marshalled to JSON before being stored. | ||
| // | ||
| // Example usage: | ||
| // | ||
| // err := fsmStorage.SetState(ctx, "123", &SomeState{Field: "value"}) | ||
| func (b *DefaultFSMStorage[T]) SetState( | ||
| ctx context.Context, | ||
| uid string, | ||
| stateData State, | ||
| ) yaerrors.Error { | ||
| val, err := json.Marshal(stateData) | ||
| if err != nil { | ||
| return yaerrors.FromError( | ||
| http.StatusInternalServerError, | ||
| err, | ||
| "failed to marshal state data", | ||
| ) | ||
| } | ||
|
|
||
| val, err = json.Marshal(StateAndData{ | ||
| State: stateData.StateName(), | ||
| StateData: string(val), | ||
| }) | ||
| if err != nil { | ||
| return yaerrors.FromError( | ||
| http.StatusInternalServerError, | ||
| err, | ||
| "failed to marshal state data", | ||
| ) | ||
| } | ||
|
|
||
| return b.storage.Set(ctx, uid, string(val), 0) | ||
| } | ||
|
|
||
| // GetState retrieves the current state and its marshalled data for a given user ID. | ||
| // If no state is found, it returns the default state. | ||
| // | ||
| // Example usage: | ||
| // | ||
| // stateName, stateData, err := fsmStorage.GetState(ctx, "123") | ||
| func (b *DefaultFSMStorage[T]) GetState( | ||
| ctx context.Context, | ||
| uid string, | ||
| ) (string, stateDataMarshalled, yaerrors.Error) { // nolint: revive | ||
| data, err := b.storage.Get(ctx, uid) | ||
| if err != nil { | ||
| return b.defaultState.StateName(), "", nil | ||
| } | ||
|
|
||
| var stateAndData map[string]string | ||
|
|
||
| if err := json.Unmarshal([]byte(data), &stateAndData); err != nil { | ||
| return "", "", yaerrors.FromError( | ||
| http.StatusInternalServerError, | ||
| err, | ||
| "failed to unmarshal state data map", | ||
| ) | ||
| } | ||
|
|
||
| state, ok := stateAndData[stateKey] | ||
|
|
||
| if !ok { | ||
| return "", "", yaerrors.FromString( | ||
| http.StatusNotFound, | ||
| "failed to get state", | ||
| ) | ||
| } | ||
|
|
||
| return state, stateDataMarshalled(data), nil | ||
| } | ||
|
|
||
| // GetStateData unmarshals the state data into the provided empty state struct. | ||
| // | ||
| // Example usage: | ||
| // | ||
| // var stateData SomeState | ||
| // | ||
| // err := fsmStorage.GetStateData(marshalledData, &stateData) | ||
| // | ||
| // if err != nil { | ||
| // // handle error | ||
| // } | ||
| func (b *DefaultFSMStorage[T]) GetStateData( | ||
| stateData stateDataMarshalled, | ||
| emptyState State, | ||
Olderestin marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| ) yaerrors.Error { | ||
| if stateData == "" { | ||
Olderestin marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| return nil | ||
| } | ||
|
|
||
| var stateAndData map[string]string | ||
|
|
||
| if err := json.Unmarshal([]byte(stateData), &stateAndData); err != nil { | ||
| return yaerrors.FromError( | ||
| http.StatusInternalServerError, | ||
| err, | ||
| "failed to unmarshal state data map", | ||
| ) | ||
| } | ||
|
|
||
| stateDataMarshalled, ok := stateAndData[stateDataKey] | ||
|
|
||
| if !ok { | ||
| return yaerrors.FromString( | ||
| http.StatusNotFound, | ||
| "failed to get state data", | ||
| ) | ||
| } | ||
|
|
||
| if err := json.Unmarshal([]byte(stateDataMarshalled), emptyState); err != nil { | ||
| return yaerrors.FromError( | ||
| http.StatusInternalServerError, | ||
| err, | ||
| "failed to unmarshal state data", | ||
| ) | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,101 @@ | ||
| package yafsm_test | ||
|
|
||
| import ( | ||
| "context" | ||
| "encoding/json" | ||
| "errors" | ||
| "testing" | ||
|
|
||
| "github.com/YaCodeDev/GoYaCodeDevUtils/yacache" | ||
| "github.com/YaCodeDev/GoYaCodeDevUtils/yafsm" | ||
| ) | ||
|
|
||
| type ExampleState struct { | ||
| yafsm.BaseState[ExampleState] | ||
|
|
||
| Param string `json:"param"` | ||
| } | ||
|
|
||
| func TestFSMStorage_SetGetRoundTrip(t *testing.T) { | ||
| ctx := context.Background() | ||
|
|
||
| cache := yacache.NewCache(yacache.NewMemoryContainer()) | ||
|
|
||
| fsm := yafsm.NewDefaultFSMStorage(cache, yafsm.EmptyState{}) | ||
|
|
||
| uid := "12345" | ||
| wantParam := "exampleparam" | ||
|
|
||
| // 1) set | ||
| if err := fsm.SetState(ctx, uid, ExampleState{Param: wantParam}); err != nil { | ||
| t.Fatalf("SetState failed: %v", err) | ||
| } | ||
|
|
||
| // 2) get state name + raw payload | ||
| stateName, raw, err := fsm.GetState(ctx, uid) | ||
| if err != nil { | ||
| t.Fatalf("GetState failed: %v", err) | ||
| } | ||
|
|
||
| if stateName != (ExampleState{}).StateName() { | ||
| t.Fatalf("unexpected state name: want %q, got %q", | ||
| (ExampleState{}).StateName(), stateName) | ||
| } | ||
|
|
||
| // 3) unmarshal into struct | ||
| var got ExampleState | ||
| if err := fsm.GetStateData(raw, &got); err != nil { | ||
| t.Fatalf("GetStateData failed: %v", err) | ||
| } | ||
|
|
||
| if got.Param != wantParam { | ||
| t.Fatalf("unexpected param: want %q, got %q", wantParam, got.Param) | ||
| } | ||
| } | ||
|
|
||
| func TestFSMStorage_DefaultStateReturned(t *testing.T) { | ||
| ctx := context.Background() | ||
|
|
||
| cache := yacache.NewCache(yacache.NewMemoryContainer()) | ||
| fsm := yafsm.NewDefaultFSMStorage(cache, yafsm.EmptyState{}) | ||
|
|
||
| uid := "non-existent" | ||
|
|
||
| name, raw, err := fsm.GetState(ctx, uid) | ||
| if err != nil { | ||
| t.Fatalf("GetState failed: %v", err) | ||
| } | ||
|
|
||
| if name != (yafsm.EmptyState{}).StateName() { | ||
| t.Fatalf("expected default state name %q, got %q", | ||
| (yafsm.EmptyState{}).StateName(), name) | ||
| } | ||
|
|
||
| if raw != "" { | ||
| t.Fatalf("expected empty raw data, got %q", raw) | ||
| } | ||
| } | ||
|
|
||
| func TestFSMStorage_CorruptedPayload(t *testing.T) { | ||
| ctx := context.Background() | ||
|
|
||
| cache := yacache.NewCache(yacache.NewMemoryContainer()) | ||
| fsm := yafsm.NewDefaultFSMStorage(cache, yafsm.EmptyState{}) | ||
|
|
||
| uid := "bad:user" | ||
|
|
||
| err := cache.Set(ctx, uid, "{not:a:json}", 0) | ||
| if err != nil { | ||
| t.Fatalf("failed to set corrupted data: %v", err) | ||
| } | ||
|
|
||
| _, _, err = fsm.GetState(ctx, uid) | ||
| if err == nil { | ||
| t.Fatal("expected error on corrupted JSON, got nil") | ||
| } | ||
|
|
||
| var syntaxErr *json.SyntaxError | ||
| if !errors.As(err, &syntaxErr) { | ||
| t.Fatalf("expected json.SyntaxError, got %v", err) | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.