From b145a284767a1339a3290aca37886e6751f2c0a9 Mon Sep 17 00:00:00 2001 From: Vlad Lavrishko Date: Wed, 30 Jul 2025 17:00:14 +0300 Subject: [PATCH 01/13] feat(yatgstorage): update `API` usage --- yatgstorage/yatgstorage.go | 81 +++++++++++++++++---------------- yatgstorage/yatgstorage_test.go | 15 +++--- 2 files changed, 50 insertions(+), 46 deletions(-) diff --git a/yatgstorage/yatgstorage.go b/yatgstorage/yatgstorage.go index b12bd8c..66df65c 100644 --- a/yatgstorage/yatgstorage.go +++ b/yatgstorage/yatgstorage.go @@ -28,6 +28,7 @@ import ( "github.com/YaCodeDev/GoYaCodeDevUtils/yacache" "github.com/YaCodeDev/GoYaCodeDevUtils/yaerrors" "github.com/YaCodeDev/GoYaCodeDevUtils/yalogger" + "github.com/YaCodeDev/GoYaCodeDevUtils/yathreadsafeset" "github.com/gotd/td/telegram" "github.com/gotd/td/telegram/updates" "github.com/gotd/td/tg" @@ -121,20 +122,14 @@ type IStorage interface { // // Example: // -// stg := yatgstorage.NewStorage(cache, dispatcher, 42, log) +// stg := yatgstorage.NewStorage(cache, log) // _ = stg // -// A single Storage instance should be used per bot (entityID). -// The struct keeps an internal map to cache “I have already created the base -// JSON object” flags for performance. -// // Because methods are safe for concurrent use (they only rely on redis, which // is thread‑safe), you may share *Storage between goroutines. type Storage struct { cache yacache.Cache[*redis.Client] - handler telegram.UpdateHandler - entityID int64 - stateKeys map[string]struct{} + stateKeys *yathreadsafeset.ThreadSafeSet[string] log yalogger.Logger } @@ -142,27 +137,21 @@ type Storage struct { // // - cache – any yacache implementation; production code passes a Redis // client, tests may pass yacache.NewMock. -// - handler – your app’s dispatcher (implements telegram.UpdateHandler). -// - entityID – unique bot identifier used to namespace all Redis keys. // - log – structured logger. // // Example: // -// stg := yatgstorage.NewStorage(cache, dispatcher, 123456, log) +// stg := yatgstorage.NewStorage(cache, log) // if err := stg.Ping(ctx); err != nil { // log.Fatalf("redis down: %v", err) // } func NewStorage( cache yacache.Cache[*redis.Client], - handler telegram.UpdateHandler, - entityID int64, log yalogger.Logger, ) *Storage { return &Storage{ cache: cache, - handler: handler, - entityID: entityID, - stateKeys: map[string]struct{}{}, + stateKeys: yathreadsafeset.NewThreadSafeSet[string](), log: log, } } @@ -214,7 +203,7 @@ func (s *Storage) GetState( ) (updates.State, bool, yaerrors.Error) { key := getBotStateKey(entityID) - log := s.initBaseFieldsLog("Fetching entity state", key) + log := s.initBaseFieldsLog("Fetching entity state", entityID, key) data, err := s.cache.Raw().JSONGet(ctx, key).Result() if err != nil { @@ -251,7 +240,8 @@ func (s *Storage) SetState( ) yaerrors.Error { key := getBotStateKey(entityID) - log := s.initBaseFieldsLog("Setting entity state", key).WithField(LoggerEntityID, entityID) + log := s.initBaseFieldsLog("Setting entity state", entityID, key). + WithField(LoggerEntityID, entityID) if err := s.cache.Raw().JSONSet(ctx, key, BasePathRedisJSON, state).Err(); err != nil { return yaerrors.FromErrorWithLog( @@ -276,7 +266,7 @@ func (s *Storage) SetPts(ctx context.Context, entityID int64, pts int) yaerrors. key := getBotStateKey(entityID) log := s. - initBaseFieldsLog("Setting pts in entity state", key). + initBaseFieldsLog("Setting pts in entity state", entityID, key). WithField(LoggerEntityID, entityID) if err := s.safetyBaseStateJSON(ctx, key, log); err != nil { @@ -306,7 +296,7 @@ func (s *Storage) SetQts(ctx context.Context, entityID int64, qts int) yaerrors. key := getBotStateKey(entityID) log := s. - initBaseFieldsLog("Setting qts in entity state", key). + initBaseFieldsLog("Setting qts in entity state", entityID, key). WithField(LoggerEntityID, entityID) if err := s.safetyBaseStateJSON(ctx, key, log); err != nil { @@ -336,7 +326,7 @@ func (s *Storage) SetDate(ctx context.Context, entityID int64, date int) yaerror key := getBotStateKey(entityID) log := s. - initBaseFieldsLog("Setting date in state", key). + initBaseFieldsLog("Setting date in state", entityID, key). WithField(LoggerEntityID, entityID) if err := s.safetyBaseStateJSON(ctx, key, log); err != nil { @@ -366,7 +356,7 @@ func (s *Storage) SetSeq(ctx context.Context, entityID int64, seq int) yaerrors. key := getBotStateKey(entityID) log := s. - initBaseFieldsLog("Setting seq in state", key). + initBaseFieldsLog("Setting seq in state", entityID, key). WithField(LoggerEntityID, entityID) if err := s.safetyBaseStateJSON(ctx, key, log); err != nil { @@ -396,7 +386,7 @@ func (s *Storage) SetDateSeq(ctx context.Context, entityID int64, date, seq int) key := getBotStateKey(entityID) log := s. - initBaseFieldsLog("Setting date and seq in state", key). + initBaseFieldsLog("Setting date and seq in state", entityID, key). WithField(LoggerEntityID, entityID) if err := s.safetyBaseStateJSON(ctx, key, log); err != nil { @@ -431,7 +421,7 @@ func (s *Storage) SetChannelPts( key := getChannelPtsKey(entityID) log := s. - initBaseFieldsLog("Setting channel pts", key). + initBaseFieldsLog("Setting channel pts", entityID, key). WithField(LoggerEntityID, entityID). WithField(LoggerChannelID, channelID) @@ -462,7 +452,7 @@ func (s *Storage) GetChannelPts( key := getChannelPtsKey(entityID) log := s. - initBaseFieldsLog("Fetching channel pts", key). + initBaseFieldsLog("Fetching channel pts", entityID, key). WithField(LoggerUserID, entityID). WithField(LoggerChannelID, channelID) @@ -509,7 +499,7 @@ func (s *Storage) ForEachChannels( ) yaerrors.Error { key := getChannelPtsKey(entityID) - log := s.initBaseFieldsLog("Start action for each channels", key). + log := s.initBaseFieldsLog("Start action for each channels", entityID, key). WithField(LoggerUserID, entityID) channels, err := s.cache.HGetAll(ctx, key) @@ -574,7 +564,7 @@ func (s *Storage) SetChannelAccessHash( key := getChannelAccessHashKey(entityID) log := s. - initBaseFieldsLog("Setting channel access hash for channel", key). + initBaseFieldsLog("Setting channel access hash for channel", entityID, key). WithField(LoggerEntityID, entityID). WithField(LoggerChannelID, channelID) @@ -605,7 +595,7 @@ func (s *Storage) GetChannelAccessHash( key := getChannelAccessHashKey(entityID) log := s. - initBaseFieldsLog("Fetching channel access hash", key). + initBaseFieldsLog("Fetching channel access hash", entityID, key). WithField(LoggerEntityID, entityID). WithField(LoggerChannelID, channelID) @@ -667,24 +657,27 @@ func (h HandlerFunc) Handle(ctx context.Context, updates tg.UpdatesClass) error // Example: // // clientOpts.UpdateHandler = storage.AccessHashSaveHandler() -func (s *Storage) AccessHashSaveHandler() HandlerFunc { +func (s *Storage) AccessHashSaveHandler( + entityID int64, + handler telegram.UpdateHandler, +) HandlerFunc { return HandlerFunc(func(ctx context.Context, updates tg.UpdatesClass) error { switch update := updates.(type) { case *tg.Updates: for _, user := range update.MapUsers().NotEmptyToMap() { - if err := s.SetUserAccessHash(ctx, user.ID, user.AccessHash); err != nil { + if err := s.SetUserAccessHash(ctx, entityID, user.ID, user.AccessHash); err != nil { s.log.Errorf("Failed to save user(%d) access hash(%d)", user.ID, user.AccessHash) } } case *tg.UpdatesCombined: for _, user := range update.MapUsers().NotEmptyToMap() { - if err := s.SetUserAccessHash(ctx, user.ID, user.AccessHash); err != nil { + if err := s.SetUserAccessHash(ctx, entityID, user.ID, user.AccessHash); err != nil { s.log.Errorf("Failed to save user(%d) access hash(%d)", user.ID, user.AccessHash) } } } - return s.handler.Handle(ctx, updates) + return handler.Handle(ctx, updates) }) } @@ -696,15 +689,17 @@ func (s *Storage) AccessHashSaveHandler() HandlerFunc { // _ = stg.SetUserAccessHash(ctx, 12345, 67890) func (s *Storage) SetUserAccessHash( ctx context.Context, + entityID int64, userID int64, accessHash int64, ) yaerrors.Error { const botChannelID = 136817688 // Ignore channel placeholder (@Channel_Bot - in Telegram) if userID != botChannelID { - key := getUserAccessHashKey(s.entityID) + key := getUserAccessHashKey(entityID) - log := s.initBaseFieldsLog("Saving access hash", key).WithField(LoggerUserID, userID) + log := s.initBaseFieldsLog("Saving access hash", entityID, key). + WithField(LoggerUserID, userID) if err := s.cache.Raw(). HSet(ctx, key, strconv.FormatInt(userID, 10), accessHash).Err(); err != nil { @@ -727,10 +722,15 @@ func (s *Storage) SetUserAccessHash( // Example: // // hash, foundErr := stg.GetUserAccessHash(ctx, 12345) -func (s *Storage) GetUserAccessHash(ctx context.Context, userID int64) (int64, yaerrors.Error) { - key := getUserAccessHashKey(s.entityID) +func (s *Storage) GetUserAccessHash( + ctx context.Context, + entityID int64, + userID int64, +) (int64, yaerrors.Error) { + key := getUserAccessHashKey(entityID) - log := s.initBaseFieldsLog("fetching user access hash", key).WithField(LoggerUserID, userID) + log := s.initBaseFieldsLog("fetching user access hash", entityID, key). + WithField(LoggerUserID, userID) hash, err := s.cache.Raw().HGet(ctx, key, strconv.FormatInt(userID, 10)).Result() if err != nil { @@ -765,9 +765,10 @@ func (s *Storage) GetUserAccessHash(ctx context.Context, userID int64) (int64, y // l := stg.initBaseFieldsLog("doing work", "redis:key") func (s *Storage) initBaseFieldsLog( entryText string, + entityID int64, botKey string, ) yalogger.Logger { - log := s.log.WithField(LoggerEntityID, s.entityID).WithField(LoggerEntityKey, botKey) + log := s.log.WithField(LoggerEntityID, entityID).WithField(LoggerEntityKey, botKey) log.Debugf("%s", entryText) @@ -785,7 +786,7 @@ func (s *Storage) safetyBaseStateJSON( key string, log yalogger.Logger, ) yaerrors.Error { - if _, ok := s.stateKeys[key]; !ok { + if s.stateKeys.Has(key) { if res, err := s.cache.Raw().JSONGet(ctx, key, BasePathRedisJSON).Result(); err != nil || len(res) == 0 { if err := s.cache.Raw().JSONSet(ctx, key, BasePathRedisJSON, updates.State{}).Err(); err != nil { @@ -798,7 +799,7 @@ func (s *Storage) safetyBaseStateJSON( } } - s.stateKeys[key] = struct{}{} + s.stateKeys.Set(key) } return nil diff --git a/yatgstorage/yatgstorage_test.go b/yatgstorage/yatgstorage_test.go index 1601fb7..3b31a0e 100644 --- a/yatgstorage/yatgstorage_test.go +++ b/yatgstorage/yatgstorage_test.go @@ -36,7 +36,7 @@ func TestStorage_CreateWorks(t *testing.T) { defer cleanup() if err := yatgstorage. - NewStorage(yacache.NewCache(client), nil, 0, yalogger.NewBaseLogger(nil).NewLogger()). + NewStorage(yacache.NewCache(client), yalogger.NewBaseLogger(nil).NewLogger()). Ping(context.Background()); err != nil { t.Fatalf("Failed to create tg storage") } @@ -56,7 +56,7 @@ func TestStorageChannel_WorkFlowWorks(t *testing.T) { defer cleanup() storage := yatgstorage. - NewStorage(yacache.NewCache(client), nil, 1001, log) + NewStorage(yacache.NewCache(client), log) t.Run("Set and Get channel pts - works", func(t *testing.T) { const expected = 1000 @@ -108,16 +108,19 @@ func TestStorageUser_WorkFlowWorks(t *testing.T) { defer cleanup() storage := yatgstorage. - NewStorage(yacache.NewCache(client), nil, 1001, log) + NewStorage(yacache.NewCache(client), log) t.Run("Set and Get user access hash - works", func(t *testing.T) { - const userID = 2222 + const ( + entityID = 1000 + userID = 2222 + ) expected := int64(200) - _ = storage.SetUserAccessHash(ctx, userID, expected) + _ = storage.SetUserAccessHash(ctx, entityID, userID, expected) - result, _ := storage.GetUserAccessHash(ctx, userID) + result, _ := storage.GetUserAccessHash(ctx, entityID, userID) assert.Equal(t, expected, result) }) From 49fde278702349208fda83b45683647db7e232a8 Mon Sep 17 00:00:00 2001 From: Vlad Lavrishko Date: Wed, 30 Jul 2025 22:04:33 +0300 Subject: [PATCH 02/13] feat(yatgstorage): add session storage --- go.mod | 3 + go.sum | 6 + yatgclient/yatgclient.go | 10 +- yatgstorage/session_storage.go | 226 +++++++++++++++++++++++++++++++++ yatgstorage/yatgstorage.go | 11 +- 5 files changed, 250 insertions(+), 6 deletions(-) create mode 100644 yatgstorage/session_storage.go diff --git a/go.mod b/go.mod index 34269f8..da70271 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/sirupsen/logrus v1.9.3 github.com/stretchr/testify v1.10.0 golang.org/x/net v0.42.0 + gorm.io/gorm v1.30.1 ) require ( @@ -29,6 +30,8 @@ require ( github.com/go-faster/yaml v0.4.6 // indirect github.com/gotd/ige v0.2.2 // indirect github.com/gotd/neo v0.1.5 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect github.com/klauspost/compress v1.18.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect diff --git a/go.sum b/go.sum index ecb752d..242ecb2 100644 --- a/go.sum +++ b/go.sum @@ -46,6 +46,10 @@ github.com/gotd/neo v0.1.5 h1:oj0iQfMbGClP8xI59x7fE/uHoTJD7NZH9oV1WNuPukQ= github.com/gotd/neo v0.1.5/go.mod h1:9A2a4bn9zL6FADufBdt7tZt+WMhvZoc5gWXihOPoiBQ= github.com/gotd/td v0.128.0 h1:OI0KyKwARNO4X+czb26+FLKXASFTWuHpgPs7Yaqm04o= github.com/gotd/td v0.128.0/go.mod h1:rSekFfPYj5UEFky5EYnadT0WRU3DGoR4PFEMugk77uI= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -117,6 +121,8 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/gorm v1.30.1 h1:lSHg33jJTBxs2mgJRfRZeLDG+WZaHYCk3Wtfl6Ngzo4= +gorm.io/gorm v1.30.1/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= nhooyr.io/websocket v1.8.17 h1:KEVeLJkUywCKVsnLIDlD/5gtayKp8VoCkksHCGGfT9Y= nhooyr.io/websocket v1.8.17/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c= rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY= diff --git a/yatgclient/yatgclient.go b/yatgclient/yatgclient.go index 9ec0c79..9ae36fb 100644 --- a/yatgclient/yatgclient.go +++ b/yatgclient/yatgclient.go @@ -189,10 +189,14 @@ func (c *Client) RunUpdatesManager( // // Example: // -// gaps := yatgclient.NewUpdateManagerWithYaStorage(storage) -func NewUpdateManagerWithYaStorage(storage yatgstorage.IStorage) *updates.Manager { +// gaps := yatgclient.NewUpdateManagerWithYaStorage(entityID, handler, storage) +func NewUpdateManagerWithYaStorage( + entityID int64, + handler telegram.UpdateHandler, + storage yatgstorage.IStorage, +) *updates.Manager { return updates.New(updates.Config{ - Handler: storage.AccessHashSaveHandler(), + Handler: storage.AccessHashSaveHandler(entityID, handler), Storage: storage.TelegramStorageCompatible(), AccessHasher: storage.TelegramAccessHasherCompatible(), }) diff --git a/yatgstorage/session_storage.go b/yatgstorage/session_storage.go new file mode 100644 index 0000000..1f9592f --- /dev/null +++ b/yatgstorage/session_storage.go @@ -0,0 +1,226 @@ +package yatgstorage + +import ( + "context" + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "crypto/sha256" + "io" + "net/http" + "time" + + "github.com/YaCodeDev/GoYaCodeDevUtils/yaerrors" + "gorm.io/gorm" + "gorm.io/gorm/clause" +) + +type IEntitySessionStorageRepo interface { + UpdateAuthKey(ctx context.Context, entityID int64, encryptedAuthKey []byte) yaerrors.Error + FetchAuthKey(ctx context.Context, entityID int64) ([]byte, yaerrors.Error) +} + +type SessionStorage struct { + entityID int64 + secret string + repo IEntitySessionStorageRepo +} + +func NewSessionStorage(entityID int64, secret string) *SessionStorage { + return &SessionStorage{ + entityID: entityID, + secret: secret, + repo: NewMemorySessionStorage(entityID), + } +} + +func NewSessionStorageWithCustomRepo(entityID int64, secret string, repo IEntitySessionStorageRepo) *SessionStorage { + return &SessionStorage{ + entityID: entityID, + secret: secret, + repo: repo, + } +} + +func EncryptAES(key string, text []byte) ([]byte, yaerrors.Error) { + block, err := aes.NewCipher([]byte(key)) + if err != nil { + return nil, yaerrors.FromError( + http.StatusInternalServerError, + err, + "could not create new cipher", + ) + } + + cipherText := make([]byte, aes.BlockSize+len(text)) + + iv := cipherText[:aes.BlockSize] + if _, err = io.ReadFull(rand.Reader, iv); err != nil { + return nil, yaerrors.FromError( + http.StatusInternalServerError, + err, + "could not encrypt", + ) + } + + stream := cipher.NewCTR(block, iv) + stream.XORKeyStream(cipherText[aes.BlockSize:], text) + + return cipherText, nil +} + +func DecryptAES(key string, text []byte) ([]byte, yaerrors.Error) { + if len(text) < aes.BlockSize { + return nil, yaerrors.FromString( + http.StatusInternalServerError, + "invalid text block size", + ) + } + + block, err := aes.NewCipher([]byte(key)) + if err != nil { + return nil, yaerrors.FromError( + http.StatusInternalServerError, + err, + "could not create new cipher", + ) + } + + iv := text[:aes.BlockSize] + text = text[aes.BlockSize:] + + stream := cipher.NewCTR(block, iv) + stream.XORKeyStream(text, text) + + return text, nil +} + +func DeriveAESKey(data string) []byte { + sum := sha256.Sum256([]byte(data)) + + return sum[:] +} + +func (s *SessionStorage) StoreSession(ctx context.Context, data []byte) error { + out, err := EncryptAES(s.secret, data) + if err != nil { + return err.Wrap("failed to encrypt AES") + } + + if err = s.repo.UpdateAuthKey(ctx, s.entityID, out); err != nil { + return err.Wrap("failed to save updated session") + } + + return nil +} + +func (s *SessionStorage) LoadSession(ctx context.Context) ([]byte, error) { + session, err := s.repo.FetchAuthKey(ctx, s.entityID) + if err != nil { + return nil, err.Wrap("failed to fetch session") + } + + if len(session) == 0 { + return nil, nil + } + + out, err := DecryptAES(s.secret, session) + if err != nil { + return nil, err.Wrap("failed to decrypt AES") + } + + return out, nil +} + +type YaTgClientSession struct { + EntityID int64 `gorm:"primaryKey;autoIncrement:false"` + EncryptedAuthKey []byte `gorm:"type:blob"` + UpdatedAt time.Time `gorm:"autoUpdatedAt"` +} + +const FieldEncryptedAuthKey = "encrypted_session" + +type GormRepo struct { + poolDB *gorm.DB +} + +func NewGormSessionStore(poolDB *gorm.DB) *GormRepo { + return &GormRepo{ + poolDB: poolDB, + } +} + +func (g *GormRepo) UpdateAuthKey( + ctx context.Context, + entityID int64, + encryptedAuthKey []byte, +) yaerrors.Error { + if err := g.poolDB.WithContext(ctx). + Clauses(clause.OnConflict{DoUpdates: clause.AssignmentColumns([]string{FieldEncryptedAuthKey})}). + Model(&YaTgClientSession{}). + Where(&YaTgClientSession{EntityID: entityID}). + Create(&YaTgClientSession{ + EntityID: entityID, + EncryptedAuthKey: encryptedAuthKey, + }).Error; err != nil { + return yaerrors.FromError( + http.StatusInternalServerError, + err, + "failed to update encrypted auth key", + ) + } + + return nil +} + +func (g *GormRepo) FetchAuthKey( + ctx context.Context, + entityID int64, +) ([]byte, yaerrors.Error) { + var botSession YaTgClientSession + + if err := g.poolDB.WithContext(ctx). + Model(&YaTgClientSession{}). + Where(&YaTgClientSession{EntityID: entityID}). + Select(FieldEncryptedAuthKey). + Take(&botSession).Error; err != nil { + return nil, yaerrors.FromError( + http.StatusInternalServerError, + err, + "failed to fetch encrypted auth key", + ) + } + + return botSession.EncryptedAuthKey, nil +} + +type MemoryRepo struct { + Client YaTgClientSession +} + +func NewMemorySessionStorage(entityID int64) *MemoryRepo { + return &MemoryRepo{ + Client: YaTgClientSession{ + EntityID: entityID, + UpdatedAt: time.Now(), + }, + } +} + +func (m *MemoryRepo) UpdateAuthKey( + _ context.Context, + _ int64, + encryptedAuthKey []byte, +) yaerrors.Error { + m.Client.EncryptedAuthKey = encryptedAuthKey + m.Client.UpdatedAt = time.Now() + + return nil +} + +func (m *MemoryRepo) FetchAuthKey( + _ context.Context, + _ int64, +) ([]byte, yaerrors.Error) { + return m.Client.EncryptedAuthKey, nil +} diff --git a/yatgstorage/yatgstorage.go b/yatgstorage/yatgstorage.go index 66df65c..a94bb7a 100644 --- a/yatgstorage/yatgstorage.go +++ b/yatgstorage/yatgstorage.go @@ -103,11 +103,16 @@ type IStorage interface { // Update‑pipeline helper: returns a handler that stores access‑hashes // from any incoming updates before forwarding to the real handler. - AccessHashSaveHandler() HandlerFunc + AccessHashSaveHandler(int64, telegram.UpdateHandler) HandlerFunc // User access‑hash bookkeeping. - SetUserAccessHash(ctx context.Context, userID int64, accessHash int64) yaerrors.Error - GetUserAccessHash(ctx context.Context, userID int64) (int64, yaerrors.Error) + SetUserAccessHash( + ctx context.Context, + entityID int64, + userID int64, + accessHash int64, + ) yaerrors.Error + GetUserAccessHash(ctx context.Context, entityID int64, userID int64) (int64, yaerrors.Error) // gotd adapters TelegramStorageCompatible() updates.StateStorage From d37ec1de93278e74b4b274323047d5be9102c3bd Mon Sep 17 00:00:00 2001 From: Vlad Lavrishko Date: Wed, 30 Jul 2025 22:42:15 +0300 Subject: [PATCH 03/13] feat(yatgstorage): add tests --- go.mod | 2 + go.sum | 4 ++ yatgstorage/session_storage.go | 12 +++- yatgstorage/session_storage_test.go | 102 ++++++++++++++++++++++++++++ 4 files changed, 117 insertions(+), 3 deletions(-) create mode 100644 yatgstorage/session_storage_test.go diff --git a/go.mod b/go.mod index da70271..fe39e53 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/sirupsen/logrus v1.9.3 github.com/stretchr/testify v1.10.0 golang.org/x/net v0.42.0 + gorm.io/driver/sqlite v1.6.0 gorm.io/gorm v1.30.1 ) @@ -35,6 +36,7 @@ require ( github.com/klauspost/compress v1.18.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-sqlite3 v1.14.22 // indirect github.com/ogen-go/ogen v1.12.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/segmentio/asm v1.2.0 // indirect diff --git a/go.sum b/go.sum index 242ecb2..442390a 100644 --- a/go.sum +++ b/go.sum @@ -60,6 +60,8 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/ogen-go/ogen v1.12.0 h1:JMkn957i9/IPaSehqpblviy6Uao3eqQ+eVKUn4LM9pg= github.com/ogen-go/ogen v1.12.0/go.mod h1:RL25amedfhq5xKTUuPBPn6nhYU59CWaVWYJ8YIjNHs0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -121,6 +123,8 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ= +gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8= gorm.io/gorm v1.30.1 h1:lSHg33jJTBxs2mgJRfRZeLDG+WZaHYCk3Wtfl6Ngzo4= gorm.io/gorm v1.30.1/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= nhooyr.io/websocket v1.8.17 h1:KEVeLJkUywCKVsnLIDlD/5gtayKp8VoCkksHCGGfT9Y= diff --git a/yatgstorage/session_storage.go b/yatgstorage/session_storage.go index 1f9592f..25326b5 100644 --- a/yatgstorage/session_storage.go +++ b/yatgstorage/session_storage.go @@ -144,10 +144,16 @@ type GormRepo struct { poolDB *gorm.DB } -func NewGormSessionStore(poolDB *gorm.DB) *GormRepo { - return &GormRepo{ - poolDB: poolDB, +func NewGormSessionStorage(poolDB *gorm.DB) (*GormRepo, yaerrors.Error) { + if err := poolDB.AutoMigrate(&YaTgClientSession{}); err != nil { + return nil, yaerrors.FromError( + http.StatusInternalServerError, + err, + "failed to make auto migrate", + ) } + + return &GormRepo{poolDB: poolDB}, nil } func (g *GormRepo) UpdateAuthKey( diff --git a/yatgstorage/session_storage_test.go b/yatgstorage/session_storage_test.go new file mode 100644 index 0000000..5f2c02f --- /dev/null +++ b/yatgstorage/session_storage_test.go @@ -0,0 +1,102 @@ +package yatgstorage_test + +import ( + "context" + "testing" + + "github.com/YaCodeDev/GoYaCodeDevUtils/yatgstorage" + "github.com/stretchr/testify/assert" + "gorm.io/driver/sqlite" + "gorm.io/gorm" +) + +const entityID = 1000 +const secret = "123456789:ABCDFEG" +const encryptedAuthKey = "stolyarovtop" + +func newMockDB(t *testing.T) *gorm.DB { + poolDB, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) + if err != nil { + t.Fatalf("failed to connect to in-memory database: %v", err) + } + + return poolDB +} + +func TestSessionStorage_WorkflowWorks(t *testing.T) { + ctx := context.Background() + + storage := yatgstorage.NewSessionStorage(entityID, secret) + + _ = storage.StoreSession(ctx, []byte(encryptedAuthKey)) + + expected, _ := yatgstorage.EncryptAES(secret, []byte(encryptedAuthKey)) + + assert.NotEqual(t, []byte(encryptedAuthKey), expected) + + expected, _ = yatgstorage.DecryptAES(secret, []byte(encryptedAuthKey)) + + result, _ := storage.LoadSession(ctx) + + assert.Equal(t, expected, result) +} + +func TestAutoMigrate_Works(t *testing.T) { + poolDB := newMockDB(t) + + _, _ = yatgstorage.NewGormSessionStorage(poolDB) + + expected := true + + assert.Equal(t, expected, poolDB.Migrator().HasTable(&yatgstorage.YaTgClientSession{})) +} + +func TestGormSessionStorage_WorkflowWorks(t *testing.T) { + ctx := context.Background() + + poolDB := newMockDB(t) + + storage, _ := yatgstorage.NewGormSessionStorage(poolDB) + + _ = storage.UpdateAuthKey(ctx, entityID, []byte(encryptedAuthKey)) + + t.Run("Create works", func(t *testing.T) { + err := poolDB.Where(&yatgstorage.YaTgClientSession{ + EntityID: entityID, + EncryptedAuthKey: []byte(encryptedAuthKey), + }).Find(&yatgstorage.YaTgClientSession{}).Error + + assert.Equal(t, nil, err) + }) + + t.Run("Fetch works", func(t *testing.T) { + expected := yatgstorage.YaTgClientSession{} + + _ = poolDB.Where(&yatgstorage.YaTgClientSession{ + EntityID: entityID, + EncryptedAuthKey: []byte(encryptedAuthKey), + }).Find(&expected) + + result, _ := storage.FetchAuthKey(ctx, entityID) + + assert.Equal(t, expected.EncryptedAuthKey, result) + }) +} + +func TestMemorySessionStorage_WorkflowWorks(t *testing.T) { + ctx := context.Background() + + storage := yatgstorage.NewMemorySessionStorage(entityID) + + _ = storage.UpdateAuthKey(ctx, entityID, []byte(encryptedAuthKey)) + + t.Run("Create works", func(t *testing.T) { + assert.Equal(t, []byte(encryptedAuthKey), storage.Client.EncryptedAuthKey) + }) + + t.Run("Fetch works", func(t *testing.T) { + result, _ := storage.FetchAuthKey(ctx, entityID) + + assert.Equal(t, []byte(encryptedAuthKey), result) + }) +} From 4644eda8f01c7172234ec7f9e4b23dfb5f4c9543 Mon Sep 17 00:00:00 2001 From: Vlad Lavrishko Date: Wed, 30 Jul 2025 23:12:17 +0300 Subject: [PATCH 04/13] feat(yatgstorage): add docs --- yatgstorage/session_storage.go | 337 ++++++++++++++++++++++------ yatgstorage/session_storage_test.go | 14 +- 2 files changed, 275 insertions(+), 76 deletions(-) diff --git a/yatgstorage/session_storage.go b/yatgstorage/session_storage.go index 25326b5..f9febb6 100644 --- a/yatgstorage/session_storage.go +++ b/yatgstorage/session_storage.go @@ -11,98 +11,108 @@ import ( "time" "github.com/YaCodeDev/GoYaCodeDevUtils/yaerrors" + "github.com/gotd/td/telegram" "gorm.io/gorm" "gorm.io/gorm/clause" ) +// ISessionStorage defines the methods for session management, including encryption, storage, and retrieval. +// It also provides compatibility with the Telegram session storage interface. +type ISessionStorage interface { + // LoadSession loads the session data from the repository and decrypts it. + // + // Example usage: + // + // sessionData, err := storage.LoadSession(ctx) + LoadSession(ctx context.Context) ([]byte, yaerrors.Error) + + // StoreSession stores the session data in the repository after encrypting it. + // + // Example usage: + // + // err := storage.StoreSession(ctx, sessionData) + StoreSession(ctx context.Context, data []byte) yaerrors.Error + + // TelegramSessionStorageCompatible provides compatibility with `gotd` session storage interface. + // + // Example usage: + // + // telegramStorage := storage.TelegramSessionStorageCompatible() + TelegramSessionStorageCompatible() telegram.SessionStorage +} + +// IEntitySessionStorageRepo defines the methods for storing and fetching encrypted authentication keys for a session. +// +// UpdateAuthKey: +// - This method allows updating or inserting an encrypted +// authentication key for a specific entity identified by `entityID`. +// +// FetchAuthKey: +// - This method retrieves the encrypted authentication key associated with the given `entityID`. type IEntitySessionStorageRepo interface { + // UpdateAuthKey updates the encrypted authentication key for a specific entity. + // + // Example usage: + // + // repo.UpdateAuthKey(ctx, entityID, encryptedAuthKey) UpdateAuthKey(ctx context.Context, entityID int64, encryptedAuthKey []byte) yaerrors.Error + + // FetchAuthKey retrieves the encrypted authentication key for a specific entity. + // + // Example usage: + // + // repo.FetchAuthKey(ctx, entityID) FetchAuthKey(ctx context.Context, entityID int64) ([]byte, yaerrors.Error) } +// SessionStorage manages session data, including encryption and storage, using the provided repository. type SessionStorage struct { entityID int64 - secret string + aes AES repo IEntitySessionStorageRepo } +// NewSessionStorage creates a new SessionStorage instance with an in-memory repository for session data storage. +// +// entityID: The ID of the entity (user, bot) whose session is being managed. +// secret: The secret key used for encrypting/decrypting session data. +// +// Returns a pointer to a new SessionStorage instance. func NewSessionStorage(entityID int64, secret string) *SessionStorage { - return &SessionStorage{ - entityID: entityID, - secret: secret, - repo: NewMemorySessionStorage(entityID), - } + return NewSessionStorageWithCustomRepo(entityID, secret, NewMemorySessionStorage(entityID)) } -func NewSessionStorageWithCustomRepo(entityID int64, secret string, repo IEntitySessionStorageRepo) *SessionStorage { +// NewSessionStorageWithCustomRepo creates a SessionStorage instance with a custom repository for session data storage. +// +// entityID: The ID of the entity (user, bot) whose session is being managed. +// secret: The secret key used for encrypting/decrypting session data. +// repo: A custom repository implementing the IEntitySessionStorageRepo interface. +// +// Returns a pointer to a new SessionStorage instance. +func NewSessionStorageWithCustomRepo( + entityID int64, + secret string, + repo IEntitySessionStorageRepo, +) *SessionStorage { return &SessionStorage{ entityID: entityID, - secret: secret, + aes: NewAES(secret), repo: repo, } } -func EncryptAES(key string, text []byte) ([]byte, yaerrors.Error) { - block, err := aes.NewCipher([]byte(key)) - if err != nil { - return nil, yaerrors.FromError( - http.StatusInternalServerError, - err, - "could not create new cipher", - ) - } - - cipherText := make([]byte, aes.BlockSize+len(text)) - - iv := cipherText[:aes.BlockSize] - if _, err = io.ReadFull(rand.Reader, iv); err != nil { - return nil, yaerrors.FromError( - http.StatusInternalServerError, - err, - "could not encrypt", - ) - } - - stream := cipher.NewCTR(block, iv) - stream.XORKeyStream(cipherText[aes.BlockSize:], text) - - return cipherText, nil -} - -func DecryptAES(key string, text []byte) ([]byte, yaerrors.Error) { - if len(text) < aes.BlockSize { - return nil, yaerrors.FromString( - http.StatusInternalServerError, - "invalid text block size", - ) - } - - block, err := aes.NewCipher([]byte(key)) - if err != nil { - return nil, yaerrors.FromError( - http.StatusInternalServerError, - err, - "could not create new cipher", - ) - } - - iv := text[:aes.BlockSize] - text = text[aes.BlockSize:] - - stream := cipher.NewCTR(block, iv) - stream.XORKeyStream(text, text) - - return text, nil -} - -func DeriveAESKey(data string) []byte { - sum := sha256.Sum256([]byte(data)) - - return sum[:] -} - -func (s *SessionStorage) StoreSession(ctx context.Context, data []byte) error { - out, err := EncryptAES(s.secret, data) +// StoreSession encrypts the session data and stores it using the provided repository. +// +// ctx: The context for the operation. +// data: The session data to be encrypted and stored. +// +// Returns an error if encryption or storage fails. +// +// Example usage: +// +// err := sessionStorage.StoreSession(ctx, sessionData) +func (s *SessionStorage) StoreSession(ctx context.Context, data []byte) yaerrors.Error { + out, err := s.aes.Encrypt(data) if err != nil { return err.Wrap("failed to encrypt AES") } @@ -114,7 +124,16 @@ func (s *SessionStorage) StoreSession(ctx context.Context, data []byte) error { return nil } -func (s *SessionStorage) LoadSession(ctx context.Context) ([]byte, error) { +// LoadSession retrieves and decrypts the session data from the repository. +// +// ctx: The context for the operation. +// +// Returns the decrypted session data or nil if no session data exists, along with an error if decryption fails. +// +// Example usage: +// +// sessionData, err := sessionStorage.LoadSession(ctx) +func (s *SessionStorage) LoadSession(ctx context.Context) ([]byte, yaerrors.Error) { session, err := s.repo.FetchAuthKey(ctx, s.entityID) if err != nil { return nil, err.Wrap("failed to fetch session") @@ -124,7 +143,7 @@ func (s *SessionStorage) LoadSession(ctx context.Context) ([]byte, error) { return nil, nil } - out, err := DecryptAES(s.secret, session) + out, err := s.aes.Decrypt(session) if err != nil { return nil, err.Wrap("failed to decrypt AES") } @@ -132,18 +151,52 @@ func (s *SessionStorage) LoadSession(ctx context.Context) ([]byte, error) { return out, nil } +// TelegramSessionStorageCompatible provides a compatibility layer to work with Telegram's SessionStorage interface. +// +// Returns a SessionStorage-compatible implementation that works with gotd. +func (s *SessionStorage) TelegramSessionStorageCompatible() telegram.SessionStorage { + return &telegramSessionStorage{ + storage: s, + } +} + +// telegramSessionStorage is an implementation of the Telegram SessionStorage interface, +// which is used to store and load sessions in a way compatible with the gotd library. +type telegramSessionStorage struct { + storage *SessionStorage +} + +// StoreSession stores the session data using the SessionStorage's StoreSession method. +func (t *telegramSessionStorage) StoreSession(ctx context.Context, data []byte) error { + return t.storage.StoreSession(ctx, data) +} + +// LoadSession loads the session data using the SessionStorage's LoadSession method. +func (t *telegramSessionStorage) LoadSession(ctx context.Context) ([]byte, error) { + return t.storage.LoadSession(ctx) +} + +// YaTgClientSession is the database model for storing encrypted session data for a client. It holds the +// entity ID, encrypted authentication key, and the timestamp of when the session was last updated. type YaTgClientSession struct { EntityID int64 `gorm:"primaryKey;autoIncrement:false"` EncryptedAuthKey []byte `gorm:"type:blob"` UpdatedAt time.Time `gorm:"autoUpdatedAt"` } +// FieldEncryptedAuthKey is the field name used for storing the encrypted authentication key in the database. const FieldEncryptedAuthKey = "encrypted_session" +// GormRepo is the repository that manages the session storage in a GORM-backed database. type GormRepo struct { poolDB *gorm.DB } +// NewGormSessionStorage creates a new GormRepo and runs the migrations for the YaTgClientSession model. +// +// poolDB: The GORM database connection. +// +// Returns a new instance of GormRepo and any errors encountered during migration. func NewGormSessionStorage(poolDB *gorm.DB) (*GormRepo, yaerrors.Error) { if err := poolDB.AutoMigrate(&YaTgClientSession{}); err != nil { return nil, yaerrors.FromError( @@ -156,6 +209,13 @@ func NewGormSessionStorage(poolDB *gorm.DB) (*GormRepo, yaerrors.Error) { return &GormRepo{poolDB: poolDB}, nil } +// UpdateAuthKey updates the encrypted authentication key for a specific entity in the database. +// +// ctx: The context for the operation. +// entityID: The ID of the entity whose session is being updated. +// encryptedAuthKey: The new encrypted authentication key. +// +// Returns an error if the update fails. func (g *GormRepo) UpdateAuthKey( ctx context.Context, entityID int64, @@ -179,6 +239,12 @@ func (g *GormRepo) UpdateAuthKey( return nil } +// FetchAuthKey retrieves the encrypted authentication key for a specific entity from the database. +// +// ctx: The context for the operation. +// entityID: The ID of the entity whose session is being fetched. +// +// Returns the encrypted authentication key or an error if the fetch operation fails. func (g *GormRepo) FetchAuthKey( ctx context.Context, entityID int64, @@ -200,10 +266,17 @@ func (g *GormRepo) FetchAuthKey( return botSession.EncryptedAuthKey, nil } +// MemoryRepo is an in-memory implementation of the IEntitySessionStorageRepo interface, +// used for testing or simple scenarios where persistence is not required. type MemoryRepo struct { Client YaTgClientSession } +// NewMemorySessionStorage initializes a new MemoryRepo instance for the given entityID. +// +// entityID: The ID of the entity whose session is being managed. +// +// Returns a new MemoryRepo instance. func NewMemorySessionStorage(entityID int64) *MemoryRepo { return &MemoryRepo{ Client: YaTgClientSession{ @@ -213,6 +286,13 @@ func NewMemorySessionStorage(entityID int64) *MemoryRepo { } } +// UpdateAuthKey updates the session's encrypted authentication key in memory. +// +// _ context.Context: The context for the operation (not used in this in-memory implementation). +// _ int64: The entityID (not used in this in-memory implementation). +// encryptedAuthKey: The encrypted authentication key to be stored. +// +// Returns nil after storing the key in memory. func (m *MemoryRepo) UpdateAuthKey( _ context.Context, _ int64, @@ -224,9 +304,124 @@ func (m *MemoryRepo) UpdateAuthKey( return nil } +// FetchAuthKey fetches the encrypted authentication key from memory. +// +// _ context.Context: The context for the operation (not used in this in-memory implementation). +// _ int64: The entityID (not used in this in-memory implementation). +// +// Returns the encrypted authentication key stored in memory. func (m *MemoryRepo) FetchAuthKey( _ context.Context, _ int64, ) ([]byte, yaerrors.Error) { return m.Client.EncryptedAuthKey, nil } + +// AES is a struct that holds the encryption key used for AES encryption and decryption. +// It provides methods to encrypt and decrypt data using AES (CTR mode). +type AES struct { + key []byte +} + +// NewAES creates a new AES instance with the given key. The key is used for encryption and decryption. +// +// key: The AES encryption key as a string. +// +// Returns an AES instance that can be used for encrypting and decrypting data. +func NewAES(key string) AES { + return AES{ + key: DeriveAESKey(key), + } +} + +// Encrypt encrypts data using AES encryption with the provided key (CTR mode). +// +// text: The data to be encrypted. +// +// Returns the encrypted data (ciphertext) and any errors encountered during the process. +// +// Example usage: +// +// encryptedData, err := aes.Encrypt(sessionData) +func (a *AES) Encrypt(text []byte) ([]byte, yaerrors.Error) { + block, err := aes.NewCipher(a.key) + if err != nil { + return nil, yaerrors.FromError( + http.StatusInternalServerError, + err, + "could not create new cipher", + ) + } + + cipherText := make([]byte, aes.BlockSize+len(text)) + + iv := cipherText[:aes.BlockSize] + if _, err = io.ReadFull(rand.Reader, iv); err != nil { + return nil, yaerrors.FromError( + http.StatusInternalServerError, + err, + "could not encrypt", + ) + } + + stream := cipher.NewCTR(block, iv) + stream.XORKeyStream(cipherText[aes.BlockSize:], text) + + return cipherText, nil +} + +// Decrypt decrypts data that was encrypted using AES encryption with the provided key (CTR mode). +// +// text: The encrypted data (ciphertext) to be decrypted. +// +// Returns the decrypted data and any errors encountered during the decryption process. +// +// Example usage: +// +// decryptedData, err := aes.Decrypt(encryptedData) +func (a *AES) Decrypt(text []byte) ([]byte, yaerrors.Error) { + if len(text) < aes.BlockSize { + return nil, yaerrors.FromString( + http.StatusInternalServerError, + "invalid text block size", + ) + } + + block, err := aes.NewCipher(a.key) + if err != nil { + return nil, yaerrors.FromError( + http.StatusInternalServerError, + err, + "could not create new cipher", + ) + } + + iv := text[:aes.BlockSize] + text = text[aes.BlockSize:] + + stream := cipher.NewCTR(block, iv) + stream.XORKeyStream(text, text) + + return text, nil +} + +// DeriveAESKey generates a 256-bit AES key from the provided input string using SHA-256 hashing. +// +// This function takes a string, hashes it using SHA-256, and returns the resulting 256-bit key +// that can be used for AES encryption (AES-256). The result is a 32-byte array, which is suitable +// for AES-256 encryption (256-bit key length). +// +// Parameters: +// - data (string): The input string used to derive the AES key. +// +// Returns: +// - []byte: A 256-bit AES key derived from the input string. +// +// Example usage: +// +// key := DeriveAESKey("my_secret_key") +func DeriveAESKey(data string) []byte { + sum := sha256.Sum256([]byte(data)) + + return sum[:] +} diff --git a/yatgstorage/session_storage_test.go b/yatgstorage/session_storage_test.go index 5f2c02f..10863c6 100644 --- a/yatgstorage/session_storage_test.go +++ b/yatgstorage/session_storage_test.go @@ -10,9 +10,11 @@ import ( "gorm.io/gorm" ) -const entityID = 1000 -const secret = "123456789:ABCDFEG" -const encryptedAuthKey = "stolyarovtop" +const ( + entityID = 1000 + secret = "123456789:ABCDFEG" + encryptedAuthKey = "stolyarovtop" +) func newMockDB(t *testing.T) *gorm.DB { poolDB, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) @@ -30,11 +32,13 @@ func TestSessionStorage_WorkflowWorks(t *testing.T) { _ = storage.StoreSession(ctx, []byte(encryptedAuthKey)) - expected, _ := yatgstorage.EncryptAES(secret, []byte(encryptedAuthKey)) + aes := yatgstorage.NewAES(secret) + + expected, _ := aes.Encrypt([]byte(encryptedAuthKey)) assert.NotEqual(t, []byte(encryptedAuthKey), expected) - expected, _ = yatgstorage.DecryptAES(secret, []byte(encryptedAuthKey)) + expected, _ = aes.Decrypt(expected) result, _ := storage.LoadSession(ctx) From 4ee34022b43e2f18c034570eea3aadd517f7a399 Mon Sep 17 00:00:00 2001 From: Vlad Lavrishko Date: Wed, 30 Jul 2025 23:40:44 +0300 Subject: [PATCH 05/13] fix(yatgstorage): check state key exists --- yatgstorage/yatgstorage.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yatgstorage/yatgstorage.go b/yatgstorage/yatgstorage.go index a94bb7a..d380cf1 100644 --- a/yatgstorage/yatgstorage.go +++ b/yatgstorage/yatgstorage.go @@ -791,7 +791,7 @@ func (s *Storage) safetyBaseStateJSON( key string, log yalogger.Logger, ) yaerrors.Error { - if s.stateKeys.Has(key) { + if !s.stateKeys.Has(key) { if res, err := s.cache.Raw().JSONGet(ctx, key, BasePathRedisJSON).Result(); err != nil || len(res) == 0 { if err := s.cache.Raw().JSONSet(ctx, key, BasePathRedisJSON, updates.State{}).Err(); err != nil { From 571d0a3a0fbe670924b2022e3d3b6106b2b812fd Mon Sep 17 00:00:00 2001 From: Vlad Lavrishko Date: Wed, 30 Jul 2025 23:43:01 +0300 Subject: [PATCH 06/13] fix(yatgstorage): naming field database key --- yatgstorage/session_storage.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yatgstorage/session_storage.go b/yatgstorage/session_storage.go index f9febb6..3d7159a 100644 --- a/yatgstorage/session_storage.go +++ b/yatgstorage/session_storage.go @@ -185,7 +185,7 @@ type YaTgClientSession struct { } // FieldEncryptedAuthKey is the field name used for storing the encrypted authentication key in the database. -const FieldEncryptedAuthKey = "encrypted_session" +const FieldEncryptedAuthKey = "encrypted_auth_key" // GormRepo is the repository that manages the session storage in a GORM-backed database. type GormRepo struct { From 04a387858aef8bc8442cc31691e885bd0fe06006 Mon Sep 17 00:00:00 2001 From: Vlad Lavrishko Date: Wed, 30 Jul 2025 23:49:03 +0300 Subject: [PATCH 07/13] ci(lint): add `CGO_ENABLED` in env --- .github/workflows/go-lint.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/go-lint.yml b/.github/workflows/go-lint.yml index 2affcd6..16e1e55 100644 --- a/.github/workflows/go-lint.yml +++ b/.github/workflows/go-lint.yml @@ -23,6 +23,9 @@ jobs: - uses: actions/setup-go@v5 with: go-version: stable + - name: Set CGO_ENABLED=1 + run: | + echo "CGO_ENABLED=1" >> $GITHUB_ENV - name: Run golangci-lint uses: golangci/golangci-lint-action@v7 with: From cff20df8e6c546d304f7b788e617329e2e683ac9 Mon Sep 17 00:00:00 2001 From: Vlad Lavrishko Date: Thu, 31 Jul 2025 00:00:00 +0300 Subject: [PATCH 08/13] ci(lint): fix `CGO_ENABLED` in env --- .github/workflows/go-lint.yml | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/.github/workflows/go-lint.yml b/.github/workflows/go-lint.yml index 16e1e55..b94711b 100644 --- a/.github/workflows/go-lint.yml +++ b/.github/workflows/go-lint.yml @@ -21,15 +21,10 @@ jobs: steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 - with: - go-version: stable - - name: Set CGO_ENABLED=1 - run: | - echo "CGO_ENABLED=1" >> $GITHUB_ENV - name: Run golangci-lint uses: golangci/golangci-lint-action@v7 with: version: latest args: --timeout=5m - name: Run Tests - run: go test ./... -v + run: CGO_ENABLED=1 go test ./... -v From b82be1bedb25214937ced62bdff82c41942d55af Mon Sep 17 00:00:00 2001 From: Vlad Lavrishko Date: Thu, 31 Jul 2025 00:01:46 +0300 Subject: [PATCH 09/13] ci(lint): fix run lint --- .github/workflows/go-lint.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/go-lint.yml b/.github/workflows/go-lint.yml index b94711b..a7d8328 100644 --- a/.github/workflows/go-lint.yml +++ b/.github/workflows/go-lint.yml @@ -21,6 +21,8 @@ jobs: steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 + with: + go-version: stable - name: Run golangci-lint uses: golangci/golangci-lint-action@v7 with: From 31b66b15491f4f46d474fab01172ded5fc4a5a23 Mon Sep 17 00:00:00 2001 From: Vlad Lavrishko Date: Thu, 31 Jul 2025 00:05:42 +0300 Subject: [PATCH 10/13] ci(lint): add install deps gcc --- .github/workflows/go-lint.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/go-lint.yml b/.github/workflows/go-lint.yml index a7d8328..42669f5 100644 --- a/.github/workflows/go-lint.yml +++ b/.github/workflows/go-lint.yml @@ -23,6 +23,10 @@ jobs: - uses: actions/setup-go@v5 with: go-version: stable + - name: Install dependencies + run: | + sudo apt update + sudo apt install -y gcc libsqlite3-dev - name: Run golangci-lint uses: golangci/golangci-lint-action@v7 with: From c49d6370f22772b5a01590cfa3d24dd12def6f7f Mon Sep 17 00:00:00 2001 From: Vlad Lavrishko Date: Thu, 31 Jul 2025 10:33:52 +0300 Subject: [PATCH 11/13] fix(yatgstorage): sqlite with CGO deps --- go.mod | 9 ++++++- go.sum | 38 +++++++++++++++++++++++++++-- yatgstorage/session_storage.go | 2 +- yatgstorage/session_storage_test.go | 15 +++++++++++- 4 files changed, 59 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index fe39e53..a523031 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( golang.org/x/net v0.42.0 gorm.io/driver/sqlite v1.6.0 gorm.io/gorm v1.30.1 + modernc.org/sqlite v1.38.2 ) require ( @@ -23,6 +24,7 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dlclark/regexp2 v1.11.5 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect github.com/fatih/color v1.18.0 // indirect github.com/ghodss/yaml v1.0.0 // indirect github.com/go-faster/errors v0.7.1 // indirect @@ -37,8 +39,10 @@ require ( github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-sqlite3 v1.14.22 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect github.com/ogen-go/ogen v1.12.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/segmentio/asm v1.2.0 // indirect github.com/yuin/gopher-lua v1.1.1 // indirect go.opentelemetry.io/otel v1.37.0 // indirect @@ -48,7 +52,7 @@ require ( go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect golang.org/x/crypto v0.40.0 // indirect - golang.org/x/exp v0.0.0-20230725093048-515e97ebf090 // indirect + golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect golang.org/x/mod v0.26.0 // indirect golang.org/x/sync v0.16.0 // indirect golang.org/x/sys v0.34.0 // indirect @@ -56,5 +60,8 @@ require ( golang.org/x/tools v0.35.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + modernc.org/libc v1.66.3 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.11.0 // indirect rsc.io/qr v0.2.0 // indirect ) diff --git a/go.sum b/go.sum index 442390a..853b352 100644 --- a/go.sum +++ b/go.sum @@ -17,6 +17,8 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/r github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= @@ -36,6 +38,8 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gotd/contrib v0.21.0 h1:4Fj05jnyBE84toXZl7mVTvt7f732n5uglvztyG6nTr4= @@ -62,12 +66,16 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/ogen-go/ogen v1.12.0 h1:JMkn957i9/IPaSehqpblviy6Uao3eqQ+eVKUn4LM9pg= github.com/ogen-go/ogen v1.12.0/go.mod h1:RL25amedfhq5xKTUuPBPn6nhYU59CWaVWYJ8YIjNHs0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/redis/go-redis/v9 v9.11.0 h1:E3S08Gl/nJNn5vkxd2i78wZxWAPNZgUNTp8WIJUAiIs= github.com/redis/go-redis/v9 v9.11.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= @@ -98,8 +106,8 @@ go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= -golang.org/x/exp v0.0.0-20230725093048-515e97ebf090 h1:Di6/M8l0O2lCLc6VVRWhgCiApHV8MnQurBnFSHsQtNY= -golang.org/x/exp v0.0.0-20230725093048-515e97ebf090/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= +golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= +golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg= golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= @@ -127,6 +135,32 @@ gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ= gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8= gorm.io/gorm v1.30.1 h1:lSHg33jJTBxs2mgJRfRZeLDG+WZaHYCk3Wtfl6Ngzo4= gorm.io/gorm v1.30.1/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= +modernc.org/cc/v4 v4.26.2 h1:991HMkLjJzYBIfha6ECZdjrIYz2/1ayr+FL8GN+CNzM= +modernc.org/cc/v4 v4.26.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU= +modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE= +modernc.org/fileutil v1.3.8 h1:qtzNm7ED75pd1C7WgAGcK4edm4fvhtBsEiI/0NQ54YM= +modernc.org/fileutil v1.3.8/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= +modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= +modernc.org/libc v1.66.3 h1:cfCbjTUcdsKyyZZfEUKfoHcP3S0Wkvz3jgSzByEWVCQ= +modernc.org/libc v1.66.3/go.mod h1:XD9zO8kt59cANKvHPXpx7yS2ELPheAey0vjIuZOhOU8= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= +modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.38.2 h1:Aclu7+tgjgcQVShZqim41Bbw9Cho0y/7WzYptXqkEek= +modernc.org/sqlite v1.38.2/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= nhooyr.io/websocket v1.8.17 h1:KEVeLJkUywCKVsnLIDlD/5gtayKp8VoCkksHCGGfT9Y= nhooyr.io/websocket v1.8.17/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c= rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY= diff --git a/yatgstorage/session_storage.go b/yatgstorage/session_storage.go index 3d7159a..f408286 100644 --- a/yatgstorage/session_storage.go +++ b/yatgstorage/session_storage.go @@ -259,7 +259,7 @@ func (g *GormRepo) FetchAuthKey( return nil, yaerrors.FromError( http.StatusInternalServerError, err, - "failed to fetch encrypted auth key", + "failed to fetch YaTgClientSession", ) } diff --git a/yatgstorage/session_storage_test.go b/yatgstorage/session_storage_test.go index 10863c6..c1d74bb 100644 --- a/yatgstorage/session_storage_test.go +++ b/yatgstorage/session_storage_test.go @@ -2,12 +2,14 @@ package yatgstorage_test import ( "context" + "database/sql" "testing" "github.com/YaCodeDev/GoYaCodeDevUtils/yatgstorage" "github.com/stretchr/testify/assert" "gorm.io/driver/sqlite" "gorm.io/gorm" + _ "modernc.org/sqlite" ) const ( @@ -17,7 +19,18 @@ const ( ) func newMockDB(t *testing.T) *gorm.DB { - poolDB, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) + sqlDB, err := sql.Open("sqlite", ":memory:") + if err != nil { + t.Fatalf("failed to open sqlite in memory") + } + + poolDB, err := gorm.Open( + gorm.Dialector( + sqlite.Dialector{ + Conn: sqlDB, + DriverName: "sqlite", + }, + ), &gorm.Config{}) if err != nil { t.Fatalf("failed to connect to in-memory database: %v", err) } From 5cfac4f846067371194dcaa6ae4da967e43481c1 Mon Sep 17 00:00:00 2001 From: Vlad Lavrishko Date: Thu, 31 Jul 2025 10:34:20 +0300 Subject: [PATCH 12/13] ci(lint): remove CGO and gcc install --- .github/workflows/go-lint.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/go-lint.yml b/.github/workflows/go-lint.yml index 42669f5..2affcd6 100644 --- a/.github/workflows/go-lint.yml +++ b/.github/workflows/go-lint.yml @@ -23,14 +23,10 @@ jobs: - uses: actions/setup-go@v5 with: go-version: stable - - name: Install dependencies - run: | - sudo apt update - sudo apt install -y gcc libsqlite3-dev - name: Run golangci-lint uses: golangci/golangci-lint-action@v7 with: version: latest args: --timeout=5m - name: Run Tests - run: CGO_ENABLED=1 go test ./... -v + run: go test ./... -v From b10be7df8b0ef9a3f7b70333617391f562ab5e1c Mon Sep 17 00:00:00 2001 From: Vlad Lavrishko Date: Thu, 31 Jul 2025 10:37:06 +0300 Subject: [PATCH 13/13] chore(yatgstorage): change log sentences --- yatgstorage/yatgstorage.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/yatgstorage/yatgstorage.go b/yatgstorage/yatgstorage.go index d380cf1..a9f4984 100644 --- a/yatgstorage/yatgstorage.go +++ b/yatgstorage/yatgstorage.go @@ -287,7 +287,7 @@ func (s *Storage) SetPts(ctx context.Context, entityID int64, pts int) yaerrors. ) } - log.Debug("Have set pts in entity state") + log.Debug("Entity state set pts") return nil } @@ -317,7 +317,7 @@ func (s *Storage) SetQts(ctx context.Context, entityID int64, qts int) yaerrors. ) } - log.Debug("Have set qts in entity state") + log.Debug("Entity state set qts") return nil } @@ -347,7 +347,7 @@ func (s *Storage) SetDate(ctx context.Context, entityID int64, date int) yaerror ) } - log.Debug("Have set date in entity state") + log.Debug("Entity state set date") return nil } @@ -377,7 +377,7 @@ func (s *Storage) SetSeq(ctx context.Context, entityID int64, seq int) yaerrors. ) } - log.Debug("Have set seq in entity state") + log.Debug("Entity state set seq") return nil } @@ -408,7 +408,7 @@ func (s *Storage) SetDateSeq(ctx context.Context, entityID int64, date, seq int) ) } - log.Debug("Have set date and seq in state") + log.Debug("Entity state set date and seq") return nil } @@ -440,7 +440,7 @@ func (s *Storage) SetChannelPts( ) } - log.Debug("Have set channel pts") + log.Debug("Channel pts set") return nil } @@ -583,7 +583,7 @@ func (s *Storage) SetChannelAccessHash( ) } - log.Debug("Have set channel access hash") + log.Debug("Channel access hash set") return nil }