From 7ce10980cc37e2443430e894e07e6b4d260af822 Mon Sep 17 00:00:00 2001 From: Maxim Lebedev Date: Fri, 28 Feb 2020 12:47:25 +0500 Subject: [PATCH] :recycle: Refactored stores by using bolthold package --- cmd/migrator/main.go | 9 +- cmd/mypackbot/catalog.go | 150 +++---- go.mod | 7 +- go.sum | 6 + internal/db/db.go | 27 +- internal/db/db_test.go | 9 +- internal/handler/callback_query.go | 4 + internal/handler/command.go | 12 +- internal/handler/handler.go | 13 +- internal/handler/inline_query.go | 41 +- internal/handler/photos.go | 32 +- internal/handler/stickers.go | 43 +- internal/middleware/acquire_photo.go | 5 +- internal/middleware/acquire_sticker.go | 18 +- internal/middleware/acquire_user.go | 8 +- internal/middleware/acquire_user_photo.go | 6 +- internal/middleware/acquire_user_sticker.go | 6 +- internal/migrator/migrator.go | 23 +- internal/model/context.go | 11 +- internal/model/model.go | 95 +++-- internal/model/photos/photos.go | 31 +- internal/model/stickers/stickers.go | 33 +- internal/model/users/photos/users_photos.go | 24 +- .../model/users/stickers/users_stickers.go | 29 +- internal/model/users/users.go | 28 +- internal/new.go | 16 +- internal/run.go | 39 +- internal/store/photos.go | 223 +++-------- internal/store/stickers.go | 279 +++---------- internal/store/store.go | 125 +++--- internal/store/users.go | 142 ++----- internal/store/users_photos.go | 261 ++++-------- internal/store/users_stickers.go | 371 ++++++------------ locales/en/out.gotext.json | 84 ++-- locales/ru/out.gotext.json | 60 +-- 35 files changed, 905 insertions(+), 1365 deletions(-) diff --git a/cmd/migrator/main.go b/cmd/migrator/main.go index deafc56..17c308d 100644 --- a/cmd/migrator/main.go +++ b/cmd/migrator/main.go @@ -5,7 +5,6 @@ import ( "log" "path/filepath" - json "github.com/json-iterator/go" bunt "github.com/tidwall/buntdb" "gitlab.com/toby3d/mypackbot/internal/db" "gitlab.com/toby3d/mypackbot/internal/migrator" @@ -40,10 +39,9 @@ func main() { } defer newDB.Close() - marshler := json.ConfigFastest - users := store.NewUsersStore(newDB, marshler) - stickers := store.NewStickersStore(newDB, marshler) - usersStickers := store.NewUsersStickersStore(newDB, users, stickers, marshler) + users := store.NewUsersStore(newDB) + stickers := store.NewStickersStore(newDB) + usersStickers := store.NewUsersStickersStore(newDB, users, stickers) if err = migrator.AutoMigrate(migrator.AutoMigrateConfig{ Bot: bot, @@ -52,7 +50,6 @@ func main() { UsersStickers: usersStickers, Users: users, OldDB: oldDB, - Marshler: marshler, }); err != nil { log.Fatalln("ERROR:", err.Error()) } diff --git a/cmd/mypackbot/catalog.go b/cmd/mypackbot/catalog.go index e72f06a..ca0c41b 100644 --- a/cmd/mypackbot/catalog.go +++ b/cmd/mypackbot/catalog.go @@ -36,90 +36,81 @@ func init() { } var messageKeyToIndex = map[string]int{ - "☺️ Oh, you missed @toby3d birthday on November 4th!\nIf you like this bot, why not send him some birthday greetings and a little birthday gift? It is not yet too late to make him happy!": 1, - "👋 Hi %s, I'm %s!\nThanks to me, you can collect almost any media content in Telegram without any limits, in any chat via inline mode.": 6, - "👍 Imported!": 11, - "👍 Removed!": 13, - "👍 Updated!": 12, - "💡 Add any text and/or emoji(s) as an argument of this command to change its search properties.": 18, - "💡 Use /del command as an reply to the sticker/photo to remove it from the feed of your collection.": 10, - "💡 Use the /add command as a reply to the sticker/photo to add this media to your collection feed. Given an argument, the result of this command will be equivalent to the /edit command.": 8, - "💡 Use the /edit command with an argument from any character set as a reply to a sticker/photo to change the search query of this media in the feed of your collection. If this media is not in the feed, then the result of this command will be equivalent to the /add command with the same argument.": 9, - "💸 Make a donation": 2, - "📥 Import %s set": 20, - "📥 Import this photo": 16, - "📥 Import this sticker": 19, - "🔥 Remove %s set": 21, - "🔥 Remove this photo": 17, - "🔧 Let's hack!": 5, - "🕵 Found %d result(s)": 14, - "🕺 HacktoberFest is here!\n\nIf you are a beginner or already an experienced golang-developer, now is a great time to help improve the quality of the code of this bot. Choose issue to your taste and offer your PR!": 4, - "🤔 What to do with this?": 15, - "🤖 Here is a list of commands that I understand, some of them [may] or (should) contain an argument:\n/start - start all over again\n/help [other command] - get a list of available commands or help and a demonstration of a specific command\n/add [query] - add media from reply to your collection [with custom search query]\n/edit (query) - change query to reply media\n/del - remove reply media from your collection": 7, - "🤝 Use referral links": 3, - "🥳 4 November? It's a @toby3d birthday!\nIf you like this bot, then why not send him a congratulation along with a small gift? This will make him incredibly happy!": 0, + "☺️ Oh, you missed @toby3d birthday on November 4th!\nIf you like this bot, why not send him some birthday greetings and a little birthday gift? It is not yet too late to make him happy!": 17, + "👋 Hi %s, I'm %s!\nThanks to me, you can collect almost any media content in Telegram without any limits, in any chat via inline mode.": 0, + "👍 Imported!": 5, + "👍 Removed!": 7, + "👍 Updated!": 6, + "💡 Add any text and/or emoji(s) as an argument of this command to change its search properties.": 12, + "💡 Use /del command as an reply to the sticker/photo to remove it from the feed of your collection.": 4, + "💡 Use the /add command as a reply to the sticker/photo to add this media to your collection feed. Given an argument, the result of this command will be equivalent to the /edit command.": 2, + "💡 Use the /edit command with an argument from any character set as a reply to a sticker/photo to change the search query of this media in the feed of your collection. If this media is not in the feed, then the result of this command will be equivalent to the /add command with the same argument.": 3, + "💸 Make a donation": 18, + "📥 Import %s set": 14, + "📥 Import this photo": 10, + "📥 Import this sticker": 13, + "🔥 Remove %s set": 15, + "🔥 Remove this photo": 11, + "🔧 Let's hack!": 21, + "🕵 Found %d result(s)": 8, + "🕺 HacktoberFest is here!\n\nIf you are a beginner or already an experienced golang-developer, now is a great time to help improve the quality of the code of this bot. Choose issue to your taste and offer your PR!": 20, + "🤔 What to do with this?": 9, + "🤖 Here is a list of commands that I understand, some of them [may] or (should) contain an argument:\n/start - start all over again\n/help [other command] - get a list of available commands or help and a demonstration of a specific command\n/add [query] - add media from reply to your collection [with custom search query]\n/edit (query) - change query to reply media\n/del - remove reply media from your collection": 1, + "🤝 Use referral links": 19, + "🥳 4 November? It's a @toby3d birthday!\nIf you like this bot, then why not send him a congratulation along with a small gift? This will make him incredibly happy!": 16, } var enIndex = []uint32{ // 23 elements - 0x00000000, 0x000000a5, 0x00000162, 0x00000177, - 0x0000018f, 0x00000265, 0x00000276, 0x00000304, - 0x000004a1, 0x0000055d, 0x00000688, 0x000006ee, - 0x000006fd, 0x0000070b, 0x00000719, 0x00000734, - 0x0000074f, 0x00000766, 0x0000077d, 0x000007df, - 0x000007f8, 0x0000080e, 0x00000824, + 0x00000000, 0x0000008e, 0x0000022b, 0x000002e7, + 0x00000412, 0x00000478, 0x00000487, 0x00000495, + 0x000004a3, 0x000004be, 0x000004d9, 0x000004f0, + 0x00000507, 0x00000569, 0x00000582, 0x00000598, + 0x000005ae, 0x00000653, 0x00000710, 0x00000725, + 0x0000073d, 0x00000813, 0x00000824, } // Size: 116 bytes const enData string = "" + // Size: 2084 bytes - "\x02🥳 4 November? It's a @toby3d birthday!\x0aIf you like this bot, then" + - " why not send him a congratulation along with a small gift? This will ma" + - "ke him incredibly happy!\x02☺️ Oh, you missed @toby3d birthday on Novemb" + - "er 4th!\x0aIf you like this bot, why not send him some birthday greeting" + - "s and a little birthday gift? It is not yet too late to make him happy!" + - "\x02💸 Make a donation\x02🤝 Use referral links\x02🕺 HacktoberFest is here" + - "!\x0a\x0aIf you are a beginner or already an experienced golang-develope" + - "r, now is a great time to help improve the quality of the code of this b" + - "ot. Choose issue to your taste and offer your PR!\x02🔧 Let's hack!\x02👋 " + - "Hi %[1]s, I'm %[2]s!\x0aThanks to me, you can collect almost any media c" + - "ontent in Telegram without any limits, in any chat via inline mode.\x02🤖" + - " Here is a list of commands that I understand, some of them [may] or (sh" + - "ould) contain an argument:\x0a/start - start all over again\x0a/help [ot" + - "her command] - get a list of available commands or help and a demonstrat" + - "ion of a specific command\x0a/add [query] - add media from reply to your" + - " collection [with custom search query]\x0a/edit (query) - change query t" + - "o reply media\x0a/del - remove reply media from your collection\x02💡 Use" + - " the /add command as a reply to the sticker/photo to add this media to y" + - "our collection feed. Given an argument, the result of this command will " + - "be equivalent to the /edit command.\x02💡 Use the /edit command with an a" + - "rgument from any character set as a reply to a sticker/photo to change t" + - "he search query of this media in the feed of your collection. If this me" + - "dia is not in the feed, then the result of this command will be equivale" + - "nt to the /add command with the same argument.\x02💡 Use /del command as " + - "an reply to the sticker/photo to remove it from the feed of your collect" + - "ion.\x02👍 Imported!\x02👍 Updated!\x02👍 Removed!\x02🕵 Found %[1]d result(" + - "s)\x02🤔 What to do with this?\x02📥 Import this photo\x02🔥 Remove this ph" + - "oto\x02💡 Add any text and/or emoji(s) as an argument of this command to " + - "change its search properties.\x02📥 Import this sticker\x02📥 Import %[1]s" + - " set\x02🔥 Remove %[1]s set" + "\x02👋 Hi %[1]s, I'm %[2]s!\x0aThanks to me, you can collect almost any m" + + "edia content in Telegram without any limits, in any chat via inline mode" + + ".\x02🤖 Here is a list of commands that I understand, some of them [may] " + + "or (should) contain an argument:\x0a/start - start all over again\x0a/he" + + "lp [other command] - get a list of available commands or help and a demo" + + "nstration of a specific command\x0a/add [query] - add media from reply t" + + "o your collection [with custom search query]\x0a/edit (query) - change q" + + "uery to reply media\x0a/del - remove reply media from your collection" + + "\x02💡 Use the /add command as a reply to the sticker/photo to add this m" + + "edia to your collection feed. Given an argument, the result of this comm" + + "and will be equivalent to the /edit command.\x02💡 Use the /edit command " + + "with an argument from any character set as a reply to a sticker/photo to" + + " change the search query of this media in the feed of your collection. I" + + "f this media is not in the feed, then the result of this command will be" + + " equivalent to the /add command with the same argument.\x02💡 Use /del co" + + "mmand as an reply to the sticker/photo to remove it from the feed of you" + + "r collection.\x02👍 Imported!\x02👍 Updated!\x02👍 Removed!\x02🕵 Found %[1]" + + "d result(s)\x02🤔 What to do with this?\x02📥 Import this photo\x02🔥 Remov" + + "e this photo\x02💡 Add any text and/or emoji(s) as an argument of this co" + + "mmand to change its search properties.\x02📥 Import this sticker\x02📥 Imp" + + "ort %[1]s set\x02🔥 Remove %[1]s set\x02🥳 4 November? It's a @toby3d birt" + + "hday!\x0aIf you like this bot, then why not send him a congratulation al" + + "ong with a small gift? This will make him incredibly happy!\x02☺️ Oh, yo" + + "u missed @toby3d birthday on November 4th!\x0aIf you like this bot, why " + + "not send him some birthday greetings and a little birthday gift? It is n" + + "ot yet too late to make him happy!\x02💸 Make a donation\x02🤝 Use referra" + + "l links\x02🕺 HacktoberFest is here!\x0a\x0aIf you are a beginner or alre" + + "ady an experienced golang-developer, now is a great time to help improve" + + " the quality of the code of this bot. Choose issue to your taste and off" + + "er your PR!\x02🔧 Let's hack!" var ruIndex = []uint32{ // 23 elements - 0x00000000, 0x00000135, 0x0000028e, 0x000002ae, - 0x000002d7, 0x00000423, 0x00000434, 0x0000053a, - 0x000007f5, 0x00000948, 0x00000b48, 0x00000bfa, - 0x00000c1b, 0x00000c34, 0x00000c49, 0x00000c7a, - 0x00000ca0, 0x00000cc9, 0x00000ce4, 0x00000da3, - 0x00000dd0, 0x00000df6, 0x00000e0e, + 0x00000000, 0x00000106, 0x000003c1, 0x00000514, + 0x00000714, 0x000007c6, 0x000007e7, 0x00000800, + 0x00000815, 0x00000846, 0x0000086c, 0x00000895, + 0x000008b0, 0x0000096f, 0x0000099c, 0x000009c2, + 0x000009da, 0x00000b0f, 0x00000c68, 0x00000c88, + 0x00000cb1, 0x00000dfd, 0x00000e0e, } // Size: 116 bytes const ruData string = "" + // Size: 3598 bytes - "\x02🥳 4-е Ноября? Это день рождения @toby3d!\x0aЕсли тебе нравится этот " + - "бот, то почему бы не отправить ему поздравления вместе с небольшим пода" + - "рком? Это несказанно его осчастливит!\x02☺️ Ой, ты пропустил день рожде" + - "ния @toby3d 4-го Ноября!\x0aЕсли тебе нравится этот бот, то почему бы н" + - "е отправить ему поздравления вместе с небольшим подарком? Ещё не слишко" + - "м поздно его порадовать!\x02💸 Пожертвование\x02🤝 Реферальные ссылки\x02" + - "🕺 Хактоберфест уже здесь!\x0a\x0aЕсли ты начинающий или уже опытный g" + - "olang-разработчик, то сейчас хорошее время помочь улучшить качество кода" + - " этого бота. Выбери issue на свой вкус и предложи PR!\x02🔧 Let's hack!" + "\x02👋 Привет %[1]s, я %[2]s!\x0aБлагодаря мне, ты можешь коллекционирова" + "ть в inline-режиме практически любой медиа-контент Telegram без огранич" + "ений, в любом чате.\x02🤖 Вот список команд которые я понимаю, некоторые" + @@ -140,6 +131,15 @@ const ruData string = "" + // Size: 3598 bytes "!\x02🕵 Найдено %[1]d результатов\x02🤔 Что с этим делать?\x02📥 Импортиров" + "ать фото\x02🔥 Убрать фото\x02💡 Добавь любой текст и/или эмодзи в качест" + "ве аргумента этой команды чтобы изменить поисковые свойства.\x02📥 Импор" + - "тировать стикер\x02📥 Импортировать %[1]s\x02🔥 Убрать %[1]s" + "тировать стикер\x02📥 Импортировать %[1]s\x02🔥 Убрать %[1]s\x02🥳 4-е Ноя" + + "бря? Это день рождения @toby3d!\x0aЕсли тебе нравится этот бот, то поче" + + "му бы не отправить ему поздравления вместе с небольшим подарком? Это не" + + "сказанно его осчастливит!\x02☺️ Ой, ты пропустил день рождения @toby3d " + + "4-го Ноября!\x0aЕсли тебе нравится этот бот, то почему бы не отправить е" + + "му поздравления вместе с небольшим подарком? Ещё не слишком поздно его " + + "порадовать!\x02💸 Пожертвование\x02🤝 Реферальные ссылки\x02🕺 Хактоберфес" + + "т уже здесь!\x0a\x0aЕсли ты начинающий или уже опытный golang-разработч" + + "ик, то сейчас хорошее время помочь улучшить качество кода этого бота. В" + + "ыбери issue на свой вкус и предложи PR!\x02🔧 Let's hack!" - // Total table size 5914 bytes (5KiB); checksum: 7091C75C + // Total table size 5914 bytes (5KiB); checksum: 490F86A diff --git a/go.mod b/go.mod index fcb6db5..eb18dfb 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,6 @@ go 1.13 require ( github.com/Masterminds/semver v1.5.0 - github.com/etcd-io/bbolt v1.3.3 github.com/json-iterator/go v1.1.9 github.com/kirillDanshin/dlog v0.0.0-20170728000807-97d876b12bf9 github.com/klauspost/compress v1.10.2 // indirect @@ -18,12 +17,16 @@ require ( github.com/tidwall/buntdb v1.1.2 github.com/tidwall/gjson v1.5.0 // indirect github.com/tidwall/pretty v1.0.1 // indirect + github.com/timshannon/bolthold v0.0.0-20200224174833-bf6268f136e7 github.com/valyala/fasthttp v1.9.0 github.com/valyala/fastjson v1.5.0 - gitlab.com/toby3d/telegram v0.0.0-20200218162808-858deeef8eeb + gitlab.com/toby3d/telegram v0.0.0-20200228130040-d70608c147c0 + go.etcd.io/bbolt v1.3.3 golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae // indirect golang.org/x/text v0.3.2 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 gopkg.in/ini.v1 v1.52.0 // indirect gopkg.in/yaml.v2 v2.2.8 // indirect ) + +replace go.etcd.io/bbolt v1.3.3 => github.com/etcd-io/bbolt v1.3.3 diff --git a/go.sum b/go.sum index 621d6d7..a59f00b 100644 --- a/go.sum +++ b/go.sum @@ -156,6 +156,8 @@ github.com/tidwall/rtree v0.0.0-20180113144539-6cd427091e0e h1:+NL1GDIUOKxVfbp2K github.com/tidwall/rtree v0.0.0-20180113144539-6cd427091e0e/go.mod h1:/h+UnNGt0IhNNJLkGikcdcJqm66zGD/uJGMRxK/9+Ao= github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563 h1:Otn9S136ELckZ3KKDyCkxapfufrqDqwmGjcHfAyXRrE= github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563/go.mod h1:mLqSmt7Dv/CNneF2wfcChfN1rvapyQr01LGKnKex0DQ= +github.com/timshannon/bolthold v0.0.0-20200224174833-bf6268f136e7 h1:jLXW7TX5THBQxw+sFH+TjBQuD8lksMwygI4BARqHdJI= +github.com/timshannon/bolthold v0.0.0-20200224174833-bf6268f136e7/go.mod h1:jUigdmrbdCxcIDEFrq82t4X9805XZfwFZoYUap0ET/U= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= @@ -171,8 +173,12 @@ github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= gitlab.com/toby3d/telegram v0.0.0-20200218162808-858deeef8eeb h1:KY5aTKkQZGNFfjBoJf3lq374ekqRYQy4MB/tyi54MoQ= gitlab.com/toby3d/telegram v0.0.0-20200218162808-858deeef8eeb/go.mod h1:vV0ZvEaNCSTcjGgN8Y4osyg1Fv8eyCvPaIfSMAqfltE= +gitlab.com/toby3d/telegram v0.0.0-20200228130040-d70608c147c0 h1:tVrOHMDJ6x0gA/t1f9S6tcyyygSQENHjvFEuLE0x6Fk= +gitlab.com/toby3d/telegram v0.0.0-20200228130040-d70608c147c0/go.mod h1:vV0ZvEaNCSTcjGgN8Y4osyg1Fv8eyCvPaIfSMAqfltE= go.etcd.io/bbolt v1.3.2 h1:Z/90sZLPOeCy2PwprqkFa25PdkusRzaj9P8zm/KNyvk= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk= +go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= diff --git a/internal/db/db.go b/internal/db/db.go index e5a818b..ceeeaae 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -3,31 +3,28 @@ package db import ( "os" - bolt "github.com/etcd-io/bbolt" + "github.com/timshannon/bolthold" "gitlab.com/toby3d/mypackbot/internal/common" + bolt "go.etcd.io/bbolt" ) -func Open(path string) (*bolt.DB, error) { - db, err := bolt.Open(path, os.ModePerm, nil) +func Open(path string) (*bolthold.Store, error) { + db, err := bolthold.Open(path, os.ModePerm, nil) if err != nil { return nil, err } - if err = AutoMigrate(db); err != nil { - _ = db.Close() - } - - return db, err -} - -func AutoMigrate(db *bolt.DB) error { - return db.Update(func(tx *bolt.Tx) (err error) { - for _, bkt := range common.Buckets { - if _, err = tx.CreateBucketIfNotExists(bkt); err != nil { - return err + err = db.Bolt().Update(func(tx *bolt.Tx) error { + for i := range common.Buckets { + if _, err := tx.CreateBucketIfNotExists(common.Buckets[i]); err == nil { + continue } + + return err } return nil }) + + return db, err } diff --git a/internal/db/db_test.go b/internal/db/db_test.go index 74d15c2..7346059 100644 --- a/internal/db/db_test.go +++ b/internal/db/db_test.go @@ -12,10 +12,7 @@ func TestOpen(t *testing.T) { t.Run("invalid", func(t *testing.T) { db, err := Open(filepath.Join("/", "invalid", "path")) assert.Error(t, err) - - t.Run("automigrate", func(t *testing.T) { - assert.Panics(t, func() { _ = AutoMigrate(db) }) - }) + assert.Nil(t, db) }) t.Run("valid", func(t *testing.T) { rootPath, err := os.Getwd() @@ -29,9 +26,5 @@ func TestOpen(t *testing.T) { assert.NoError(t, db.Close()) assert.NoError(t, os.Remove(testPath)) }() - - t.Run("automigrate", func(t *testing.T) { - assert.NoError(t, AutoMigrate(db)) - }) }) } diff --git a/internal/handler/callback_query.go b/internal/handler/callback_query.go index 05950b9..ea00e80 100644 --- a/internal/handler/callback_query.go +++ b/internal/handler/callback_query.go @@ -48,6 +48,7 @@ func (h *Handler) CallbackAdd(ctx *model.Context) (err error) { case ctx.Sticker != nil: if ctx.Request.CallbackQuery.Data == common.DataAddSet { err = h.CommandAddSet(ctx) + ctx.HasSet = true } else { err = h.CommandAddSticker(ctx) } @@ -56,6 +57,7 @@ func (h *Handler) CallbackAdd(ctx *model.Context) (err error) { return err } + ctx.HasSticker = true editMessage.ReplyMarkup = h.GetStickerKeyboard(ctx) default: return err @@ -87,6 +89,7 @@ func (h *Handler) CallbackDel(ctx *model.Context) (err error) { case ctx.Sticker != nil: if ctx.Request.CallbackQuery.Data == common.DataDelSet { err = h.CommandDelSet(ctx) + ctx.HasSet = false } else { err = h.CommandDelSticker(ctx) } @@ -95,6 +98,7 @@ func (h *Handler) CallbackDel(ctx *model.Context) (err error) { return err } + ctx.HasSticker = false editMessage.ReplyMarkup = h.GetStickerKeyboard(ctx) default: return err diff --git a/internal/handler/command.go b/internal/handler/command.go index 40744f5..6104ef9 100644 --- a/internal/handler/command.go +++ b/internal/handler/command.go @@ -41,7 +41,7 @@ func (h *Handler) IsCommand(ctx *model.Context) (err error) { // CommandPing send common ping message. func (h *Handler) CommandPing(ctx *model.Context) (err error) { - reply := tg.NewMessage(ctx.User.UserID, "🏓") + reply := tg.NewMessage(int64(ctx.User.ID), "🏓") reply.ReplyMarkup = tg.NewReplyKeyboardRemove(false) reply.ReplyToMessageID = ctx.Request.Message.ID _, err = ctx.SendMessage(reply) @@ -53,7 +53,7 @@ func (h *Handler) CommandPing(ctx *model.Context) (err error) { // NOTE(toby3d): REQUIRED by Telegram Bot API platform func (h *Handler) CommandStart(ctx *model.Context) (err error) { p := ctx.Get("printer").(*message.Printer) - reply := tg.NewMessage(ctx.User.UserID, p.Sprintf("👋 Hi %s, I'm %s!\nThanks to me, you can collect almost any"+ + reply := tg.NewMessage(int64(ctx.User.ID), p.Sprintf("👋 Hi %s, I'm %s!\nThanks to me, you can collect almost any"+ " media content in Telegram without any limits, in any chat via inline mode.", ctx.Request.Message.From.FullName(), ctx.FullName())) reply.ReplyMarkup = tg.NewReplyKeyboardRemove(false) @@ -67,7 +67,7 @@ func (h *Handler) CommandStart(ctx *model.Context) (err error) { // NOTE(toby3d): REQUIRED by Telegram Bot API platform func (h *Handler) CommandHelp(ctx *model.Context) (err error) { p := ctx.Get("printer").(*message.Printer) - reply := tg.NewMessage(ctx.User.UserID, p.Sprintf("🤖 Here is a list of commands that I understand, some of"+ + reply := tg.NewMessage(int64(ctx.User.ID), p.Sprintf("🤖 Here is a list of commands that I understand, some of"+ " them [may] or (should) contain an argument:\n/start - start all over again\n/help [other command] "+ "- get a list of available commands or help and a demonstration of a specific command\n/add [query] "+ "- add media from reply to your collection [with custom search query]\n/edit (query) - change query "+ @@ -137,7 +137,7 @@ func (h *Handler) CommandAdd(ctx *model.Context) (err error) { } p := ctx.Get("printer").(*message.Printer) - reply := tg.NewMessage(ctx.User.UserID, p.Sprintf("👍 Imported!")) + reply := tg.NewMessage(int64(ctx.User.ID), p.Sprintf("👍 Imported!")) reply.ReplyMarkup = tg.NewReplyKeyboardRemove(false) reply.ReplyToMessageID = ctx.Request.Message.ID _, err = ctx.SendMessage(reply) @@ -164,7 +164,7 @@ func (h *Handler) CommandEdit(ctx *model.Context) (err error) { } p := ctx.Get("printer").(*message.Printer) - reply := tg.NewMessage(ctx.User.UserID, p.Sprintf("👍 Updated!")) + reply := tg.NewMessage(int64(ctx.User.ID), p.Sprintf("👍 Updated!")) reply.ReplyMarkup = tg.NewReplyKeyboardRemove(false) reply.ReplyToMessageID = ctx.Request.Message.ID _, err = ctx.SendMessage(reply) @@ -191,7 +191,7 @@ func (h *Handler) CommandDel(ctx *model.Context) (err error) { } p := ctx.Get("printer").(*message.Printer) - reply := tg.NewMessage(ctx.User.UserID, p.Sprintf("👍 Removed!")) + reply := tg.NewMessage(int64(ctx.User.ID), p.Sprintf("👍 Removed!")) reply.ReplyMarkup = tg.NewReplyKeyboardRemove(false) reply.ReplyToMessageID = ctx.Request.Message.ID _, err = ctx.SendMessage(reply) diff --git a/internal/handler/handler.go b/internal/handler/handler.go index c53c263..9bcc420 100644 --- a/internal/handler/handler.go +++ b/internal/handler/handler.go @@ -11,15 +11,16 @@ import ( ) type Handler struct { - users users.Manager - stickers stickers.Manager - photos photos.Manager - usersStickers us.Manager - usersPhotos up.Manager + users users.ReadWriter + stickers stickers.ReadWriter + photos photos.ReadWriter + usersStickers us.ReadWriter + usersPhotos up.ReadWriter store *store.Store } -func NewHandler(us users.Manager, ss stickers.Manager, ps photos.Manager, uss us.Manager, ups up.Manager) *Handler { +func NewHandler(us users.ReadWriter, ss stickers.ReadWriter, ps photos.ReadWriter, uss us.ReadWriter, + ups up.ReadWriter) *Handler { return &Handler{ photos: ps, stickers: ss, diff --git a/internal/handler/inline_query.go b/internal/handler/inline_query.go index 52e6e70..9ab24e2 100644 --- a/internal/handler/inline_query.go +++ b/internal/handler/inline_query.go @@ -12,26 +12,32 @@ import ( "golang.org/x/text/message" ) +const defaultLimit int = 50 + func (h *Handler) IsInlineQuery(ctx *model.Context) (err error) { answer := tg.NewAnswerInline(ctx.Request.InlineQuery.ID) answer.CacheTime = 1 // NOTE(toby3d): add setting for change this - answer.IsPersonal = !strings.Contains(ctx.Request.InlineQuery.Query, "personal:false") filter := getFilter(ctx.Request.InlineQuery) - items, count := h.store.GetList(ctx.User.ID, filter) - if filter.Offset+50 < count { - answer.NextOffset = strconv.Itoa(filter.Offset + 50) + if answer.IsPersonal = !strings.Contains(ctx.Request.InlineQuery.Query, "personal:false"); answer.IsPersonal { + filter.UserID = ctx.User.ID } - for i := range items { - switch item := items[i].(type) { - case *model.Sticker: + results, count, _ := h.store.GetList(filter.Offset, filter.Limit, filter) + + if filter.Offset+filter.Limit < count { + answer.NextOffset = strconv.Itoa(filter.Offset + filter.Limit) + } + + for i := range results { + switch results[i].GetType() { + case tg.TypeSticker: answer.Results = append(answer.Results, tg.NewInlineQueryResultCachedSticker( - tg.TypeSticker+common.DataSeparator+strconv.FormatUint(item.ID, 10), item.FileID, + tg.TypeSticker+common.DataSeparator+results[i].GetID(), results[i].GetFileID(), )) - case *model.Photo: + case tg.TypePhoto: answer.Results = append(answer.Results, tg.NewInlineQueryResultCachedPhoto( - tg.TypePhoto+common.DataSeparator+strconv.FormatUint(item.ID, 10), item.FileID, + tg.TypePhoto+common.DataSeparator+results[i].GetID(), results[i].GetFileID(), )) } } @@ -46,7 +52,7 @@ func (h *Handler) IsInlineQuery(ctx *model.Context) (err error) { func getFilter(iq *tg.InlineQuery) *store.Filter { f := new(store.Filter) - f.Limit = 50 + f.Limit = defaultLimit f.Offset, _ = strconv.Atoi(iq.Offset) if !strings.Contains(iq.Query, "photos:false") { @@ -62,17 +68,20 @@ func getFilter(iq *tg.InlineQuery) *store.Filter { } f.Query, _ = utils.FixEmojiTone(strings.TrimSpace(iq.Query)) + for _, field := range strings.Fields(f.Query) { i := strings.Index(f.Query, field) switch { case strings.HasPrefix(field, "offset:"): f.Offset, _ = strconv.Atoi(strings.TrimPrefix(field, "offset:")) - case strings.HasPrefix(field, "animated:"): - isAnimated, _ := strconv.ParseBool(strings.TrimPrefix(field, "animated:")) - f.IsAnimated = &isAnimated - case strings.HasPrefix(field, "set:"): - f.SetName = strings.TrimPrefix(field, "set:") + /* + case strings.HasPrefix(field, "animated:"): + isAnimated, _ := strconv.ParseBool(strings.TrimPrefix(field, "animated:")) + f.IsAnimated = &isAnimated + case strings.HasPrefix(field, "set:"): + f.SetName = strings.TrimPrefix(field, "set:") + */ case strings.HasPrefix(field, "photos:"), strings.HasPrefix(field, "stickers:"): default: continue diff --git a/internal/handler/photos.go b/internal/handler/photos.go index c9d5722..c67fffd 100644 --- a/internal/handler/photos.go +++ b/internal/handler/photos.go @@ -1,6 +1,8 @@ package handler import ( + "time" + "gitlab.com/toby3d/mypackbot/internal/common" "gitlab.com/toby3d/mypackbot/internal/model" tg "gitlab.com/toby3d/telegram" @@ -13,16 +15,15 @@ func (h *Handler) GetPhotoKeyboard(ctx *model.Context) *tg.InlineKeyboardMarkup } p := ctx.Get("printer").(*message.Printer) - ctx.UserPhoto = h.usersPhotos.Get(&model.UserPhoto{ + userPhoto := h.usersPhotos.Get(&model.UserPhoto{ UserID: ctx.User.ID, PhotoID: ctx.Photo.ID, }) - markup := tg.NewInlineKeyboardMarkup(tg.NewInlineKeyboardRow( tg.NewInlineKeyboardButton(p.Sprintf("📥 Import this photo"), common.DataAdd), )) - if ctx.UserPhoto != nil { + if userPhoto != nil { markup = tg.NewInlineKeyboardMarkup(tg.NewInlineKeyboardRow( tg.NewInlineKeyboardButton(p.Sprintf("🔥 Remove this photo"), common.DataDel), )) @@ -32,11 +33,13 @@ func (h *Handler) GetPhotoKeyboard(ctx *model.Context) *tg.InlineKeyboardMarkup } func (h *Handler) CommandAddPhoto(ctx *model.Context) (err error) { - if ctx.Photo == nil || ctx.UserPhoto != nil { + if ctx.Photo == nil || ctx.HasPhoto { return nil } + now := time.Now().UTC().Unix() userPhoto := new(model.UserPhoto) + userPhoto.CreatedAt, userPhoto.UpdatedAt = now, now userPhoto.UserID = ctx.User.ID userPhoto.PhotoID = ctx.Photo.ID @@ -54,7 +57,7 @@ func (h *Handler) CommandEditPhoto(ctx *model.Context) (err error) { if !ctx.Request.Message.HasCommandArgument() { p := ctx.Get("printer").(*message.Printer) - reply := tg.NewMessage(ctx.User.UserID, p.Sprintf("💡 Add any text and/or emoji(s) as an argument "+ + reply := tg.NewMessage(int64(ctx.User.ID), p.Sprintf("💡 Add any text and/or emoji(s) as an argument "+ "of this command to change its search properties.")) reply.ReplyMarkup = tg.NewReplyKeyboardRemove(false) reply.ParseMode = tg.ParseModeMarkdownV2 @@ -65,22 +68,27 @@ func (h *Handler) CommandEditPhoto(ctx *model.Context) (err error) { return err } - if ctx.UserPhoto == nil { + if !ctx.HasPhoto { if err = h.CommandAddPhoto(ctx); err != nil { return err } } - ctx.UserPhoto.UpdatedAt = ctx.Request.Message.Date - ctx.UserPhoto.Query = ctx.Request.Message.CommandArgument() - - return h.usersPhotos.Update(ctx.UserPhoto) + return h.usersPhotos.Update(&model.UserPhoto{ + UserID: ctx.User.ID, + PhotoID: ctx.Photo.ID, + UpdatedAt: ctx.Request.Message.Date, + Query: ctx.Request.Message.CommandArgument(), + }) } func (h *Handler) CommandDelPhoto(ctx *model.Context) (err error) { - if ctx.Photo == nil || ctx.UserPhoto == nil { + if ctx.Photo == nil || !ctx.HasPhoto { return nil } - return h.usersPhotos.Remove(ctx.UserPhoto) + return h.usersPhotos.Remove(&model.UserPhoto{ + UserID: ctx.User.ID, + PhotoID: ctx.Photo.ID, + }) } diff --git a/internal/handler/stickers.go b/internal/handler/stickers.go index 8aee7e4..5d41e0b 100644 --- a/internal/handler/stickers.go +++ b/internal/handler/stickers.go @@ -1,6 +1,8 @@ package handler import ( + "time" + "gitlab.com/toby3d/mypackbot/internal/common" "gitlab.com/toby3d/mypackbot/internal/model" tg "gitlab.com/toby3d/telegram" @@ -13,17 +15,11 @@ func (h *Handler) GetStickerKeyboard(ctx *model.Context) *tg.InlineKeyboardMarku } p := ctx.Get("printer").(*message.Printer) - ctx.UserSticker = h.usersStickers.Get(&model.UserSticker{ - UserID: ctx.User.ID, - StickerID: ctx.Sticker.ID, - }) - markup := tg.NewInlineKeyboardMarkup(tg.NewInlineKeyboardRow( tg.NewInlineKeyboardButton("🔥 Remove this sticker", common.DataDel), )) - if (ctx.Request.IsCallbackQuery() && ctx.Request.CallbackQuery.Data == common.DataDelSet) || - ctx.UserSticker == nil { + if (ctx.Request.IsCallbackQuery() && ctx.Request.CallbackQuery.Data == common.DataDelSet) || !ctx.HasSticker { markup = tg.NewInlineKeyboardMarkup(tg.NewInlineKeyboardRow( tg.NewInlineKeyboardButton(p.Sprintf("📥 Import this sticker"), common.DataAdd), )) @@ -35,8 +31,7 @@ func (h *Handler) GetStickerKeyboard(ctx *model.Context) *tg.InlineKeyboardMarku tg.NewInlineKeyboardButton(p.Sprintf("📥 Import %s set", setName), common.DataAddSet), )) - if (ctx.Request.IsCallbackQuery() && ctx.Request.CallbackQuery.Data == common.DataAddSet) || - ctx.UserSticker != nil { + if ctx.Request.IsCallbackQuery() && ctx.Request.CallbackQuery.Data == common.DataAddSet || ctx.HasSet { markup.InlineKeyboard[1][0] = tg.NewInlineKeyboardButton( p.Sprintf("🔥 Remove %s set", setName), common.DataDelSet, ) @@ -49,13 +44,16 @@ func (h *Handler) GetStickerKeyboard(ctx *model.Context) *tg.InlineKeyboardMarku // CommandAddSticker import single Sticker by ReplyMessage. // NOTE(toby3d): DEPRECATED, used for backward compatibility func (h *Handler) CommandAddSticker(ctx *model.Context) (err error) { - if ctx.Sticker == nil || ctx.UserSticker != nil { + if ctx.HasSticker || ctx.Sticker == nil { return nil } + now := time.Now().UTC().Unix() userSticker := new(model.UserSticker) - userSticker.UserID = ctx.User.ID + userSticker.CreatedAt, userSticker.UpdatedAt = now, now + userSticker.Query = ctx.Sticker.Emoji userSticker.StickerID = ctx.Sticker.ID + userSticker.UserID = ctx.User.ID if ctx.Request.IsMessage() && ctx.Request.Message.HasCommandArgument() { userSticker.Query = ctx.Request.Message.CommandArgument() @@ -81,8 +79,8 @@ func (h *Handler) CommandEditSticker(ctx *model.Context) (err error) { if !ctx.Request.Message.HasCommandArgument() { p := ctx.Get("printer").(*message.Printer) - reply := tg.NewMessage(ctx.User.UserID, p.Sprintf("💡 Add any text and/or emoji(s) as an argument "+ - "of this command to change its search properties.")) + reply := tg.NewMessage(int64(ctx.User.ID), p.Sprintf("💡 Add any text and/or emoji(s) as an "+ + "argument of this command to change its search properties.")) reply.ReplyMarkup = tg.NewReplyKeyboardRemove(false) reply.ParseMode = tg.ParseModeMarkdownV2 reply.ReplyToMessageID = ctx.Request.Message.ID @@ -92,26 +90,31 @@ func (h *Handler) CommandEditSticker(ctx *model.Context) (err error) { return err } - if ctx.UserSticker == nil { + if !ctx.HasSticker { if err = h.CommandAddSticker(ctx); err != nil { return err } } - ctx.UserSticker.UpdatedAt = ctx.Request.Message.Date - ctx.UserSticker.Query = ctx.Request.Message.CommandArgument() - - return h.usersStickers.Update(ctx.UserSticker) + return h.usersStickers.Update(&model.UserSticker{ + UserID: ctx.User.ID, + StickerID: ctx.Sticker.ID, + UpdatedAt: ctx.Request.Message.Date, + Query: ctx.Request.Message.CommandArgument(), + }) } // CommandDelSticker remove single Sticker by ReplyMessage. // NOTE(toby3d): DEPRECATED, used for backward compatibility func (h *Handler) CommandDelSticker(ctx *model.Context) (err error) { - if ctx.Sticker == nil || ctx.UserSticker == nil { + if ctx.Sticker == nil || !ctx.HasSticker { return nil } - return h.usersStickers.Remove(ctx.UserSticker) + return h.usersStickers.Remove(&model.UserSticker{ + UserID: ctx.User.ID, + StickerID: ctx.Sticker.ID, + }) } // CommandDelPack remove whole Sticker pack by ReplyMessage. diff --git a/internal/middleware/acquire_photo.go b/internal/middleware/acquire_photo.go index 9931e6c..0cdd502 100644 --- a/internal/middleware/acquire_photo.go +++ b/internal/middleware/acquire_photo.go @@ -6,7 +6,7 @@ import ( tg "gitlab.com/toby3d/telegram" ) -func AcquirePhoto(store photos.Manager) Interceptor { +func AcquirePhoto(store photos.ReadWriter) Interceptor { return func(ctx *model.Context, next model.UpdateFunc) (err error) { switch { case ctx.Request.IsMessage(): @@ -46,9 +46,10 @@ func AcquirePhoto(store photos.Manager) Interceptor { func photoToModel(photoSize tg.Photo) *model.Photo { p := photoSize[len(photoSize)-1] photo := new(model.Photo) - photo.FileID = p.FileID + photo.ID = p.FileUniqueID photo.Width = p.Width photo.Height = p.Height + photo.FileID = p.FileID return photo } diff --git a/internal/middleware/acquire_sticker.go b/internal/middleware/acquire_sticker.go index dcab307..93f5db3 100644 --- a/internal/middleware/acquire_sticker.go +++ b/internal/middleware/acquire_sticker.go @@ -1,6 +1,8 @@ package middleware import ( + "time" + "gitlab.com/toby3d/mypackbot/internal/common" "gitlab.com/toby3d/mypackbot/internal/model" "gitlab.com/toby3d/mypackbot/internal/model/stickers" @@ -50,12 +52,13 @@ func AcquireSticker(store stickers.Manager) Interceptor { func stickerToModel(s *tg.Sticker) *model.Sticker { sticker := new(model.Sticker) - sticker.FileID = s.FileID + sticker.ID = s.FileUniqueID sticker.Emoji = s.Emoji sticker.Width = s.Width sticker.Height = s.Height sticker.IsAnimated = s.IsAnimated sticker.SetName = s.SetName + sticker.FileID = s.FileID if !sticker.InSet() { sticker.SetName = common.SetNameUploaded @@ -67,7 +70,7 @@ func stickerToModel(s *tg.Sticker) *model.Sticker { func migrateSet(ctx *model.Context, store stickers.Manager) { tgSet, err := ctx.GetStickerSet(ctx.Sticker.SetName) if err != nil || tgSet == nil || len(tgSet.Stickers) == 0 { - stickers, _ := store.GetSet(ctx.Sticker.SetName) + stickers, _, _ := store.GetList(0, 0, &model.Sticker{SetName: ctx.Sticker.SetName}) ctx.Sticker.SetName = common.SetNameUploaded go func() { @@ -80,14 +83,17 @@ func migrateSet(ctx *model.Context, store stickers.Manager) { } else { ctx.Set("set_name", tgSet.Title) - dbSet, _ := store.GetSet(tgSet.Name) for i := range tgSet.Stickers { - for j := range dbSet { - if tgSet.Stickers[i].FileID == dbSet[j].FileID { + for _, sticker := range store.GetSet(tgSet.Name) { + if sticker.ID == tgSet.Stickers[i].FileUniqueID { continue } - _ = store.Create(stickerToModel(tgSet.Stickers[i])) + now := time.Now().UTC().Unix() + s := stickerToModel(tgSet.Stickers[i]) + s.CreatedAt, s.UpdatedAt = now, now + + _, _ = store.GetOrCreate(s) } } } diff --git a/internal/middleware/acquire_user.go b/internal/middleware/acquire_user.go index fc39694..4df8de2 100644 --- a/internal/middleware/acquire_user.go +++ b/internal/middleware/acquire_user.go @@ -7,26 +7,26 @@ import ( "gitlab.com/toby3d/mypackbot/internal/model/users" ) -func AcquireUser(us users.Manager) Interceptor { +func AcquireUser(us users.ReadWriter) Interceptor { return func(ctx *model.Context, next model.UpdateFunc) (err error) { ctx.User = new(model.User) switch { case ctx.Request.IsMessage(): - ctx.User.UserID = int64(ctx.Request.Message.From.ID) + ctx.User.ID = ctx.Request.Message.From.ID ctx.User.CreatedAt = ctx.Request.Message.Date ctx.User.UpdatedAt = ctx.Request.Message.Date ctx.User.LanguageCode = ctx.Request.Message.From.LanguageCode ctx.User.LastSeen = ctx.Request.Message.Date case ctx.Request.IsInlineQuery(): now := time.Now().UTC().Unix() - ctx.User.UserID = int64(ctx.Request.InlineQuery.From.ID) + ctx.User.ID = ctx.Request.InlineQuery.From.ID ctx.User.CreatedAt = now ctx.User.UpdatedAt = now ctx.User.LanguageCode = ctx.Request.InlineQuery.From.LanguageCode ctx.User.LastSeen = now case ctx.Request.IsCallbackQuery(): - ctx.User.UserID = int64(ctx.Request.CallbackQuery.From.ID) + ctx.User.ID = ctx.Request.CallbackQuery.From.ID ctx.User.CreatedAt = ctx.Request.CallbackQuery.Message.Date ctx.User.UpdatedAt = ctx.Request.CallbackQuery.Message.Date ctx.User.LanguageCode = ctx.Request.CallbackQuery.From.LanguageCode diff --git a/internal/middleware/acquire_user_photo.go b/internal/middleware/acquire_user_photo.go index 1bef0e1..206bd3b 100644 --- a/internal/middleware/acquire_user_photo.go +++ b/internal/middleware/acquire_user_photo.go @@ -5,16 +5,16 @@ import ( "gitlab.com/toby3d/mypackbot/internal/model/users/photos" ) -func AcquireUserPhoto(store photos.Manager) Interceptor { +func AcquireUserPhoto(store photos.Reader) Interceptor { return func(ctx *model.Context, next model.UpdateFunc) error { if ctx.Photo == nil { return next(ctx) } - ctx.UserPhoto = store.Get(&model.UserPhoto{ + ctx.HasPhoto = store.Get(&model.UserPhoto{ UserID: ctx.User.ID, PhotoID: ctx.Photo.ID, - }) + }) != nil return next(ctx) } diff --git a/internal/middleware/acquire_user_sticker.go b/internal/middleware/acquire_user_sticker.go index 9de15cb..4db372d 100644 --- a/internal/middleware/acquire_user_sticker.go +++ b/internal/middleware/acquire_user_sticker.go @@ -5,16 +5,16 @@ import ( "gitlab.com/toby3d/mypackbot/internal/model/users/stickers" ) -func AcquireUserSticker(store stickers.Manager) Interceptor { +func AcquireUserSticker(store stickers.Reader) Interceptor { return func(ctx *model.Context, next model.UpdateFunc) error { if ctx.Sticker == nil { return next(ctx) } - ctx.UserSticker = store.Get(&model.UserSticker{ + ctx.HasSticker = store.Get(&model.UserSticker{ UserID: ctx.User.ID, StickerID: ctx.Sticker.ID, - }) + }) != nil return next(ctx) } diff --git a/internal/migrator/migrator.go b/internal/migrator/migrator.go index 0589ce8..4746486 100644 --- a/internal/migrator/migrator.go +++ b/internal/migrator/migrator.go @@ -7,6 +7,7 @@ import ( "sort" "strconv" "strings" + "time" json "github.com/json-iterator/go" bunt "github.com/tidwall/buntdb" @@ -25,7 +26,7 @@ type ( } Record struct { - UserID int64 + UserID int SetName string FileID string Emoji string @@ -34,7 +35,7 @@ type ( Records []*Record Backup struct { - Users []int64 `json:"users"` + Users []int `json:"users"` Stickers []string `json:"stickers"` ImportedSets []string `json:"imported_sets"` BlockedSets []string `json:"blocked_sets"` @@ -43,7 +44,7 @@ type ( AutoMigrateConfig struct { OldDB *bunt.DB Stickers stickers.Manager - UsersStickers usersstickers.Manager + UsersStickers usersstickers.ReadWriter Users users.Manager Bot *tg.Bot GroupID int64 @@ -95,7 +96,7 @@ func (cfg *AutoMigrateConfig) importOldData() (data *Data, err error) { // NOTE(toby3d): this part always contains user/chat id var err error - r.UserID, err = strconv.ParseInt(parts[1], 10, 64) + r.UserID, err = strconv.Atoi(parts[1]) if err != nil || r.UserID == 0 || !strings.EqualFold(parts[2], partSet) { return true } @@ -151,9 +152,13 @@ func (cfg *AutoMigrateConfig) migrateUsers(data *Data) (err error) { continue } + now := time.Now().UTC().Unix() + _ = cfg.Users.Create(&model.User{ - UserID: data.Records[i].UserID, + ID: data.Records[i].UserID, LanguageCode: "en", + CreatedAt: now, + UpdatedAt: now, }) data.Users = append(data.Users, data.Records[i].UserID) @@ -197,7 +202,7 @@ func (cfg *AutoMigrateConfig) migrateSets(data *Data) (err error) { _ = cfg.Stickers.Create(stickerToModel(setSticker)) } - u := cfg.Users.GetByUserID(data.Records[i].UserID) + u := cfg.Users.Get(data.Records[i].UserID) _ = cfg.UsersStickers.AddSet(u.ID, set.Name) data.ImportedSets = append(data.ImportedSets, set.Name) _ = cfg.saveBackup(data.Backup) @@ -243,8 +248,8 @@ func (cfg *AutoMigrateConfig) migrateStickers(data *Data) (err error) { // NOTE(toby3d): store old-new stickers _ = cfg.Stickers.Create(s) - u := cfg.Users.GetByUserID(data.Records[i].UserID) - s = cfg.Stickers.GetByFileID(s.FileID) + u := cfg.Users.Get(data.Records[i].UserID) + s = cfg.Stickers.Get(s.FileID) _ = cfg.UsersStickers.Add(&model.UserSticker{ UserID: u.ID, StickerID: s.ID, @@ -258,7 +263,7 @@ func (cfg *AutoMigrateConfig) migrateStickers(data *Data) (err error) { return nil } -func containsInt(src []int64, find int64) bool { +func containsInt(src []int, find int) bool { for i := range src { if src[i] != find { continue diff --git a/internal/model/context.go b/internal/model/context.go index 4af9932..92ca081 100644 --- a/internal/model/context.go +++ b/internal/model/context.go @@ -13,11 +13,12 @@ type ( *tg.Bot Request *tg.Update - User *User - Sticker *Sticker - Photo *Photo - UserPhoto *UserPhoto - UserSticker *UserSticker + User *User + Sticker *Sticker + HasSticker bool + HasSet bool + Photo *Photo + HasPhoto bool userValues context.Context } diff --git a/internal/model/model.go b/internal/model/model.go index 94a88f3..66dec01 100644 --- a/internal/model/model.go +++ b/internal/model/model.go @@ -4,64 +4,91 @@ import ( "strings" "gitlab.com/toby3d/mypackbot/internal/common" + tg "gitlab.com/toby3d/telegram" ) type ( - Model struct { - ID uint64 `json:"id"` - CreatedAt int64 `json:"created_at"` - UpdatedAt int64 `json:"updated_at"` - } - User struct { - Model - UserID int64 `json:"user_id"` - LanguageCode string `json:"language_code"` - LastSeen int64 `json:"last_seen"` + ID int `boltholdKey:"ID"` + CreatedAt int64 + UpdatedAt int64 + LanguageCode string + LastSeen int64 } Users []*User Sticker struct { - Model - FileID string `json:"file_id"` - Width int `json:"width"` - Height int `json:"height"` - IsAnimated bool `json:"is_animated"` - SetName string `json:"set_name"` - Emoji string `json:"emoji"` + ID string `boltholdKey:"ID"` + CreatedAt int64 + UpdatedAt int64 + FileID string + Width int + Height int + IsAnimated bool + SetName string + Emoji string } Stickers []*Sticker - UserSticker struct { - Model - StickerID uint64 `json:"sticker_id"` - UserID uint64 `json:"user_id"` - Query string `json:"query"` - } - - UserStickers []*UserSticker - Photo struct { - Model - FileID string `json:"file_id"` - Width int `json:"width"` - Height int `json:"height"` + ID string `boltholdKey:"ID"` + CreatedAt int64 + UpdatedAt int64 + FileID string + Width int + Height int } Photos []*Photo + UserSticker struct { + ID uint64 `boltholdKey:"ID"` + CreatedAt int64 + UpdatedAt int64 + UserID int + StickerID string + Query string + } + + UserStickers []*UserSticker + UserPhoto struct { - Model - PhotoID uint64 `json:"photo_id"` - UserID uint64 `json:"user_id"` - Query string `json:"query"` + ID uint64 `boltholdKey:"ID"` + CreatedAt int64 + UpdatedAt int64 + UserID int + PhotoID string + Query string } UserPhotos []*UserPhoto + + InlineResult interface { + GetType() string + GetID() string + GetFileID() string + GetUpdatedAt() int64 + } ) func (s *Sticker) InSet() bool { return s.SetName != "" && !strings.EqualFold(s.SetName, common.SetNameUploaded) } + +func (Sticker) GetType() string { return tg.TypeSticker } + +func (s Sticker) GetID() string { return s.ID } + +func (s Sticker) GetFileID() string { return s.FileID } + +func (s Sticker) GetUpdatedAt() int64 { return s.UpdatedAt } + +func (Photo) GetType() string { return tg.TypePhoto } + +func (p Photo) GetID() string { return p.ID } + +func (p Photo) GetFileID() string { return p.FileID } + +func (p Photo) GetUpdatedAt() int64 { return p.UpdatedAt } diff --git a/internal/model/photos/photos.go b/internal/model/photos/photos.go index 6e03a6d..824179f 100644 --- a/internal/model/photos/photos.go +++ b/internal/model/photos/photos.go @@ -2,12 +2,25 @@ package photos import "gitlab.com/toby3d/mypackbot/internal/model" -type Manager interface { - Create(*model.Photo) error - Get(uint64) *model.Photo - GetByFileID(string) *model.Photo - GetList(int, int) (model.Photos, int) - GetOrCreate(*model.Photo) (*model.Photo, error) - Remove(uint64) error - Update(*model.Photo) error -} +type ( + Manager interface { + Reader + Writer + ReadWriter + } + + ReadWriter interface { + GetOrCreate(*model.Photo) (*model.Photo, error) + } + + Reader interface { + Get(string) *model.Photo + GetList(int, int, *model.Photo) (model.Photos, int, error) + } + + Writer interface { + Create(*model.Photo) error + Update(*model.Photo) error + Remove(string) error + } +) diff --git a/internal/model/stickers/stickers.go b/internal/model/stickers/stickers.go index 17ae467..31c3b09 100644 --- a/internal/model/stickers/stickers.go +++ b/internal/model/stickers/stickers.go @@ -2,13 +2,26 @@ package stickers import "gitlab.com/toby3d/mypackbot/internal/model" -type Manager interface { - Create(*model.Sticker) error - Get(uint64) *model.Sticker - GetByFileID(string) *model.Sticker - GetList(int, int) (model.Stickers, int) - GetOrCreate(*model.Sticker) (*model.Sticker, error) - GetSet(string) (model.Stickers, int) - Remove(uint64) error - Update(*model.Sticker) error -} +type ( + Manager interface { + Reader + Writer + ReadWriter + } + + ReadWriter interface { + GetOrCreate(s *model.Sticker) (*model.Sticker, error) + } + + Reader interface { + Get(id string) *model.Sticker + GetSet(name string) model.Stickers + GetList(offset int, limit int, filter *model.Sticker) (model.Stickers, int, error) + } + + Writer interface { + Create(s *model.Sticker) error + Remove(id string) error + Update(s *model.Sticker) error + } +) diff --git a/internal/model/users/photos/users_photos.go b/internal/model/users/photos/users_photos.go index d87865d..e02b782 100644 --- a/internal/model/users/photos/users_photos.go +++ b/internal/model/users/photos/users_photos.go @@ -2,10 +2,20 @@ package photos import "gitlab.com/toby3d/mypackbot/internal/model" -type Manager interface { - Add(*model.UserPhoto) error - Get(*model.UserPhoto) *model.UserPhoto - GetList(uint64, int, int, string) (model.Photos, int) - Remove(*model.UserPhoto) error - Update(*model.UserPhoto) error -} +type ( + ReadWriter interface { + Reader + Writer + } + + Reader interface { + Get(up *model.UserPhoto) *model.Photo + GetList(offset int, limit int, filter *model.UserPhoto) (model.Photos, int, error) + } + + Writer interface { + Add(up *model.UserPhoto) error + Update(up *model.UserPhoto) error + Remove(up *model.UserPhoto) error + } +) diff --git a/internal/model/users/stickers/users_stickers.go b/internal/model/users/stickers/users_stickers.go index 46ca118..35965b7 100644 --- a/internal/model/users/stickers/users_stickers.go +++ b/internal/model/users/stickers/users_stickers.go @@ -2,13 +2,22 @@ package stickers import "gitlab.com/toby3d/mypackbot/internal/model" -type Manager interface { - Add(*model.UserSticker) error - AddSet(uint64, string) error - Get(*model.UserSticker) *model.UserSticker - GetList(uint64, int, int, string) (model.Stickers, int) - GetSet(uint64, int, int, string) (model.Stickers, int) - Remove(*model.UserSticker) error - RemoveSet(uint64, string) error - Update(*model.UserSticker) error -} +type ( + ReadWriter interface { + Reader + Writer + } + + Reader interface { + Get(up *model.UserSticker) *model.Sticker + GetList(offset int, limit int, filter *model.UserSticker) (model.Stickers, int, error) + } + + Writer interface { + Add(up *model.UserSticker) error + AddSet(uid int, setName string) error + Update(up *model.UserSticker) error + Remove(up *model.UserSticker) error + RemoveSet(uid int, setName string) error + } +) diff --git a/internal/model/users/users.go b/internal/model/users/users.go index 65d97b0..d1985d5 100644 --- a/internal/model/users/users.go +++ b/internal/model/users/users.go @@ -2,10 +2,24 @@ package users import "gitlab.com/toby3d/mypackbot/internal/model" -type Manager interface { - Create(*model.User) error - Get(uint64) *model.User - GetByUserID(int64) *model.User - GetOrCreate(*model.User) (*model.User, error) - Update(*model.User) error -} +type ( + Manager interface { + Reader + Writer + ReadWriter + } + + ReadWriter interface { + GetOrCreate(u *model.User) (*model.User, error) + } + + Reader interface { + Get(id int) *model.User + GetList(offset, limit int, filter *model.User) (model.Users, int, error) + } + + Writer interface { + Create(u *model.User) error + Update(u *model.User) error + } +) diff --git a/internal/new.go b/internal/new.go index 76ab935..ec54d60 100644 --- a/internal/new.go +++ b/internal/new.go @@ -1,7 +1,6 @@ package internal import ( - json "github.com/json-iterator/go" "github.com/spf13/viper" "gitlab.com/toby3d/mypackbot/internal/config" "gitlab.com/toby3d/mypackbot/internal/db" @@ -20,8 +19,8 @@ type MyPackBot struct { photos photos.Manager stickers stickers.Manager users users.Manager - usersPhotos usersphotos.Manager - usersStickers usersstickers.Manager + usersPhotos usersphotos.ReadWriter + usersStickers usersstickers.ReadWriter } func New(path string) (mpb *MyPackBot, err error) { @@ -36,12 +35,11 @@ func New(path string) (mpb *MyPackBot, err error) { return nil, err } - marshler := json.ConfigFastest - mpb.photos = store.NewPhotosStore(conn, marshler) - mpb.stickers = store.NewStickersStore(conn, marshler) - mpb.users = store.NewUsersStore(conn, marshler) - mpb.usersPhotos = store.NewUsersPhotosStore(conn, mpb.users, mpb.photos, marshler) - mpb.usersStickers = store.NewUsersStickersStore(conn, mpb.users, mpb.stickers, marshler) + mpb.photos = store.NewPhotosStore(conn) + mpb.stickers = store.NewStickersStore(conn) + mpb.users = store.NewUsersStore(conn) + mpb.usersPhotos = store.NewUsersPhotosStore(conn, mpb.users, mpb.photos) + mpb.usersStickers = store.NewUsersStickersStore(conn, mpb.users, mpb.stickers) if mpb.bot, err = tg.New(mpb.config.GetString("telegram.token")); err != nil { return nil, err diff --git a/internal/run.go b/internal/run.go index 6fe2d42..06a4fc8 100644 --- a/internal/run.go +++ b/internal/run.go @@ -6,6 +6,7 @@ import ( "github.com/kirillDanshin/dlog" http "github.com/valyala/fasthttp" + "gitlab.com/toby3d/mypackbot/internal/common" "gitlab.com/toby3d/mypackbot/internal/handler" "gitlab.com/toby3d/mypackbot/internal/middleware" "gitlab.com/toby3d/mypackbot/internal/model" @@ -24,27 +25,41 @@ func (mpb *MyPackBot) Run() error { middleware.AcquireUser(mpb.users), middleware.AcquirePrinter(), middleware.ChatAction(), - middleware.AcquirePhoto(mpb.photos), - middleware.AcquireUserPhoto(mpb.usersPhotos), - middleware.AcquireSticker(mpb.stickers), - middleware.AcquireUserSticker(mpb.usersStickers), - middleware.Birthday(time.Date(0, time.November, 4, 0, 0, 0, 0, time.UTC)), - middleware.Hacktober(), + middleware.AcquirePhoto(mpb.photos), middleware.AcquireUserPhoto(mpb.usersPhotos), + middleware.AcquireSticker(mpb.stickers), middleware.AcquireUserSticker(mpb.usersStickers), + func() middleware.Interceptor { + return func(ctx *model.Context, next model.UpdateFunc) error { + if ctx.Sticker == nil || ctx.Sticker.SetName == common.SetNameUploaded { + return next(ctx) + } + + for _, sticker := range mpb.stickers.GetSet(ctx.Sticker.SetName) { + if mpb.usersStickers.Get(&model.UserSticker{ + UserID: ctx.User.ID, + StickerID: sticker.ID, + }) == nil { + return next(ctx) + } + } + + ctx.HasSet = true + + return next(ctx) + } + }(), + middleware.Birthday(time.Date(0, time.November, 4, 0, 0, 0, 0, time.UTC)), middleware.Hacktober(), middleware.UpdateLastSeen(mpb.users), } h := chain.UpdateHandler(handler.NewHandler( - mpb.users, - mpb.stickers, - mpb.photos, - mpb.usersStickers, - mpb.usersPhotos, + mpb.users, mpb.stickers, mpb.photos, mpb.usersStickers, mpb.usersPhotos, ).UpdateHandler) for update := range mpb.bot.Updates { ctx := new(model.Context) ctx.Request = update ctx.Bot = mpb.bot - if err := h(ctx); err != nil { + + if err = h(ctx); err != nil { dlog.D(err) } } diff --git a/internal/store/photos.go b/internal/store/photos.go index c1ff241..1228cc1 100644 --- a/internal/store/photos.go +++ b/internal/store/photos.go @@ -1,226 +1,97 @@ package store import ( - "sort" - "strconv" - "time" - - bolt "github.com/etcd-io/bbolt" - json "github.com/json-iterator/go" - "github.com/valyala/fastjson" + "github.com/timshannon/bolthold" "gitlab.com/toby3d/mypackbot/internal/common" "gitlab.com/toby3d/mypackbot/internal/model" - "golang.org/x/xerrors" + bolt "go.etcd.io/bbolt" ) type PhotosStore struct { - conn *bolt.DB - marshler json.API - parser fastjson.Parser + conn *bolthold.Store } -var ( - ErrPhotoInvalid = model.Error{ - Message: "Invalid photo", - } - - ErrPhotoExist = model.Error{ - Message: "Photo already exist", - } - - ErrPhotoNotExist = model.Error{ - Message: "Photo not exist", - } -) - -func NewPhotosStore(conn *bolt.DB, marshler json.API) *PhotosStore { - return &PhotosStore{ - conn: conn, - marshler: marshler, - parser: fastjson.Parser{}, - } +func NewPhotosStore(conn *bolthold.Store) *PhotosStore { + return &PhotosStore{conn: conn} } func (store *PhotosStore) Create(p *model.Photo) error { - if p == nil || p.FileID == "" { - return ErrPhotoInvalid + if store.Get(p.ID) != nil { + return bolthold.ErrKeyExists } - if store.Get(p.ID) != nil || store.GetByFileID(p.FileID) != nil { - return ErrPhotoExist + return store.conn.Bolt().Update(func(tx *bolt.Tx) error { + return store.conn.InsertIntoBucket(tx.Bucket(common.BucketPhotos), p.ID, p) + }) +} + +func (store *PhotosStore) Get(id string) *model.Photo { + result := new(model.Photo) + + if err := store.conn.Bolt().View(func(tx *bolt.Tx) error { + return store.conn.GetFromBucket(tx.Bucket(common.BucketPhotos), id, result) + }); err != nil { + return nil } - now := time.Now().UTC().Unix() + return result +} - if p.CreatedAt <= 0 { - p.CreatedAt = now +func (store *PhotosStore) GetList(offset, limit int, filter *model.Photo) (list model.Photos, count int, err error) { + q := bolthold.Where("ID").Ne("") + + if offset > 0 { + q = q.Skip(offset) } - if p.UpdatedAt <= 0 { - p.UpdatedAt = now + if limit > 0 { + q = q.Limit(limit) } - return store.conn.Update(func(tx *bolt.Tx) (err error) { + // TODO(toby3d): implement filter here + list = make(model.Photos, limit) + + if err := store.conn.Bolt().View(func(tx *bolt.Tx) error { bkt := tx.Bucket(common.BucketPhotos) - if p.ID, err = bkt.NextSequence(); err != nil { + if count, err = store.conn.CountInBucket(bkt, &model.Photo{}, q); err != nil { return err } - src, err := store.marshler.Marshal(p) - if err != nil { - return err - } - - return bkt.Put([]byte(strconv.FormatUint(p.ID, 10)), src) - }) -} - -func (store *PhotosStore) Get(id uint64) *model.Photo { - p := new(model.Photo) - - if err := store.conn.View(func(tx *bolt.Tx) error { - return store.marshler.Unmarshal( - tx.Bucket(common.BucketPhotos).Get([]byte(strconv.FormatUint(id, 10))), p, - ) - }); err != nil || p.ID == 0 { - return nil + return store.conn.FindInBucket(bkt, &list, q) + }); err != nil { + return list, count, err } - return p -} - -func (store *PhotosStore) GetByFileID(id string) *model.Photo { - p := new(model.Photo) - - if err := store.conn.View(func(tx *bolt.Tx) error { - if err := tx.Bucket(common.BucketPhotos).ForEach(func(key, val []byte) error { - v, err := store.parser.ParseBytes(val) - if err != nil { - return err - } - - if string(v.GetStringBytes("file_id")) != id { - return nil - } - - if err = store.marshler.Unmarshal(val, p); err != nil { - return err - } - - return ErrForEachStop - }); err != nil && !xerrors.Is(err, ErrForEachStop) { - return err - } - - return nil - }); err != nil || p.ID == 0 { - return nil - } - - return p -} - -func (store *PhotosStore) GetList(offset, limit int) (model.Photos, int) { - if limit <= 0 { - limit = 0 - } - - count := 0 - photos := make(model.Photos, 0, limit) - _ = store.conn.View(func(tx *bolt.Tx) error { - return tx.Bucket(common.BucketPhotos).ForEach(func(key, val []byte) (err error) { - count++ - - if count <= offset || (limit > 0 && count > offset+limit) { - return nil - } - - p := new(model.Photo) - if err = store.marshler.Unmarshal(val, p); err != nil { - return err - } - - photos = append(photos, p) - - return nil - }) - }) - - sort.Slice(photos, func(i, j int) bool { - return photos[i].UpdatedAt < photos[j].UpdatedAt - }) - - return photos, count + return list, count, err } func (store *PhotosStore) Update(p *model.Photo) error { - if p == nil || p.FileID == "" { - return ErrPhotoInvalid - } - - if store.Get(p.ID) == nil && store.GetByFileID(p.FileID) == nil { + if store.Get(p.ID) == nil { return store.Create(p) } - if p.UpdatedAt <= 0 { - p.UpdatedAt = time.Now().UTC().Unix() - } - - src, err := store.marshler.Marshal(p) - if err != nil { - return err - } - - return store.conn.Update(func(tx *bolt.Tx) error { - return tx.Bucket(common.BucketPhotos).Put([]byte(strconv.FormatUint(p.ID, 10)), src) + return store.conn.Bolt().Update(func(tx *bolt.Tx) error { + return store.conn.UpdateBucket(tx.Bucket(common.BucketPhotos), p.ID, p) }) } -func (store *PhotosStore) Remove(id uint64) error { +func (store *PhotosStore) Remove(id string) error { if store.Get(id) == nil { - return ErrPhotoNotExist + return bolthold.ErrNotFound } - return store.conn.Update(func(tx *bolt.Tx) (err error) { - if err = tx.Bucket(common.BucketPhotos).Delete([]byte(strconv.FormatUint(id, 10))); err != nil { - return err - } - - bkt := tx.Bucket(common.BucketUsersPhotos) - if err = bkt.ForEach(func(key, val []byte) (err error) { - v, err := store.parser.ParseBytes(val) - if err != nil { - return err - } - - if v.GetUint64("photo_id") != id { - return nil - } - - if err = bkt.Delete(key); err != nil { - return err - } - - return ErrForEachStop - }); err != nil && !xerrors.Is(err, ErrForEachStop) { - return err - } - - return nil + return store.conn.Bolt().Update(func(tx *bolt.Tx) error { + return store.conn.DeleteFromBucket(tx.Bucket(common.BucketPhotos), id, &model.Photo{}) }) } -func (store *PhotosStore) GetOrCreate(p *model.Photo) (photo *model.Photo, err error) { - if photo = store.GetByFileID(p.FileID); photo != nil { +func (store *PhotosStore) GetOrCreate(p *model.Photo) (*model.Photo, error) { + if photo := store.Get(p.ID); photo != nil { return photo, nil } - if photo = store.Get(p.ID); photo != nil { - return photo, nil - } - - if err = store.Create(p); err != nil { + if err := store.Create(p); err != nil { return nil, err } diff --git a/internal/store/stickers.go b/internal/store/stickers.go index e1be4e4..6074cc9 100644 --- a/internal/store/stickers.go +++ b/internal/store/stickers.go @@ -1,271 +1,112 @@ package store import ( - "sort" - "strconv" - "strings" - "time" - - bolt "github.com/etcd-io/bbolt" - json "github.com/json-iterator/go" - "github.com/valyala/fastjson" + "github.com/timshannon/bolthold" "gitlab.com/toby3d/mypackbot/internal/common" "gitlab.com/toby3d/mypackbot/internal/model" - "golang.org/x/xerrors" + bolt "go.etcd.io/bbolt" ) type StickersStore struct { - conn *bolt.DB - marshler json.API - parser fastjson.Parser + conn *bolthold.Store } -var ( - ErrStickerInvalid = model.Error{ - Message: "Invalid sticker", - } - - ErrStickerExist = model.Error{ - Message: "Sticker already exist", - } - - ErrStickerNotExist = model.Error{ - Message: "Sticker not exist", - } -) - -func NewStickersStore(conn *bolt.DB, marshler json.API) *StickersStore { - return &StickersStore{ - conn: conn, - marshler: marshler, - parser: fastjson.Parser{}, - } +func NewStickersStore(conn *bolthold.Store) *StickersStore { + return &StickersStore{conn: conn} } func (store *StickersStore) Create(s *model.Sticker) error { - if s == nil || s.FileID == "" { - return ErrStickerInvalid + if store.Get(s.ID) != nil { + return bolthold.ErrKeyExists } - if store.Get(s.ID) != nil || store.GetByFileID(s.FileID) != nil { - return ErrStickerExist + return store.conn.Bolt().Update(func(tx *bolt.Tx) error { + return store.conn.InsertIntoBucket(tx.Bucket(common.BucketStickers), s.ID, s) + }) +} + +func (store *StickersStore) Get(id string) *model.Sticker { + result := new(model.Sticker) + + if err := store.conn.Bolt().View(func(tx *bolt.Tx) error { + return store.conn.GetFromBucket(tx.Bucket(common.BucketStickers), id, result) + }); err != nil { + return nil } - now := time.Now().UTC().Unix() + return result +} - if s.CreatedAt <= 0 { - s.CreatedAt = now +func (store *StickersStore) GetSet(name string) model.Stickers { + list, _, _ := store.GetList(0, 0, &model.Sticker{SetName: name}) + return list +} + +func (store *StickersStore) GetList(offset, limit int, filter *model.Sticker) (list model.Stickers, count int, + err error) { + q := bolthold.Where("ID").Ne("") + + if offset > 0 { + q = q.Skip(offset) } - if s.UpdatedAt <= 0 { - s.UpdatedAt = now + if limit > 0 { + q = q.Limit(limit) } - if s.SetName == "" { - s.SetName = common.SetNameUploaded + if filter != nil { + if filter.Emoji != "" { + q = q.And("Emoji").ContainsAny(filter.Emoji) + } + + if filter.SetName != "" { + q = q.And("SetName").Eq(filter.SetName) + } } - return store.conn.Update(func(tx *bolt.Tx) (err error) { + list = make(model.Stickers, limit) + + if err := store.conn.Bolt().View(func(tx *bolt.Tx) error { bkt := tx.Bucket(common.BucketStickers) - if s.ID, err = bkt.NextSequence(); err != nil { + if count, err = store.conn.CountInBucket(bkt, &model.Sticker{}, q); err != nil { return err } - src, err := store.marshler.Marshal(s) - if err != nil { - return err - } - - return bkt.Put([]byte(strconv.FormatUint(s.ID, 10)), src) - }) -} - -func (store *StickersStore) Get(id uint64) *model.Sticker { - s := new(model.Sticker) - - if err := store.conn.View(func(tx *bolt.Tx) error { - return store.marshler.Unmarshal( - tx.Bucket(common.BucketStickers).Get([]byte(strconv.FormatUint(id, 10))), s, - ) - }); err != nil || s.ID == 0 { - return nil + return store.conn.FindInBucket(bkt, &list, q) + }); err != nil { + return list, count, err } - return s -} - -func (store *StickersStore) GetByFileID(id string) *model.Sticker { - s := new(model.Sticker) - - if err := store.conn.View(func(tx *bolt.Tx) error { - if err := tx.Bucket(common.BucketStickers).ForEach(func(key, val []byte) error { - v, err := store.parser.ParseBytes(val) - if err != nil { - return err - } - - if string(v.GetStringBytes("file_id")) != id { - return nil - } - - if err = store.marshler.Unmarshal(val, s); err != nil { - return err - } - - return ErrForEachStop - }); err != nil && !xerrors.Is(err, ErrForEachStop) { - return err - } - - return nil - }); err != nil || s.ID == 0 { - return nil - } - - return s -} - -func (store *StickersStore) GetList(offset, limit int) (model.Stickers, int) { - if limit <= 0 { - limit = 0 - } - - count := 0 - stickers := make(model.Stickers, 0, limit) - _ = store.conn.View(func(tx *bolt.Tx) error { - return tx.Bucket(common.BucketStickers).ForEach(func(key, val []byte) (err error) { - count++ - - if count <= offset || (limit > 0 && count > offset+limit) { - return nil - } - - s := new(model.Sticker) - if err = store.marshler.Unmarshal(val, s); err != nil { - return err - } - - stickers = append(stickers, s) - - return nil - }) - }) - - sort.Slice(stickers, func(i, j int) bool { - return stickers[i].SetName < stickers[j].SetName || stickers[i].UpdatedAt < stickers[j].UpdatedAt - }) - - return stickers, count -} - -func (store *StickersStore) GetSet(name string) (model.Stickers, int) { - count := 0 - stickers := make(model.Stickers, 0) - - _ = store.conn.View(func(tx *bolt.Tx) error { - return tx.Bucket(common.BucketStickers).ForEach(func(key, val []byte) (err error) { - v, err := store.parser.ParseBytes(val) - if err != nil { - return err - } - - if !strings.EqualFold(string(v.GetStringBytes("set_name")), name) { - return nil - } - - count++ - - s := new(model.Sticker) - if err = store.marshler.Unmarshal(val, s); err != nil { - return err - } - - stickers = append(stickers, s) - - return nil - }) - }) - - sort.Slice(stickers, func(i, j int) bool { - return stickers[i].UpdatedAt < stickers[j].UpdatedAt - }) - - return stickers, count + return list, count, err } func (store *StickersStore) Update(s *model.Sticker) error { - if s == nil || s.FileID == "" { - return ErrStickerInvalid - } - - if store.Get(s.ID) == nil && store.GetByFileID(s.FileID) == nil { + if store.Get(s.ID) == nil { return store.Create(s) } - if s.UpdatedAt <= 0 { - s.UpdatedAt = time.Now().UTC().Unix() - } - - if s.SetName == "" { - s.SetName = common.SetNameUploaded - } - - src, err := store.marshler.Marshal(s) - if err != nil { - return err - } - - return store.conn.Update(func(tx *bolt.Tx) error { - return tx.Bucket(common.BucketStickers).Put([]byte(strconv.FormatUint(s.ID, 10)), src) + return store.conn.Bolt().Update(func(tx *bolt.Tx) error { + return store.conn.UpdateBucket(tx.Bucket(common.BucketStickers), s.ID, s) }) } -func (store *StickersStore) Remove(id uint64) error { +func (store *StickersStore) Remove(id string) error { if store.Get(id) == nil { - return ErrStickerNotExist + return bolthold.ErrNotFound } - return store.conn.Update(func(tx *bolt.Tx) (err error) { - if err = tx.Bucket(common.BucketStickers).Delete([]byte(strconv.FormatUint(id, 10))); err != nil { - return err - } - - bkt := tx.Bucket(common.BucketUsersStickers) - - if err = bkt.ForEach(func(key, val []byte) (err error) { - v, err := store.parser.ParseBytes(val) - if err != nil { - return err - } - - if v.GetUint64("sticker_id") != id { - return nil - } - - if err = bkt.Delete(key); err != nil { - return err - } - - return ErrForEachStop - }); err != nil && !xerrors.Is(err, ErrForEachStop) { - return err - } - - return nil + return store.conn.Bolt().Update(func(tx *bolt.Tx) error { + return store.conn.DeleteFromBucket(tx.Bucket(common.BucketStickers), id, &model.Sticker{}) }) } -func (store *StickersStore) GetOrCreate(s *model.Sticker) (sticker *model.Sticker, err error) { - if sticker = store.GetByFileID(s.FileID); sticker != nil { +func (store *StickersStore) GetOrCreate(s *model.Sticker) (*model.Sticker, error) { + if sticker := store.Get(s.ID); sticker != nil { return sticker, nil } - if sticker = store.Get(s.ID); sticker != nil { - return sticker, nil - } - - if err = store.Create(s); err != nil { + if err := store.Create(s); err != nil { return nil, err } diff --git a/internal/store/store.go b/internal/store/store.go index 67dcdf6..a19f072 100644 --- a/internal/store/store.go +++ b/internal/store/store.go @@ -1,9 +1,7 @@ package store import ( - "errors" "sort" - "time" "gitlab.com/toby3d/mypackbot/internal/model" usersphotos "gitlab.com/toby3d/mypackbot/internal/model/users/photos" @@ -13,97 +11,90 @@ import ( type ( Store struct { - usersStickers usersstickers.Manager - usersPhotos usersphotos.Manager + usersStickers usersstickers.ReadWriter + usersPhotos usersphotos.ReadWriter } Filter struct { + UserID int AllowedTypes []string Query string Offset int Limit int - IsPersonal bool - IsAnimated *bool - Width string - Height string - SetName string } ) -// ErrForEachStop used in ForEach loops in database for forse stop iterations -var ErrForEachStop = errors.New("for each stop stop") +func (f *Filter) offset() int { + if len(f.AllowedTypes) == 0 || f.Offset <= len(f.AllowedTypes) { + return f.Offset + } -func NewStore(us usersstickers.Manager, up usersphotos.Manager) *Store { + return f.Offset / len(f.AllowedTypes) +} + +func (f *Filter) limit() int { + if len(f.AllowedTypes) == 0 || f.Limit <= len(f.AllowedTypes) { + return f.Limit + } + + return f.Limit / len(f.AllowedTypes) +} + +func NewStore(us usersstickers.ReadWriter, up usersphotos.ReadWriter) *Store { return &Store{ usersStickers: us, usersPhotos: up, } } -func (store *Store) GetList(uid uint64, f *Filter) ([]interface{}, int) { - results := make([]interface{}, 0) - count := 0 - - if len(f.AllowedTypes) == 0 { - return results, count +func (store *Store) GetList(offset, limit int, filter *Filter) (list []model.InlineResult, count int, err error) { + if filter == nil { + filter = new(Filter) + filter.AllowedTypes = []string{tg.TypeSticker, tg.TypePhoto} } - for _, t := range f.AllowedTypes { + list = make([]model.InlineResult, 0) + + for _, t := range filter.AllowedTypes { switch t { - case tg.TypePhoto: - if f.IsAnimated != nil && *f.IsAnimated { - continue - } - - photos, photosCount := store.usersPhotos.GetList(uid, 0, -1, f.Query) - for i := range photos { - results = append(results, photos[i]) - } - - count += photosCount case tg.TypeSticker: - stickers, stickersCount := store.usersStickers.GetList(uid, 0, -1, f.Query) - for i := range stickers { - if f.IsAnimated != nil && stickers[i].IsAnimated != *f.IsAnimated || - f.SetName != "" && stickers[i].SetName != f.SetName { - stickersCount-- - continue - } - - results = append(results, stickers[i]) + l, c, err := store.usersStickers.GetList( + filter.offset(), filter.limit(), &model.UserSticker{ + UserID: filter.UserID, + Query: filter.Query, + }, + ) + if err != nil { + return list, count, err } - count += stickersCount + for i := range l { + list = append(list, l[i]) + } + + count += c + case tg.TypePhoto: + l, c, err := store.usersPhotos.GetList( + filter.offset(), filter.limit(), &model.UserPhoto{ + UserID: filter.UserID, + Query: filter.Query, + }, + ) + if err != nil { + return list, count, err + } + + for i := range l { + list = append(list, l[i]) + } + + count += c } } - sort.Slice(results, func(i, j int) bool { - var a, b time.Time - - switch result := results[i].(type) { - case *model.Sticker: - a = time.Unix(result.CreatedAt, 0) - case *model.Photo: - a = time.Unix(result.CreatedAt, 0) - } - - switch result := results[j].(type) { - case *model.Sticker: - b = time.Unix(result.CreatedAt, 0) - case *model.Photo: - b = time.Unix(result.CreatedAt, 0) - } - - return a.Before(b) + sort.Slice(list, func(i, j int) bool { + return list[i].GetUpdatedAt() < list[j].GetUpdatedAt() }) - if len(results) <= f.Offset { - return make([]interface{}, 0), count - } - - if tail := len(results[f.Offset:]); tail < f.Limit { - return results[f.Offset : f.Offset+tail], count - } - - return results[f.Offset : f.Offset+f.Limit-1], count + return list, count, err } diff --git a/internal/store/users.go b/internal/store/users.go index dced6e1..a9307fa 100644 --- a/internal/store/users.go +++ b/internal/store/users.go @@ -1,152 +1,88 @@ package store import ( - "strconv" - "time" - - bolt "github.com/etcd-io/bbolt" - json "github.com/json-iterator/go" - "github.com/valyala/fastjson" + "github.com/timshannon/bolthold" "gitlab.com/toby3d/mypackbot/internal/common" "gitlab.com/toby3d/mypackbot/internal/model" - "golang.org/x/xerrors" + bolt "go.etcd.io/bbolt" ) type UsersStore struct { - conn *bolt.DB - marshler json.API - parser fastjson.Parser + conn *bolthold.Store } -var ( - ErrUserExist = model.Error{ - Message: "User already exist", - } - - ErrUserNotExist = model.Error{ - Message: "User not exist", - } -) - -func NewUsersStore(conn *bolt.DB, marshler json.API) *UsersStore { - return &UsersStore{ - conn: conn, - marshler: marshler, - parser: fastjson.Parser{}, - } +func NewUsersStore(conn *bolthold.Store) *UsersStore { + return &UsersStore{conn: conn} } func (store *UsersStore) Create(u *model.User) error { - if store.Get(u.ID) != nil || store.GetByUserID(u.UserID) != nil { - return ErrUserExist + if store.Get(u.ID) != nil { + return bolthold.ErrKeyExists } - now := time.Now().UTC().Unix() - - if u.CreatedAt <= 0 { - u.CreatedAt = now - } - - if u.UpdatedAt <= 0 { - u.UpdatedAt = now - } - - if u.LastSeen <= 0 { - u.LastSeen = now - } - - return store.conn.Update(func(tx *bolt.Tx) (err error) { - bkt := tx.Bucket(common.BucketUsers) - - if u.ID, err = bkt.NextSequence(); err != nil { - return err - } - - src, err := store.marshler.Marshal(u) - if err != nil { - return err - } - - return bkt.Put([]byte(strconv.FormatUint(u.ID, 10)), src) + return store.conn.Bolt().Update(func(tx *bolt.Tx) error { + return store.conn.InsertIntoBucket(tx.Bucket(common.BucketUsers), u.ID, u) }) } -func (store *UsersStore) Get(id uint64) *model.User { - u := new(model.User) +func (store *UsersStore) Get(id int) *model.User { + result := new(model.User) - if err := store.conn.View(func(tx *bolt.Tx) error { - return store.marshler.Unmarshal( - tx.Bucket(common.BucketUsers).Get([]byte(strconv.FormatUint(id, 10))), u, - ) - }); err != nil || u.ID == 0 { + if err := store.conn.Bolt().View(func(tx *bolt.Tx) error { + return store.conn.GetFromBucket(tx.Bucket(common.BucketUsers), id, result) + }); err != nil { return nil } - return u + return result } -func (store *UsersStore) GetByUserID(id int64) *model.User { - u := new(model.User) +func (store *UsersStore) GetList(offset, limit int, filter *model.User) (list model.Users, count int, err error) { + list = make(model.Users, limit) + q := bolthold.Where("ID").Ne("") - if err := store.conn.View(func(tx *bolt.Tx) error { - if err := tx.Bucket(common.BucketUsers).ForEach(func(key, val []byte) error { - v, err := store.parser.ParseBytes(val) - if err != nil { - return err - } + if offset > 0 { + q = q.Skip(offset) + } - if v.GetInt64("user_id") != id { - return nil - } + if limit > 0 { + q = q.Limit(limit) + } - if err = store.marshler.Unmarshal(val, u); err != nil { - return err - } + // TODO(toby3d): implement filter here - return ErrForEachStop - }); err != nil && !xerrors.Is(err, ErrForEachStop) { + if err := store.conn.Bolt().View(func(tx *bolt.Tx) (err error) { + bkt := tx.Bucket(common.BucketUsers) + if count, err = store.conn.CountInBucket(bkt, &model.User{}, q); err != nil { return err } - return nil - }); err != nil || u.ID == 0 { - return nil + return store.conn.FindInBucket(bkt, &list, q) + }); err != nil { + return nil, 0, err } - return u + return list, count, err } func (store *UsersStore) Update(u *model.User) error { - if store.Get(u.ID) == nil && store.GetByUserID(u.UserID) == nil { + if store.Get(u.ID) == nil { return store.Create(u) } - if u.UpdatedAt <= 0 { - u.UpdatedAt = time.Now().UTC().Unix() - } - - src, err := store.marshler.Marshal(u) - if err != nil { - return err - } - - return store.conn.Update(func(tx *bolt.Tx) error { - return tx.Bucket(common.BucketUsers).Put([]byte(strconv.FormatUint(u.ID, 10)), src) + return store.conn.Bolt().Update(func(tx *bolt.Tx) (err error) { + return store.conn.UpdateBucket(tx.Bucket(common.BucketUsers), u.ID, u) }) } -func (store *UsersStore) GetOrCreate(u *model.User) (user *model.User, err error) { - if user = store.Get(u.ID); user != nil { +func (store *UsersStore) GetOrCreate(u *model.User) (*model.User, error) { + if user := store.Get(u.ID); user != nil { return user, nil } - if user = store.GetByUserID(u.UserID); user != nil { - return user, nil - } - - if err = store.Create(u); err != nil { + if err := store.Create(u); err != nil { return nil, err } - return store.GetOrCreate(u) + return u, nil } diff --git a/internal/store/users_photos.go b/internal/store/users_photos.go index 5b7f73d..22297bc 100644 --- a/internal/store/users_photos.go +++ b/internal/store/users_photos.go @@ -1,228 +1,133 @@ package store import ( - "sort" - "strconv" "strings" - "time" - bolt "github.com/etcd-io/bbolt" - json "github.com/json-iterator/go" - "github.com/valyala/fastjson" + "github.com/timshannon/bolthold" "gitlab.com/toby3d/mypackbot/internal/common" "gitlab.com/toby3d/mypackbot/internal/model" "gitlab.com/toby3d/mypackbot/internal/model/photos" "gitlab.com/toby3d/mypackbot/internal/model/users" + bolt "go.etcd.io/bbolt" "golang.org/x/xerrors" ) type UsersPhotosStore struct { - conn *bolt.DB - marshler json.API - parser fastjson.Parser - photos photos.Manager - users users.Manager + conn *bolthold.Store + photos photos.Reader + users users.Reader } -var ( - ErrUserPhotoExist = model.Error{ - Message: "Photo already imported", - } - - ErrUserPhotoNotExist = model.Error{ - Message: "Photo already removed", - } -) - -func NewUsersPhotosStore(conn *bolt.DB, us users.Manager, ps photos.Manager, marshler json.API) *UsersPhotosStore { +func NewUsersPhotosStore(conn *bolthold.Store, us users.Reader, ps photos.Reader) *UsersPhotosStore { return &UsersPhotosStore{ - conn: conn, - marshler: marshler, - parser: fastjson.Parser{}, - photos: ps, - users: us, + conn: conn, + photos: ps, + users: us, } } func (store *UsersPhotosStore) Add(up *model.UserPhoto) (err error) { - if up == nil || up.UserID == 0 || up.PhotoID == 0 { - return nil + if store.users.Get(up.UserID) == nil || store.photos.Get(up.PhotoID) == nil { + return bolthold.ErrNotFound } - userPhoto := store.Get(up) - if userPhoto != nil { - return ErrUserPhotoExist - } - - timeStamp := time.Now().UTC().Unix() - - if up.CreatedAt == 0 { - up.CreatedAt = timeStamp - } - - if up.UpdatedAt == 0 { - up.UpdatedAt = timeStamp - } - - return store.conn.Update(func(tx *bolt.Tx) (err error) { - bkt := tx.Bucket(common.BucketUsersPhotos) - - up.ID, err = bkt.NextSequence() - if err != nil { - return err - } - - src, err := store.marshler.Marshal(up) - if err != nil { - return err - } - - return bkt.Put([]byte(strconv.FormatUint(up.ID, 10)), src) + return store.conn.Bolt().Update(func(tx *bolt.Tx) error { + return store.conn.InsertIntoBucket(tx.Bucket(common.BucketUsersPhotos), bolthold.NextSequence(), up) }) } func (store *UsersPhotosStore) Update(up *model.UserPhoto) (err error) { - if up == nil || up.UserID == 0 || up.PhotoID == 0 { - return nil - } + return store.conn.Bolt().Update(func(tx *bolt.Tx) error { + return store.conn.UpdateMatchingInBucket( + tx.Bucket(common.BucketUsersPhotos), &model.UserPhoto{}, + bolthold.Where("UserID").Eq(up.UserID).And("PhotoID").Eq(up.PhotoID), + func(record interface{}) error { + result, ok := record.(*model.UserPhoto) // record will always be a pointer + if !ok { + return xerrors.New("invalid type") + } - userPhoto := store.Get(up) - if userPhoto == nil { - return store.Add(up) - } + result.Query, result.UpdatedAt = up.Query, up.UpdatedAt - if up.ID == 0 { - up.ID = userPhoto.ID - } - - if up.CreatedAt == 0 { - up.CreatedAt = userPhoto.CreatedAt - } - - if up.UpdatedAt <= userPhoto.UpdatedAt { - up.UpdatedAt = time.Now().UTC().Unix() - } - - src, err := store.marshler.Marshal(up) - if err != nil { - return err - } - - return store.conn.Update(func(tx *bolt.Tx) error { - return tx.Bucket(common.BucketUsersPhotos).Put([]byte(strconv.FormatUint(up.ID, 10)), src) + return nil + }) }) + } -func (store *UsersPhotosStore) Get(up *model.UserPhoto) *model.UserPhoto { - if up == nil || up.UserID == 0 || up.PhotoID == 0 { +func (store *UsersPhotosStore) Get(up *model.UserPhoto) *model.Photo { + result := new(model.UserPhoto) + + if err := store.conn.Bolt().View(func(tx *bolt.Tx) error { + return store.conn.FindOneInBucket( + tx.Bucket(common.BucketUsersPhotos), result, + bolthold.Where("UserID").Eq(up.UserID).And("PhotoID").Eq(up.PhotoID), + ) + }); err != nil { return nil } - userPhoto := new(model.UserPhoto) - if err := store.conn.View(func(tx *bolt.Tx) (err error) { - if err = tx.Bucket(common.BucketUsersPhotos).ForEach(func(key, val []byte) (err error) { - v, err := store.parser.ParseBytes(val) - if err != nil { - return err - } + return store.photos.Get(result.PhotoID) +} - if v.GetUint64("user_id") != up.UserID || v.GetUint64("photo_id") != up.PhotoID { - return nil - } +func (store *UsersPhotosStore) GetList(offset, limit int, filter *model.UserPhoto) (list model.Photos, count int, + err error) { - if err = store.marshler.Unmarshal(val, userPhoto); err != nil { - return err - } + q := bolthold.Where("UserID").Ne(0).And("PhotoID").Ne("") + qCount := bolthold.Where("UserID").Ne(0).And("PhotoID").Ne("") - return ErrForEachStop - }); err != nil && !xerrors.Is(err, ErrForEachStop) { + if offset != 0 { + q = q.Skip(offset) + } + + if limit != 0 { + q = q.Limit(limit) + } + + if filter != nil { + if filter.UserID != 0 { + q = q.And("UserID").Eq(filter.UserID) + qCount.And("UserID").Eq(filter.UserID) + } + + if filter.Query != "" { + q = q.And("Query").MatchFunc(func(field string) (bool, error) { + return strings.ContainsAny(field, filter.Query), nil + }) + qCount.And("Query").MatchFunc(func(field string) (bool, error) { + return strings.ContainsAny(field, filter.Query), nil + }) + } + } + + results := make(model.UserPhotos, 0) + + if err = store.conn.Bolt().View(func(tx *bolt.Tx) (err error) { + bkt := tx.Bucket(common.BucketUsersPhotos) + + if count, err = store.conn.CountInBucket(bkt, &model.UserPhoto{}, qCount); err != nil { return err } - return nil - }); err != nil || userPhoto.PhotoID == 0 || userPhoto.UserID == 0 { - return nil + return store.conn.FindInBucket(bkt, &results, q) + }); err != nil { + return nil, 0, err } - return userPhoto -} + list = make(model.Photos, 0) -func (store *UsersPhotosStore) GetList(uid uint64, offset, limit int, query string) (model.Photos, int) { - if limit <= 0 { - limit = 0 + for i := range results { + list = append(list, store.photos.Get(results[i].PhotoID)) } - count := 0 - photos := make(model.Photos, 0, limit) - _ = store.conn.View(func(tx *bolt.Tx) error { - bkt := tx.Bucket(common.BucketPhotos) - return tx.Bucket(common.BucketUsersPhotos).ForEach(func(key, val []byte) error { - v, err := store.parser.ParseBytes(val) - if err != nil { - return err - } - - if v.GetUint64("user_id") != uid { - return nil - } - - if query != "" && !strings.ContainsAny(string(v.GetStringBytes("query")), query) { - return nil - } - - count++ - - if (offset != 0 && count <= offset) || (limit > 0 && count > offset+limit) { - return nil - } - - p := new(model.Photo) - if err = store.marshler.Unmarshal( - bkt.Get([]byte(strconv.FormatUint(v.GetUint64("photo_id"), 10))), p, - ); err != nil { - return err - } - - photos = append(photos, p) - - return nil - }) - }) - - sort.Slice(photos, func(i, j int) bool { - return photos[i].UpdatedAt < photos[j].UpdatedAt - }) - - return photos, count + return list, count, err } func (store *UsersPhotosStore) Remove(up *model.UserPhoto) (err error) { - userPhoto := store.Get(up) - if userPhoto == nil { - return ErrUserPhotoNotExist - } - - return store.conn.Update(func(tx *bolt.Tx) (err error) { - bkt := tx.Bucket(common.BucketUsersPhotos) - if err = bkt.ForEach(func(key, val []byte) (err error) { - v, err := store.parser.ParseBytes(val) - if err != nil { - return err - } - - if v.GetUint64("user_id") != up.UserID || v.GetUint64("photo_id") != up.PhotoID { - return nil - } - - if err = bkt.Delete(key); err != nil { - return err - } - - return ErrForEachStop - }); err != nil && !xerrors.Is(err, ErrForEachStop) { - return err - } - - return nil + return store.conn.Bolt().Update(func(tx *bolt.Tx) error { + return store.conn.DeleteMatchingFromBucket( + tx.Bucket(common.BucketUsersPhotos), &model.UserPhoto{}, + bolthold.Where("UserID").Eq(up.UserID).And("PhotoID").Eq(up.PhotoID), + ) }) } diff --git a/internal/store/users_stickers.go b/internal/store/users_stickers.go index 7ea0937..46bbd57 100644 --- a/internal/store/users_stickers.go +++ b/internal/store/users_stickers.go @@ -1,306 +1,169 @@ package store import ( - "sort" - "strconv" "strings" "time" - bolt "github.com/etcd-io/bbolt" - json "github.com/json-iterator/go" - "github.com/valyala/fastjson" + "github.com/timshannon/bolthold" "gitlab.com/toby3d/mypackbot/internal/common" "gitlab.com/toby3d/mypackbot/internal/model" "gitlab.com/toby3d/mypackbot/internal/model/stickers" "gitlab.com/toby3d/mypackbot/internal/model/users" + bolt "go.etcd.io/bbolt" "golang.org/x/xerrors" ) type UsersStickersStore struct { - conn *bolt.DB - marshler json.API - parser fastjson.Parser - stickers stickers.Manager - users users.Manager + conn *bolthold.Store + stickers stickers.Reader + users users.Reader } -var ( - ErrUserStickerExist = model.Error{ - Message: "Sticker already imported", - } - - ErrUserStickerNotExist = model.Error{ - Message: "Sticker already removed", - } -) - -func NewUsersStickersStore(db *bolt.DB, us users.Manager, ss stickers.Manager, m json.API) *UsersStickersStore { +func NewUsersStickersStore(conn *bolthold.Store, us users.Reader, ss stickers.Reader) *UsersStickersStore { return &UsersStickersStore{ - conn: db, - marshler: m, - parser: fastjson.Parser{}, + conn: conn, stickers: ss, users: us, } } -func (store *UsersStickersStore) Add(us *model.UserSticker) (err error) { - if us == nil || us.UserID == 0 || us.StickerID == 0 { - return nil +func (store *UsersStickersStore) Add(us *model.UserSticker) error { + if store.users.Get(us.UserID) == nil || store.stickers.Get(us.StickerID) == nil { + return bolthold.ErrNotFound } - userSticker := store.Get(us) - if userSticker != nil { - return ErrUserStickerExist - } - - timeStamp := time.Now().UTC().Unix() - - if us.CreatedAt == 0 { - us.CreatedAt = timeStamp - } - - if us.UpdatedAt == 0 { - us.UpdatedAt = timeStamp - } - - return store.conn.Update(func(tx *bolt.Tx) (err error) { - bkt := tx.Bucket(common.BucketUsersStickers) - - us.ID, err = bkt.NextSequence() - if err != nil { - return err - } - - src, err := store.marshler.Marshal(us) - if err != nil { - return err - } - - return bkt.Put([]byte(strconv.FormatUint(us.ID, 10)), src) + return store.conn.Bolt().Update(func(tx *bolt.Tx) error { + return store.conn.InsertIntoBucket(tx.Bucket(common.BucketUsersStickers), bolthold.NextSequence(), us) }) } -func (store *UsersStickersStore) AddSet(uid uint64, setName string) (err error) { - set, _ := store.stickers.GetSet(setName) - for i := range set { - _ = store.Add(&model.UserSticker{ - UserID: uid, - StickerID: set[i].ID, - }) +func (store *UsersStickersStore) AddSet(uid int, setName string) error { + for _, sticker := range store.stickers.GetSet(setName) { + us := model.UserSticker{UserID: uid, StickerID: sticker.ID} + + if store.Get(&us) != nil { + continue + } + + now := time.Now().UTC().Unix() + us.Query = sticker.Emoji + us.CreatedAt = now + us.UpdatedAt = now + + if err := store.Add(&us); err != nil { + return err + } } - return err + return nil } func (store *UsersStickersStore) Update(us *model.UserSticker) (err error) { - if us == nil || us.UserID == 0 || us.StickerID == 0 { - return nil - } - - userSticker := store.Get(us) - if userSticker == nil { - return store.Add(us) - } - - if us.ID == 0 { - us.ID = userSticker.ID - } - - if us.CreatedAt == 0 { - us.CreatedAt = userSticker.CreatedAt - } - - if us.UpdatedAt <= userSticker.UpdatedAt { - us.UpdatedAt = time.Now().UTC().Unix() - } - - src, err := store.marshler.Marshal(us) - if err != nil { - return err - } - - return store.conn.Update(func(tx *bolt.Tx) error { - return tx.Bucket(common.BucketUsersStickers).Put([]byte(strconv.FormatUint(us.ID, 10)), src) - }) -} - -func (store *UsersStickersStore) Get(us *model.UserSticker) *model.UserSticker { - if us == nil || us.UserID == 0 || us.StickerID == 0 { - return nil - } - - userSticker := new(model.UserSticker) - if err := store.conn.View(func(tx *bolt.Tx) (err error) { - if err = tx.Bucket(common.BucketUsersStickers).ForEach(func(key, val []byte) (err error) { - v, err := store.parser.ParseBytes(val) - if err != nil { - return err - } - - if v.GetUint64("user_id") != us.UserID || v.GetUint64("sticker_id") != us.StickerID { - return nil - } - - if err = store.marshler.Unmarshal(val, userSticker); err != nil { - return err - } - - return ErrForEachStop - }); err != nil && !xerrors.Is(err, ErrForEachStop) { - return err - } - - return nil - }); err != nil || userSticker.UserID == 0 || userSticker.StickerID == 0 { - return nil - } - - return userSticker -} - -//nolint: gocognit -func (store *UsersStickersStore) GetList(uid uint64, offset, limit int, q string) (list model.Stickers, count int) { - if limit <= 0 { - limit = 0 - } - - list = make(model.Stickers, 0, limit) - _ = store.conn.View(func(tx *bolt.Tx) error { - bkt := tx.Bucket(common.BucketStickers) - return tx.Bucket(common.BucketUsersStickers).ForEach(func(key, val []byte) error { - v, err := store.parser.ParseBytes(val) - if err != nil || v.GetUint64("user_id") != uid { - return err - } - - src := bkt.Get([]byte(strconv.FormatUint(v.GetUint64("sticker_id"), 10))) - if q != "" { - vQuery := string(v.GetStringBytes("query")) - switch { - case vQuery != "" && !strings.ContainsAny(vQuery, q): - return nil - case vQuery == "": - s, err := store.parser.ParseBytes(src) - if err != nil || !strings.ContainsAny(string(s.GetStringBytes("emoji")), q) { - return err - } + return store.conn.Bolt().Update(func(tx *bolt.Tx) error { + return store.conn.UpdateMatchingInBucket( + tx.Bucket(common.BucketUsersStickers), &model.UserSticker{}, + bolthold.Where("UserID").Eq(us.UserID).And("StickerID").Eq(us.StickerID), + func(record interface{}) error { + result, ok := record.(*model.UserSticker) // record will always be a pointer + if !ok { + return xerrors.New("invalid type") } - } - if (offset != 0 && count <= offset) || (limit > 0 && count > offset+limit) { + result.Query, result.UpdatedAt = us.Query, us.UpdatedAt + return nil - } - - count++ - - s := new(model.Sticker) - if err = store.marshler.Unmarshal(src, s); err != nil { - return err - } - - list = append(list, s) - - return nil - }) + }) }) - - sort.Slice(list, func(i, j int) bool { - return list[i].SetName < list[j].SetName || list[i].UpdatedAt < list[j].UpdatedAt - }) - - return list, count } -func (store *UsersStickersStore) GetSet(uid uint64, offset, limit int, setName string) (model.Stickers, int) { - count := 0 - stickers := make(model.Stickers, 0, limit) +func (store *UsersStickersStore) Get(us *model.UserSticker) *model.Sticker { + result := new(model.UserSticker) - _ = store.conn.View(func(tx *bolt.Tx) error { - bkt := tx.Bucket(common.BucketStickers) - return tx.Bucket(common.BucketUsersStickers).ForEach(func(key, val []byte) (err error) { - v, err := store.parser.ParseBytes(val) - if err != nil { - return err - } - - if v.GetUint64("user_id") != uid { - return nil - } - - src := bkt.Get([]byte(strconv.FormatUint(v.GetUint64("sticker_id"), 10))) - if v, err = store.parser.ParseBytes(src); err != nil { - return err - } - - if !strings.EqualFold(string(v.GetStringBytes("set_name")), setName) { - return nil - } - - count++ - - if count <= offset || count > limit { - return nil - } - - s := new(model.Sticker) - if err = store.marshler.Unmarshal(src, s); err != nil { - return err - } - - stickers = append(stickers, s) - - return nil - }) - }) - - sort.Slice(stickers, func(i, j int) bool { - return stickers[i].UpdatedAt < stickers[j].UpdatedAt - }) - - return stickers, count -} - -func (store *UsersStickersStore) Remove(us *model.UserSticker) (err error) { - userSticker := store.Get(us) - if userSticker == nil { - return ErrUserStickerNotExist + if err := store.conn.Bolt().View(func(tx *bolt.Tx) error { + return store.conn.FindOneInBucket( + tx.Bucket(common.BucketUsersStickers), result, + bolthold.Where("UserID").Eq(us.UserID).And("StickerID").Eq(us.StickerID), + ) + }); err != nil { + return nil } - return store.conn.Update(func(tx *bolt.Tx) (err error) { + return store.stickers.Get(result.StickerID) +} + +func (store *UsersStickersStore) GetList(offset, limit int, filter *model.UserSticker) (list model.Stickers, count int, + err error) { + + q := bolthold.Where("UserID").Ne(0).And("StickerID").Ne("") + qCount := bolthold.Where("UserID").Ne(0).And("StickerID").Ne("") + + if offset != 0 { + q = q.Skip(offset) + } + + if limit != 0 { + q = q.Limit(limit) + } + + if filter != nil { + if filter.UserID != 0 { + q = q.And("UserID").Eq(filter.UserID) + qCount = qCount.And("UserID").Eq(filter.UserID) + } + + if filter.Query != "" { + q = q.And("Query").MatchFunc(func(field string) (bool, error) { + return strings.ContainsAny(field, filter.Query), nil + }) + qCount = qCount.And("Query").MatchFunc(func(field string) (bool, error) { + return strings.ContainsAny(field, filter.Query), nil + }) + } + } + + results := make(model.UserStickers, 0) + + if err = store.conn.Bolt().View(func(tx *bolt.Tx) (err error) { bkt := tx.Bucket(common.BucketUsersStickers) - if err = bkt.ForEach(func(key, val []byte) (err error) { - v, err := store.parser.ParseBytes(val) - if err != nil { - return err - } - if userSticker.UserID != v.GetUint64("user_id") || - (userSticker.StickerID != 0 && userSticker.StickerID != v.GetUint64("sticker_id")) { - return nil - } - - if err = bkt.Delete(key); err != nil { - return err - } - - return ErrForEachStop - }); err != nil && !xerrors.Is(err, ErrForEachStop) { + if count, err = store.conn.CountInBucket(bkt, &model.UserSticker{}, qCount); err != nil { return err } - return nil + return store.conn.FindInBucket(bkt, &results, q) + }); err != nil { + return nil, 0, err + } + + list = make(model.Stickers, 0) + + for i := range results { + list = append(list, store.stickers.Get(results[i].StickerID)) + } + + return list, count, err +} + +func (store *UsersStickersStore) Remove(us *model.UserSticker) error { + return store.conn.Bolt().Update(func(tx *bolt.Tx) error { + return store.conn.DeleteMatchingFromBucket( + tx.Bucket(common.BucketUsersStickers), &model.UserSticker{}, + bolthold.Where("UserID").Eq(us.UserID).And("StickerID").Eq(us.StickerID), + ) }) } -func (store *UsersStickersStore) RemoveSet(uid uint64, setName string) (err error) { - set, _ := store.stickers.GetSet(setName) - for i := range set { - _ = store.Remove(&model.UserSticker{ - UserID: uid, - StickerID: set[i].ID, - }) +func (store *UsersStickersStore) RemoveSet(uid int, setName string) error { + ids := make([]string, 0) + + for _, sticker := range store.stickers.GetSet(setName) { + ids = append(ids, sticker.ID) } - return err + return store.conn.Bolt().Update(func(tx *bolt.Tx) error { + return store.conn.DeleteMatchingFromBucket( + tx.Bucket(common.BucketUsersStickers), &model.UserSticker{}, + bolthold.Where("UserID").Eq(uid).And("StickerID").ContainsAny(ids), + ) + }) } diff --git a/locales/en/out.gotext.json b/locales/en/out.gotext.json index c885c3c..9620317 100644 --- a/locales/en/out.gotext.json +++ b/locales/en/out.gotext.json @@ -1,48 +1,6 @@ { "language": "en", "messages": [ - { - "id": "🥳 4 November? It's a @toby3d birthday!\nIf you like this bot, then why not send him a congratulation along with a small gift? This will make him incredibly happy!", - "message": "🥳 4 November? It's a @toby3d birthday!\nIf you like this bot, then why not send him a congratulation along with a small gift? This will make him incredibly happy!", - "translation": "🥳 4 November? It's a @toby3d birthday!\nIf you like this bot, then why not send him a congratulation along with a small gift? This will make him incredibly happy!", - "translatorComment": "Copied from source.", - "fuzzy": true - }, - { - "id": "☺️ Oh, you missed @toby3d birthday on November 4th!\nIf you like this bot, why not send him some birthday greetings and a little birthday gift? It is not yet too late to make him happy!", - "message": "☺️ Oh, you missed @toby3d birthday on November 4th!\nIf you like this bot, why not send him some birthday greetings and a little birthday gift? It is not yet too late to make him happy!", - "translation": "☺️ Oh, you missed @toby3d birthday on November 4th!\nIf you like this bot, why not send him some birthday greetings and a little birthday gift? It is not yet too late to make him happy!", - "translatorComment": "Copied from source.", - "fuzzy": true - }, - { - "id": "💸 Make a donation", - "message": "💸 Make a donation", - "translation": "💸 Make a donation", - "translatorComment": "Copied from source.", - "fuzzy": true - }, - { - "id": "🤝 Use referral links", - "message": "🤝 Use referral links", - "translation": "🤝 Use referral links", - "translatorComment": "Copied from source.", - "fuzzy": true - }, - { - "id": "🕺 HacktoberFest is here!\n\nIf you are a beginner or already an experienced golang-developer, now is a great time to help improve the quality of the code of this bot. Choose issue to your taste and offer your PR!", - "message": "🕺 HacktoberFest is here!\n\nIf you are a beginner or already an experienced golang-developer, now is a great time to help improve the quality of the code of this bot. Choose issue to your taste and offer your PR!", - "translation": "🕺 HacktoberFest is here!\n\nIf you are a beginner or already an experienced golang-developer, now is a great time to help improve the quality of the code of this bot. Choose issue to your taste and offer your PR!", - "translatorComment": "Copied from source.", - "fuzzy": true - }, - { - "id": "🔧 Let's hack!", - "message": "🔧 Let's hack!", - "translation": "🔧 Let's hack!", - "translatorComment": "Copied from source.", - "fuzzy": true - }, { "id": "👋 Hi {FullName}, I'm {FullName_1}!\nThanks to me, you can collect almost any media content in Telegram without any limits, in any chat via inline mode.", "message": "👋 Hi {FullName}, I'm {FullName_1}!\nThanks to me, you can collect almost any media content in Telegram without any limits, in any chat via inline mode.", @@ -202,6 +160,48 @@ } ], "fuzzy": true + }, + { + "id": "🥳 4 November? It's a @toby3d birthday!\nIf you like this bot, then why not send him a congratulation along with a small gift? This will make him incredibly happy!", + "message": "🥳 4 November? It's a @toby3d birthday!\nIf you like this bot, then why not send him a congratulation along with a small gift? This will make him incredibly happy!", + "translation": "🥳 4 November? It's a @toby3d birthday!\nIf you like this bot, then why not send him a congratulation along with a small gift? This will make him incredibly happy!", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "☺️ Oh, you missed @toby3d birthday on November 4th!\nIf you like this bot, why not send him some birthday greetings and a little birthday gift? It is not yet too late to make him happy!", + "message": "☺️ Oh, you missed @toby3d birthday on November 4th!\nIf you like this bot, why not send him some birthday greetings and a little birthday gift? It is not yet too late to make him happy!", + "translation": "☺️ Oh, you missed @toby3d birthday on November 4th!\nIf you like this bot, why not send him some birthday greetings and a little birthday gift? It is not yet too late to make him happy!", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "💸 Make a donation", + "message": "💸 Make a donation", + "translation": "💸 Make a donation", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "🤝 Use referral links", + "message": "🤝 Use referral links", + "translation": "🤝 Use referral links", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "🕺 HacktoberFest is here!\n\nIf you are a beginner or already an experienced golang-developer, now is a great time to help improve the quality of the code of this bot. Choose issue to your taste and offer your PR!", + "message": "🕺 HacktoberFest is here!\n\nIf you are a beginner or already an experienced golang-developer, now is a great time to help improve the quality of the code of this bot. Choose issue to your taste and offer your PR!", + "translation": "🕺 HacktoberFest is here!\n\nIf you are a beginner or already an experienced golang-developer, now is a great time to help improve the quality of the code of this bot. Choose issue to your taste and offer your PR!", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "🔧 Let's hack!", + "message": "🔧 Let's hack!", + "translation": "🔧 Let's hack!", + "translatorComment": "Copied from source.", + "fuzzy": true } ] } \ No newline at end of file diff --git a/locales/ru/out.gotext.json b/locales/ru/out.gotext.json index d1599b2..8f04366 100644 --- a/locales/ru/out.gotext.json +++ b/locales/ru/out.gotext.json @@ -1,36 +1,6 @@ { "language": "ru", "messages": [ - { - "id": "🥳 4 November? It's a @toby3d birthday!\nIf you like this bot, then why not send him a congratulation along with a small gift? This will make him incredibly happy!", - "message": "🥳 4 November? It's a @toby3d birthday!\nIf you like this bot, then why not send him a congratulation along with a small gift? This will make him incredibly happy!", - "translation": "🥳 4-е Ноября? Это день рождения @toby3d!\nЕсли тебе нравится этот бот, то почему бы не отправить ему поздравления вместе с небольшим подарком? Это несказанно его осчастливит!" - }, - { - "id": "☺️ Oh, you missed @toby3d birthday on November 4th!\nIf you like this bot, why not send him some birthday greetings and a little birthday gift? It is not yet too late to make him happy!", - "message": "☺️ Oh, you missed @toby3d birthday on November 4th!\nIf you like this bot, why not send him some birthday greetings and a little birthday gift? It is not yet too late to make him happy!", - "translation": "☺️ Ой, ты пропустил день рождения @toby3d 4-го Ноября!\nЕсли тебе нравится этот бот, то почему бы не отправить ему поздравления вместе с небольшим подарком? Ещё не слишком поздно его порадовать!" - }, - { - "id": "💸 Make a donation", - "message": "💸 Make a donation", - "translation": "💸 Пожертвование" - }, - { - "id": "🤝 Use referral links", - "message": "🤝 Use referral links", - "translation": "🤝 Реферальные ссылки" - }, - { - "id": "🕺 HacktoberFest is here!\n\nIf you are a beginner or already an experienced golang-developer, now is a great time to help improve the quality of the code of this bot. Choose issue to your taste and offer your PR!", - "message": "🕺 HacktoberFest is here!\n\nIf you are a beginner or already an experienced golang-developer, now is a great time to help improve the quality of the code of this bot. Choose issue to your taste and offer your PR!", - "translation": "🕺 Хактоберфест уже здесь!\n\nЕсли ты начинающий или уже опытный golang-разработчик, то сейчас хорошее время помочь улучшить качество кода этого бота. Выбери issue на свой вкус и предложи PR!" - }, - { - "id": "🔧 Let's hack!", - "message": "🔧 Let's hack!", - "translation": "🔧 Let's hack!" - }, { "id": "👋 Hi {FullName}, I'm {FullName_1}!\nThanks to me, you can collect almost any media content in Telegram without any limits, in any chat via inline mode.", "message": "👋 Hi {FullName}, I'm {FullName_1}!\nThanks to me, you can collect almost any media content in Telegram without any limits, in any chat via inline mode.", @@ -158,6 +128,36 @@ "expr": "setName" } ] + }, + { + "id": "🥳 4 November? It's a @toby3d birthday!\nIf you like this bot, then why not send him a congratulation along with a small gift? This will make him incredibly happy!", + "message": "🥳 4 November? It's a @toby3d birthday!\nIf you like this bot, then why not send him a congratulation along with a small gift? This will make him incredibly happy!", + "translation": "🥳 4-е Ноября? Это день рождения @toby3d!\nЕсли тебе нравится этот бот, то почему бы не отправить ему поздравления вместе с небольшим подарком? Это несказанно его осчастливит!" + }, + { + "id": "☺️ Oh, you missed @toby3d birthday on November 4th!\nIf you like this bot, why not send him some birthday greetings and a little birthday gift? It is not yet too late to make him happy!", + "message": "☺️ Oh, you missed @toby3d birthday on November 4th!\nIf you like this bot, why not send him some birthday greetings and a little birthday gift? It is not yet too late to make him happy!", + "translation": "☺️ Ой, ты пропустил день рождения @toby3d 4-го Ноября!\nЕсли тебе нравится этот бот, то почему бы не отправить ему поздравления вместе с небольшим подарком? Ещё не слишком поздно его порадовать!" + }, + { + "id": "💸 Make a donation", + "message": "💸 Make a donation", + "translation": "💸 Пожертвование" + }, + { + "id": "🤝 Use referral links", + "message": "🤝 Use referral links", + "translation": "🤝 Реферальные ссылки" + }, + { + "id": "🕺 HacktoberFest is here!\n\nIf you are a beginner or already an experienced golang-developer, now is a great time to help improve the quality of the code of this bot. Choose issue to your taste and offer your PR!", + "message": "🕺 HacktoberFest is here!\n\nIf you are a beginner or already an experienced golang-developer, now is a great time to help improve the quality of the code of this bot. Choose issue to your taste and offer your PR!", + "translation": "🕺 Хактоберфест уже здесь!\n\nЕсли ты начинающий или уже опытный golang-разработчик, то сейчас хорошее время помочь улучшить качество кода этого бота. Выбери issue на свой вкус и предложи PR!" + }, + { + "id": "🔧 Let's hack!", + "message": "🔧 Let's hack!", + "translation": "🔧 Let's hack!" } ] } \ No newline at end of file