From 469997e7b0953852bb39387cb52373c06704401c Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Tue, 4 May 2021 14:48:20 +0300 Subject: [PATCH 001/439] faggot game v2 --- .gitignore | 2 + Makefile | 13 ++ api/player_factory.go | 21 +++ api/telebot.go | 33 ++++ api/telebot_game.go | 68 ++++++++ api/telebot_info.go | 48 ++++++ core/game_storage.go | 8 + core/localizer.go | 6 + core/player.go | 5 + core/round.go | 6 + go.mod | 10 ++ go.sum | 30 ++++ infrastructure/game_localizer.go | 108 ++++++++++++ infrastructure/game_storage.go | 70 ++++++++ infrastructure/player.go | 14 ++ infrastructure/player_factory.go | 5 + infrastructure/round.go | 12 ++ pullanusbot.go | 23 +++ use_cases/faggot_game.go | 161 ++++++++++++++++++ use_cases/faggot_game_test.go | 276 +++++++++++++++++++++++++++++++ use_cases/faggot_stat.go | 19 +++ 21 files changed, 938 insertions(+) create mode 100644 Makefile create mode 100644 api/player_factory.go create mode 100644 api/telebot.go create mode 100644 api/telebot_game.go create mode 100644 api/telebot_info.go create mode 100644 core/game_storage.go create mode 100644 core/localizer.go create mode 100644 core/player.go create mode 100644 core/round.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 infrastructure/game_localizer.go create mode 100644 infrastructure/game_storage.go create mode 100644 infrastructure/player.go create mode 100644 infrastructure/player_factory.go create mode 100644 infrastructure/round.go create mode 100644 pullanusbot.go create mode 100644 use_cases/faggot_game.go create mode 100644 use_cases/faggot_game_test.go create mode 100644 use_cases/faggot_stat.go diff --git a/.gitignore b/.gitignore index a482b96..5d4515d 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,8 @@ *.dll *.so *.dylib +*.db +cover.txt # Test binary, built with `go test -c` *.test diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..c264c78 --- /dev/null +++ b/Makefile @@ -0,0 +1,13 @@ +.PHONY: test run build clean + +run: build + ./pullanusbot + +test: + go test ./... -coverprofile=cover.txt + +build: clean *.go + go build . + +clean: + rm -f pullanusbot \ No newline at end of file diff --git a/api/player_factory.go b/api/player_factory.go new file mode 100644 index 0000000..09263c3 --- /dev/null +++ b/api/player_factory.go @@ -0,0 +1,21 @@ +package api + +import ( + "github.com/ailinykh/pullanusbot/v2/infrastructure" + tb "gopkg.in/tucnak/telebot.v2" +) + +type PlayerFactory struct { + m *tb.Message +} + +func (p *PlayerFactory) Make(string) infrastructure.Player { + return infrastructure.Player{ + GameID: p.m.Chat.ID, + UserID: p.m.Sender.ID, + FirstName: p.m.Sender.FirstName, + LastName: p.m.Sender.LastName, + Username: p.m.Sender.Username, + LanguageCode: p.m.Sender.LanguageCode, + } +} diff --git a/api/telebot.go b/api/telebot.go new file mode 100644 index 0000000..e8af3fe --- /dev/null +++ b/api/telebot.go @@ -0,0 +1,33 @@ +package api + +import ( + "time" + + tb "gopkg.in/tucnak/telebot.v2" +) + +type Telebot struct { + bot *tb.Bot +} + +func CreateTelebot(token string) *Telebot { + poller := tb.NewMiddlewarePoller(&tb.LongPoller{Timeout: 10 * time.Second}, func(upd *tb.Update) bool { + return true + }) + + var err error + bot, err := tb.NewBot(tb.Settings{ + Token: token, + Poller: poller, + }) + + if err != nil { + panic(err) + } + + return &Telebot{bot} +} + +func (t *Telebot) Run() { + t.bot.Start() +} diff --git a/api/telebot_game.go b/api/telebot_game.go new file mode 100644 index 0000000..0ad1d50 --- /dev/null +++ b/api/telebot_game.go @@ -0,0 +1,68 @@ +package api + +import ( + "math/rand" + "sync" + "time" + + "github.com/ailinykh/pullanusbot/v2/core" + "github.com/ailinykh/pullanusbot/v2/infrastructure" + "github.com/ailinykh/pullanusbot/v2/use_cases" + + tb "gopkg.in/tucnak/telebot.v2" +) + +func (t *Telebot) SetupGame(g use_cases.GameFlow) { + t.bot.Handle("/pidorules", func(m *tb.Message) { + text := g.Rules() + t.bot.Send(m.Chat, text, &tb.SendOptions{ParseMode: tb.ModeHTML}) + }) + + t.bot.Handle("/pidoreg", func(m *tb.Message) { + text := g.Add(makePlayer(m), makeStorage(m)) + t.bot.Send(m.Chat, text, &tb.SendOptions{ParseMode: tb.ModeHTML}) + }) + + var mutex sync.Mutex + + t.bot.Handle("/pidor", func(m *tb.Message) { + mutex.Lock() + defer mutex.Unlock() + + messages := g.Play(makePlayer(m), makeStorage(m)) + if len(messages) > 1 { + for _, msg := range messages { + t.bot.Send(m.Chat, msg, &tb.SendOptions{ParseMode: tb.ModeHTML}) + r := rand.Intn(3) + 1 + time.Sleep(time.Duration(r) * time.Second) + } + } else { + t.bot.Send(m.Chat, messages[0], &tb.SendOptions{ParseMode: tb.ModeHTML}) + } + }) + + t.bot.Handle("/pidorall", func(m *tb.Message) { + text := g.All(makeStorage(m)) + t.bot.Send(m.Chat, text, &tb.SendOptions{ParseMode: tb.ModeHTML}) + }) + + t.bot.Handle("/pidorstats", func(m *tb.Message) { + text := g.Stats(makeStorage(m)) + t.bot.Send(m.Chat, text, &tb.SendOptions{ParseMode: tb.ModeHTML}) + }) + + t.bot.Handle("/pidorme", func(m *tb.Message) { + text := g.Me(makePlayer(m), makeStorage(m)) + t.bot.Send(m.Chat, text, &tb.SendOptions{ParseMode: tb.ModeHTML}) + }) +} + +func makePlayer(m *tb.Message) core.Player { + return core.Player{Username: m.Sender.Username} +} + +func makeStorage(m *tb.Message) core.IGameStorage { + factory := PlayerFactory{m} + storage := infrastructure.CreateGameStorage(m.Chat.ID, &factory) + return &storage +} diff --git a/api/telebot_info.go b/api/telebot_info.go new file mode 100644 index 0000000..f769aa6 --- /dev/null +++ b/api/telebot_info.go @@ -0,0 +1,48 @@ +package api + +import ( + "fmt" + "strings" + + tb "gopkg.in/tucnak/telebot.v2" +) + +func (t *Telebot) SetupInfo() { + t.bot.Handle("/info", func(m *tb.Message) { + info := []string{ + "💬 Chat", + fmt.Sprintf("ID: *%d*", m.Chat.ID), + fmt.Sprintf("Title: *%s*", m.Chat.Title), + fmt.Sprintf("Type: *%s*", m.Chat.Type), + "", + "👤 Sender", + fmt.Sprintf("ID: *%d*", m.Sender.ID), + fmt.Sprintf("First: *%s*", m.Sender.FirstName), + fmt.Sprintf("Last: *%s*", m.Sender.LastName), + "", + } + + if m.ReplyTo != nil { + if m.ReplyTo.OriginalChat != nil { + info = append(info, + "💬 OriginalChat", + fmt.Sprintf("ID: *%d*", m.ReplyTo.OriginalChat.ID), + fmt.Sprintf("Title: *%s*", m.ReplyTo.OriginalChat.Title), + fmt.Sprintf("Type: *%s*", m.ReplyTo.OriginalChat.Type), + "", + ) + } + if m.ReplyTo.OriginalSender != nil { + info = append(info, + "👤 OriginalSender", + fmt.Sprintf("ID: *%d*", m.ReplyTo.OriginalSender.ID), + fmt.Sprintf("First: *%s*", m.ReplyTo.OriginalSender.FirstName), + fmt.Sprintf("Last: *%s*", m.ReplyTo.OriginalSender.LastName), + "", + ) + } + } + + t.bot.Send(m.Chat, strings.Join(info, "\n"), &tb.SendOptions{ParseMode: tb.ModeMarkdown}) + }) +} diff --git a/core/game_storage.go b/core/game_storage.go new file mode 100644 index 0000000..c644652 --- /dev/null +++ b/core/game_storage.go @@ -0,0 +1,8 @@ +package core + +type IGameStorage interface { + GetPlayers() ([]Player, error) + GetRounds() ([]Round, error) + AddPlayer(Player) error + AddRound(Round) error +} diff --git a/core/localizer.go b/core/localizer.go new file mode 100644 index 0000000..aa2facd --- /dev/null +++ b/core/localizer.go @@ -0,0 +1,6 @@ +package core + +type ILocalizer interface { + I18n(string, ...interface{}) string + AllKeys() []string +} diff --git a/core/player.go b/core/player.go new file mode 100644 index 0000000..d711579 --- /dev/null +++ b/core/player.go @@ -0,0 +1,5 @@ +package core + +type Player struct { + Username string +} diff --git a/core/round.go b/core/round.go new file mode 100644 index 0000000..7812a98 --- /dev/null +++ b/core/round.go @@ -0,0 +1,6 @@ +package core + +type Round struct { + Day string + Winner Player +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..9a65db9 --- /dev/null +++ b/go.mod @@ -0,0 +1,10 @@ +module github.com/ailinykh/pullanusbot/v2 + +go 1.16 + +require ( + github.com/stretchr/testify v1.7.0 + gopkg.in/tucnak/telebot.v2 v2.3.4 + gorm.io/driver/sqlite v1.1.3 + gorm.io/gorm v1.20.2 +) \ No newline at end of file diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a245871 --- /dev/null +++ b/go.sum @@ -0,0 +1,30 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.1 h1:g39TucaRWyV3dwDO++eEc6qf8TVIQ/Da48WmqjZ3i7E= +github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/mattn/go-sqlite3 v1.14.3 h1:j7a/xn1U6TKA/PHHxqZuzh64CdtRc7rU9M+AvkOl5bA= +github.com/mattn/go-sqlite3 v1.14.3/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/tucnak/telebot.v2 v2.3.4 h1:LtZ1hahdWDYFX723PlkLDMo56p99uMzrvBL9BRhyNy4= +gopkg.in/tucnak/telebot.v2 v2.3.4/go.mod h1:t+KVAiqFsG9ZDF0hz1ZPFTyENtlrDrDS3qmRRqhICBg= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/sqlite v1.1.3 h1:BYfdVuZB5He/u9dt4qDpZqiqDJ6KhPqs5QUqsr/Eeuc= +gorm.io/driver/sqlite v1.1.3/go.mod h1:AKDgRWk8lcSQSw+9kxCJnX/yySj8G3rdwYlU57cB45c= +gorm.io/gorm v1.20.1/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= +gorm.io/gorm v1.20.2 h1:bZzSEnq7NDGsrd+n3evOOedDrY5oLM5QPlCjZJUK2ro= +gorm.io/gorm v1.20.2/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= diff --git a/infrastructure/game_localizer.go b/infrastructure/game_localizer.go new file mode 100644 index 0000000..121ec57 --- /dev/null +++ b/infrastructure/game_localizer.go @@ -0,0 +1,108 @@ +package infrastructure + +import ( + "fmt" + "runtime" +) + +type GameLocalizer struct{} + +var ru = map[string]string{ + // Faggot game + "faggot_rules": `Правила игры Пидор Дня (только для групповых чатов): + 1. Зарегистрируйтесь в игру по команде /pidoreg + 2. Подождите пока зарегиструются все (или большинство :) + 3. Запустите розыгрыш по команде /pidor + 4. Просмотр статистики канала по команде /pidorstats, /pidorall + 5. Личная статистика по команде /pidorme + 6. Статистика за 2018 год по комнаде /pidor2018 (так же есть за 2016-2017) + + Важно, розыгрыш проходит только раз в день, повторная команда выведет результат игры. + + Сброс розыгрыша происходит каждый день в 12 часов ночи по UTC+2 (или два часа ночи по Москве).`, + + "faggot_not_available_for_private": "Извините, данная команда недоступна в личных чатах.", + "faggot_added_to_game": "Ты в игре!", + "faggot_already_in_game": "Эй! Ты уже в игре!", + "faggot_no_players": "Зарегистрированных в игру еще нет, а значит пидор ты - %s", + "faggot_not_enough_players": "Нужно как минимум два игрока, чтобы начать игру! Зарегистрируйся используя /pidoreg", + "faggot_winner_known": "Согласно моей информации, по результатам сегодняшнего розыгрыша пидор дня - %s!", + "faggot_winner_left": "Я нашел пидора дня, но похоже, что он вышел из этого чата (вот пидор!), так что попробуйте еще раз!", + // 0 + "faggot_game_0_0": "Осторожно! Пидор дня активирован!", + "faggot_game_0_1": "Система взломана. Нанесён урон. Запущено планирование контрмер.", + "faggot_game_0_2": "Сейчас поколдуем...", + "faggot_game_0_3": "Инициирую поиск пидора дня...", + "faggot_game_0_4": "Итак... кто же сегодня пидор дня?", + "faggot_game_0_5": "Кто сегодня счастливчик?", + "faggot_game_0_6": "Зачем вы меня разбудили...", + "faggot_game_0_7": "### RUNNING 'TYPIDOR.SH'...", + "faggot_game_0_8": "Woop-woop! That's the sound of da pidor-police!", + "faggot_game_0_9": "Опять в эти ваши игрульки играете? Ну ладно...", + // 1 + "faggot_game_1_0": "Шаманим-шаманим...", + "faggot_game_1_1": "Где-же он...", + "faggot_game_1_2": "Сканирую...", + "faggot_game_1_3": "Военный спутник запущен, коды доступа внутри...", + "faggot_game_1_4": "Хм...", + "faggot_game_1_5": "Интересно...", + "faggot_game_1_6": "Ведётся поиск в базе данных...", + "faggot_game_1_7": "Машины выехали", + "faggot_game_1_8": "(Ворчит) А могли бы на работе делом заниматься", + "faggot_game_1_9": "Выезжаю на место...", + // 2 + "faggot_game_2_0": "Так-так, что же тут у нас...", + "faggot_game_2_1": "КЕК!", + "faggot_game_2_2": "Доступ получен. Аннулирование протокола.", + "faggot_game_2_3": "Проверяю данные...", + "faggot_game_2_4": "Ох...", + "faggot_game_2_5": "Высокий приоритет мобильному юниту.", + "faggot_game_2_6": "Ведётся захват подозреваемого...", + "faggot_game_2_7": "Что с нами стало...", + "faggot_game_2_8": "Сонно смотрит на бумаги", + "faggot_game_2_9": "В этом совершенно нет смысла...", + // 3 + "faggot_game_3_0": "Ого, вы посмотрите только! А пидор дня то - %s", + "faggot_game_3_1": "Кажется, пидор дня - %s", + "faggot_game_3_2": ` ​ .∧_∧ + ( ・ω・。)つ━☆・*。 + ⊂ ノ ・゜+. + しーJ °。+ *´¨) + .· ´¸.·*´¨) + (¸.·´ (¸.·"* ☆ ВЖУХ И ТЫ ПИДОР, %s`, + "faggot_game_3_3": "И прекрасный человек дня сегодня... а нет, ошибка, всего-лишь пидор - %s", + "faggot_game_3_4": "Анализ завершен. Ты пидор, %s", + "faggot_game_3_5": "Ага! Поздравляю! Сегодня ты пидор, %s", + "faggot_game_3_6": "Что? Где? Когда? А ты пидор дня - %s", + "faggot_game_3_7": "Ну ты и пидор, %s", + "faggot_game_3_8": "Кто бы мог подумать, но пидор дня - %s", + "faggot_game_3_9": "Стоять! Не двигаться! Вы объявлены пидором дня, %s", + + "faggot_stats_top": "Топ-10 пидоров за текущий год:", + "faggot_stats_entry": "%d. %s — %d раз(а)", + "faggot_stats_bottom": "Всего участников — %d", + + "faggot_all_top": "Топ-10 пидоров за всё время:", + "faggot_all_entry": "%d. %s — %d раз(а)", + "faggot_all_bottom": "Всего участников — %d", + + "faggot_me": "%s, ты был(а) пидором дня — %d раз!", +} + +func (l GameLocalizer) I18n(key string, args ...interface{}) string { + + if val, ok := ru[key]; ok { + return fmt.Sprintf(val, args...) + } + + _, file, line, _ := runtime.Caller(0) + return fmt.Sprintf("%s:%d KEY_MISSED:\"%s\"", file, line, key) +} + +func (l GameLocalizer) AllKeys() []string { + keys := make([]string, 0, len(ru)) + for k := range ru { + keys = append(keys, k) + } + return keys +} diff --git a/infrastructure/game_storage.go b/infrastructure/game_storage.go new file mode 100644 index 0000000..8dd3bb8 --- /dev/null +++ b/infrastructure/game_storage.go @@ -0,0 +1,70 @@ +package infrastructure + +import ( + "log" + "path" + + "github.com/ailinykh/pullanusbot/v2/core" + "gorm.io/driver/sqlite" + "gorm.io/gorm" +) + +func CreateGameStorage(gameID int64, factory IPlayerFactory) GameStorage { + dbFile := path.Join(".", "pullanusbot.db") + // logger.Info("Using database: ", dbFile) + conn, err := gorm.Open(sqlite.Open(dbFile+"?cache=shared"), &gorm.Config{ + // Logger: loger.Default.LogMode(loger.Error), + }) + if err != nil { + log.Fatal(err) + } + conn.AutoMigrate(&Player{}, &Round{}) + + s := GameStorage{conn, gameID, factory} + return s +} + +type GameStorage struct { + conn *gorm.DB + gameID int64 + playerFactory IPlayerFactory +} + +func (db *GameStorage) GetPlayers() ([]core.Player, error) { + var dbPlayers []Player + var corePlayers []core.Player + db.conn.Where("game_id = ?", db.gameID).Find(&dbPlayers) + for _, p := range dbPlayers { + corePlayers = append(corePlayers, core.Player{Username: p.Username}) + } + return corePlayers, nil +} + +func (s *GameStorage) GetRounds() ([]core.Round, error) { + var dbRounds []Round + var coreRounds []core.Round + s.conn.Where("game_id = ?", s.gameID).Find(&dbRounds) + for _, r := range dbRounds { + player := core.Player{Username: r.Username} + coreRounds = append(coreRounds, core.Round{Day: r.Day, Winner: player}) + } + return coreRounds, nil +} + +func (s *GameStorage) AddPlayer(player core.Player) error { + dbPlayer := s.playerFactory.Make(player.Username) + s.conn.Create(&dbPlayer) + return nil +} + +func (s *GameStorage) AddRound(round core.Round) error { + player := s.playerFactory.Make(round.Winner.Username) + dbRound := Round{ + GameID: s.gameID, + UserID: player.UserID, + Day: round.Day, + Username: round.Winner.Username, + } + s.conn.Create(&dbRound) + return nil +} diff --git a/infrastructure/player.go b/infrastructure/player.go new file mode 100644 index 0000000..80c62e0 --- /dev/null +++ b/infrastructure/player.go @@ -0,0 +1,14 @@ +package infrastructure + +type Player struct { + GameID int64 `gorm:"primaryKey"` + UserID int `gorm:"primaryKey"` + FirstName string + LastName string + Username string + LanguageCode string +} + +func (Player) TableName() string { + return "faggot_players" +} diff --git a/infrastructure/player_factory.go b/infrastructure/player_factory.go new file mode 100644 index 0000000..a2114bd --- /dev/null +++ b/infrastructure/player_factory.go @@ -0,0 +1,5 @@ +package infrastructure + +type IPlayerFactory interface { + Make(string) Player +} diff --git a/infrastructure/round.go b/infrastructure/round.go new file mode 100644 index 0000000..d69d934 --- /dev/null +++ b/infrastructure/round.go @@ -0,0 +1,12 @@ +package infrastructure + +type Round struct { + GameID int64 + UserID int + Day string `gorm:"primaryKey"` + Username string +} + +func (Round) TableName() string { + return "faggot_rounds" +} diff --git a/pullanusbot.go b/pullanusbot.go new file mode 100644 index 0000000..f20a966 --- /dev/null +++ b/pullanusbot.go @@ -0,0 +1,23 @@ +package main + +import ( + "math/rand" + "time" + + "github.com/ailinykh/pullanusbot/v2/api" + "github.com/ailinykh/pullanusbot/v2/infrastructure" + "github.com/ailinykh/pullanusbot/v2/use_cases" +) + +func main() { + rand.Seed(time.Now().UTC().UnixNano()) + + localizer := infrastructure.GameLocalizer{} + game := use_cases.CreateGameFlow(localizer) + telebot := api.CreateTelebot("TOKEN") + + telebot.SetupGame(game) + telebot.SetupInfo() + // Start endless loop + telebot.Run() +} diff --git a/use_cases/faggot_game.go b/use_cases/faggot_game.go new file mode 100644 index 0000000..fea0b22 --- /dev/null +++ b/use_cases/faggot_game.go @@ -0,0 +1,161 @@ +package use_cases + +import ( + "fmt" + "math/rand" + "sort" + "strconv" + "strings" + "time" + + "github.com/ailinykh/pullanusbot/v2/core" +) + +func CreateGameFlow(l core.ILocalizer) GameFlow { + return GameFlow{l} +} + +type GameFlow struct { + l core.ILocalizer +} + +func (flow *GameFlow) Rules() string { + return flow.l.I18n("faggot_rules") +} + +func (flow *GameFlow) Add(player core.Player, storage core.IGameStorage) string { + players, _ := storage.GetPlayers() + for _, p := range players { + if p == player { + return flow.l.I18n("faggot_already_in_game") + } + } + + err := storage.AddPlayer(player) + + if err != nil { + return "Unexpected error" + } + + return flow.l.I18n("faggot_added_to_game") +} + +func (flow *GameFlow) Play(player core.Player, storage core.IGameStorage) []string { + players, _ := storage.GetPlayers() + switch len(players) { + case 0: + return []string{flow.l.I18n("faggot_no_players", player.Username)} + case 1: + return []string{flow.l.I18n("faggot_not_enough_players")} + } + + games, _ := storage.GetRounds() + loc, _ := time.LoadLocation("Europe/Zurich") + day := time.Now().In(loc).Format("2006-01-02") + + for _, r := range games { + if r.Day == day { + return []string{flow.l.I18n("faggot_winner_known", r.Winner.Username)} + } + } + + winner := players[rand.Intn(len(players))] + round := core.Round{Day: day, Winner: winner} + storage.AddRound(round) + + phrases := make([]string, 0, 4) + for i := 0; i <= 3; i++ { + templates := []string{} + for _, key := range flow.l.AllKeys() { + if strings.HasPrefix(key, fmt.Sprintf("faggot_game_%d", i)) { + templates = append(templates, key) + } + } + template := templates[rand.Intn(len(templates))] + phrase := flow.l.I18n(template) + + if i == 3 { + // TODO: implementation detail leaked + phrase = flow.l.I18n(template, "@"+winner.Username) + } + + phrases = append(phrases, phrase) + } + + return phrases +} + +func (flow *GameFlow) All(storage core.IGameStorage) string { + entries, _ := flow.getStat(storage) + messages := []string{flow.l.I18n("faggot_all_top"), ""} + for i, e := range entries { + message := flow.l.I18n("faggot_all_entry", i+1, e.Player.Username, e.Score) + messages = append(messages, message) + } + messages = append(messages, "", flow.l.I18n("faggot_all_bottom", len(entries))) + return strings.Join(messages, "\n") +} + +func (flow *GameFlow) Stats(storage core.IGameStorage) string { + year := strconv.Itoa(time.Now().Year()) + rounds, _ := storage.GetRounds() + entries := []Stat{} + + for _, r := range rounds { + if strings.HasPrefix(r.Day, year) { + index := Find(entries, r.Winner.Username) + if index == -1 { + entries = append(entries, Stat{Player: r.Winner, Score: 1}) + } else { + entries[index].Score++ + } + } + } + + sort.Slice(entries, func(i, j int) bool { + return entries[i].Score > entries[j].Score + }) + + messages := []string{flow.l.I18n("faggot_stats_top"), ""} + for i, e := range entries { + message := flow.l.I18n("faggot_stats_entry", i+1, e.Player.Username, e.Score) + messages = append(messages, message) + } + messages = append(messages, "", flow.l.I18n("faggot_stats_bottom", len(entries))) + return strings.Join(messages, "\n") +} + +func (flow *GameFlow) Me(player core.Player, storage core.IGameStorage) string { + entries, _ := flow.getStat(storage) + score := 0 + for _, e := range entries { + if e.Player == player { + score = e.Score + } + } + return flow.l.I18n("faggot_me", player.Username, score) +} + +func (flow *GameFlow) getStat(storage core.IGameStorage) ([]Stat, error) { + entries := []Stat{} + rounds, err := storage.GetRounds() + + if err != nil { + return nil, err + } + + for _, r := range rounds { + index := Find(entries, r.Winner.Username) + if index == -1 { + entries = append(entries, Stat{Player: r.Winner, Score: 1}) + } else { + entries[index].Score++ + } + } + + sort.Slice(entries, func(i, j int) bool { + return entries[i].Score > entries[j].Score + }) + + return entries, nil +} diff --git a/use_cases/faggot_game_test.go b/use_cases/faggot_game_test.go new file mode 100644 index 0000000..6e4ba19 --- /dev/null +++ b/use_cases/faggot_game_test.go @@ -0,0 +1,276 @@ +package use_cases + +import ( + "errors" + "fmt" + "strconv" + "strings" + "testing" + "time" + + "github.com/ailinykh/pullanusbot/v2/core" + "github.com/stretchr/testify/assert" +) + +func Test_RulesCommand_DeliversRules(t *testing.T) { + game, _, l := makeSUT(LocalizerDict{"faggot_rules": "Game rules:"}) + expected := l.I18n("faggot_rules") + rules := game.Rules() + + assert.Equal(t, rules, expected) +} + +func Test_RulesCommand_DeliversRulesInDifferentTranslations(t *testing.T) { + game, _, l := makeSUT(LocalizerDict{"faggot_rules": "Правила игры:"}) + expected := l.I18n("faggot_rules") + rules := game.Rules() + + assert.Equal(t, rules, expected) +} + +func Test_Add_ReturnsErrorOnStorageError(t *testing.T) { + game, storage, _ := makeSUT() + storage.err = errors.New("Unexpected error") + player := core.Player{Username: "Faggot"} + message := game.Add(player, storage) + + assert.Equal(t, message, storage.err.Error()) +} + +func Test_Add_AppendsPlayerInGameOnlyOnce(t *testing.T) { + game, storage, localizer := makeSUT(LocalizerDict{ + "faggot_added_to_game": "Player added", + "faggot_already_in_game": "Player already in game", + }) + player := core.Player{Username: "Faggot"} + + message := game.Add(player, storage) + + assert.Equal(t, storage.players, []core.Player{player}) + assert.Equal(t, message, localizer.I18n("faggot_added_to_game")) + + message = game.Add(player, storage) + + assert.Equal(t, storage.players, []core.Player{player}) + assert.Equal(t, message, localizer.I18n("faggot_already_in_game")) +} + +func Test_Play_RespondsWithNoPlayers(t *testing.T) { + game, storage, localizer := makeSUT(LocalizerDict{ + "faggot_no_players": "Nobody in game. So you win, %s!", + }) + player := core.Player{Username: "Faggot"} + messages := game.Play(player, storage) + expected := []string{localizer.I18n("faggot_no_players", player.Username)} + assert.Equal(t, messages, expected) +} + +func Test_Play_RespondsNotEnoughPlayers(t *testing.T) { + game, storage, localizer := makeSUT(LocalizerDict{ + "faggot_not_enough_players": "Not enough players", + }) + player := core.Player{Username: "Faggot"} + game.Add(player, storage) + + messages := game.Play(player, storage) + expected := []string{localizer.I18n("faggot_not_enough_players")} + assert.Equal(t, messages, expected) +} + +func Test_Play_RespondsWinnerAlreadyKnown(t *testing.T) { + game, storage, localizer := makeSUT(LocalizerDict{ + "faggot_game_0_0": "0", + "faggot_game_1_0": "1", + "faggot_game_2_0": "2", + "faggot_game_3_0": "3 %s", + "faggot_winner_known": "Winner already known %s", + }) + player1 := core.Player{Username: "Faggot1"} + player2 := core.Player{Username: "Faggot2"} + game.Add(player1, storage) + game.Add(player2, storage) + + messages := game.Play(player1, storage) + expected := []string{"0", "1", "2", fmt.Sprintf("3 @%s", storage.rounds[0].Winner.Username)} + assert.Equal(t, messages, expected) + + messages = game.Play(player1, storage) + expected = []string{localizer.I18n("faggot_winner_known", storage.rounds[0].Winner.Username)} + assert.Equal(t, messages, expected) +} + +func Test_Stats_RespondsWithDescendingResultsForCurrentYear(t *testing.T) { + year := strconv.Itoa(time.Now().Year()) + game, storage, _ := makeSUT(LocalizerDict{ + "faggot_stats_top": "top", + "faggot_stats_entry": "index:%d,player:%s,scores:%d", + "faggot_stats_bottom": "total_players:%d", + }) + + expected := []string{ + "top", + "", + "index:1,player:Faggot3,scores:3", + "index:2,player:Faggot1,scores:2", + "index:3,player:Faggot2,scores:1", + "", + "total_players:3", + } + + player1 := core.Player{Username: "Faggot1"} + player2 := core.Player{Username: "Faggot2"} + player3 := core.Player{Username: "Faggot3"} + + storage.rounds = []core.Round{ + {Day: year + "-01-01", Winner: player2}, + {Day: "2020-01-02", Winner: player3}, + {Day: year + "-01-02", Winner: player3}, + {Day: year + "-01-03", Winner: player3}, + {Day: year + "-01-04", Winner: player3}, + {Day: year + "-01-05", Winner: player1}, + {Day: year + "-01-06", Winner: player1}, + } + + message := game.Stats(storage) + assert.Equal(t, strings.Split(message, "\n"), expected) +} + +func Test_All_RespondsWithDescendingResultsForAllTime(t *testing.T) { + game, storage, _ := makeSUT(LocalizerDict{ + "faggot_all_top": "top", + "faggot_all_entry": "index:%d,player:%s,scores:%d", + "faggot_all_bottom": "total_players:%d", + }) + + expected := []string{ + "top", + "", + "index:1,player:Faggot3,scores:4", + "index:2,player:Faggot1,scores:2", + "index:3,player:Faggot2,scores:1", + "", + "total_players:3", + } + + player1 := core.Player{Username: "Faggot1"} + player2 := core.Player{Username: "Faggot2"} + player3 := core.Player{Username: "Faggot3"} + + storage.rounds = []core.Round{ + {Day: "2021-01-01", Winner: player2}, + {Day: "2020-01-02", Winner: player3}, + {Day: "2020-01-02", Winner: player3}, + {Day: "2021-01-03", Winner: player3}, + {Day: "2021-01-04", Winner: player3}, + {Day: "2021-01-05", Winner: player1}, + {Day: "2021-01-06", Winner: player1}, + } + + message := game.All(storage) + assert.Equal(t, strings.Split(message, "\n"), expected) +} + +func Test_Me_RespondsWithPersonalStat(t *testing.T) { + game, storage, localizer := makeSUT(LocalizerDict{ + "faggot_me": "username:%s,scores:%d", + }) + + player1 := core.Player{Username: "Faggot1"} + player2 := core.Player{Username: "Faggot2"} + + storage.rounds = []core.Round{ + {Day: "2021-01-01", Winner: player2}, + {Day: "2021-01-05", Winner: player1}, + {Day: "2021-01-06", Winner: player1}, + } + + var message string + message = game.Me(player1, storage) + assert.Equal(t, message, localizer.I18n("faggot_me", player1.Username, 2)) + + message = game.Me(player2, storage) + assert.Equal(t, message, localizer.I18n("faggot_me", player2.Username, 1)) +} + +// Helpers + +func makeSUT(args ...interface{}) (*GameFlow, *GameStorageMock, *LocalizerMock) { + dict := LocalizerDict{} + storage := &GameStorageMock{players: []core.Player{}} + + for _, arg := range args { + switch opt := arg.(type) { + case LocalizerDict: + dict = opt + } + } + + l := &LocalizerMock{dict: dict} + game := &GameFlow{l} + return game, storage, l +} + +// LocalizerMock + +type LocalizerMock struct { + dict LocalizerDict +} + +type LocalizerDict = map[string]string + +func (l *LocalizerMock) I18n(key string, args ...interface{}) string { + if val, ok := l.dict[key]; ok { + return fmt.Sprintf(val, args...) + } + return key +} + +func (l *LocalizerMock) AllKeys() []string { + keys := make([]string, 0, len(l.dict)) + for k := range l.dict { + keys = append(keys, k) + } + return keys +} + +// GameStorageMock + +type GameStorageMock struct { + players []core.Player + rounds []core.Round + err error +} + +func (s *GameStorageMock) AddPlayer(player core.Player) error { + if s.err != nil { + return s.err + } + + s.players = append(s.players, player) + return nil +} + +func (s *GameStorageMock) GetPlayers() ([]core.Player, error) { + if s.err != nil { + return []core.Player{}, s.err + } + + return s.players, nil +} + +func (s *GameStorageMock) AddRound(round core.Round) error { + if s.err != nil { + return s.err + } + + s.rounds = append(s.rounds, round) + return nil +} + +func (s *GameStorageMock) GetRounds() ([]core.Round, error) { + if s.err != nil { + return []core.Round{}, s.err + } + + return s.rounds, nil +} diff --git a/use_cases/faggot_stat.go b/use_cases/faggot_stat.go new file mode 100644 index 0000000..3c92980 --- /dev/null +++ b/use_cases/faggot_stat.go @@ -0,0 +1,19 @@ +package use_cases + +import ( + "github.com/ailinykh/pullanusbot/v2/core" +) + +type Stat struct { + Player core.Player + Score int +} + +func Find(a []Stat, username string) int { + for i, n := range a { + if username == n.Player.Username { + return i + } + } + return -1 +} From 79cc2f8569ab09ac07affb1c41871f1181a9d01a Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Fri, 7 May 2021 16:48:46 +0300 Subject: [PATCH 002/439] setup logger --- .gitignore | 1 + Makefile | 2 ++ api/telebot.go | 8 +++++--- core/logger.go | 10 ++++++++++ go.mod | 1 + go.sum | 4 ++++ infrastructure/game_storage.go | 4 ++-- pullanusbot.go | 21 ++++++++++++++++++++- 8 files changed, 45 insertions(+), 6 deletions(-) create mode 100644 core/logger.go diff --git a/.gitignore b/.gitignore index 5d4515d..f52b52d 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ *.dylib *.db cover.txt +.env # Test binary, built with `go test -c` *.test diff --git a/Makefile b/Makefile index c264c78..00b7746 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,5 @@ +-include .env + .PHONY: test run build clean run: build diff --git a/api/telebot.go b/api/telebot.go index e8af3fe..dcc652c 100644 --- a/api/telebot.go +++ b/api/telebot.go @@ -3,14 +3,16 @@ package api import ( "time" + "github.com/ailinykh/pullanusbot/v2/core" tb "gopkg.in/tucnak/telebot.v2" ) type Telebot struct { - bot *tb.Bot + bot *tb.Bot + logger core.ILogger } -func CreateTelebot(token string) *Telebot { +func CreateTelebot(token string, logger core.ILogger) *Telebot { poller := tb.NewMiddlewarePoller(&tb.LongPoller{Timeout: 10 * time.Second}, func(upd *tb.Update) bool { return true }) @@ -25,7 +27,7 @@ func CreateTelebot(token string) *Telebot { panic(err) } - return &Telebot{bot} + return &Telebot{bot, logger} } func (t *Telebot) Run() { diff --git a/core/logger.go b/core/logger.go new file mode 100644 index 0000000..ea4abc9 --- /dev/null +++ b/core/logger.go @@ -0,0 +1,10 @@ +package core + +type ILogger interface { + Error(...interface{}) + Errorf(string, ...interface{}) + Info(...interface{}) + Infof(string, ...interface{}) + Warning(...interface{}) + Warningf(string, ...interface{}) +} diff --git a/go.mod b/go.mod index 9a65db9..0dd28a7 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/ailinykh/pullanusbot/v2 go 1.16 require ( + github.com/google/logger v1.1.0 github.com/stretchr/testify v1.7.0 gopkg.in/tucnak/telebot.v2 v2.3.4 gorm.io/driver/sqlite v1.1.3 diff --git a/go.sum b/go.sum index a245871..87bff63 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/logger v1.1.0 h1:saB74Etb4EAJNH3z74CVbCKk75hld/8T0CsXKetWCwM= +github.com/google/logger v1.1.0/go.mod h1:w7O8nrRr0xufejBlQMI83MXqRusvREoJdaAxV+CoAB4= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.1 h1:g39TucaRWyV3dwDO++eEc6qf8TVIQ/Da48WmqjZ3i7E= @@ -15,6 +17,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/tucnak/telebot.v2 v2.3.4 h1:LtZ1hahdWDYFX723PlkLDMo56p99uMzrvBL9BRhyNy4= diff --git a/infrastructure/game_storage.go b/infrastructure/game_storage.go index 8dd3bb8..d742783 100644 --- a/infrastructure/game_storage.go +++ b/infrastructure/game_storage.go @@ -7,13 +7,13 @@ import ( "github.com/ailinykh/pullanusbot/v2/core" "gorm.io/driver/sqlite" "gorm.io/gorm" + "gorm.io/gorm/logger" ) func CreateGameStorage(gameID int64, factory IPlayerFactory) GameStorage { dbFile := path.Join(".", "pullanusbot.db") - // logger.Info("Using database: ", dbFile) conn, err := gorm.Open(sqlite.Open(dbFile+"?cache=shared"), &gorm.Config{ - // Logger: loger.Default.LogMode(loger.Error), + Logger: logger.Default.LogMode(logger.Error), }) if err != nil { log.Fatal(err) diff --git a/pullanusbot.go b/pullanusbot.go index f20a966..9a3c6d2 100644 --- a/pullanusbot.go +++ b/pullanusbot.go @@ -2,22 +2,41 @@ package main import ( "math/rand" + "os" "time" "github.com/ailinykh/pullanusbot/v2/api" + "github.com/ailinykh/pullanusbot/v2/core" "github.com/ailinykh/pullanusbot/v2/infrastructure" "github.com/ailinykh/pullanusbot/v2/use_cases" + "github.com/google/logger" ) func main() { rand.Seed(time.Now().UTC().UnixNano()) + logger, close := createLogger() + defer close() localizer := infrastructure.GameLocalizer{} game := use_cases.CreateGameFlow(localizer) - telebot := api.CreateTelebot("TOKEN") + telebot := api.CreateTelebot(os.Getenv("BOT_TOKEN"), logger) telebot.SetupGame(game) telebot.SetupInfo() // Start endless loop telebot.Run() } + +func createLogger() (core.ILogger, func()) { + lf, err := os.OpenFile("pullanusbot.txt", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0660) + if err != nil { + panic(err) + } + + l := logger.Init("pullanusbot", true, false, lf) + close := func() { + lf.Close() + l.Close() + } + return l, close +} From b5b4226d9e192fdb912a35aaf242ec05f46fe5e9 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Sat, 8 May 2021 14:50:29 +0300 Subject: [PATCH 003/439] video file converter --- api/telebot.go | 9 +- api/telebot_video.go | 91 ++++++++++++++++++++ core/video_file.go | 12 +++ core/video_file_converter.go | 5 ++ core/video_file_factory.go | 5 ++ infrastructure/ffmpeg_converter.go | 128 +++++++++++++++++++++++++++++ pullanusbot.go | 10 ++- use_cases/video_flow.go | 45 ++++++++++ 8 files changed, 299 insertions(+), 6 deletions(-) create mode 100644 api/telebot_video.go create mode 100644 core/video_file.go create mode 100644 core/video_file_converter.go create mode 100644 core/video_file_factory.go create mode 100644 infrastructure/ffmpeg_converter.go create mode 100644 use_cases/video_flow.go diff --git a/api/telebot.go b/api/telebot.go index dcc652c..1d0c958 100644 --- a/api/telebot.go +++ b/api/telebot.go @@ -8,11 +8,12 @@ import ( ) type Telebot struct { - bot *tb.Bot - logger core.ILogger + bot *tb.Bot + logger core.ILogger + videoFileFactory core.IVideoFileFactory } -func CreateTelebot(token string, logger core.ILogger) *Telebot { +func CreateTelebot(token string, logger core.ILogger, videoFileFactory core.IVideoFileFactory) *Telebot { poller := tb.NewMiddlewarePoller(&tb.LongPoller{Timeout: 10 * time.Second}, func(upd *tb.Update) bool { return true }) @@ -27,7 +28,7 @@ func CreateTelebot(token string, logger core.ILogger) *Telebot { panic(err) } - return &Telebot{bot, logger} + return &Telebot{bot, logger, videoFileFactory} } func (t *Telebot) Run() { diff --git a/api/telebot_video.go b/api/telebot_video.go new file mode 100644 index 0000000..8a4216b --- /dev/null +++ b/api/telebot_video.go @@ -0,0 +1,91 @@ +package api + +import ( + "fmt" + "os" + "path" + "strconv" + "sync" + + "github.com/ailinykh/pullanusbot/v2/core" + "github.com/ailinykh/pullanusbot/v2/use_cases" + tb "gopkg.in/tucnak/telebot.v2" +) + +func (t *Telebot) SetupVideo(video_flow *use_cases.VideoFlow) { + var mutex sync.Mutex + + t.bot.Handle(tb.OnDocument, func(m *tb.Message) { + if m.Document.MIME[:5] == "video" || m.Document.MIME == "image/gif" { + mutex.Lock() + defer mutex.Unlock() + + t.logger.Infof("Attempt to download %s %s (sent by %s)", m.Document.FileName, m.Document.MIME, m.Sender.Username) + + path := path.Join(os.TempDir(), m.Document.FileName) + err := t.bot.Download(&m.Document.File, path) + if err != nil { + t.logger.Error(err) + return + } + + t.logger.Infof("Downloaded to %s", path) + defer os.Remove(path) + inputFile, err := t.videoFileFactory.CreateVideoFile(path) + + if err != nil { + t.logger.Error(err) + return + } + + defer os.Remove(inputFile.ThumbPath) + + outputFile, err := video_flow.Process(inputFile) + + if err != nil { + chatID, e := strconv.ParseInt(os.Getenv("ADMIN_CHAT_ID"), 10, 64) + if e == nil { + chat := &tb.Chat{ID: chatID} + t.bot.Forward(chat, m) + t.bot.Send(chat, err.Error()) + } + return + } + + defer os.Remove(outputFile.FilePath) + defer os.Remove(outputFile.ThumbPath) + + caption := fmt.Sprintf("%s (by %s)", m.Document.FileName, m.Sender.Username) + if inputFile.FilePath != outputFile.FilePath { + fi, _ := os.Stat(outputFile.FilePath) + caption = fmt.Sprintf("%s (by %s)\nOriginal size: %.2f MB (%d kb/s)\nConverted size: %.2f MB (%d kb/s)", m.Document.FileName, m.Sender.Username, float32(m.Document.FileSize)/1048576, inputFile.Bitrate/1024, float32(fi.Size())/1048576, outputFile.Bitrate/1024) + } + video := makeVideoFile(outputFile, caption) + t.bot.Notify(m.Chat, tb.UploadingVideo) + _, err = video.Send(t.bot, m.Chat, &tb.SendOptions{ParseMode: tb.ModeHTML}) + if err != nil { + t.logger.Error(err) + } else { + t.logger.Infof("%s sent successfully", outputFile.FileName) + t.bot.Delete(m) + } + } else { + t.logger.Infof("%s not supported yet", m.Document.MIME) + } + }) + + t.bot.Handle(tb.OnAnimation, func(m *tb.Message) { + t.logger.Info(m.Animation) + }) +} + +func makeVideoFile(vf *core.VideoFile, caption string) tb.Video { + video := tb.Video{File: tb.FromDisk(vf.FilePath)} + video.Width = vf.Width + video.Height = vf.Height + video.Caption = caption + video.Duration = vf.Duration + video.SupportsStreaming = true + video.Thumbnail = &tb.Photo{File: tb.FromDisk(vf.ThumbPath)} + return video +} diff --git a/core/video_file.go b/core/video_file.go new file mode 100644 index 0000000..1a0f0cd --- /dev/null +++ b/core/video_file.go @@ -0,0 +1,12 @@ +package core + +type VideoFile struct { + Width int + Height int + Bitrate int + Duration int + Codec string + FileName string + FilePath string + ThumbPath string +} diff --git a/core/video_file_converter.go b/core/video_file_converter.go new file mode 100644 index 0000000..6124d6f --- /dev/null +++ b/core/video_file_converter.go @@ -0,0 +1,5 @@ +package core + +type IVideoFileConverter interface { + Convert(*VideoFile, int) (*VideoFile, error) +} diff --git a/core/video_file_factory.go b/core/video_file_factory.go new file mode 100644 index 0000000..16f3a18 --- /dev/null +++ b/core/video_file_factory.go @@ -0,0 +1,5 @@ +package core + +type IVideoFileFactory interface { + CreateVideoFile(path string) (*VideoFile, error) +} diff --git a/infrastructure/ffmpeg_converter.go b/infrastructure/ffmpeg_converter.go new file mode 100644 index 0000000..e3c7bf6 --- /dev/null +++ b/infrastructure/ffmpeg_converter.go @@ -0,0 +1,128 @@ +package infrastructure + +import ( + "encoding/json" + "errors" + "fmt" + "os" + "os/exec" + "path" + "strconv" + + "github.com/ailinykh/pullanusbot/v2/core" +) + +func CreateFfmpegConverter() *FfmpegConverter { + return &FfmpegConverter{} +} + +type FfmpegConverter struct{} + +func (c *FfmpegConverter) Convert(vf *core.VideoFile, bitrate int) (*core.VideoFile, error) { + convertedVideoFilePath := path.Join(os.TempDir(), vf.FileName+"_converted.mp4") + // cmd := fmt.Sprintf(`ffmpeg -y -i "%s" -pix_fmt yuv420p -vf "scale=trunc(iw/2)*2:trunc(ih/2)*2" "%s"`, vf.FilePath, convertedVideoFilePath) + // if bitrate > 0 { + cmd := fmt.Sprintf(`ffmpeg -v error -y -i "%s" -c:v libx264 -preset medium -b:v %dk -pass 1 -b:a 128k -f mp4 /dev/null && ffmpeg -y -i "%s" -c:v libx264 -preset medium -b:v %dk -pass 2 -b:a 128k "%s"`, vf.FilePath, bitrate/1024, vf.FilePath, bitrate/1024, convertedVideoFilePath) + // } + out, err := exec.Command("/bin/sh", "-c", cmd).CombinedOutput() + if err != nil { + os.Remove(convertedVideoFilePath) + return nil, errors.New(string(out)) + } + + return c.CreateVideoFile(convertedVideoFilePath) +} + +func (c *FfmpegConverter) CreateVideoFile(path string) (*core.VideoFile, error) { + ffprobe, err := c.getFFProbe(path) + if err != nil { + return nil, err + } + + stream, err := ffprobe.getVideoStream() + if err != nil { + return nil, err + } + + bitrate, _ := strconv.Atoi(stream.BitRate) // empty for .gif + + duration, err := strconv.ParseFloat(ffprobe.Format.Duration, 32) + if err != nil { + return nil, err + } + + thumbpath := path + ".jpg" + scale := "320:-1" + if stream.Width < stream.Height { + scale = "-1:320" + } + cmd := fmt.Sprintf(`ffmpeg -v error -i "%s" -ss 00:00:01.000 -vframes 1 -filter:v scale="%s" "%s"`, path, scale, thumbpath) + out, err := exec.Command("/bin/sh", "-c", cmd).CombinedOutput() + if err != nil { + os.Remove(thumbpath) + return nil, errors.New(string(out)) + } + + stat, err := os.Stat(path) + if err != nil { + return nil, err + } + + return &core.VideoFile{ + Width: stream.Width, + Height: stream.Height, + Bitrate: bitrate, + Duration: int(duration), + Codec: stream.CodecName, + FileName: stat.Name(), + FilePath: path, + ThumbPath: thumbpath}, nil +} + +func (c *FfmpegConverter) getFFProbe(file string) (*ffpResponse, error) { + cmd := fmt.Sprintf(`ffprobe -v error -of json -show_streams -show_format "%s"`, file) + out, err := exec.Command("/bin/sh", "-c", cmd).CombinedOutput() + if err != nil { + return nil, errors.New(string(out)) + } + + var resp ffpResponse + err = json.Unmarshal(out, &resp) + if err != nil { + return nil, err + } + + return &resp, nil +} + +type ffpResponse struct { + Streams []ffpStream `json:"streams"` + Format ffpFormat `json:"format"` +} + +type ffpStream struct { + Index int `json:"index"` + CodecName string `json:"codec_name"` + CodecType string `json:"codec_type"` + Width int `json:"width"` + Height int `json:"height"` + BitRate string `json:"bit_rate"` +} + +type ffpFormat struct { + Filename string `json:"filename"` + NbStreams int `json:"nb_streams"` + FormatName string `json:"format_name"` + FormatLongName string `json:"format_long_name"` + Duration string `json:"duration"` + Size string `json:"size"` +} + +func (f ffpResponse) getVideoStream() (ffpStream, error) { + for _, stream := range f.Streams { + if stream.CodecType == "video" { + return stream, nil + } + } + return ffpStream{}, errors.New("no video stream found") +} diff --git a/pullanusbot.go b/pullanusbot.go index 9a3c6d2..42601c8 100644 --- a/pullanusbot.go +++ b/pullanusbot.go @@ -17,12 +17,18 @@ func main() { logger, close := createLogger() defer close() + + converter := infrastructure.CreateFfmpegConverter() + telebot := api.CreateTelebot(os.Getenv("BOT_TOKEN"), logger, converter) + localizer := infrastructure.GameLocalizer{} game := use_cases.CreateGameFlow(localizer) - telebot := api.CreateTelebot(os.Getenv("BOT_TOKEN"), logger) - telebot.SetupGame(game) + telebot.SetupInfo() + + video_flow := use_cases.CreateVideoFlow(logger, converter, converter) + telebot.SetupVideo(video_flow) // Start endless loop telebot.Run() } diff --git a/use_cases/video_flow.go b/use_cases/video_flow.go new file mode 100644 index 0000000..f95d970 --- /dev/null +++ b/use_cases/video_flow.go @@ -0,0 +1,45 @@ +package use_cases + +import ( + "math" + + "github.com/ailinykh/pullanusbot/v2/core" +) + +type VideoFlow struct { + c core.IVideoFileConverter + f core.IVideoFileFactory + l core.ILogger +} + +func CreateVideoFlow(l core.ILogger, f core.IVideoFileFactory, c core.IVideoFileConverter) *VideoFlow { + return &VideoFlow{c, f, l} +} + +func (f *VideoFlow) Process(vf *core.VideoFile) (*core.VideoFile, error) { + + expectedBitrate := int(math.Min(float64(vf.Bitrate), 568320)) + + if expectedBitrate != vf.Bitrate { + f.l.Infof("Converting %s because of bitrate", vf.FileName) + convertedFile, err := f.c.Convert(vf, expectedBitrate) + if err != nil { + f.l.Error(err) + return nil, err + } + return convertedFile, nil + } + + if vf.Codec != "h264" { + f.l.Infof("Converting %s because of codec %s", vf.FileName, vf.Codec) + convertedFile, err := f.c.Convert(vf, 0) + if err != nil { + f.l.Error(err) + return nil, err + } + return convertedFile, nil + } + + f.l.Infof("No need to convert %s", vf.FileName) + return vf, nil +} From 97fccd512a3c5160a4034934283b4ad117ba885a Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Mon, 10 May 2021 10:44:52 +0300 Subject: [PATCH 004/439] architecture refactoring with handlers and adapters --- api/player_factory.go | 11 ++++ api/telebot.go | 68 +++++++++++++++++++++- api/telebot_adapter.go | 29 ++++++++++ api/telebot_video.go | 91 ------------------------------ api/twitter.go | 3 + api/video_file_factory.go | 17 ++++++ api/video_file_handler.go | 7 +++ core/bot.go | 6 ++ core/command_handler.go | 5 ++ core/document.go | 8 +++ core/document_handler.go | 5 ++ core/text_handler.go | 5 ++ core/video_file.go | 7 +++ infrastructure/ffmpeg_converter.go | 8 +-- pullanusbot.go | 10 +++- use_cases/twitter_flow.go | 16 ++++++ use_cases/video_flow.go | 31 +++++++--- 17 files changed, 218 insertions(+), 109 deletions(-) create mode 100644 api/telebot_adapter.go delete mode 100644 api/telebot_video.go create mode 100644 api/twitter.go create mode 100644 api/video_file_factory.go create mode 100644 api/video_file_handler.go create mode 100644 core/bot.go create mode 100644 core/command_handler.go create mode 100644 core/document.go create mode 100644 core/document_handler.go create mode 100644 core/text_handler.go create mode 100644 use_cases/twitter_flow.go diff --git a/api/player_factory.go b/api/player_factory.go index 09263c3..1a2d20d 100644 --- a/api/player_factory.go +++ b/api/player_factory.go @@ -19,3 +19,14 @@ func (p *PlayerFactory) Make(string) infrastructure.Player { LanguageCode: p.m.Sender.LanguageCode, } } + +// func makePlayer(m *tb.Message) infrastructure.Player { +// return infrastructure.Player{ +// GameID: m.Chat.ID, +// UserID: m.Sender.ID, +// FirstName: m.Sender.FirstName, +// LastName: m.Sender.LastName, +// Username: m.Sender.Username, +// LanguageCode: m.Sender.LanguageCode, +// } +// } diff --git a/api/telebot.go b/api/telebot.go index 1d0c958..2323548 100644 --- a/api/telebot.go +++ b/api/telebot.go @@ -1,6 +1,9 @@ package api import ( + "os" + "path" + "sync" "time" "github.com/ailinykh/pullanusbot/v2/core" @@ -10,10 +13,12 @@ import ( type Telebot struct { bot *tb.Bot logger core.ILogger - videoFileFactory core.IVideoFileFactory + commandHandlers []string + textHandlers []core.ITextHandler + documentHandlers []core.IDocumentHandler } -func CreateTelebot(token string, logger core.ILogger, videoFileFactory core.IVideoFileFactory) *Telebot { +func CreateTelebot(token string, logger core.ILogger) *Telebot { poller := tb.NewMiddlewarePoller(&tb.LongPoller{Timeout: 10 * time.Second}, func(upd *tb.Update) bool { return true }) @@ -28,7 +33,64 @@ func CreateTelebot(token string, logger core.ILogger, videoFileFactory core.IVid panic(err) } - return &Telebot{bot, logger, videoFileFactory} + telebot := &Telebot{bot, logger, []string{}, []core.ITextHandler{}, []core.IDocumentHandler{}} + + bot.Handle(tb.OnText, func(m *tb.Message) { + for _, h := range telebot.textHandlers { + h.HandleText(m.Text, &TelebotAdapter{m, telebot}) + } + }) + + var mutex sync.Mutex + + bot.Handle(tb.OnDocument, func(m *tb.Message) { + // TODO: inject `download` to get rid of MIME cheking + if m.Document.MIME[:5] == "video" || m.Document.MIME == "image/gif" { + mutex.Lock() + defer mutex.Unlock() + + logger.Infof("Attempt to download %s %s (sent by %s)", m.Document.FileName, m.Document.MIME, m.Sender.Username) + + path := path.Join(os.TempDir(), m.Document.FileName) + err := bot.Download(&m.Document.File, path) + if err != nil { + logger.Error(err) + return + } + + logger.Infof("Downloaded to %s", path) + defer os.Remove(path) + + for _, h := range telebot.documentHandlers { + h.HandleDocument(&core.Document{ + Author: m.Sender.Username, + FileName: m.Document.FileName, + FilePath: path, + MIME: m.Document.MIME, + }, &TelebotAdapter{m, telebot}) + } + } + }) + return telebot +} + +func (t *Telebot) AddHandler(handlers ...interface{}) { + switch h := handlers[0].(type) { + case core.IDocumentHandler: + t.documentHandlers = append(t.documentHandlers, h) + case core.ITextHandler: + t.textHandlers = append(t.textHandlers, h) + case string: + for _, command := range t.commandHandlers { + if command == h { + panic("Handler for " + command + " already set!") + } + } + t.commandHandlers = append(t.commandHandlers, h) + t.bot.Handle(h, func(m *tb.Message) { + handlers[1].(core.ICommandHandler).HandleCommand(m.Text, &TelebotAdapter{m, t}) + }) + } } func (t *Telebot) Run() { diff --git a/api/telebot_adapter.go b/api/telebot_adapter.go new file mode 100644 index 0000000..eedac84 --- /dev/null +++ b/api/telebot_adapter.go @@ -0,0 +1,29 @@ +package api + +import ( + "github.com/ailinykh/pullanusbot/v2/core" + tb "gopkg.in/tucnak/telebot.v2" +) + +type TelebotAdapter struct { + m *tb.Message + t *Telebot +} + +func (a *TelebotAdapter) SendVideo(vf *core.VideoFile, caption string) error { + video := makeVideoFile(vf, caption) + a.t.bot.Notify(a.m.Chat, tb.UploadingVideo) + _, err := video.Send(a.t.bot, a.m.Chat, &tb.SendOptions{ParseMode: tb.ModeHTML}) + if err != nil { + return err + } else { + a.t.logger.Infof("%s sent successfully", vf.FileName) + a.t.bot.Delete(a.m) + } + return nil +} + +func (a *TelebotAdapter) SendText(text string) error { + _, err := a.t.bot.Send(a.m.Chat, text, &tb.SendOptions{ParseMode: tb.ModeHTML}) + return err +} diff --git a/api/telebot_video.go b/api/telebot_video.go deleted file mode 100644 index 8a4216b..0000000 --- a/api/telebot_video.go +++ /dev/null @@ -1,91 +0,0 @@ -package api - -import ( - "fmt" - "os" - "path" - "strconv" - "sync" - - "github.com/ailinykh/pullanusbot/v2/core" - "github.com/ailinykh/pullanusbot/v2/use_cases" - tb "gopkg.in/tucnak/telebot.v2" -) - -func (t *Telebot) SetupVideo(video_flow *use_cases.VideoFlow) { - var mutex sync.Mutex - - t.bot.Handle(tb.OnDocument, func(m *tb.Message) { - if m.Document.MIME[:5] == "video" || m.Document.MIME == "image/gif" { - mutex.Lock() - defer mutex.Unlock() - - t.logger.Infof("Attempt to download %s %s (sent by %s)", m.Document.FileName, m.Document.MIME, m.Sender.Username) - - path := path.Join(os.TempDir(), m.Document.FileName) - err := t.bot.Download(&m.Document.File, path) - if err != nil { - t.logger.Error(err) - return - } - - t.logger.Infof("Downloaded to %s", path) - defer os.Remove(path) - inputFile, err := t.videoFileFactory.CreateVideoFile(path) - - if err != nil { - t.logger.Error(err) - return - } - - defer os.Remove(inputFile.ThumbPath) - - outputFile, err := video_flow.Process(inputFile) - - if err != nil { - chatID, e := strconv.ParseInt(os.Getenv("ADMIN_CHAT_ID"), 10, 64) - if e == nil { - chat := &tb.Chat{ID: chatID} - t.bot.Forward(chat, m) - t.bot.Send(chat, err.Error()) - } - return - } - - defer os.Remove(outputFile.FilePath) - defer os.Remove(outputFile.ThumbPath) - - caption := fmt.Sprintf("%s (by %s)", m.Document.FileName, m.Sender.Username) - if inputFile.FilePath != outputFile.FilePath { - fi, _ := os.Stat(outputFile.FilePath) - caption = fmt.Sprintf("%s (by %s)\nOriginal size: %.2f MB (%d kb/s)\nConverted size: %.2f MB (%d kb/s)", m.Document.FileName, m.Sender.Username, float32(m.Document.FileSize)/1048576, inputFile.Bitrate/1024, float32(fi.Size())/1048576, outputFile.Bitrate/1024) - } - video := makeVideoFile(outputFile, caption) - t.bot.Notify(m.Chat, tb.UploadingVideo) - _, err = video.Send(t.bot, m.Chat, &tb.SendOptions{ParseMode: tb.ModeHTML}) - if err != nil { - t.logger.Error(err) - } else { - t.logger.Infof("%s sent successfully", outputFile.FileName) - t.bot.Delete(m) - } - } else { - t.logger.Infof("%s not supported yet", m.Document.MIME) - } - }) - - t.bot.Handle(tb.OnAnimation, func(m *tb.Message) { - t.logger.Info(m.Animation) - }) -} - -func makeVideoFile(vf *core.VideoFile, caption string) tb.Video { - video := tb.Video{File: tb.FromDisk(vf.FilePath)} - video.Width = vf.Width - video.Height = vf.Height - video.Caption = caption - video.Duration = vf.Duration - video.SupportsStreaming = true - video.Thumbnail = &tb.Photo{File: tb.FromDisk(vf.ThumbPath)} - return video -} diff --git a/api/twitter.go b/api/twitter.go new file mode 100644 index 0000000..7168a5a --- /dev/null +++ b/api/twitter.go @@ -0,0 +1,3 @@ +package api + +type Twitter struct{} diff --git a/api/video_file_factory.go b/api/video_file_factory.go new file mode 100644 index 0000000..a965cda --- /dev/null +++ b/api/video_file_factory.go @@ -0,0 +1,17 @@ +package api + +import ( + "github.com/ailinykh/pullanusbot/v2/core" + tb "gopkg.in/tucnak/telebot.v2" +) + +func makeVideoFile(vf *core.VideoFile, caption string) tb.Video { + video := tb.Video{File: tb.FromDisk(vf.FilePath)} + video.Width = vf.Width + video.Height = vf.Height + video.Caption = caption + video.Duration = vf.Duration + video.SupportsStreaming = true + video.Thumbnail = &tb.Photo{File: tb.FromDisk(vf.ThumbPath)} + return video +} diff --git a/api/video_file_handler.go b/api/video_file_handler.go new file mode 100644 index 0000000..6364efe --- /dev/null +++ b/api/video_file_handler.go @@ -0,0 +1,7 @@ +package api + +import "github.com/ailinykh/pullanusbot/v2/core" + +type IVdeoFileHandler interface { + HandleVideoFile(*core.VideoFile, core.IBot) error +} diff --git a/core/bot.go b/core/bot.go new file mode 100644 index 0000000..20701dd --- /dev/null +++ b/core/bot.go @@ -0,0 +1,6 @@ +package core + +type IBot interface { + SendText(string) error + SendVideo(*VideoFile, string) error +} diff --git a/core/command_handler.go b/core/command_handler.go new file mode 100644 index 0000000..423957b --- /dev/null +++ b/core/command_handler.go @@ -0,0 +1,5 @@ +package core + +type ICommandHandler interface { + HandleCommand(string, IBot) error +} diff --git a/core/document.go b/core/document.go new file mode 100644 index 0000000..df882b2 --- /dev/null +++ b/core/document.go @@ -0,0 +1,8 @@ +package core + +type Document struct { + Author string + FileName string + FilePath string + MIME string +} diff --git a/core/document_handler.go b/core/document_handler.go new file mode 100644 index 0000000..79a9608 --- /dev/null +++ b/core/document_handler.go @@ -0,0 +1,5 @@ +package core + +type IDocumentHandler interface { + HandleDocument(*Document, IBot) error +} diff --git a/core/text_handler.go b/core/text_handler.go new file mode 100644 index 0000000..b437d59 --- /dev/null +++ b/core/text_handler.go @@ -0,0 +1,5 @@ +package core + +type ITextHandler interface { + HandleText(string, IBot) error +} diff --git a/core/video_file.go b/core/video_file.go index 1a0f0cd..0b386ae 100644 --- a/core/video_file.go +++ b/core/video_file.go @@ -1,5 +1,7 @@ package core +import "os" + type VideoFile struct { Width int Height int @@ -10,3 +12,8 @@ type VideoFile struct { FilePath string ThumbPath string } + +func (vf *VideoFile) Dispose() { + os.Remove(vf.FilePath) + os.Remove(vf.ThumbPath) +} diff --git a/infrastructure/ffmpeg_converter.go b/infrastructure/ffmpeg_converter.go index e3c7bf6..a24bc5b 100644 --- a/infrastructure/ffmpeg_converter.go +++ b/infrastructure/ffmpeg_converter.go @@ -20,10 +20,10 @@ type FfmpegConverter struct{} func (c *FfmpegConverter) Convert(vf *core.VideoFile, bitrate int) (*core.VideoFile, error) { convertedVideoFilePath := path.Join(os.TempDir(), vf.FileName+"_converted.mp4") - // cmd := fmt.Sprintf(`ffmpeg -y -i "%s" -pix_fmt yuv420p -vf "scale=trunc(iw/2)*2:trunc(ih/2)*2" "%s"`, vf.FilePath, convertedVideoFilePath) - // if bitrate > 0 { - cmd := fmt.Sprintf(`ffmpeg -v error -y -i "%s" -c:v libx264 -preset medium -b:v %dk -pass 1 -b:a 128k -f mp4 /dev/null && ffmpeg -y -i "%s" -c:v libx264 -preset medium -b:v %dk -pass 2 -b:a 128k "%s"`, vf.FilePath, bitrate/1024, vf.FilePath, bitrate/1024, convertedVideoFilePath) - // } + cmd := fmt.Sprintf(`ffmpeg -y -i "%s" -pix_fmt yuv420p -vf "scale=trunc(iw/2)*2:trunc(ih/2)*2" "%s"`, vf.FilePath, convertedVideoFilePath) + if bitrate > 0 { + cmd = fmt.Sprintf(`ffmpeg -v error -y -i "%s" -c:v libx264 -preset medium -b:v %dk -pass 1 -b:a 128k -f mp4 /dev/null && ffmpeg -y -i "%s" -c:v libx264 -preset medium -b:v %dk -pass 2 -b:a 128k "%s"`, vf.FilePath, bitrate/1024, vf.FilePath, bitrate/1024, convertedVideoFilePath) + } out, err := exec.Command("/bin/sh", "-c", cmd).CombinedOutput() if err != nil { os.Remove(convertedVideoFilePath) diff --git a/pullanusbot.go b/pullanusbot.go index 42601c8..2b24ccb 100644 --- a/pullanusbot.go +++ b/pullanusbot.go @@ -18,8 +18,7 @@ func main() { logger, close := createLogger() defer close() - converter := infrastructure.CreateFfmpegConverter() - telebot := api.CreateTelebot(os.Getenv("BOT_TOKEN"), logger, converter) + telebot := api.CreateTelebot(os.Getenv("BOT_TOKEN"), logger) localizer := infrastructure.GameLocalizer{} game := use_cases.CreateGameFlow(localizer) @@ -27,8 +26,13 @@ func main() { telebot.SetupInfo() + converter := infrastructure.CreateFfmpegConverter() video_flow := use_cases.CreateVideoFlow(logger, converter, converter) - telebot.SetupVideo(video_flow) + telebot.AddHandler(video_flow) + + twitter_flow := use_cases.CreateTwitterFlow(logger) + telebot.AddHandler(twitter_flow) + // telebot.SetupVideo(video_flow) // Start endless loop telebot.Run() } diff --git a/use_cases/twitter_flow.go b/use_cases/twitter_flow.go new file mode 100644 index 0000000..aa3d035 --- /dev/null +++ b/use_cases/twitter_flow.go @@ -0,0 +1,16 @@ +package use_cases + +import "github.com/ailinykh/pullanusbot/v2/core" + +func CreateTwitterFlow(l core.ILogger) *TwitterFlow { + return &TwitterFlow{l} +} + +type TwitterFlow struct { + l core.ILogger +} + +func (tf *TwitterFlow) HandleText(text string, bot core.IBot) error { + tf.l.Infof("Got message %s", text) + return nil +} diff --git a/use_cases/video_flow.go b/use_cases/video_flow.go index f95d970..7c8f093 100644 --- a/use_cases/video_flow.go +++ b/use_cases/video_flow.go @@ -1,7 +1,9 @@ package use_cases import ( + "fmt" "math" + "os" "github.com/ailinykh/pullanusbot/v2/core" ) @@ -16,30 +18,43 @@ func CreateVideoFlow(l core.ILogger, f core.IVideoFileFactory, c core.IVideoFile return &VideoFlow{c, f, l} } -func (f *VideoFlow) Process(vf *core.VideoFile) (*core.VideoFile, error) { +func (f *VideoFlow) HandleDocument(document *core.Document, b core.IBot) error { + vf, err := f.f.CreateVideoFile(document.FilePath) + if err != nil { + f.l.Error(err) + return b.SendText(err.Error()) + } + defer vf.Dispose() expectedBitrate := int(math.Min(float64(vf.Bitrate), 568320)) if expectedBitrate != vf.Bitrate { f.l.Infof("Converting %s because of bitrate", vf.FileName) - convertedFile, err := f.c.Convert(vf, expectedBitrate) + cvf, err := f.c.Convert(vf, expectedBitrate) if err != nil { f.l.Error(err) - return nil, err + return err } - return convertedFile, nil + defer cvf.Dispose() + fi1, _ := os.Stat(vf.FilePath) + fi2, _ := os.Stat(cvf.FilePath) + caption := fmt.Sprintf("%s (by %s)\nOriginal size: %.2f MB (%d kb/s)\nConverted size: %.2f MB (%d kb/s)", vf.FileName, document.Author, float32(fi1.Size())/1048576, vf.Bitrate/1024, float32(fi2.Size())/1048576, cvf.Bitrate/1024) + return b.SendVideo(cvf, caption) } if vf.Codec != "h264" { f.l.Infof("Converting %s because of codec %s", vf.FileName, vf.Codec) - convertedFile, err := f.c.Convert(vf, 0) + cvf, err := f.c.Convert(vf, 0) if err != nil { f.l.Error(err) - return nil, err + return err } - return convertedFile, nil + defer cvf.Dispose() + caption := fmt.Sprintf("%s (by %s)", vf.FileName, document.Author) + return b.SendVideo(cvf, caption) } f.l.Infof("No need to convert %s", vf.FileName) - return vf, nil + caption := fmt.Sprintf("%s (by %s)", vf.FileName, document.Author) + return b.SendVideo(vf, caption) } From a2e89abe3e649c72ecc9be31c7a854025fb624ca Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Mon, 10 May 2021 10:57:59 +0300 Subject: [PATCH 005/439] get rid of redundant entities --- api/player_factory.go | 32 -------------------------------- api/telebot_adapter.go | 12 ++++++++++++ api/telebot_game.go | 15 +++++++-------- infrastructure/game_storage.go | 4 ++-- infrastructure/player_factory.go | 2 +- 5 files changed, 22 insertions(+), 43 deletions(-) delete mode 100644 api/player_factory.go diff --git a/api/player_factory.go b/api/player_factory.go deleted file mode 100644 index 1a2d20d..0000000 --- a/api/player_factory.go +++ /dev/null @@ -1,32 +0,0 @@ -package api - -import ( - "github.com/ailinykh/pullanusbot/v2/infrastructure" - tb "gopkg.in/tucnak/telebot.v2" -) - -type PlayerFactory struct { - m *tb.Message -} - -func (p *PlayerFactory) Make(string) infrastructure.Player { - return infrastructure.Player{ - GameID: p.m.Chat.ID, - UserID: p.m.Sender.ID, - FirstName: p.m.Sender.FirstName, - LastName: p.m.Sender.LastName, - Username: p.m.Sender.Username, - LanguageCode: p.m.Sender.LanguageCode, - } -} - -// func makePlayer(m *tb.Message) infrastructure.Player { -// return infrastructure.Player{ -// GameID: m.Chat.ID, -// UserID: m.Sender.ID, -// FirstName: m.Sender.FirstName, -// LastName: m.Sender.LastName, -// Username: m.Sender.Username, -// LanguageCode: m.Sender.LanguageCode, -// } -// } diff --git a/api/telebot_adapter.go b/api/telebot_adapter.go index eedac84..f1daf45 100644 --- a/api/telebot_adapter.go +++ b/api/telebot_adapter.go @@ -2,6 +2,7 @@ package api import ( "github.com/ailinykh/pullanusbot/v2/core" + "github.com/ailinykh/pullanusbot/v2/infrastructure" tb "gopkg.in/tucnak/telebot.v2" ) @@ -27,3 +28,14 @@ func (a *TelebotAdapter) SendText(text string) error { _, err := a.t.bot.Send(a.m.Chat, text, &tb.SendOptions{ParseMode: tb.ModeHTML}) return err } + +func (a *TelebotAdapter) CreatePlayer(string) infrastructure.Player { + return infrastructure.Player{ + GameID: a.m.Chat.ID, + UserID: a.m.Sender.ID, + FirstName: a.m.Sender.FirstName, + LastName: a.m.Sender.LastName, + Username: a.m.Sender.Username, + LanguageCode: a.m.Sender.LanguageCode, + } +} diff --git a/api/telebot_game.go b/api/telebot_game.go index 0ad1d50..b5eb33a 100644 --- a/api/telebot_game.go +++ b/api/telebot_game.go @@ -19,7 +19,7 @@ func (t *Telebot) SetupGame(g use_cases.GameFlow) { }) t.bot.Handle("/pidoreg", func(m *tb.Message) { - text := g.Add(makePlayer(m), makeStorage(m)) + text := g.Add(makePlayer(m), makeStorage(m, t)) t.bot.Send(m.Chat, text, &tb.SendOptions{ParseMode: tb.ModeHTML}) }) @@ -29,7 +29,7 @@ func (t *Telebot) SetupGame(g use_cases.GameFlow) { mutex.Lock() defer mutex.Unlock() - messages := g.Play(makePlayer(m), makeStorage(m)) + messages := g.Play(makePlayer(m), makeStorage(m, t)) if len(messages) > 1 { for _, msg := range messages { t.bot.Send(m.Chat, msg, &tb.SendOptions{ParseMode: tb.ModeHTML}) @@ -42,17 +42,17 @@ func (t *Telebot) SetupGame(g use_cases.GameFlow) { }) t.bot.Handle("/pidorall", func(m *tb.Message) { - text := g.All(makeStorage(m)) + text := g.All(makeStorage(m, t)) t.bot.Send(m.Chat, text, &tb.SendOptions{ParseMode: tb.ModeHTML}) }) t.bot.Handle("/pidorstats", func(m *tb.Message) { - text := g.Stats(makeStorage(m)) + text := g.Stats(makeStorage(m, t)) t.bot.Send(m.Chat, text, &tb.SendOptions{ParseMode: tb.ModeHTML}) }) t.bot.Handle("/pidorme", func(m *tb.Message) { - text := g.Me(makePlayer(m), makeStorage(m)) + text := g.Me(makePlayer(m), makeStorage(m, t)) t.bot.Send(m.Chat, text, &tb.SendOptions{ParseMode: tb.ModeHTML}) }) } @@ -61,8 +61,7 @@ func makePlayer(m *tb.Message) core.Player { return core.Player{Username: m.Sender.Username} } -func makeStorage(m *tb.Message) core.IGameStorage { - factory := PlayerFactory{m} - storage := infrastructure.CreateGameStorage(m.Chat.ID, &factory) +func makeStorage(m *tb.Message, t *Telebot) core.IGameStorage { + storage := infrastructure.CreateGameStorage(m.Chat.ID, &TelebotAdapter{m, t}) return &storage } diff --git a/infrastructure/game_storage.go b/infrastructure/game_storage.go index d742783..b63b8c2 100644 --- a/infrastructure/game_storage.go +++ b/infrastructure/game_storage.go @@ -52,13 +52,13 @@ func (s *GameStorage) GetRounds() ([]core.Round, error) { } func (s *GameStorage) AddPlayer(player core.Player) error { - dbPlayer := s.playerFactory.Make(player.Username) + dbPlayer := s.playerFactory.CreatePlayer(player.Username) s.conn.Create(&dbPlayer) return nil } func (s *GameStorage) AddRound(round core.Round) error { - player := s.playerFactory.Make(round.Winner.Username) + player := s.playerFactory.CreatePlayer(round.Winner.Username) dbRound := Round{ GameID: s.gameID, UserID: player.UserID, diff --git a/infrastructure/player_factory.go b/infrastructure/player_factory.go index a2114bd..c9fe6b7 100644 --- a/infrastructure/player_factory.go +++ b/infrastructure/player_factory.go @@ -1,5 +1,5 @@ package infrastructure type IPlayerFactory interface { - Make(string) Player + CreatePlayer(string) Player } From 3ff44d2c98c660ac2bd906b26f13caea701570c0 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Mon, 10 May 2021 22:38:23 +0300 Subject: [PATCH 006/439] twitter flow and player renamed to user --- api/telebot.go | 8 ++- api/telebot_adapter.go | 45 ++++++++++-- api/telebot_game.go | 10 +-- api/tweet.go | 57 ++++++++++++++++ api/twitter.go | 84 +++++++++++++++++++++++ core/bot.go | 5 +- core/file_downloader.go | 5 ++ core/game_storage.go | 8 +-- core/media.go | 15 ++++ core/media_loader.go | 5 ++ core/round.go | 2 +- core/text_handler.go | 2 +- core/{player.go => user.go} | 2 +- infrastructure/file_downloader.go | 33 +++++++++ infrastructure/game_storage.go | 18 ++--- pullanusbot.go | 4 +- use_cases/faggot_game.go | 8 +-- use_cases/faggot_game_test.go | 56 +++++++-------- use_cases/faggot_stat.go | 2 +- use_cases/twitter_flow.go | 109 ++++++++++++++++++++++++++++-- use_cases/video_flow.go | 6 +- 21 files changed, 412 insertions(+), 72 deletions(-) create mode 100644 api/tweet.go create mode 100644 core/file_downloader.go create mode 100644 core/media.go create mode 100644 core/media_loader.go rename core/{player.go => user.go} (61%) create mode 100644 infrastructure/file_downloader.go diff --git a/api/telebot.go b/api/telebot.go index 2323548..98ccb7d 100644 --- a/api/telebot.go +++ b/api/telebot.go @@ -1,6 +1,7 @@ package api import ( + "fmt" "os" "path" "sync" @@ -37,7 +38,10 @@ func CreateTelebot(token string, logger core.ILogger) *Telebot { bot.Handle(tb.OnText, func(m *tb.Message) { for _, h := range telebot.textHandlers { - h.HandleText(m.Text, &TelebotAdapter{m, telebot}) + err := h.HandleText(m.Text, makeUser(m), &TelebotAdapter{m, telebot}) + if err != nil { + logger.Errorf("TextHandler %#v error: %s", h, err) + } } }) @@ -90,6 +94,8 @@ func (t *Telebot) AddHandler(handlers ...interface{}) { t.bot.Handle(h, func(m *tb.Message) { handlers[1].(core.ICommandHandler).HandleCommand(m.Text, &TelebotAdapter{m, t}) }) + default: + panic(fmt.Sprintf("something wrong with %s", h)) } } diff --git a/api/telebot_adapter.go b/api/telebot_adapter.go index f1daf45..92e05e6 100644 --- a/api/telebot_adapter.go +++ b/api/telebot_adapter.go @@ -11,7 +11,45 @@ type TelebotAdapter struct { t *Telebot } -func (a *TelebotAdapter) SendVideo(vf *core.VideoFile, caption string) error { +func (a *TelebotAdapter) SendText(text string) error { + _, err := a.t.bot.Send(a.m.Chat, text, &tb.SendOptions{ParseMode: tb.ModeHTML, DisableWebPagePreview: true}) + return err +} + +func (a *TelebotAdapter) SendPhoto(media *core.Media) error { + file := &tb.Photo{File: tb.FromURL(media.URL)} + file.Caption = media.Caption + a.t.bot.Notify(a.m.Chat, tb.UploadingPhoto) + _, err := a.t.bot.Send(a.m.Chat, file, &tb.SendOptions{ParseMode: tb.ModeHTML}) + return err +} + +func (a *TelebotAdapter) SendPhotoAlbum(medias []*core.Media) error { + var photo *tb.Photo + var album = tb.Album{} + + for i, m := range medias { + photo = &tb.Photo{File: tb.FromURL(m.URL)} + if i == len(medias)-1 { + photo.Caption = m.Caption + photo.ParseMode = tb.ModeHTML + } + album = append(album, photo) + } + + _, err := a.t.bot.SendAlbum(a.m.Chat, album) + return err +} + +func (a *TelebotAdapter) SendVideo(media *core.Media) error { + file := &tb.Video{File: tb.FromURL(media.URL)} + file.Caption = media.Caption + a.t.bot.Notify(a.m.Chat, tb.UploadingVideo) + _, err := a.t.bot.Send(a.m.Chat, file, &tb.SendOptions{ParseMode: tb.ModeHTML}) + return err +} + +func (a *TelebotAdapter) SendVideoFile(vf *core.VideoFile, caption string) error { video := makeVideoFile(vf, caption) a.t.bot.Notify(a.m.Chat, tb.UploadingVideo) _, err := video.Send(a.t.bot, a.m.Chat, &tb.SendOptions{ParseMode: tb.ModeHTML}) @@ -24,11 +62,6 @@ func (a *TelebotAdapter) SendVideo(vf *core.VideoFile, caption string) error { return nil } -func (a *TelebotAdapter) SendText(text string) error { - _, err := a.t.bot.Send(a.m.Chat, text, &tb.SendOptions{ParseMode: tb.ModeHTML}) - return err -} - func (a *TelebotAdapter) CreatePlayer(string) infrastructure.Player { return infrastructure.Player{ GameID: a.m.Chat.ID, diff --git a/api/telebot_game.go b/api/telebot_game.go index b5eb33a..844898f 100644 --- a/api/telebot_game.go +++ b/api/telebot_game.go @@ -19,7 +19,7 @@ func (t *Telebot) SetupGame(g use_cases.GameFlow) { }) t.bot.Handle("/pidoreg", func(m *tb.Message) { - text := g.Add(makePlayer(m), makeStorage(m, t)) + text := g.Add(makeUser(m), makeStorage(m, t)) t.bot.Send(m.Chat, text, &tb.SendOptions{ParseMode: tb.ModeHTML}) }) @@ -29,7 +29,7 @@ func (t *Telebot) SetupGame(g use_cases.GameFlow) { mutex.Lock() defer mutex.Unlock() - messages := g.Play(makePlayer(m), makeStorage(m, t)) + messages := g.Play(makeUser(m), makeStorage(m, t)) if len(messages) > 1 { for _, msg := range messages { t.bot.Send(m.Chat, msg, &tb.SendOptions{ParseMode: tb.ModeHTML}) @@ -52,13 +52,13 @@ func (t *Telebot) SetupGame(g use_cases.GameFlow) { }) t.bot.Handle("/pidorme", func(m *tb.Message) { - text := g.Me(makePlayer(m), makeStorage(m, t)) + text := g.Me(makeUser(m), makeStorage(m, t)) t.bot.Send(m.Chat, text, &tb.SendOptions{ParseMode: tb.ModeHTML}) }) } -func makePlayer(m *tb.Message) core.Player { - return core.Player{Username: m.Sender.Username} +func makeUser(m *tb.Message) *core.User { + return &core.User{Username: m.Sender.Username} } func makeStorage(m *tb.Message, t *Telebot) core.IGameStorage { diff --git a/api/tweet.go b/api/tweet.go new file mode 100644 index 0000000..4eb01e5 --- /dev/null +++ b/api/tweet.go @@ -0,0 +1,57 @@ +package api + +type Tweet struct { + ID string `json:"id_str"` + FullText string `json:"full_text"` + Entities Entity `json:"entities"` + ExtendedEntities Entity `json:"extended_entities,omitempty"` + User User `json:"user"` + QuotedStatus *Tweet `json:"quoted_status,omitempty"` + Errors []Error `json:"errors,omitempty"` +} + +type User struct { + Name string `json:"name"` + ScreenName string `json:"screen_name"` +} + +type Entity struct { + Urls []URL `json:"urls,omitempty"` + Media []Media `json:"media"` +} + +type Media struct { + MediaURL string `json:"media_url"` + MediaURLHTTPS string `json:"media_url_https"` + Type string `json:"type"` + VideoInfo VideoInfo `json:"video_info,omitempty"` +} + +type URL struct { + ExpandedURL string `json:"expanded_url"` +} + +type VideoInfo struct { + Variants []VideoInfoVariant `json:"variants"` +} + +type Error struct { + Message string `json:"message"` + Code int `json:"code"` +} + +func (info *VideoInfo) best() VideoInfoVariant { + variant := info.Variants[0] + for _, v := range info.Variants { + if v.ContentType == "video/mp4" && v.Bitrate > variant.Bitrate { + return v + } + } + return variant +} + +type VideoInfoVariant struct { + Bitrate int `json:"bitrate"` + ContentType string `json:"content_type"` + URL string `json:"url"` +} diff --git a/api/twitter.go b/api/twitter.go index 7168a5a..faf27c7 100644 --- a/api/twitter.go +++ b/api/twitter.go @@ -1,3 +1,87 @@ package api +import ( + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/http" + "regexp" + + "github.com/ailinykh/pullanusbot/v2/core" +) + +func CreateTwitterAPI() *Twitter { + return &Twitter{} +} + type Twitter struct{} + +func (Twitter) get(tweetID string) (*Tweet, error) { + client := http.DefaultClient + req, _ := http.NewRequest("GET", fmt.Sprintf("https://api.twitter.com/1.1/statuses/show.json?id=%s&tweet_mode=extended", tweetID), nil) + req.Header.Add("Authorization", "Bearer AAAAAAAAAAAAAAAAAAAAAPYXBAAAAAAACLXUNDekMxqa8h%2F40K4moUkGsoc%3DTYfbDKbT3jJPCEVnMYqilB28NHfOPqkca3qaAxGfsyKCs0wRbw") + res, err := client.Do(req) + if err != nil { + return nil, err + } + defer res.Body.Close() + + var tweet Tweet + body, _ := ioutil.ReadAll(res.Body) + + err = json.Unmarshal(body, &tweet) + if err != nil { + return nil, err + } + + if len(tweet.Errors) > 0 { + if tweet.Errors[0].Code == 88 { // "Rate limit exceeded 88" + return nil, errors.New(tweet.Errors[0].Message + " " + res.Header["X-Rate-Limit-Reset"][0]) + } else { + return nil, errors.New(tweet.Errors[0].Message) + } + } + + return &tweet, err +} + +func (t *Twitter) Load(tweetID string, author *core.User) ([]*core.Media, error) { + tweet, err := t.get(tweetID) + if err != nil { + return nil, err + } + + if len(tweet.ExtendedEntities.Media) == 0 && tweet.QuotedStatus != nil && len(tweet.QuotedStatus.ExtendedEntities.Media) > 0 { + tweet = tweet.QuotedStatus + // logger.Warningf("tweet media is empty, using QuotedStatus instead %s", tweet.ID) + } + + media := tweet.ExtendedEntities.Media + + switch len(media) { + case 0: + return []*core.Media{{URL: "", Caption: t.makeCaption(author.Username, tweet), Type: core.Text}}, nil + case 1: + if media[0].Type == "video" || media[0].Type == "animated_gif" { + return []*core.Media{{URL: media[0].VideoInfo.best().URL, Caption: t.makeCaption(author.Username, tweet), Type: core.Video}}, nil + } else if media[0].Type == "photo" { + return []*core.Media{{URL: media[0].MediaURL, Caption: t.makeCaption(author.Username, tweet), Type: core.Photo}}, nil + } else { + return nil, errors.New("Unknown type: " + media[0].Type) + } + default: + // t.sendAlbum(media, tweet, m) + medias := []*core.Media{} + for _, m := range media { + medias = append(medias, &core.Media{URL: m.MediaURL, Caption: t.makeCaption(author.Username, tweet), Type: core.Photo}) + } + return medias, nil + } +} + +func (Twitter) makeCaption(author string, tweet *Tweet) string { + re := regexp.MustCompile(`\s?http\S+$`) + text := re.ReplaceAllString(tweet.FullText, "") + return fmt.Sprintf("🐦 %s (by %s)\n%s", tweet.User.ScreenName, tweet.ID, tweet.User.Name, author, text) +} diff --git a/core/bot.go b/core/bot.go index 20701dd..cdf3a3e 100644 --- a/core/bot.go +++ b/core/bot.go @@ -2,5 +2,8 @@ package core type IBot interface { SendText(string) error - SendVideo(*VideoFile, string) error + SendPhoto(*Media) error + SendPhotoAlbum([]*Media) error + SendVideo(*Media) error + SendVideoFile(*VideoFile, string) error } diff --git a/core/file_downloader.go b/core/file_downloader.go new file mode 100644 index 0000000..4b2f12f --- /dev/null +++ b/core/file_downloader.go @@ -0,0 +1,5 @@ +package core + +type IFileDownloader interface { + Download(string, string) error +} diff --git a/core/game_storage.go b/core/game_storage.go index c644652..02c3631 100644 --- a/core/game_storage.go +++ b/core/game_storage.go @@ -1,8 +1,8 @@ package core type IGameStorage interface { - GetPlayers() ([]Player, error) - GetRounds() ([]Round, error) - AddPlayer(Player) error - AddRound(Round) error + GetPlayers() ([]*User, error) + GetRounds() ([]*Round, error) + AddPlayer(*User) error + AddRound(*Round) error } diff --git a/core/media.go b/core/media.go new file mode 100644 index 0000000..e15a2f8 --- /dev/null +++ b/core/media.go @@ -0,0 +1,15 @@ +package core + +type MediaType int + +const ( + Video MediaType = iota + Photo + Text +) + +type Media struct { + URL string + Caption string + Type MediaType +} diff --git a/core/media_loader.go b/core/media_loader.go new file mode 100644 index 0000000..a3772de --- /dev/null +++ b/core/media_loader.go @@ -0,0 +1,5 @@ +package core + +type IMediaLoader interface { + Load(string, *User) ([]*Media, error) +} diff --git a/core/round.go b/core/round.go index 7812a98..2a36e5f 100644 --- a/core/round.go +++ b/core/round.go @@ -2,5 +2,5 @@ package core type Round struct { Day string - Winner Player + Winner *User } diff --git a/core/text_handler.go b/core/text_handler.go index b437d59..0953d7f 100644 --- a/core/text_handler.go +++ b/core/text_handler.go @@ -1,5 +1,5 @@ package core type ITextHandler interface { - HandleText(string, IBot) error + HandleText(string, *User, IBot) error } diff --git a/core/player.go b/core/user.go similarity index 61% rename from core/player.go rename to core/user.go index d711579..aa2164b 100644 --- a/core/player.go +++ b/core/user.go @@ -1,5 +1,5 @@ package core -type Player struct { +type User struct { Username string } diff --git a/infrastructure/file_downloader.go b/infrastructure/file_downloader.go new file mode 100644 index 0000000..ba81b08 --- /dev/null +++ b/infrastructure/file_downloader.go @@ -0,0 +1,33 @@ +package infrastructure + +import ( + "io" + "net/http" + "os" +) + +func CreateFileDownloader() *FileDownloader { + return &FileDownloader{} +} + +type FileDownloader struct{} + +func (FileDownloader) Download(url string, path string) error { + // Get the data + resp, err := http.Get(url) + if err != nil { + return err + } + defer resp.Body.Close() + + // Create the file + out, err := os.Create(path) + if err != nil { + return err + } + defer out.Close() + + // Write the body to file + _, err = io.Copy(out, resp.Body) + return err +} diff --git a/infrastructure/game_storage.go b/infrastructure/game_storage.go index b63b8c2..e16ec87 100644 --- a/infrastructure/game_storage.go +++ b/infrastructure/game_storage.go @@ -30,34 +30,34 @@ type GameStorage struct { playerFactory IPlayerFactory } -func (db *GameStorage) GetPlayers() ([]core.Player, error) { +func (db *GameStorage) GetPlayers() ([]*core.User, error) { var dbPlayers []Player - var corePlayers []core.Player + var corePlayers []*core.User db.conn.Where("game_id = ?", db.gameID).Find(&dbPlayers) for _, p := range dbPlayers { - corePlayers = append(corePlayers, core.Player{Username: p.Username}) + corePlayers = append(corePlayers, &core.User{Username: p.Username}) } return corePlayers, nil } -func (s *GameStorage) GetRounds() ([]core.Round, error) { +func (s *GameStorage) GetRounds() ([]*core.Round, error) { var dbRounds []Round - var coreRounds []core.Round + var coreRounds []*core.Round s.conn.Where("game_id = ?", s.gameID).Find(&dbRounds) for _, r := range dbRounds { - player := core.Player{Username: r.Username} - coreRounds = append(coreRounds, core.Round{Day: r.Day, Winner: player}) + player := &core.User{Username: r.Username} + coreRounds = append(coreRounds, &core.Round{Day: r.Day, Winner: player}) } return coreRounds, nil } -func (s *GameStorage) AddPlayer(player core.Player) error { +func (s *GameStorage) AddPlayer(player *core.User) error { dbPlayer := s.playerFactory.CreatePlayer(player.Username) s.conn.Create(&dbPlayer) return nil } -func (s *GameStorage) AddRound(round core.Round) error { +func (s *GameStorage) AddRound(round *core.Round) error { player := s.playerFactory.CreatePlayer(round.Winner.Username) dbRound := Round{ GameID: s.gameID, diff --git a/pullanusbot.go b/pullanusbot.go index 2b24ccb..a174469 100644 --- a/pullanusbot.go +++ b/pullanusbot.go @@ -30,7 +30,9 @@ func main() { video_flow := use_cases.CreateVideoFlow(logger, converter, converter) telebot.AddHandler(video_flow) - twitter_flow := use_cases.CreateTwitterFlow(logger) + file_downloader := infrastructure.CreateFileDownloader() + twitter_api := api.CreateTwitterAPI() + twitter_flow := use_cases.CreateTwitterFlow(logger, twitter_api, file_downloader, converter) telebot.AddHandler(twitter_flow) // telebot.SetupVideo(video_flow) // Start endless loop diff --git a/use_cases/faggot_game.go b/use_cases/faggot_game.go index fea0b22..80c231c 100644 --- a/use_cases/faggot_game.go +++ b/use_cases/faggot_game.go @@ -23,7 +23,7 @@ func (flow *GameFlow) Rules() string { return flow.l.I18n("faggot_rules") } -func (flow *GameFlow) Add(player core.Player, storage core.IGameStorage) string { +func (flow *GameFlow) Add(player *core.User, storage core.IGameStorage) string { players, _ := storage.GetPlayers() for _, p := range players { if p == player { @@ -40,7 +40,7 @@ func (flow *GameFlow) Add(player core.Player, storage core.IGameStorage) string return flow.l.I18n("faggot_added_to_game") } -func (flow *GameFlow) Play(player core.Player, storage core.IGameStorage) []string { +func (flow *GameFlow) Play(player *core.User, storage core.IGameStorage) []string { players, _ := storage.GetPlayers() switch len(players) { case 0: @@ -60,7 +60,7 @@ func (flow *GameFlow) Play(player core.Player, storage core.IGameStorage) []stri } winner := players[rand.Intn(len(players))] - round := core.Round{Day: day, Winner: winner} + round := &core.Round{Day: day, Winner: winner} storage.AddRound(round) phrases := make([]string, 0, 4) @@ -125,7 +125,7 @@ func (flow *GameFlow) Stats(storage core.IGameStorage) string { return strings.Join(messages, "\n") } -func (flow *GameFlow) Me(player core.Player, storage core.IGameStorage) string { +func (flow *GameFlow) Me(player *core.User, storage core.IGameStorage) string { entries, _ := flow.getStat(storage) score := 0 for _, e := range entries { diff --git a/use_cases/faggot_game_test.go b/use_cases/faggot_game_test.go index 6e4ba19..3fec90d 100644 --- a/use_cases/faggot_game_test.go +++ b/use_cases/faggot_game_test.go @@ -31,7 +31,7 @@ func Test_RulesCommand_DeliversRulesInDifferentTranslations(t *testing.T) { func Test_Add_ReturnsErrorOnStorageError(t *testing.T) { game, storage, _ := makeSUT() storage.err = errors.New("Unexpected error") - player := core.Player{Username: "Faggot"} + player := &core.User{Username: "Faggot"} message := game.Add(player, storage) assert.Equal(t, message, storage.err.Error()) @@ -42,16 +42,16 @@ func Test_Add_AppendsPlayerInGameOnlyOnce(t *testing.T) { "faggot_added_to_game": "Player added", "faggot_already_in_game": "Player already in game", }) - player := core.Player{Username: "Faggot"} + player := &core.User{Username: "Faggot"} message := game.Add(player, storage) - assert.Equal(t, storage.players, []core.Player{player}) + assert.Equal(t, storage.players, []*core.User{player}) assert.Equal(t, message, localizer.I18n("faggot_added_to_game")) message = game.Add(player, storage) - assert.Equal(t, storage.players, []core.Player{player}) + assert.Equal(t, storage.players, []*core.User{player}) assert.Equal(t, message, localizer.I18n("faggot_already_in_game")) } @@ -59,7 +59,7 @@ func Test_Play_RespondsWithNoPlayers(t *testing.T) { game, storage, localizer := makeSUT(LocalizerDict{ "faggot_no_players": "Nobody in game. So you win, %s!", }) - player := core.Player{Username: "Faggot"} + player := &core.User{Username: "Faggot"} messages := game.Play(player, storage) expected := []string{localizer.I18n("faggot_no_players", player.Username)} assert.Equal(t, messages, expected) @@ -69,7 +69,7 @@ func Test_Play_RespondsNotEnoughPlayers(t *testing.T) { game, storage, localizer := makeSUT(LocalizerDict{ "faggot_not_enough_players": "Not enough players", }) - player := core.Player{Username: "Faggot"} + player := &core.User{Username: "Faggot"} game.Add(player, storage) messages := game.Play(player, storage) @@ -85,8 +85,8 @@ func Test_Play_RespondsWinnerAlreadyKnown(t *testing.T) { "faggot_game_3_0": "3 %s", "faggot_winner_known": "Winner already known %s", }) - player1 := core.Player{Username: "Faggot1"} - player2 := core.Player{Username: "Faggot2"} + player1 := &core.User{Username: "Faggot1"} + player2 := &core.User{Username: "Faggot2"} game.Add(player1, storage) game.Add(player2, storage) @@ -117,11 +117,11 @@ func Test_Stats_RespondsWithDescendingResultsForCurrentYear(t *testing.T) { "total_players:3", } - player1 := core.Player{Username: "Faggot1"} - player2 := core.Player{Username: "Faggot2"} - player3 := core.Player{Username: "Faggot3"} + player1 := &core.User{Username: "Faggot1"} + player2 := &core.User{Username: "Faggot2"} + player3 := &core.User{Username: "Faggot3"} - storage.rounds = []core.Round{ + storage.rounds = []*core.Round{ {Day: year + "-01-01", Winner: player2}, {Day: "2020-01-02", Winner: player3}, {Day: year + "-01-02", Winner: player3}, @@ -152,11 +152,11 @@ func Test_All_RespondsWithDescendingResultsForAllTime(t *testing.T) { "total_players:3", } - player1 := core.Player{Username: "Faggot1"} - player2 := core.Player{Username: "Faggot2"} - player3 := core.Player{Username: "Faggot3"} + player1 := &core.User{Username: "Faggot1"} + player2 := &core.User{Username: "Faggot2"} + player3 := &core.User{Username: "Faggot3"} - storage.rounds = []core.Round{ + storage.rounds = []*core.Round{ {Day: "2021-01-01", Winner: player2}, {Day: "2020-01-02", Winner: player3}, {Day: "2020-01-02", Winner: player3}, @@ -175,10 +175,10 @@ func Test_Me_RespondsWithPersonalStat(t *testing.T) { "faggot_me": "username:%s,scores:%d", }) - player1 := core.Player{Username: "Faggot1"} - player2 := core.Player{Username: "Faggot2"} + player1 := &core.User{Username: "Faggot1"} + player2 := &core.User{Username: "Faggot2"} - storage.rounds = []core.Round{ + storage.rounds = []*core.Round{ {Day: "2021-01-01", Winner: player2}, {Day: "2021-01-05", Winner: player1}, {Day: "2021-01-06", Winner: player1}, @@ -196,7 +196,7 @@ func Test_Me_RespondsWithPersonalStat(t *testing.T) { func makeSUT(args ...interface{}) (*GameFlow, *GameStorageMock, *LocalizerMock) { dict := LocalizerDict{} - storage := &GameStorageMock{players: []core.Player{}} + storage := &GameStorageMock{players: []*core.User{}} for _, arg := range args { switch opt := arg.(type) { @@ -236,12 +236,12 @@ func (l *LocalizerMock) AllKeys() []string { // GameStorageMock type GameStorageMock struct { - players []core.Player - rounds []core.Round + players []*core.User + rounds []*core.Round err error } -func (s *GameStorageMock) AddPlayer(player core.Player) error { +func (s *GameStorageMock) AddPlayer(player *core.User) error { if s.err != nil { return s.err } @@ -250,15 +250,15 @@ func (s *GameStorageMock) AddPlayer(player core.Player) error { return nil } -func (s *GameStorageMock) GetPlayers() ([]core.Player, error) { +func (s *GameStorageMock) GetPlayers() ([]*core.User, error) { if s.err != nil { - return []core.Player{}, s.err + return []*core.User{}, s.err } return s.players, nil } -func (s *GameStorageMock) AddRound(round core.Round) error { +func (s *GameStorageMock) AddRound(round *core.Round) error { if s.err != nil { return s.err } @@ -267,9 +267,9 @@ func (s *GameStorageMock) AddRound(round core.Round) error { return nil } -func (s *GameStorageMock) GetRounds() ([]core.Round, error) { +func (s *GameStorageMock) GetRounds() ([]*core.Round, error) { if s.err != nil { - return []core.Round{}, s.err + return []*core.Round{}, s.err } return s.rounds, nil diff --git a/use_cases/faggot_stat.go b/use_cases/faggot_stat.go index 3c92980..f15f0ad 100644 --- a/use_cases/faggot_stat.go +++ b/use_cases/faggot_stat.go @@ -5,7 +5,7 @@ import ( ) type Stat struct { - Player core.Player + Player *core.User Score int } diff --git a/use_cases/twitter_flow.go b/use_cases/twitter_flow.go index aa3d035..d1182d0 100644 --- a/use_cases/twitter_flow.go +++ b/use_cases/twitter_flow.go @@ -1,16 +1,113 @@ package use_cases -import "github.com/ailinykh/pullanusbot/v2/core" +import ( + "errors" + "math" + "os" + "path" + "regexp" + "strconv" + "strings" + "time" -func CreateTwitterFlow(l core.ILogger) *TwitterFlow { - return &TwitterFlow{l} + "github.com/ailinykh/pullanusbot/v2/core" +) + +func CreateTwitterFlow(l core.ILogger, ml core.IMediaLoader, fd core.IFileDownloader, vff core.IVideoFileFactory) *TwitterFlow { + return &TwitterFlow{l, ml, fd, vff} } type TwitterFlow struct { - l core.ILogger + l core.ILogger + ml core.IMediaLoader + fd core.IFileDownloader + vff core.IVideoFileFactory +} + +func (tf *TwitterFlow) HandleText(text string, author *core.User, bot core.IBot) error { + r := regexp.MustCompile(`twitter\.com.+/(\d+)\S*$`) + match := r.FindStringSubmatch(text) + if len(match) < 2 { + return errors.New("tweet not found") + } + return tf.process(match[1], author, bot) } -func (tf *TwitterFlow) HandleText(text string, bot core.IBot) error { - tf.l.Infof("Got message %s", text) +func (tf *TwitterFlow) process(tweetID string, author *core.User, bot core.IBot) error { + tf.l.Infof("processing tweet %s", tweetID) + medias, err := tf.ml.Load(tweetID, author) + if err != nil { + if strings.HasPrefix(err.Error(), "Rate limit exceeded") { + return tf.handleTimeout(err, tweetID, author, bot) + } + return err + } + + switch len(medias) { + case 0: + return errors.New("unexpected 0 media count") + case 1: + switch medias[0].Type { + case core.Text: + return bot.SendText(medias[0].Caption) + case core.Photo: + return bot.SendPhoto(medias[0]) + case core.Video: + err := bot.SendVideo(medias[0]) + if err != nil { + if strings.Contains(err.Error(), "failed to get HTTP URL content") || strings.Contains(err.Error(), "wrong file identifier/HTTP URL specified") { + return tf.sendByUploading(medias[0], bot) + } + } + return err + } + default: + return bot.SendPhotoAlbum(medias) + } return nil } + +func (tf *TwitterFlow) handleTimeout(err error, tweetID string, author *core.User, bot core.IBot) error { + r := regexp.MustCompile(`(\-?\d+)$`) + match := r.FindStringSubmatch(err.Error()) + if len(match) < 2 { + return errors.New("rate limit not found") + } + + limit, err := strconv.ParseInt(match[1], 10, 64) + if err != nil { + return err + } + + timeout := limit - time.Now().Unix() + tf.l.Infof("Twitter api timeout %d seconds", timeout) + timeout = int64(math.Max(float64(timeout), 1)) // Twitter api timeout might be negative + go func() { + time.Sleep(time.Duration(timeout) * time.Second) + tf.process(tweetID, author, bot) + }() + return nil // TODO: is it ok? +} + +func (tf *TwitterFlow) sendByUploading(media *core.Media, bot core.IBot) error { + // Try to upload file to telegram + tf.l.Info("Sending by uploading") + + filename := path.Base(media.URL) + filepath := path.Join(os.TempDir(), filename) + defer os.Remove(filepath) + + err := tf.fd.Download(media.URL, filepath) + if err != nil { + tf.l.Errorf("video download error: %v", err) + return err + } + + vf, err := tf.vff.CreateVideoFile(filepath) + if err != nil { + tf.l.Errorf("Can't create video file for %s, %v", filepath, err) + return err + } + defer vf.Dispose() + return bot.SendVideoFile(vf, media.Caption) +} diff --git a/use_cases/video_flow.go b/use_cases/video_flow.go index 7c8f093..f238da4 100644 --- a/use_cases/video_flow.go +++ b/use_cases/video_flow.go @@ -39,7 +39,7 @@ func (f *VideoFlow) HandleDocument(document *core.Document, b core.IBot) error { fi1, _ := os.Stat(vf.FilePath) fi2, _ := os.Stat(cvf.FilePath) caption := fmt.Sprintf("%s (by %s)\nOriginal size: %.2f MB (%d kb/s)\nConverted size: %.2f MB (%d kb/s)", vf.FileName, document.Author, float32(fi1.Size())/1048576, vf.Bitrate/1024, float32(fi2.Size())/1048576, cvf.Bitrate/1024) - return b.SendVideo(cvf, caption) + return b.SendVideoFile(cvf, caption) } if vf.Codec != "h264" { @@ -51,10 +51,10 @@ func (f *VideoFlow) HandleDocument(document *core.Document, b core.IBot) error { } defer cvf.Dispose() caption := fmt.Sprintf("%s (by %s)", vf.FileName, document.Author) - return b.SendVideo(cvf, caption) + return b.SendVideoFile(cvf, caption) } f.l.Infof("No need to convert %s", vf.FileName) caption := fmt.Sprintf("%s (by %s)", vf.FileName, document.Author) - return b.SendVideo(vf, caption) + return b.SendVideoFile(vf, caption) } From fc1ff84a607a90bb42e0a3c8673115b4395de37d Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Tue, 11 May 2021 22:56:00 +0300 Subject: [PATCH 007/439] ignore tweet not found error --- use_cases/twitter_flow.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/use_cases/twitter_flow.go b/use_cases/twitter_flow.go index d1182d0..f395324 100644 --- a/use_cases/twitter_flow.go +++ b/use_cases/twitter_flow.go @@ -28,7 +28,7 @@ func (tf *TwitterFlow) HandleText(text string, author *core.User, bot core.IBot) r := regexp.MustCompile(`twitter\.com.+/(\d+)\S*$`) match := r.FindStringSubmatch(text) if len(match) < 2 { - return errors.New("tweet not found") + return nil // no tweet } return tf.process(match[1], author, bot) } From ebea29879ae4df42e020900a04f2be9eab6087d5 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Tue, 11 May 2021 22:56:50 +0300 Subject: [PATCH 008/439] download and send video by link --- pullanusbot.go | 7 ++- use_cases/link_flow.go | 103 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+), 2 deletions(-) create mode 100644 use_cases/link_flow.go diff --git a/pullanusbot.go b/pullanusbot.go index a174469..34e5191 100644 --- a/pullanusbot.go +++ b/pullanusbot.go @@ -34,13 +34,16 @@ func main() { twitter_api := api.CreateTwitterAPI() twitter_flow := use_cases.CreateTwitterFlow(logger, twitter_api, file_downloader, converter) telebot.AddHandler(twitter_flow) - // telebot.SetupVideo(video_flow) + + link_flow := use_cases.CreateLinkFlow(logger, file_downloader, converter, converter) + telebot.AddHandler(link_flow) + // Start endless loop telebot.Run() } func createLogger() (core.ILogger, func()) { - lf, err := os.OpenFile("pullanusbot.txt", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0660) + lf, err := os.OpenFile("pullanusbot.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0660) if err != nil { panic(err) } diff --git a/use_cases/link_flow.go b/use_cases/link_flow.go new file mode 100644 index 0000000..0b0d4ee --- /dev/null +++ b/use_cases/link_flow.go @@ -0,0 +1,103 @@ +package use_cases + +import ( + "fmt" + "net/http" + "os" + "path" + "regexp" + + "github.com/ailinykh/pullanusbot/v2/core" +) + +func CreateLinkFlow(l core.ILogger, fd core.IFileDownloader, vff core.IVideoFileFactory, vfc core.IVideoFileConverter) *LinkFlow { + return &LinkFlow{l, fd, vff, vfc} +} + +type LinkFlow struct { + l core.ILogger + fd core.IFileDownloader + vff core.IVideoFileFactory + vfc core.IVideoFileConverter +} + +func (lf *LinkFlow) HandleText(text string, author *core.User, bot core.IBot) error { + r := regexp.MustCompile(`^http(\S+)$`) + if r.MatchString(text) { + return lf.processLink(text, author, bot) + } + return nil +} + +func (lf *LinkFlow) processLink(link string, author *core.User, bot core.IBot) error { + resp, err := http.Get(link) + + if err != nil { + lf.l.Error(err) + return err + } + + media := &core.Media{URL: resp.Request.URL.String()} + media.Caption = fmt.Sprintf(`🎞 %s (by %s)`, link, path.Base(resp.Request.URL.Path), author.Username) + + switch resp.Header["Content-Type"][0] { + case "video/mp4": + lf.l.Infof("found mp4 file %s", link) + err := bot.SendVideo(media) + + if err != nil { + lf.l.Errorf("%s. Fallback to uploading", err) + return lf.sendByUploading(media, bot) + } + case "video/webm": + vf, err := lf.downloadMedia(media) + if err != nil { + return err + } + defer vf.Dispose() + + vfc, err := lf.vfc.Convert(vf, 0) + if err != nil { + lf.l.Errorf("cant convert video file: %v", err) + return err + } + defer vfc.Dispose() + + return bot.SendVideoFile(vfc, media.Caption) + + default: + lf.l.Warningf("Unsupported content type: %s", resp.Header["Content-Type"]) + } + return nil +} + +func (lf *LinkFlow) sendByUploading(media *core.Media, bot core.IBot) error { + // Try to upload file to telegram + lf.l.Info("Sending by uploading") + + vf, err := lf.downloadMedia(media) + if err != nil { + return err + } + defer vf.Dispose() + return bot.SendVideoFile(vf, media.Caption) +} + +func (lf *LinkFlow) downloadMedia(media *core.Media) (*core.VideoFile, error) { + filename := path.Base(media.URL) + filepath := path.Join(os.TempDir(), filename) + defer os.Remove(filepath) + + err := lf.fd.Download(media.URL, filepath) + if err != nil { + lf.l.Errorf("video download error: %v", err) + return nil, err + } + + vf, err := lf.vff.CreateVideoFile(filepath) + if err != nil { + lf.l.Errorf("Can't create video file for %s, %v", filepath, err) + return nil, err + } + return vf, nil +} From 99e33f8b39c3eb3c9ec12e7b1d16441dbc43d807 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Tue, 11 May 2021 23:27:51 +0300 Subject: [PATCH 009/439] more logs needed --- api/telebot.go | 2 +- use_cases/link_flow.go | 8 +++++++- use_cases/twitter_flow.go | 7 +++++++ 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/api/telebot.go b/api/telebot.go index 98ccb7d..cd27697 100644 --- a/api/telebot.go +++ b/api/telebot.go @@ -40,7 +40,7 @@ func CreateTelebot(token string, logger core.ILogger) *Telebot { for _, h := range telebot.textHandlers { err := h.HandleText(m.Text, makeUser(m), &TelebotAdapter{m, telebot}) if err != nil { - logger.Errorf("TextHandler %#v error: %s", h, err) + logger.Error(err) } } }) diff --git a/use_cases/link_flow.go b/use_cases/link_flow.go index 0b0d4ee..d6ba56b 100644 --- a/use_cases/link_flow.go +++ b/use_cases/link_flow.go @@ -86,7 +86,6 @@ func (lf *LinkFlow) sendByUploading(media *core.Media, bot core.IBot) error { func (lf *LinkFlow) downloadMedia(media *core.Media) (*core.VideoFile, error) { filename := path.Base(media.URL) filepath := path.Join(os.TempDir(), filename) - defer os.Remove(filepath) err := lf.fd.Download(media.URL, filepath) if err != nil { @@ -94,6 +93,13 @@ func (lf *LinkFlow) downloadMedia(media *core.Media) (*core.VideoFile, error) { return nil, err } + stat, err := os.Stat(filepath) + if err != nil { + return nil, err + } + + lf.l.Infof("File downloaded: %s %0.2fMB", filename, float64(stat.Size())/1024/1024) + vf, err := lf.vff.CreateVideoFile(filepath) if err != nil { lf.l.Errorf("Can't create video file for %s, %v", filepath, err) diff --git a/use_cases/twitter_flow.go b/use_cases/twitter_flow.go index f395324..bcf583c 100644 --- a/use_cases/twitter_flow.go +++ b/use_cases/twitter_flow.go @@ -103,6 +103,13 @@ func (tf *TwitterFlow) sendByUploading(media *core.Media, bot core.IBot) error { return err } + stat, err := os.Stat(filepath) + if err != nil { + return err + } + + tf.l.Infof("File downloaded: %s %0.2fMB", filename, float64(stat.Size())/1024/1024) + vf, err := tf.vff.CreateVideoFile(filepath) if err != nil { tf.l.Errorf("Can't create video file for %s, %v", filepath, err) From d7681b2c4469e9dbf05059ee9aafd6e5d8234497 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Sun, 13 Jun 2021 22:18:28 +0300 Subject: [PATCH 010/439] extract core.File interface from core.VideoFile --- api/telebot_adapter.go | 2 +- api/video_file_factory.go | 2 +- core/file.go | 6 ++++++ core/file_downloader.go | 5 ----- core/file_loader.go | 9 +++++++++ core/media_loader.go | 6 ++++-- core/video_file.go | 5 ++--- infrastructure/ffmpeg_converter.go | 10 +++++----- infrastructure/file_downloader.go | 14 ++++++++++---- use_cases/link_flow.go | 15 +++++++-------- use_cases/twitter_flow.go | 26 +++++++++++--------------- use_cases/video_flow.go | 16 ++++++++-------- 12 files changed, 64 insertions(+), 52 deletions(-) create mode 100644 core/file.go delete mode 100644 core/file_downloader.go create mode 100644 core/file_loader.go diff --git a/api/telebot_adapter.go b/api/telebot_adapter.go index 92e05e6..d2855a8 100644 --- a/api/telebot_adapter.go +++ b/api/telebot_adapter.go @@ -56,7 +56,7 @@ func (a *TelebotAdapter) SendVideoFile(vf *core.VideoFile, caption string) error if err != nil { return err } else { - a.t.logger.Infof("%s sent successfully", vf.FileName) + a.t.logger.Infof("%s sent successfully", vf.Name) a.t.bot.Delete(a.m) } return nil diff --git a/api/video_file_factory.go b/api/video_file_factory.go index a965cda..c8223ea 100644 --- a/api/video_file_factory.go +++ b/api/video_file_factory.go @@ -6,7 +6,7 @@ import ( ) func makeVideoFile(vf *core.VideoFile, caption string) tb.Video { - video := tb.Video{File: tb.FromDisk(vf.FilePath)} + video := tb.Video{File: tb.FromDisk(vf.Path)} video.Width = vf.Width video.Height = vf.Height video.Caption = caption diff --git a/core/file.go b/core/file.go new file mode 100644 index 0000000..c760b51 --- /dev/null +++ b/core/file.go @@ -0,0 +1,6 @@ +package core + +type File struct { + Name string + Path string +} diff --git a/core/file_downloader.go b/core/file_downloader.go deleted file mode 100644 index 4b2f12f..0000000 --- a/core/file_downloader.go +++ /dev/null @@ -1,5 +0,0 @@ -package core - -type IFileDownloader interface { - Download(string, string) error -} diff --git a/core/file_loader.go b/core/file_loader.go new file mode 100644 index 0000000..2b94457 --- /dev/null +++ b/core/file_loader.go @@ -0,0 +1,9 @@ +package core + +type IFileDownloader interface { + Download(URL) (*File, error) +} + +type IFileUploader interface { + Upload(*File) (URL, error) +} \ No newline at end of file diff --git a/core/media_loader.go b/core/media_loader.go index a3772de..282ba5f 100644 --- a/core/media_loader.go +++ b/core/media_loader.go @@ -1,5 +1,7 @@ package core -type IMediaLoader interface { - Load(string, *User) ([]*Media, error) +type URL = string + +type IMediaFactory interface { + CreateMedia(URL, *User) ([]*Media, error) } diff --git a/core/video_file.go b/core/video_file.go index 0b386ae..f9a70e7 100644 --- a/core/video_file.go +++ b/core/video_file.go @@ -3,17 +3,16 @@ package core import "os" type VideoFile struct { + File Width int Height int Bitrate int Duration int Codec string - FileName string - FilePath string ThumbPath string } func (vf *VideoFile) Dispose() { - os.Remove(vf.FilePath) + os.Remove(vf.Path) os.Remove(vf.ThumbPath) } diff --git a/infrastructure/ffmpeg_converter.go b/infrastructure/ffmpeg_converter.go index a24bc5b..3f09ead 100644 --- a/infrastructure/ffmpeg_converter.go +++ b/infrastructure/ffmpeg_converter.go @@ -18,11 +18,12 @@ func CreateFfmpegConverter() *FfmpegConverter { type FfmpegConverter struct{} +// core.IVideoFileConverter func (c *FfmpegConverter) Convert(vf *core.VideoFile, bitrate int) (*core.VideoFile, error) { - convertedVideoFilePath := path.Join(os.TempDir(), vf.FileName+"_converted.mp4") - cmd := fmt.Sprintf(`ffmpeg -y -i "%s" -pix_fmt yuv420p -vf "scale=trunc(iw/2)*2:trunc(ih/2)*2" "%s"`, vf.FilePath, convertedVideoFilePath) + convertedVideoFilePath := path.Join(os.TempDir(), vf.Name+"_converted.mp4") + cmd := fmt.Sprintf(`ffmpeg -y -i "%s" -pix_fmt yuv420p -vf "scale=trunc(iw/2)*2:trunc(ih/2)*2" "%s"`, vf.Path, convertedVideoFilePath) if bitrate > 0 { - cmd = fmt.Sprintf(`ffmpeg -v error -y -i "%s" -c:v libx264 -preset medium -b:v %dk -pass 1 -b:a 128k -f mp4 /dev/null && ffmpeg -y -i "%s" -c:v libx264 -preset medium -b:v %dk -pass 2 -b:a 128k "%s"`, vf.FilePath, bitrate/1024, vf.FilePath, bitrate/1024, convertedVideoFilePath) + cmd = fmt.Sprintf(`ffmpeg -v error -y -i "%s" -c:v libx264 -preset medium -b:v %dk -pass 1 -b:a 128k -f mp4 /dev/null && ffmpeg -y -i "%s" -c:v libx264 -preset medium -b:v %dk -pass 2 -b:a 128k "%s"`, vf.Path, bitrate/1024, vf.Path, bitrate/1024, convertedVideoFilePath) } out, err := exec.Command("/bin/sh", "-c", cmd).CombinedOutput() if err != nil { @@ -69,13 +70,12 @@ func (c *FfmpegConverter) CreateVideoFile(path string) (*core.VideoFile, error) } return &core.VideoFile{ + File: core.File{Name: stat.Name(), Path: path}, Width: stream.Width, Height: stream.Height, Bitrate: bitrate, Duration: int(duration), Codec: stream.CodecName, - FileName: stat.Name(), - FilePath: path, ThumbPath: thumbpath}, nil } diff --git a/infrastructure/file_downloader.go b/infrastructure/file_downloader.go index ba81b08..153bdd2 100644 --- a/infrastructure/file_downloader.go +++ b/infrastructure/file_downloader.go @@ -4,6 +4,9 @@ import ( "io" "net/http" "os" + "path" + + "github.com/ailinykh/pullanusbot/v2/core" ) func CreateFileDownloader() *FileDownloader { @@ -12,22 +15,25 @@ func CreateFileDownloader() *FileDownloader { type FileDownloader struct{} -func (FileDownloader) Download(url string, path string) error { +// core.IFileDownloader +func (FileDownloader) Download(url core.URL) (*core.File, error) { + name := path.Base(url) + path := path.Join(os.TempDir(), name) // Get the data resp, err := http.Get(url) if err != nil { - return err + return nil, err } defer resp.Body.Close() // Create the file out, err := os.Create(path) if err != nil { - return err + return nil, err } defer out.Close() // Write the body to file _, err = io.Copy(out, resp.Body) - return err + return &core.File{Name: name, Path: path}, err } diff --git a/use_cases/link_flow.go b/use_cases/link_flow.go index d6ba56b..8b2e1ce 100644 --- a/use_cases/link_flow.go +++ b/use_cases/link_flow.go @@ -84,25 +84,24 @@ func (lf *LinkFlow) sendByUploading(media *core.Media, bot core.IBot) error { } func (lf *LinkFlow) downloadMedia(media *core.Media) (*core.VideoFile, error) { - filename := path.Base(media.URL) - filepath := path.Join(os.TempDir(), filename) - - err := lf.fd.Download(media.URL, filepath) + file, err := lf.fd.Download(media.URL) if err != nil { lf.l.Errorf("video download error: %v", err) return nil, err } - stat, err := os.Stat(filepath) + defer os.Remove(file.Path) + + stat, err := os.Stat(file.Path) if err != nil { return nil, err } - lf.l.Infof("File downloaded: %s %0.2fMB", filename, float64(stat.Size())/1024/1024) + lf.l.Infof("File downloaded: %s %0.2fMB", file.Name, float64(stat.Size())/1024/1024) - vf, err := lf.vff.CreateVideoFile(filepath) + vf, err := lf.vff.CreateVideoFile(file.Path) if err != nil { - lf.l.Errorf("Can't create video file for %s, %v", filepath, err) + lf.l.Errorf("Can't create video file for %s, %v", file.Path, err) return nil, err } return vf, nil diff --git a/use_cases/twitter_flow.go b/use_cases/twitter_flow.go index bcf583c..e8817c0 100644 --- a/use_cases/twitter_flow.go +++ b/use_cases/twitter_flow.go @@ -4,7 +4,6 @@ import ( "errors" "math" "os" - "path" "regexp" "strconv" "strings" @@ -13,13 +12,13 @@ import ( "github.com/ailinykh/pullanusbot/v2/core" ) -func CreateTwitterFlow(l core.ILogger, ml core.IMediaLoader, fd core.IFileDownloader, vff core.IVideoFileFactory) *TwitterFlow { - return &TwitterFlow{l, ml, fd, vff} +func CreateTwitterFlow(l core.ILogger, mf core.IMediaFactory, fd core.IFileDownloader, vff core.IVideoFileFactory) *TwitterFlow { + return &TwitterFlow{l, mf, fd, vff} } type TwitterFlow struct { l core.ILogger - ml core.IMediaLoader + mf core.IMediaFactory fd core.IFileDownloader vff core.IVideoFileFactory } @@ -35,7 +34,7 @@ func (tf *TwitterFlow) HandleText(text string, author *core.User, bot core.IBot) func (tf *TwitterFlow) process(tweetID string, author *core.User, bot core.IBot) error { tf.l.Infof("processing tweet %s", tweetID) - medias, err := tf.ml.Load(tweetID, author) + medias, err := tf.mf.CreateMedia(tweetID, author) if err != nil { if strings.HasPrefix(err.Error(), "Rate limit exceeded") { return tf.handleTimeout(err, tweetID, author, bot) @@ -92,27 +91,24 @@ func (tf *TwitterFlow) handleTimeout(err error, tweetID string, author *core.Use func (tf *TwitterFlow) sendByUploading(media *core.Media, bot core.IBot) error { // Try to upload file to telegram tf.l.Info("Sending by uploading") - - filename := path.Base(media.URL) - filepath := path.Join(os.TempDir(), filename) - defer os.Remove(filepath) - - err := tf.fd.Download(media.URL, filepath) + file, err := tf.fd.Download(media.URL) if err != nil { tf.l.Errorf("video download error: %v", err) return err } - stat, err := os.Stat(filepath) + defer os.Remove(file.Path) + + stat, err := os.Stat(file.Path) if err != nil { return err } - tf.l.Infof("File downloaded: %s %0.2fMB", filename, float64(stat.Size())/1024/1024) + tf.l.Infof("File downloaded: %s %0.2fMB", file.Name, float64(stat.Size())/1024/1024) - vf, err := tf.vff.CreateVideoFile(filepath) + vf, err := tf.vff.CreateVideoFile(file.Path) if err != nil { - tf.l.Errorf("Can't create video file for %s, %v", filepath, err) + tf.l.Errorf("Can't create video file for %s, %v", file.Path, err) return err } defer vf.Dispose() diff --git a/use_cases/video_flow.go b/use_cases/video_flow.go index f238da4..6f86a32 100644 --- a/use_cases/video_flow.go +++ b/use_cases/video_flow.go @@ -29,32 +29,32 @@ func (f *VideoFlow) HandleDocument(document *core.Document, b core.IBot) error { expectedBitrate := int(math.Min(float64(vf.Bitrate), 568320)) if expectedBitrate != vf.Bitrate { - f.l.Infof("Converting %s because of bitrate", vf.FileName) + f.l.Infof("Converting %s because of bitrate", vf.Name) cvf, err := f.c.Convert(vf, expectedBitrate) if err != nil { f.l.Error(err) return err } defer cvf.Dispose() - fi1, _ := os.Stat(vf.FilePath) - fi2, _ := os.Stat(cvf.FilePath) - caption := fmt.Sprintf("%s (by %s)\nOriginal size: %.2f MB (%d kb/s)\nConverted size: %.2f MB (%d kb/s)", vf.FileName, document.Author, float32(fi1.Size())/1048576, vf.Bitrate/1024, float32(fi2.Size())/1048576, cvf.Bitrate/1024) + fi1, _ := os.Stat(vf.Path) + fi2, _ := os.Stat(cvf.Path) + caption := fmt.Sprintf("%s (by %s)\nOriginal size: %.2f MB (%d kb/s)\nConverted size: %.2f MB (%d kb/s)", vf.Name, document.Author, float32(fi1.Size())/1048576, vf.Bitrate/1024, float32(fi2.Size())/1048576, cvf.Bitrate/1024) return b.SendVideoFile(cvf, caption) } if vf.Codec != "h264" { - f.l.Infof("Converting %s because of codec %s", vf.FileName, vf.Codec) + f.l.Infof("Converting %s because of codec %s", vf.Name, vf.Codec) cvf, err := f.c.Convert(vf, 0) if err != nil { f.l.Error(err) return err } defer cvf.Dispose() - caption := fmt.Sprintf("%s (by %s)", vf.FileName, document.Author) + caption := fmt.Sprintf("%s (by %s)", vf.Name, document.Author) return b.SendVideoFile(cvf, caption) } - f.l.Infof("No need to convert %s", vf.FileName) - caption := fmt.Sprintf("%s (by %s)", vf.FileName, document.Author) + f.l.Infof("No need to convert %s", vf.Name) + caption := fmt.Sprintf("%s (by %s)", vf.Name, document.Author) return b.SendVideoFile(vf, caption) } From b7c0d6ce5e16706476cb4c403975fd27829a7305 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Sun, 13 Jun 2021 22:19:43 +0300 Subject: [PATCH 011/439] Combine all the handlers into one file --- core/command_handler.go | 5 ----- core/document_handler.go | 5 ----- core/handlers.go | 17 +++++++++++++++++ core/text_handler.go | 5 ----- 4 files changed, 17 insertions(+), 15 deletions(-) delete mode 100644 core/command_handler.go delete mode 100644 core/document_handler.go create mode 100644 core/handlers.go delete mode 100644 core/text_handler.go diff --git a/core/command_handler.go b/core/command_handler.go deleted file mode 100644 index 423957b..0000000 --- a/core/command_handler.go +++ /dev/null @@ -1,5 +0,0 @@ -package core - -type ICommandHandler interface { - HandleCommand(string, IBot) error -} diff --git a/core/document_handler.go b/core/document_handler.go deleted file mode 100644 index 79a9608..0000000 --- a/core/document_handler.go +++ /dev/null @@ -1,5 +0,0 @@ -package core - -type IDocumentHandler interface { - HandleDocument(*Document, IBot) error -} diff --git a/core/handlers.go b/core/handlers.go new file mode 100644 index 0000000..294a7c8 --- /dev/null +++ b/core/handlers.go @@ -0,0 +1,17 @@ +package core + +type ICommandHandler interface { + HandleCommand(string, IBot) error +} + +type IDocumentHandler interface { + HandleDocument(*Document, IBot) error +} + +type ITextHandler interface { + HandleText(string, *User, IBot) error +} + +type IImageHandler interface { + HandleImage(*File, IBot) error +} diff --git a/core/text_handler.go b/core/text_handler.go deleted file mode 100644 index 0953d7f..0000000 --- a/core/text_handler.go +++ /dev/null @@ -1,5 +0,0 @@ -package core - -type ITextHandler interface { - HandleText(string, *User, IBot) error -} From 511a54ade4838acf3640e15e80692781a2443e70 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Sun, 13 Jun 2021 22:31:20 +0300 Subject: [PATCH 012/439] Add first Image handler to upload it via telegraph --- api/telebot.go | 26 ++++++++++++++- api/telebot_adapter.go | 4 +++ api/telegraph.go | 72 +++++++++++++++++++++++++++++++++++++++++ api/twitter.go | 2 +- core/bot.go | 1 + pullanusbot.go | 3 ++ use_cases/image_flow.go | 26 +++++++++++++++ 7 files changed, 132 insertions(+), 2 deletions(-) create mode 100644 api/telegraph.go create mode 100644 use_cases/image_flow.go diff --git a/api/telebot.go b/api/telebot.go index cd27697..f4fd2e2 100644 --- a/api/telebot.go +++ b/api/telebot.go @@ -17,6 +17,7 @@ type Telebot struct { commandHandlers []string textHandlers []core.ITextHandler documentHandlers []core.IDocumentHandler + imageHandlers []core.IImageHandler } func CreateTelebot(token string, logger core.ILogger) *Telebot { @@ -34,7 +35,7 @@ func CreateTelebot(token string, logger core.ILogger) *Telebot { panic(err) } - telebot := &Telebot{bot, logger, []string{}, []core.ITextHandler{}, []core.IDocumentHandler{}} + telebot := &Telebot{bot, logger, []string{}, []core.ITextHandler{}, []core.IDocumentHandler{}, []core.IImageHandler{}} bot.Handle(tb.OnText, func(m *tb.Message) { for _, h := range telebot.textHandlers { @@ -75,6 +76,27 @@ func CreateTelebot(token string, logger core.ILogger) *Telebot { } } }) + + bot.Handle(tb.OnPhoto, func(m *tb.Message) { + name := path.Base(m.Photo.FileURL) + path := path.Join(os.TempDir(), m.Photo.File.FileID+".jpg") + err := bot.Download(&m.Photo.File, path) + if err != nil { + logger.Error(err) + return + } + + logger.Infof("Downloaded to %s", path) + defer os.Remove(path) + + for _, h := range telebot.imageHandlers { + h.HandleImage(&core.File{ + Name: name, + Path: path, + }, &TelebotAdapter{m, telebot}) + } + }) + return telebot } @@ -84,6 +106,8 @@ func (t *Telebot) AddHandler(handlers ...interface{}) { t.documentHandlers = append(t.documentHandlers, h) case core.ITextHandler: t.textHandlers = append(t.textHandlers, h) + case core.IImageHandler: + t.imageHandlers = append(t.imageHandlers, h) case string: for _, command := range t.commandHandlers { if command == h { diff --git a/api/telebot_adapter.go b/api/telebot_adapter.go index d2855a8..4477ea8 100644 --- a/api/telebot_adapter.go +++ b/api/telebot_adapter.go @@ -11,6 +11,10 @@ type TelebotAdapter struct { t *Telebot } +func (a *TelebotAdapter) IsPrivate() bool { + return a.m.Private() +} + func (a *TelebotAdapter) SendText(text string) error { _, err := a.t.bot.Send(a.m.Chat, text, &tb.SendOptions{ParseMode: tb.ModeHTML, DisableWebPagePreview: true}) return err diff --git a/api/telegraph.go b/api/telegraph.go new file mode 100644 index 0000000..469e87b --- /dev/null +++ b/api/telegraph.go @@ -0,0 +1,72 @@ +package api + +import ( + "bytes" + "encoding/json" + "io" + "io/ioutil" + "mime/multipart" + "net/http" + "os" + + "github.com/ailinykh/pullanusbot/v2/core" +) + +func CreateTelegraphAPI() *Telegraph { + return &Telegraph{} +} + +type Telegraph struct { +} + +type telegraphImage struct { + Src string `json:"src"` +} + +// IFileUploader +func (t *Telegraph) Upload(file *core.File) (core.URL, error) { + fd, err := os.Open(file.Path) + if err != nil { + return "", err + } + defer fd.Close() + + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + part, err := writer.CreateFormFile("file", file.Name) + if err != nil { + return "", err + } + + _, err = io.Copy(part, fd) + if err != nil { + return "", err + } + + err = writer.Close() + if err != nil { + return "", err + } + + req, _ := http.NewRequest("POST", "https://telegra.ph/upload", body) + req.Header.Set("Content-Type", writer.FormDataContentType()) + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return "", err + } + + body2, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "", err + } + + var images []telegraphImage + err = json.Unmarshal(body2, &images) + if err != nil { + return "", err + } + + url := "https://telegra.ph" + images[0].Src + return url, nil +} diff --git a/api/twitter.go b/api/twitter.go index faf27c7..e2db4f2 100644 --- a/api/twitter.go +++ b/api/twitter.go @@ -46,7 +46,7 @@ func (Twitter) get(tweetID string) (*Tweet, error) { return &tweet, err } -func (t *Twitter) Load(tweetID string, author *core.User) ([]*core.Media, error) { +func (t *Twitter) CreateMedia(tweetID string, author *core.User) ([]*core.Media, error) { tweet, err := t.get(tweetID) if err != nil { return nil, err diff --git a/core/bot.go b/core/bot.go index cdf3a3e..507d033 100644 --- a/core/bot.go +++ b/core/bot.go @@ -1,6 +1,7 @@ package core type IBot interface { + IsPrivate() bool SendText(string) error SendPhoto(*Media) error SendPhotoAlbum([]*Media) error diff --git a/pullanusbot.go b/pullanusbot.go index 34e5191..c044d1b 100644 --- a/pullanusbot.go +++ b/pullanusbot.go @@ -38,6 +38,9 @@ func main() { link_flow := use_cases.CreateLinkFlow(logger, file_downloader, converter, converter) telebot.AddHandler(link_flow) + file_uploader := api.CreateTelegraphAPI() + image_flow := use_cases.CreateImageFlow(logger, file_uploader) + telebot.AddHandler(image_flow) // Start endless loop telebot.Run() } diff --git a/use_cases/image_flow.go b/use_cases/image_flow.go new file mode 100644 index 0000000..e6d0d68 --- /dev/null +++ b/use_cases/image_flow.go @@ -0,0 +1,26 @@ +package use_cases + +import "github.com/ailinykh/pullanusbot/v2/core" + +func CreateImageFlow(l core.ILogger, fu core.IFileUploader) *ImageFlow { + return &ImageFlow{l, fu} +} + +type ImageFlow struct { + l core.ILogger + fu core.IFileUploader +} + +// IImageHandler +func (f *ImageFlow) HandleImage(file *core.File, bot core.IBot) error { + if !bot.IsPrivate() { + return nil + } + + url, err := f.fu.Upload(file) + if err != nil { + return err + } + + return bot.SendText(url) +} From 2acd60b4aac28ef723e055e9c67f270fdebfb527 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Sun, 13 Jun 2021 22:42:55 +0300 Subject: [PATCH 013/439] Create core.Message to discribe message struct --- api/telebot.go | 2 +- api/telebot_adapter.go | 4 ---- core/bot.go | 1 - core/handlers.go | 2 +- core/message.go | 6 ++++++ use_cases/image_flow.go | 4 ++-- 6 files changed, 10 insertions(+), 9 deletions(-) create mode 100644 core/message.go diff --git a/api/telebot.go b/api/telebot.go index f4fd2e2..cab583e 100644 --- a/api/telebot.go +++ b/api/telebot.go @@ -93,7 +93,7 @@ func CreateTelebot(token string, logger core.ILogger) *Telebot { h.HandleImage(&core.File{ Name: name, Path: path, - }, &TelebotAdapter{m, telebot}) + }, core.Message{ID: m.ID, IsPrivate: m.Private()}, &TelebotAdapter{m, telebot}) } }) diff --git a/api/telebot_adapter.go b/api/telebot_adapter.go index 4477ea8..d2855a8 100644 --- a/api/telebot_adapter.go +++ b/api/telebot_adapter.go @@ -11,10 +11,6 @@ type TelebotAdapter struct { t *Telebot } -func (a *TelebotAdapter) IsPrivate() bool { - return a.m.Private() -} - func (a *TelebotAdapter) SendText(text string) error { _, err := a.t.bot.Send(a.m.Chat, text, &tb.SendOptions{ParseMode: tb.ModeHTML, DisableWebPagePreview: true}) return err diff --git a/core/bot.go b/core/bot.go index 507d033..cdf3a3e 100644 --- a/core/bot.go +++ b/core/bot.go @@ -1,7 +1,6 @@ package core type IBot interface { - IsPrivate() bool SendText(string) error SendPhoto(*Media) error SendPhotoAlbum([]*Media) error diff --git a/core/handlers.go b/core/handlers.go index 294a7c8..e5da066 100644 --- a/core/handlers.go +++ b/core/handlers.go @@ -13,5 +13,5 @@ type ITextHandler interface { } type IImageHandler interface { - HandleImage(*File, IBot) error + HandleImage(*File, Message, IBot) error } diff --git a/core/message.go b/core/message.go new file mode 100644 index 0000000..cdcf812 --- /dev/null +++ b/core/message.go @@ -0,0 +1,6 @@ +package core + +type Message struct { + ID int + IsPrivate bool +} diff --git a/use_cases/image_flow.go b/use_cases/image_flow.go index e6d0d68..857241b 100644 --- a/use_cases/image_flow.go +++ b/use_cases/image_flow.go @@ -12,8 +12,8 @@ type ImageFlow struct { } // IImageHandler -func (f *ImageFlow) HandleImage(file *core.File, bot core.IBot) error { - if !bot.IsPrivate() { +func (f *ImageFlow) HandleImage(file *core.File, message core.Message, bot core.IBot) error { + if !message.IsPrivate { return nil } From 7450758dc05019f62520908f865aa5c26d961282 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Mon, 14 Jun 2021 15:55:41 +0300 Subject: [PATCH 014/439] Extend IBot interface with SendImage and SendAlbum methods --- api/telebot_adapter.go | 32 ++++++++++++++++++++++++++++++++ core/bot.go | 3 +++ 2 files changed, 35 insertions(+) diff --git a/api/telebot_adapter.go b/api/telebot_adapter.go index d2855a8..435aedc 100644 --- a/api/telebot_adapter.go +++ b/api/telebot_adapter.go @@ -16,6 +16,38 @@ func (a *TelebotAdapter) SendText(text string) error { return err } +func (a *TelebotAdapter) Delete(message *core.Message) error { + return a.t.bot.Delete(&tb.Message{ID: message.ID, Chat: &tb.Chat{ID: message.ChatID}}) +} + +func (a *TelebotAdapter) SendImage(image *core.Image) (*core.Message, error) { + photo := &tb.Photo{File: tb.File{FileID: image.ID}} + sent, err := a.t.bot.Send(a.m.Chat, photo) + if err != nil { + return nil, err + } + return createMessage(sent), nil +} + +func (a *TelebotAdapter) SendAlbum(images []*core.Image) ([]*core.Message, error) { + album := tb.Album{} + for _, i := range images { + photo := &tb.Photo{File: tb.File{FileID: i.ID}} + album = append(album, photo) + } + + sent, err := a.t.bot.SendAlbum(a.m.Chat, album) + if err != nil { + return nil, err + } + + var messages []*core.Message + for _, m := range sent { + messages = append(messages, createMessage(&m)) + } + return messages, nil +} + func (a *TelebotAdapter) SendPhoto(media *core.Media) error { file := &tb.Photo{File: tb.FromURL(media.URL)} file.Caption = media.Caption diff --git a/core/bot.go b/core/bot.go index cdf3a3e..d253396 100644 --- a/core/bot.go +++ b/core/bot.go @@ -1,7 +1,10 @@ package core type IBot interface { + Delete(*Message) error SendText(string) error + SendImage(*Image) (*Message, error) + SendAlbum([]*Image) ([]*Message, error) SendPhoto(*Media) error SendPhotoAlbum([]*Media) error SendVideo(*Media) error From 52f29db97bab58c462c9c5e26e593b785d086f8b Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Mon, 14 Jun 2021 15:56:09 +0300 Subject: [PATCH 015/439] Appends username to /info output --- api/telebot_info.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/api/telebot_info.go b/api/telebot_info.go index f769aa6..11d1bbb 100644 --- a/api/telebot_info.go +++ b/api/telebot_info.go @@ -19,6 +19,7 @@ func (t *Telebot) SetupInfo() { fmt.Sprintf("ID: *%d*", m.Sender.ID), fmt.Sprintf("First: *%s*", m.Sender.FirstName), fmt.Sprintf("Last: *%s*", m.Sender.LastName), + fmt.Sprintf("Username: *%s*", m.Sender.Username), "", } @@ -38,6 +39,8 @@ func (t *Telebot) SetupInfo() { fmt.Sprintf("ID: *%d*", m.ReplyTo.OriginalSender.ID), fmt.Sprintf("First: *%s*", m.ReplyTo.OriginalSender.FirstName), fmt.Sprintf("Last: *%s*", m.ReplyTo.OriginalSender.LastName), + fmt.Sprintf("Username: *%s*", m.ReplyTo.OriginalSender.Username), + fmt.Sprintf("Name: *%s*", m.ReplyTo.OriginalSenderName), "", ) } From 54acf9702efa29ee4f4884033d5ae08dda7dccb3 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Mon, 14 Jun 2021 15:57:48 +0300 Subject: [PATCH 016/439] Some comments added --- use_cases/link_flow.go | 1 + use_cases/twitter_flow.go | 1 + 2 files changed, 2 insertions(+) diff --git a/use_cases/link_flow.go b/use_cases/link_flow.go index 8b2e1ce..6741bb8 100644 --- a/use_cases/link_flow.go +++ b/use_cases/link_flow.go @@ -21,6 +21,7 @@ type LinkFlow struct { vfc core.IVideoFileConverter } +// core.ITextHandler func (lf *LinkFlow) HandleText(text string, author *core.User, bot core.IBot) error { r := regexp.MustCompile(`^http(\S+)$`) if r.MatchString(text) { diff --git a/use_cases/twitter_flow.go b/use_cases/twitter_flow.go index e8817c0..0657490 100644 --- a/use_cases/twitter_flow.go +++ b/use_cases/twitter_flow.go @@ -23,6 +23,7 @@ type TwitterFlow struct { vff core.IVideoFileFactory } +// core.ITextHandler func (tf *TwitterFlow) HandleText(text string, author *core.User, bot core.IBot) error { r := regexp.MustCompile(`twitter\.com.+/(\d+)\S*$`) match := r.FindStringSubmatch(text) From 741d8e9230c3c5bd87a2b0c12a29bae4aff699ba Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Mon, 14 Jun 2021 15:58:32 +0300 Subject: [PATCH 017/439] Add Image and Message to the core package --- api/telebot.go | 60 ++++++++++++++++++++++++++--------------- core/handlers.go | 4 +-- core/image.go | 42 +++++++++++++++++++++++++++++ core/message.go | 3 +++ use_cases/image_flow.go | 10 +++++-- 5 files changed, 94 insertions(+), 25 deletions(-) create mode 100644 core/image.go diff --git a/api/telebot.go b/api/telebot.go index cab583e..365fbed 100644 --- a/api/telebot.go +++ b/api/telebot.go @@ -78,22 +78,22 @@ func CreateTelebot(token string, logger core.ILogger) *Telebot { }) bot.Handle(tb.OnPhoto, func(m *tb.Message) { - name := path.Base(m.Photo.FileURL) - path := path.Join(os.TempDir(), m.Photo.File.FileID+".jpg") - err := bot.Download(&m.Photo.File, path) - if err != nil { - logger.Error(err) - return - } - logger.Infof("Downloaded to %s", path) - defer os.Remove(path) + image := core.CreateImage(m.Photo.FileID, func() (string, error) { + path := path.Join(os.TempDir(), m.Photo.File.UniqueID+".jpg") + err := bot.Download(&m.Photo.File, path) + if err != nil { + logger.Error(err) + return "", err + } + + logger.Infof("image %s downloaded to %s", m.Photo.UniqueID, path) + return path, nil + }) + defer image.Dispose() for _, h := range telebot.imageHandlers { - h.HandleImage(&core.File{ - Name: name, - Path: path, - }, core.Message{ID: m.ID, IsPrivate: m.Private()}, &TelebotAdapter{m, telebot}) + h.HandleImage(&image, createMessage(m), &TelebotAdapter{m, telebot}) } }) @@ -109,15 +109,14 @@ func (t *Telebot) AddHandler(handlers ...interface{}) { case core.IImageHandler: t.imageHandlers = append(t.imageHandlers, h) case string: - for _, command := range t.commandHandlers { - if command == h { - panic("Handler for " + command + " already set!") - } + t.registerCommand(h) + if handler, ok := handlers[1].(core.ICommandHandler); ok { + t.bot.Handle(h, func(m *tb.Message) { + handler.HandleCommand(createMessage(m), &TelebotAdapter{m, t}) + }) + } else { + panic("interface must implement core.ICommandHandler") } - t.commandHandlers = append(t.commandHandlers, h) - t.bot.Handle(h, func(m *tb.Message) { - handlers[1].(core.ICommandHandler).HandleCommand(m.Text, &TelebotAdapter{m, t}) - }) default: panic(fmt.Sprintf("something wrong with %s", h)) } @@ -126,3 +125,22 @@ func (t *Telebot) AddHandler(handlers ...interface{}) { func (t *Telebot) Run() { t.bot.Start() } + +func (t *Telebot) registerCommand(command string) { + for _, c := range t.commandHandlers { + if c == command { + panic("Handler for " + command + " already set!") + } + } + t.commandHandlers = append(t.commandHandlers, command) +} + +func createMessage(m *tb.Message) *core.Message { + return &core.Message{ + ID: m.ID, + ChatID: m.Chat.ID, + IsPrivate: m.Private(), + Sender: &core.User{Username: m.Sender.Username}, + Text: m.Text, + } +} diff --git a/core/handlers.go b/core/handlers.go index e5da066..71ac401 100644 --- a/core/handlers.go +++ b/core/handlers.go @@ -1,7 +1,7 @@ package core type ICommandHandler interface { - HandleCommand(string, IBot) error + HandleCommand(*Message, IBot) error } type IDocumentHandler interface { @@ -13,5 +13,5 @@ type ITextHandler interface { } type IImageHandler interface { - HandleImage(*File, Message, IBot) error + HandleImage(*Image, *Message, IBot) error } diff --git a/core/image.go b/core/image.go new file mode 100644 index 0000000..aaf7d72 --- /dev/null +++ b/core/image.go @@ -0,0 +1,42 @@ +package core + +import ( + "os" + "path" + "sync" +) + +func CreateImage(id string, dl func() (string, error)) Image { + return Image{ID: id, dl: dl} +} + +type Image struct { + File + ID string + + dl func() (string, error) + mutex sync.Mutex +} + +func (i *Image) Download() error { + if _, err := os.Stat(i.File.Path); err == nil { + return nil + } + + i.mutex.Lock() + defer i.mutex.Unlock() + + filepath, err := i.dl() + if err != nil { + return nil + } + + i.File.Name = path.Base(filepath) + i.File.Path = filepath + + return nil +} + +func (i *Image) Dispose() error { + return os.Remove(i.File.Path) +} diff --git a/core/message.go b/core/message.go index cdcf812..b0b0242 100644 --- a/core/message.go +++ b/core/message.go @@ -2,5 +2,8 @@ package core type Message struct { ID int + ChatID int64 IsPrivate bool + Sender *User + Text string } diff --git a/use_cases/image_flow.go b/use_cases/image_flow.go index 857241b..1273050 100644 --- a/use_cases/image_flow.go +++ b/use_cases/image_flow.go @@ -12,15 +12,21 @@ type ImageFlow struct { } // IImageHandler -func (f *ImageFlow) HandleImage(file *core.File, message core.Message, bot core.IBot) error { +func (f *ImageFlow) HandleImage(image *core.Image, message *core.Message, bot core.IBot) error { if !message.IsPrivate { return nil } - url, err := f.fu.Upload(file) + err := image.Download() if err != nil { return err } + url, err := f.fu.Upload(&image.File) + if err != nil { + return err + } + + f.l.Info(url) return bot.SendText(url) } From 5e131cf7353ba29c89790de430ce4305d3ce402b Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Mon, 14 Jun 2021 15:58:47 +0300 Subject: [PATCH 018/439] Add publisher flow --- pullanusbot.go | 4 ++ use_cases/publisher_flow.go | 129 ++++++++++++++++++++++++++++++++++++ 2 files changed, 133 insertions(+) create mode 100644 use_cases/publisher_flow.go diff --git a/pullanusbot.go b/pullanusbot.go index c044d1b..7d46e32 100644 --- a/pullanusbot.go +++ b/pullanusbot.go @@ -41,6 +41,10 @@ func main() { file_uploader := api.CreateTelegraphAPI() image_flow := use_cases.CreateImageFlow(logger, file_uploader) telebot.AddHandler(image_flow) + + publisher_flow := use_cases.CreatePublisherFlow(logger) + telebot.AddHandler(publisher_flow) + telebot.AddHandler("/loh666", publisher_flow) // Start endless loop telebot.Run() } diff --git a/use_cases/publisher_flow.go b/use_cases/publisher_flow.go new file mode 100644 index 0000000..4462c4e --- /dev/null +++ b/use_cases/publisher_flow.go @@ -0,0 +1,129 @@ +package use_cases + +import ( + "os" + "strconv" + "time" + + "github.com/ailinykh/pullanusbot/v2/core" +) + +func CreatePublisherFlow(l core.ILogger) *PublisherFlow { + chatID, err := strconv.ParseInt(os.Getenv("PUBLISER_CHAT_ID"), 10, 64) + if err != nil { + chatID = 0 + } + + username := os.Getenv("PUBLISER_USERNAME") + + publisher := PublisherFlow{ + l: l, + chatID: chatID, + username: username, + imageChan: make(chan imgSource), + requestChan: make(chan msgSource), + } + + go publisher.runLoop() + return &publisher +} + +type PublisherFlow struct { + l core.ILogger + + chatID int64 + username string + imageChan chan imgSource + requestChan chan msgSource +} + +type imgSource struct { + imageID string + bot core.IBot +} + +type msgSource struct { + message core.Message + bot core.IBot +} + +// core.IImageHandler +func (p *PublisherFlow) HandleImage(image *core.Image, message *core.Message, bot core.IBot) error { + if message.ChatID != p.chatID || message.Sender.Username != p.username { + return nil + } + + p.imageChan <- imgSource{image.ID, bot} + return nil +} + +// core.ICommandHandler +func (p *PublisherFlow) HandleCommand(message *core.Message, bot core.IBot) error { + if message.ChatID != p.chatID { + return nil + } + + p.requestChan <- msgSource{*message, bot} + return nil +} + +func (p *PublisherFlow) runLoop() { + photos := []string{} + queue := []string{} + + disposal := func(m core.Message, bot core.IBot, timeout int) { + time.Sleep(time.Duration(timeout) * time.Second) + p.l.Infof("disposing message %d from chat %d", m.ID, m.ChatID) + err := bot.Delete(&m) + if err != nil { + p.l.Error(err) + } + } + + for { + select { + case is := <-p.imageChan: + p.l.Infof("got photo %s", is.imageID) + queue = append(queue, is.imageID) + + case <-time.After(1 * time.Second): + if len(queue) > 0 { + p.l.Infof("had %d actual photo(s)", len(queue)) + photos = queue + queue = []string{} + } + + case ms := <-p.requestChan: + go disposal(ms.message, ms.bot, 0) + switch count := len(photos); count { + case 0: + err := ms.bot.SendText("I have nothing for you comrade major") + if err != nil { + p.l.Error(err) + } + case 1: + p.l.Info("have one actual photo") + sent, err := ms.bot.SendImage(&core.Image{ID: photos[0]}) + if err != nil { + p.l.Error(err) + } else { + go disposal(*sent, ms.bot, 30) + } + default: + p.l.Infof("have %d actual photos", count) + album := []*core.Image{} + for _, p := range photos { + album = append(album, &core.Image{ID: p}) + } + sent, err := ms.bot.SendAlbum(album) + if err != nil { + p.l.Error(err) + } else { + for _, m := range sent { + go disposal(*m, ms.bot, 30) + } + } + } + } + } +} From 759a2a9b66714b397b7c12bece4b1c92c5f8a88b Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Mon, 14 Jun 2021 16:13:50 +0300 Subject: [PATCH 019/439] Replace string with *Message in ITextHandler interface --- api/telebot.go | 2 +- core/handlers.go | 2 +- use_cases/link_flow.go | 6 +++--- use_cases/twitter_flow.go | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/api/telebot.go b/api/telebot.go index 365fbed..0281460 100644 --- a/api/telebot.go +++ b/api/telebot.go @@ -39,7 +39,7 @@ func CreateTelebot(token string, logger core.ILogger) *Telebot { bot.Handle(tb.OnText, func(m *tb.Message) { for _, h := range telebot.textHandlers { - err := h.HandleText(m.Text, makeUser(m), &TelebotAdapter{m, telebot}) + err := h.HandleText(createMessage(m), makeUser(m), &TelebotAdapter{m, telebot}) if err != nil { logger.Error(err) } diff --git a/core/handlers.go b/core/handlers.go index 71ac401..fd33e7e 100644 --- a/core/handlers.go +++ b/core/handlers.go @@ -9,7 +9,7 @@ type IDocumentHandler interface { } type ITextHandler interface { - HandleText(string, *User, IBot) error + HandleText(*Message, *User, IBot) error } type IImageHandler interface { diff --git a/use_cases/link_flow.go b/use_cases/link_flow.go index 6741bb8..a04392a 100644 --- a/use_cases/link_flow.go +++ b/use_cases/link_flow.go @@ -22,10 +22,10 @@ type LinkFlow struct { } // core.ITextHandler -func (lf *LinkFlow) HandleText(text string, author *core.User, bot core.IBot) error { +func (lf *LinkFlow) HandleText(message *core.Message, author *core.User, bot core.IBot) error { r := regexp.MustCompile(`^http(\S+)$`) - if r.MatchString(text) { - return lf.processLink(text, author, bot) + if r.MatchString(message.Text) { + return lf.processLink(message.Text, author, bot) } return nil } diff --git a/use_cases/twitter_flow.go b/use_cases/twitter_flow.go index 0657490..5583c0a 100644 --- a/use_cases/twitter_flow.go +++ b/use_cases/twitter_flow.go @@ -24,9 +24,9 @@ type TwitterFlow struct { } // core.ITextHandler -func (tf *TwitterFlow) HandleText(text string, author *core.User, bot core.IBot) error { +func (tf *TwitterFlow) HandleText(message *core.Message, author *core.User, bot core.IBot) error { r := regexp.MustCompile(`twitter\.com.+/(\d+)\S*$`) - match := r.FindStringSubmatch(text) + match := r.FindStringSubmatch(message.Text) if len(match) < 2 { return nil // no tweet } From aa4d220c8962829fe1c607081900010b45b3ca7c Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Mon, 14 Jun 2021 16:15:04 +0300 Subject: [PATCH 020/439] Rename createMessage to makeMessage --- api/telebot.go | 8 ++++---- api/telebot_adapter.go | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/api/telebot.go b/api/telebot.go index 0281460..293b515 100644 --- a/api/telebot.go +++ b/api/telebot.go @@ -39,7 +39,7 @@ func CreateTelebot(token string, logger core.ILogger) *Telebot { bot.Handle(tb.OnText, func(m *tb.Message) { for _, h := range telebot.textHandlers { - err := h.HandleText(createMessage(m), makeUser(m), &TelebotAdapter{m, telebot}) + err := h.HandleText(makeMessage(m), makeUser(m), &TelebotAdapter{m, telebot}) if err != nil { logger.Error(err) } @@ -93,7 +93,7 @@ func CreateTelebot(token string, logger core.ILogger) *Telebot { defer image.Dispose() for _, h := range telebot.imageHandlers { - h.HandleImage(&image, createMessage(m), &TelebotAdapter{m, telebot}) + h.HandleImage(&image, makeMessage(m), &TelebotAdapter{m, telebot}) } }) @@ -112,7 +112,7 @@ func (t *Telebot) AddHandler(handlers ...interface{}) { t.registerCommand(h) if handler, ok := handlers[1].(core.ICommandHandler); ok { t.bot.Handle(h, func(m *tb.Message) { - handler.HandleCommand(createMessage(m), &TelebotAdapter{m, t}) + handler.HandleCommand(makeMessage(m), &TelebotAdapter{m, t}) }) } else { panic("interface must implement core.ICommandHandler") @@ -135,7 +135,7 @@ func (t *Telebot) registerCommand(command string) { t.commandHandlers = append(t.commandHandlers, command) } -func createMessage(m *tb.Message) *core.Message { +func makeMessage(m *tb.Message) *core.Message { return &core.Message{ ID: m.ID, ChatID: m.Chat.ID, diff --git a/api/telebot_adapter.go b/api/telebot_adapter.go index 435aedc..0f6bf8f 100644 --- a/api/telebot_adapter.go +++ b/api/telebot_adapter.go @@ -26,7 +26,7 @@ func (a *TelebotAdapter) SendImage(image *core.Image) (*core.Message, error) { if err != nil { return nil, err } - return createMessage(sent), nil + return makeMessage(sent), nil } func (a *TelebotAdapter) SendAlbum(images []*core.Image) ([]*core.Message, error) { @@ -43,7 +43,7 @@ func (a *TelebotAdapter) SendAlbum(images []*core.Image) ([]*core.Message, error var messages []*core.Message for _, m := range sent { - messages = append(messages, createMessage(&m)) + messages = append(messages, makeMessage(&m)) } return messages, nil } From f6ee78725ef7d0c8a6c030b52bf653c5933c8036 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Mon, 14 Jun 2021 16:24:31 +0300 Subject: [PATCH 021/439] Return message and error without just error in SendText --- api/telebot_adapter.go | 9 ++++++--- core/bot.go | 2 +- use_cases/image_flow.go | 3 ++- use_cases/publisher_flow.go | 12 +++++------- use_cases/twitter_flow.go | 3 ++- use_cases/video_flow.go | 3 ++- 6 files changed, 18 insertions(+), 14 deletions(-) diff --git a/api/telebot_adapter.go b/api/telebot_adapter.go index 0f6bf8f..8e8b84a 100644 --- a/api/telebot_adapter.go +++ b/api/telebot_adapter.go @@ -11,9 +11,12 @@ type TelebotAdapter struct { t *Telebot } -func (a *TelebotAdapter) SendText(text string) error { - _, err := a.t.bot.Send(a.m.Chat, text, &tb.SendOptions{ParseMode: tb.ModeHTML, DisableWebPagePreview: true}) - return err +func (a *TelebotAdapter) SendText(text string) (*core.Message, error) { + sent, err := a.t.bot.Send(a.m.Chat, text, &tb.SendOptions{ParseMode: tb.ModeHTML, DisableWebPagePreview: true}) + if err != nil { + return nil, err + } + return makeMessage(sent), nil } func (a *TelebotAdapter) Delete(message *core.Message) error { diff --git a/core/bot.go b/core/bot.go index d253396..e9cbb0c 100644 --- a/core/bot.go +++ b/core/bot.go @@ -2,7 +2,7 @@ package core type IBot interface { Delete(*Message) error - SendText(string) error + SendText(string) (*Message, error) SendImage(*Image) (*Message, error) SendAlbum([]*Image) ([]*Message, error) SendPhoto(*Media) error diff --git a/use_cases/image_flow.go b/use_cases/image_flow.go index 1273050..34bab40 100644 --- a/use_cases/image_flow.go +++ b/use_cases/image_flow.go @@ -28,5 +28,6 @@ func (f *ImageFlow) HandleImage(image *core.Image, message *core.Message, bot co } f.l.Info(url) - return bot.SendText(url) + _, err = bot.SendText(url) + return err } diff --git a/use_cases/publisher_flow.go b/use_cases/publisher_flow.go index 4462c4e..3a1ecde 100644 --- a/use_cases/publisher_flow.go +++ b/use_cases/publisher_flow.go @@ -49,21 +49,19 @@ type msgSource struct { // core.IImageHandler func (p *PublisherFlow) HandleImage(image *core.Image, message *core.Message, bot core.IBot) error { - if message.ChatID != p.chatID || message.Sender.Username != p.username { - return nil + if message.ChatID == p.chatID && message.Sender.Username == p.username { + p.imageChan <- imgSource{image.ID, bot} } - p.imageChan <- imgSource{image.ID, bot} return nil } // core.ICommandHandler func (p *PublisherFlow) HandleCommand(message *core.Message, bot core.IBot) error { - if message.ChatID != p.chatID { - return nil + if message.ChatID == p.chatID { + p.requestChan <- msgSource{*message, bot} } - p.requestChan <- msgSource{*message, bot} return nil } @@ -97,7 +95,7 @@ func (p *PublisherFlow) runLoop() { go disposal(ms.message, ms.bot, 0) switch count := len(photos); count { case 0: - err := ms.bot.SendText("I have nothing for you comrade major") + _, err := ms.bot.SendText("I have nothing for you comrade major") if err != nil { p.l.Error(err) } diff --git a/use_cases/twitter_flow.go b/use_cases/twitter_flow.go index 5583c0a..503f773 100644 --- a/use_cases/twitter_flow.go +++ b/use_cases/twitter_flow.go @@ -49,7 +49,8 @@ func (tf *TwitterFlow) process(tweetID string, author *core.User, bot core.IBot) case 1: switch medias[0].Type { case core.Text: - return bot.SendText(medias[0].Caption) + _, err := bot.SendText(medias[0].Caption) + return err case core.Photo: return bot.SendPhoto(medias[0]) case core.Video: diff --git a/use_cases/video_flow.go b/use_cases/video_flow.go index 6f86a32..0ec315c 100644 --- a/use_cases/video_flow.go +++ b/use_cases/video_flow.go @@ -22,7 +22,8 @@ func (f *VideoFlow) HandleDocument(document *core.Document, b core.IBot) error { vf, err := f.f.CreateVideoFile(document.FilePath) if err != nil { f.l.Error(err) - return b.SendText(err.Error()) + b.SendText(err.Error()) + return err } defer vf.Dispose() From 18669890d9498508ba5286fe0be125dcb9631418 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Mon, 14 Jun 2021 16:28:53 +0300 Subject: [PATCH 022/439] Replace error with message and error in SendPhoto --- api/telebot_adapter.go | 16 +++++----------- core/bot.go | 2 +- use_cases/twitter_flow.go | 3 ++- 3 files changed, 8 insertions(+), 13 deletions(-) diff --git a/api/telebot_adapter.go b/api/telebot_adapter.go index 8e8b84a..b10a43f 100644 --- a/api/telebot_adapter.go +++ b/api/telebot_adapter.go @@ -13,10 +13,7 @@ type TelebotAdapter struct { func (a *TelebotAdapter) SendText(text string) (*core.Message, error) { sent, err := a.t.bot.Send(a.m.Chat, text, &tb.SendOptions{ParseMode: tb.ModeHTML, DisableWebPagePreview: true}) - if err != nil { - return nil, err - } - return makeMessage(sent), nil + return makeMessage(sent), err } func (a *TelebotAdapter) Delete(message *core.Message) error { @@ -26,10 +23,7 @@ func (a *TelebotAdapter) Delete(message *core.Message) error { func (a *TelebotAdapter) SendImage(image *core.Image) (*core.Message, error) { photo := &tb.Photo{File: tb.File{FileID: image.ID}} sent, err := a.t.bot.Send(a.m.Chat, photo) - if err != nil { - return nil, err - } - return makeMessage(sent), nil + return makeMessage(sent), err } func (a *TelebotAdapter) SendAlbum(images []*core.Image) ([]*core.Message, error) { @@ -51,12 +45,12 @@ func (a *TelebotAdapter) SendAlbum(images []*core.Image) ([]*core.Message, error return messages, nil } -func (a *TelebotAdapter) SendPhoto(media *core.Media) error { +func (a *TelebotAdapter) SendPhoto(media *core.Media) (*core.Message, error) { file := &tb.Photo{File: tb.FromURL(media.URL)} file.Caption = media.Caption a.t.bot.Notify(a.m.Chat, tb.UploadingPhoto) - _, err := a.t.bot.Send(a.m.Chat, file, &tb.SendOptions{ParseMode: tb.ModeHTML}) - return err + sent, err := a.t.bot.Send(a.m.Chat, file, &tb.SendOptions{ParseMode: tb.ModeHTML}) + return makeMessage(sent), err } func (a *TelebotAdapter) SendPhotoAlbum(medias []*core.Media) error { diff --git a/core/bot.go b/core/bot.go index e9cbb0c..2d8a7a5 100644 --- a/core/bot.go +++ b/core/bot.go @@ -5,7 +5,7 @@ type IBot interface { SendText(string) (*Message, error) SendImage(*Image) (*Message, error) SendAlbum([]*Image) ([]*Message, error) - SendPhoto(*Media) error + SendPhoto(*Media) (*Message, error) SendPhotoAlbum([]*Media) error SendVideo(*Media) error SendVideoFile(*VideoFile, string) error diff --git a/use_cases/twitter_flow.go b/use_cases/twitter_flow.go index 503f773..122bb76 100644 --- a/use_cases/twitter_flow.go +++ b/use_cases/twitter_flow.go @@ -52,7 +52,8 @@ func (tf *TwitterFlow) process(tweetID string, author *core.User, bot core.IBot) _, err := bot.SendText(medias[0].Caption) return err case core.Photo: - return bot.SendPhoto(medias[0]) + _, err := bot.SendPhoto(medias[0]) + return err case core.Video: err := bot.SendVideo(medias[0]) if err != nil { From dd677ad99642eca167dd1f5f2e9fb157a0140360 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Mon, 14 Jun 2021 16:32:31 +0300 Subject: [PATCH 023/439] Replace error with slice of message and error in SendPhotoAlbum --- api/telebot_adapter.go | 16 ++++++++-------- core/bot.go | 2 +- use_cases/twitter_flow.go | 3 ++- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/api/telebot_adapter.go b/api/telebot_adapter.go index b10a43f..661224d 100644 --- a/api/telebot_adapter.go +++ b/api/telebot_adapter.go @@ -34,15 +34,11 @@ func (a *TelebotAdapter) SendAlbum(images []*core.Image) ([]*core.Message, error } sent, err := a.t.bot.SendAlbum(a.m.Chat, album) - if err != nil { - return nil, err - } - var messages []*core.Message for _, m := range sent { messages = append(messages, makeMessage(&m)) } - return messages, nil + return messages, err } func (a *TelebotAdapter) SendPhoto(media *core.Media) (*core.Message, error) { @@ -53,7 +49,7 @@ func (a *TelebotAdapter) SendPhoto(media *core.Media) (*core.Message, error) { return makeMessage(sent), err } -func (a *TelebotAdapter) SendPhotoAlbum(medias []*core.Media) error { +func (a *TelebotAdapter) SendPhotoAlbum(medias []*core.Media) ([]*core.Message, error) { var photo *tb.Photo var album = tb.Album{} @@ -66,8 +62,12 @@ func (a *TelebotAdapter) SendPhotoAlbum(medias []*core.Media) error { album = append(album, photo) } - _, err := a.t.bot.SendAlbum(a.m.Chat, album) - return err + sent, err := a.t.bot.SendAlbum(a.m.Chat, album) + var messages []*core.Message + for _, m := range sent { + messages = append(messages, makeMessage(&m)) + } + return messages, err } func (a *TelebotAdapter) SendVideo(media *core.Media) error { diff --git a/core/bot.go b/core/bot.go index 2d8a7a5..a7da0d9 100644 --- a/core/bot.go +++ b/core/bot.go @@ -6,7 +6,7 @@ type IBot interface { SendImage(*Image) (*Message, error) SendAlbum([]*Image) ([]*Message, error) SendPhoto(*Media) (*Message, error) - SendPhotoAlbum([]*Media) error + SendPhotoAlbum([]*Media) ([]*Message, error) SendVideo(*Media) error SendVideoFile(*VideoFile, string) error } diff --git a/use_cases/twitter_flow.go b/use_cases/twitter_flow.go index 122bb76..92e32fb 100644 --- a/use_cases/twitter_flow.go +++ b/use_cases/twitter_flow.go @@ -64,7 +64,8 @@ func (tf *TwitterFlow) process(tweetID string, author *core.User, bot core.IBot) return err } default: - return bot.SendPhotoAlbum(medias) + _, err := bot.SendPhotoAlbum(medias) + return err } return nil } From b547b26d9ca2cd43d35c3d6ae1c21ba296956842 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Mon, 14 Jun 2021 16:41:08 +0300 Subject: [PATCH 024/439] Replace error with Message and error in SendVideo --- api/telebot_adapter.go | 6 +++--- core/bot.go | 2 +- use_cases/link_flow.go | 2 +- use_cases/twitter_flow.go | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/api/telebot_adapter.go b/api/telebot_adapter.go index 661224d..a6634df 100644 --- a/api/telebot_adapter.go +++ b/api/telebot_adapter.go @@ -70,12 +70,12 @@ func (a *TelebotAdapter) SendPhotoAlbum(medias []*core.Media) ([]*core.Message, return messages, err } -func (a *TelebotAdapter) SendVideo(media *core.Media) error { +func (a *TelebotAdapter) SendVideo(media *core.Media) (*core.Message, error) { file := &tb.Video{File: tb.FromURL(media.URL)} file.Caption = media.Caption a.t.bot.Notify(a.m.Chat, tb.UploadingVideo) - _, err := a.t.bot.Send(a.m.Chat, file, &tb.SendOptions{ParseMode: tb.ModeHTML}) - return err + sent, err := a.t.bot.Send(a.m.Chat, file, &tb.SendOptions{ParseMode: tb.ModeHTML}) + return makeMessage(sent), err } func (a *TelebotAdapter) SendVideoFile(vf *core.VideoFile, caption string) error { diff --git a/core/bot.go b/core/bot.go index a7da0d9..703f8fd 100644 --- a/core/bot.go +++ b/core/bot.go @@ -7,6 +7,6 @@ type IBot interface { SendAlbum([]*Image) ([]*Message, error) SendPhoto(*Media) (*Message, error) SendPhotoAlbum([]*Media) ([]*Message, error) - SendVideo(*Media) error + SendVideo(*Media) (*Message, error) SendVideoFile(*VideoFile, string) error } diff --git a/use_cases/link_flow.go b/use_cases/link_flow.go index a04392a..6164c5b 100644 --- a/use_cases/link_flow.go +++ b/use_cases/link_flow.go @@ -44,7 +44,7 @@ func (lf *LinkFlow) processLink(link string, author *core.User, bot core.IBot) e switch resp.Header["Content-Type"][0] { case "video/mp4": lf.l.Infof("found mp4 file %s", link) - err := bot.SendVideo(media) + _, err := bot.SendVideo(media) if err != nil { lf.l.Errorf("%s. Fallback to uploading", err) diff --git a/use_cases/twitter_flow.go b/use_cases/twitter_flow.go index 92e32fb..427c713 100644 --- a/use_cases/twitter_flow.go +++ b/use_cases/twitter_flow.go @@ -28,7 +28,7 @@ func (tf *TwitterFlow) HandleText(message *core.Message, author *core.User, bot r := regexp.MustCompile(`twitter\.com.+/(\d+)\S*$`) match := r.FindStringSubmatch(message.Text) if len(match) < 2 { - return nil // no tweet + return nil // no tweet id found } return tf.process(match[1], author, bot) } @@ -55,7 +55,7 @@ func (tf *TwitterFlow) process(tweetID string, author *core.User, bot core.IBot) _, err := bot.SendPhoto(medias[0]) return err case core.Video: - err := bot.SendVideo(medias[0]) + _, err := bot.SendVideo(medias[0]) if err != nil { if strings.Contains(err.Error(), "failed to get HTTP URL content") || strings.Contains(err.Error(), "wrong file identifier/HTTP URL specified") { return tf.sendByUploading(medias[0], bot) From e2f8a6a42395129214ff08061550e436c8499c0d Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Mon, 14 Jun 2021 16:47:16 +0300 Subject: [PATCH 025/439] Replace error with message and error in SendVideoFile --- api/telebot_adapter.go | 10 ++++------ core/bot.go | 2 +- use_cases/link_flow.go | 6 ++++-- use_cases/twitter_flow.go | 3 ++- use_cases/video_flow.go | 9 ++++++--- 5 files changed, 17 insertions(+), 13 deletions(-) diff --git a/api/telebot_adapter.go b/api/telebot_adapter.go index a6634df..374e81e 100644 --- a/api/telebot_adapter.go +++ b/api/telebot_adapter.go @@ -78,17 +78,15 @@ func (a *TelebotAdapter) SendVideo(media *core.Media) (*core.Message, error) { return makeMessage(sent), err } -func (a *TelebotAdapter) SendVideoFile(vf *core.VideoFile, caption string) error { +func (a *TelebotAdapter) SendVideoFile(vf *core.VideoFile, caption string) (*core.Message, error) { video := makeVideoFile(vf, caption) a.t.bot.Notify(a.m.Chat, tb.UploadingVideo) - _, err := video.Send(a.t.bot, a.m.Chat, &tb.SendOptions{ParseMode: tb.ModeHTML}) - if err != nil { - return err - } else { + sent, err := video.Send(a.t.bot, a.m.Chat, &tb.SendOptions{ParseMode: tb.ModeHTML}) + if err == nil { a.t.logger.Infof("%s sent successfully", vf.Name) a.t.bot.Delete(a.m) } - return nil + return makeMessage(sent), err } func (a *TelebotAdapter) CreatePlayer(string) infrastructure.Player { diff --git a/core/bot.go b/core/bot.go index 703f8fd..f439e81 100644 --- a/core/bot.go +++ b/core/bot.go @@ -8,5 +8,5 @@ type IBot interface { SendPhoto(*Media) (*Message, error) SendPhotoAlbum([]*Media) ([]*Message, error) SendVideo(*Media) (*Message, error) - SendVideoFile(*VideoFile, string) error + SendVideoFile(*VideoFile, string) (*Message, error) } diff --git a/use_cases/link_flow.go b/use_cases/link_flow.go index 6164c5b..6578e90 100644 --- a/use_cases/link_flow.go +++ b/use_cases/link_flow.go @@ -64,7 +64,8 @@ func (lf *LinkFlow) processLink(link string, author *core.User, bot core.IBot) e } defer vfc.Dispose() - return bot.SendVideoFile(vfc, media.Caption) + _, err = bot.SendVideoFile(vfc, media.Caption) + return err default: lf.l.Warningf("Unsupported content type: %s", resp.Header["Content-Type"]) @@ -81,7 +82,8 @@ func (lf *LinkFlow) sendByUploading(media *core.Media, bot core.IBot) error { return err } defer vf.Dispose() - return bot.SendVideoFile(vf, media.Caption) + _, err = bot.SendVideoFile(vf, media.Caption) + return err } func (lf *LinkFlow) downloadMedia(media *core.Media) (*core.VideoFile, error) { diff --git a/use_cases/twitter_flow.go b/use_cases/twitter_flow.go index 427c713..e5d8d9e 100644 --- a/use_cases/twitter_flow.go +++ b/use_cases/twitter_flow.go @@ -116,5 +116,6 @@ func (tf *TwitterFlow) sendByUploading(media *core.Media, bot core.IBot) error { return err } defer vf.Dispose() - return bot.SendVideoFile(vf, media.Caption) + _, err = bot.SendVideoFile(vf, media.Caption) + return err } diff --git a/use_cases/video_flow.go b/use_cases/video_flow.go index 0ec315c..1342ac9 100644 --- a/use_cases/video_flow.go +++ b/use_cases/video_flow.go @@ -40,7 +40,8 @@ func (f *VideoFlow) HandleDocument(document *core.Document, b core.IBot) error { fi1, _ := os.Stat(vf.Path) fi2, _ := os.Stat(cvf.Path) caption := fmt.Sprintf("%s (by %s)\nOriginal size: %.2f MB (%d kb/s)\nConverted size: %.2f MB (%d kb/s)", vf.Name, document.Author, float32(fi1.Size())/1048576, vf.Bitrate/1024, float32(fi2.Size())/1048576, cvf.Bitrate/1024) - return b.SendVideoFile(cvf, caption) + _, err = b.SendVideoFile(cvf, caption) + return err } if vf.Codec != "h264" { @@ -52,10 +53,12 @@ func (f *VideoFlow) HandleDocument(document *core.Document, b core.IBot) error { } defer cvf.Dispose() caption := fmt.Sprintf("%s (by %s)", vf.Name, document.Author) - return b.SendVideoFile(cvf, caption) + _, err = b.SendVideoFile(cvf, caption) + return err } f.l.Infof("No need to convert %s", vf.Name) caption := fmt.Sprintf("%s (by %s)", vf.Name, document.Author) - return b.SendVideoFile(vf, caption) + _, err = b.SendVideoFile(vf, caption) + return err } From 7d37ec450b50822b27badee9e8951a020a49e817 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Mon, 14 Jun 2021 17:12:13 +0300 Subject: [PATCH 026/439] Replace SendPhoto and SendVideo with SendMedia --- api/telebot_adapter.go | 31 ++++++++++++++++++------------- core/bot.go | 3 +-- use_cases/link_flow.go | 2 +- use_cases/twitter_flow.go | 20 +++++--------------- 4 files changed, 25 insertions(+), 31 deletions(-) diff --git a/api/telebot_adapter.go b/api/telebot_adapter.go index 374e81e..731e7c8 100644 --- a/api/telebot_adapter.go +++ b/api/telebot_adapter.go @@ -41,11 +41,24 @@ func (a *TelebotAdapter) SendAlbum(images []*core.Image) ([]*core.Message, error return messages, err } -func (a *TelebotAdapter) SendPhoto(media *core.Media) (*core.Message, error) { - file := &tb.Photo{File: tb.FromURL(media.URL)} - file.Caption = media.Caption - a.t.bot.Notify(a.m.Chat, tb.UploadingPhoto) - sent, err := a.t.bot.Send(a.m.Chat, file, &tb.SendOptions{ParseMode: tb.ModeHTML}) +func (a *TelebotAdapter) SendMedia(media *core.Media) (*core.Message, error) { + var sent *tb.Message + var err error + switch media.Type { + case core.Photo: + file := &tb.Photo{File: tb.FromURL(media.URL)} + file.Caption = media.Caption + a.t.bot.Notify(a.m.Chat, tb.UploadingPhoto) + sent, err = a.t.bot.Send(a.m.Chat, file, &tb.SendOptions{ParseMode: tb.ModeHTML}) + case core.Video: + file := &tb.Video{File: tb.FromURL(media.URL)} + file.Caption = media.Caption + a.t.bot.Notify(a.m.Chat, tb.UploadingVideo) + sent, err = a.t.bot.Send(a.m.Chat, file, &tb.SendOptions{ParseMode: tb.ModeHTML}) + case core.Text: + sent, err = a.t.bot.Send(a.m.Chat, media.Caption, &tb.SendOptions{ParseMode: tb.ModeHTML}) + } + return makeMessage(sent), err } @@ -70,14 +83,6 @@ func (a *TelebotAdapter) SendPhotoAlbum(medias []*core.Media) ([]*core.Message, return messages, err } -func (a *TelebotAdapter) SendVideo(media *core.Media) (*core.Message, error) { - file := &tb.Video{File: tb.FromURL(media.URL)} - file.Caption = media.Caption - a.t.bot.Notify(a.m.Chat, tb.UploadingVideo) - sent, err := a.t.bot.Send(a.m.Chat, file, &tb.SendOptions{ParseMode: tb.ModeHTML}) - return makeMessage(sent), err -} - func (a *TelebotAdapter) SendVideoFile(vf *core.VideoFile, caption string) (*core.Message, error) { video := makeVideoFile(vf, caption) a.t.bot.Notify(a.m.Chat, tb.UploadingVideo) diff --git a/core/bot.go b/core/bot.go index f439e81..5f4947e 100644 --- a/core/bot.go +++ b/core/bot.go @@ -5,8 +5,7 @@ type IBot interface { SendText(string) (*Message, error) SendImage(*Image) (*Message, error) SendAlbum([]*Image) ([]*Message, error) - SendPhoto(*Media) (*Message, error) + SendMedia(*Media) (*Message, error) SendPhotoAlbum([]*Media) ([]*Message, error) - SendVideo(*Media) (*Message, error) SendVideoFile(*VideoFile, string) (*Message, error) } diff --git a/use_cases/link_flow.go b/use_cases/link_flow.go index 6578e90..3a13062 100644 --- a/use_cases/link_flow.go +++ b/use_cases/link_flow.go @@ -44,7 +44,7 @@ func (lf *LinkFlow) processLink(link string, author *core.User, bot core.IBot) e switch resp.Header["Content-Type"][0] { case "video/mp4": lf.l.Infof("found mp4 file %s", link) - _, err := bot.SendVideo(media) + _, err := bot.SendMedia(media) if err != nil { lf.l.Errorf("%s. Fallback to uploading", err) diff --git a/use_cases/twitter_flow.go b/use_cases/twitter_flow.go index e5d8d9e..3e2d9b8 100644 --- a/use_cases/twitter_flow.go +++ b/use_cases/twitter_flow.go @@ -47,27 +47,17 @@ func (tf *TwitterFlow) process(tweetID string, author *core.User, bot core.IBot) case 0: return errors.New("unexpected 0 media count") case 1: - switch medias[0].Type { - case core.Text: - _, err := bot.SendText(medias[0].Caption) - return err - case core.Photo: - _, err := bot.SendPhoto(medias[0]) - return err - case core.Video: - _, err := bot.SendVideo(medias[0]) - if err != nil { - if strings.Contains(err.Error(), "failed to get HTTP URL content") || strings.Contains(err.Error(), "wrong file identifier/HTTP URL specified") { - return tf.sendByUploading(medias[0], bot) - } + _, err := bot.SendMedia(medias[0]) + if err != nil && medias[0].Type == core.Video { + if strings.Contains(err.Error(), "failed to get HTTP URL content") || strings.Contains(err.Error(), "wrong file identifier/HTTP URL specified") { + return tf.sendByUploading(medias[0], bot) } - return err } + return err default: _, err := bot.SendPhotoAlbum(medias) return err } - return nil } func (tf *TwitterFlow) handleTimeout(err error, tweetID string, author *core.User, bot core.IBot) error { From 4e9a5d24b445262e4d7e93432c6509afb2c833ec Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Mon, 14 Jun 2021 17:33:19 +0300 Subject: [PATCH 027/439] Remove user from ITextHandler --- api/telebot.go | 2 +- core/handlers.go | 2 +- use_cases/link_flow.go | 12 ++++++------ use_cases/twitter_flow.go | 14 +++++++------- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/api/telebot.go b/api/telebot.go index 293b515..9b143e6 100644 --- a/api/telebot.go +++ b/api/telebot.go @@ -39,7 +39,7 @@ func CreateTelebot(token string, logger core.ILogger) *Telebot { bot.Handle(tb.OnText, func(m *tb.Message) { for _, h := range telebot.textHandlers { - err := h.HandleText(makeMessage(m), makeUser(m), &TelebotAdapter{m, telebot}) + err := h.HandleText(makeMessage(m), &TelebotAdapter{m, telebot}) if err != nil { logger.Error(err) } diff --git a/core/handlers.go b/core/handlers.go index fd33e7e..d231c50 100644 --- a/core/handlers.go +++ b/core/handlers.go @@ -9,7 +9,7 @@ type IDocumentHandler interface { } type ITextHandler interface { - HandleText(*Message, *User, IBot) error + HandleText(*Message, IBot) error } type IImageHandler interface { diff --git a/use_cases/link_flow.go b/use_cases/link_flow.go index 3a13062..f990bda 100644 --- a/use_cases/link_flow.go +++ b/use_cases/link_flow.go @@ -22,16 +22,16 @@ type LinkFlow struct { } // core.ITextHandler -func (lf *LinkFlow) HandleText(message *core.Message, author *core.User, bot core.IBot) error { +func (lf *LinkFlow) HandleText(message *core.Message, bot core.IBot) error { r := regexp.MustCompile(`^http(\S+)$`) if r.MatchString(message.Text) { - return lf.processLink(message.Text, author, bot) + return lf.processLink(message, bot) } return nil } -func (lf *LinkFlow) processLink(link string, author *core.User, bot core.IBot) error { - resp, err := http.Get(link) +func (lf *LinkFlow) processLink(message *core.Message, bot core.IBot) error { + resp, err := http.Get(message.Text) if err != nil { lf.l.Error(err) @@ -39,11 +39,11 @@ func (lf *LinkFlow) processLink(link string, author *core.User, bot core.IBot) e } media := &core.Media{URL: resp.Request.URL.String()} - media.Caption = fmt.Sprintf(`🎞 %s (by %s)`, link, path.Base(resp.Request.URL.Path), author.Username) + media.Caption = fmt.Sprintf(`🎞 %s (by %s)`, message.Text, path.Base(resp.Request.URL.Path), message.Sender.Username) switch resp.Header["Content-Type"][0] { case "video/mp4": - lf.l.Infof("found mp4 file %s", link) + lf.l.Infof("found mp4 file %s", message.Text) _, err := bot.SendMedia(media) if err != nil { diff --git a/use_cases/twitter_flow.go b/use_cases/twitter_flow.go index 3e2d9b8..f170161 100644 --- a/use_cases/twitter_flow.go +++ b/use_cases/twitter_flow.go @@ -24,21 +24,21 @@ type TwitterFlow struct { } // core.ITextHandler -func (tf *TwitterFlow) HandleText(message *core.Message, author *core.User, bot core.IBot) error { +func (tf *TwitterFlow) HandleText(message *core.Message, bot core.IBot) error { r := regexp.MustCompile(`twitter\.com.+/(\d+)\S*$`) match := r.FindStringSubmatch(message.Text) if len(match) < 2 { return nil // no tweet id found } - return tf.process(match[1], author, bot) + return tf.process(match[1], message, bot) } -func (tf *TwitterFlow) process(tweetID string, author *core.User, bot core.IBot) error { +func (tf *TwitterFlow) process(tweetID string, message *core.Message, bot core.IBot) error { tf.l.Infof("processing tweet %s", tweetID) - medias, err := tf.mf.CreateMedia(tweetID, author) + medias, err := tf.mf.CreateMedia(tweetID, message.Sender) if err != nil { if strings.HasPrefix(err.Error(), "Rate limit exceeded") { - return tf.handleTimeout(err, tweetID, author, bot) + return tf.handleTimeout(err, tweetID, message, bot) } return err } @@ -60,7 +60,7 @@ func (tf *TwitterFlow) process(tweetID string, author *core.User, bot core.IBot) } } -func (tf *TwitterFlow) handleTimeout(err error, tweetID string, author *core.User, bot core.IBot) error { +func (tf *TwitterFlow) handleTimeout(err error, tweetID string, message *core.Message, bot core.IBot) error { r := regexp.MustCompile(`(\-?\d+)$`) match := r.FindStringSubmatch(err.Error()) if len(match) < 2 { @@ -77,7 +77,7 @@ func (tf *TwitterFlow) handleTimeout(err error, tweetID string, author *core.Use timeout = int64(math.Max(float64(timeout), 1)) // Twitter api timeout might be negative go func() { time.Sleep(time.Duration(timeout) * time.Second) - tf.process(tweetID, author, bot) + tf.process(tweetID, message, bot) }() return nil // TODO: is it ok? } From baee387b5f76cb559bb5b2098198ffc31300e73a Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Mon, 14 Jun 2021 18:13:08 +0300 Subject: [PATCH 028/439] TwitterFlow refactoring --- use_cases/twitter_flow.go | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/use_cases/twitter_flow.go b/use_cases/twitter_flow.go index f170161..783c603 100644 --- a/use_cases/twitter_flow.go +++ b/use_cases/twitter_flow.go @@ -35,7 +35,7 @@ func (tf *TwitterFlow) HandleText(message *core.Message, bot core.IBot) error { func (tf *TwitterFlow) process(tweetID string, message *core.Message, bot core.IBot) error { tf.l.Infof("processing tweet %s", tweetID) - medias, err := tf.mf.CreateMedia(tweetID, message.Sender) + media, err := tf.mf.CreateMedia(tweetID, message.Sender) if err != nil { if strings.HasPrefix(err.Error(), "Rate limit exceeded") { return tf.handleTimeout(err, tweetID, message, bot) @@ -43,19 +43,23 @@ func (tf *TwitterFlow) process(tweetID string, message *core.Message, bot core.I return err } - switch len(medias) { + return tf.handleMedia(media, message, bot) +} + +func (tf *TwitterFlow) handleMedia(media []*core.Media, message *core.Message, bot core.IBot) error { + switch len(media) { case 0: return errors.New("unexpected 0 media count") case 1: - _, err := bot.SendMedia(medias[0]) - if err != nil && medias[0].Type == core.Video { + _, err := bot.SendMedia(media[0]) + if err != nil && media[0].Type == core.Video { if strings.Contains(err.Error(), "failed to get HTTP URL content") || strings.Contains(err.Error(), "wrong file identifier/HTTP URL specified") { - return tf.sendByUploading(medias[0], bot) + return tf.fallbackToUploading(media[0], bot) } } return err default: - _, err := bot.SendPhotoAlbum(medias) + _, err := bot.SendPhotoAlbum(media) return err } } @@ -82,7 +86,7 @@ func (tf *TwitterFlow) handleTimeout(err error, tweetID string, message *core.Me return nil // TODO: is it ok? } -func (tf *TwitterFlow) sendByUploading(media *core.Media, bot core.IBot) error { +func (tf *TwitterFlow) fallbackToUploading(media *core.Media, bot core.IBot) error { // Try to upload file to telegram tf.l.Info("Sending by uploading") file, err := tf.fd.Download(media.URL) From 4a71f23f47a27f65f17aa3df25aa5372697f2089 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Tue, 15 Jun 2021 11:14:19 +0300 Subject: [PATCH 029/439] Ability to reply to message if needed --- api/telebot_adapter.go | 13 +++++++++++-- core/bot.go | 2 +- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/api/telebot_adapter.go b/api/telebot_adapter.go index 731e7c8..79ab441 100644 --- a/api/telebot_adapter.go +++ b/api/telebot_adapter.go @@ -11,8 +11,17 @@ type TelebotAdapter struct { t *Telebot } -func (a *TelebotAdapter) SendText(text string) (*core.Message, error) { - sent, err := a.t.bot.Send(a.m.Chat, text, &tb.SendOptions{ParseMode: tb.ModeHTML, DisableWebPagePreview: true}) +func (a *TelebotAdapter) SendText(text string, params ...interface{}) (*core.Message, error) { + opts := tb.SendOptions{ParseMode: tb.ModeHTML, DisableWebPagePreview: true} + for _, param := range params { + switch m := param.(type) { + case *core.Message: + opts.ReplyTo = &tb.Message{ID: m.ID} + default: + break + } + } + sent, err := a.t.bot.Send(a.m.Chat, text, &opts) return makeMessage(sent), err } diff --git a/core/bot.go b/core/bot.go index 5f4947e..8a10332 100644 --- a/core/bot.go +++ b/core/bot.go @@ -2,7 +2,7 @@ package core type IBot interface { Delete(*Message) error - SendText(string) (*Message, error) + SendText(string, ...interface{}) (*Message, error) SendImage(*Image) (*Message, error) SendAlbum([]*Image) ([]*Message, error) SendMedia(*Media) (*Message, error) From f5099abfe412f5c9b9f4c4b48d0b25f30d2e5ce9 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Tue, 15 Jun 2021 11:14:58 +0300 Subject: [PATCH 030/439] Do nothing in case of text/html to avoid redundant logging --- use_cases/link_flow.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/use_cases/link_flow.go b/use_cases/link_flow.go index f990bda..445a140 100644 --- a/use_cases/link_flow.go +++ b/use_cases/link_flow.go @@ -66,7 +66,7 @@ func (lf *LinkFlow) processLink(message *core.Message, bot core.IBot) error { _, err = bot.SendVideoFile(vfc, media.Caption) return err - + case "text/html; charset=utf-8": default: lf.l.Warningf("Unsupported content type: %s", resp.Header["Content-Type"]) } From 54f7b3600c73dd71441550d70c10aee17e22ec4f Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Tue, 15 Jun 2021 11:15:42 +0300 Subject: [PATCH 031/439] Twitter timeout replys --- use_cases/twitter_flow.go | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/use_cases/twitter_flow.go b/use_cases/twitter_flow.go index 783c603..b1880a2 100644 --- a/use_cases/twitter_flow.go +++ b/use_cases/twitter_flow.go @@ -2,6 +2,7 @@ package use_cases import ( "errors" + "fmt" "math" "os" "regexp" @@ -13,14 +14,15 @@ import ( ) func CreateTwitterFlow(l core.ILogger, mf core.IMediaFactory, fd core.IFileDownloader, vff core.IVideoFileFactory) *TwitterFlow { - return &TwitterFlow{l, mf, fd, vff} + return &TwitterFlow{l, mf, fd, vff, make(map[core.Message]core.Message)} } type TwitterFlow struct { - l core.ILogger - mf core.IMediaFactory - fd core.IFileDownloader - vff core.IVideoFileFactory + l core.ILogger + mf core.IMediaFactory + fd core.IFileDownloader + vff core.IVideoFileFactory + timeoutReplies map[core.Message]core.Message } // core.ITextHandler @@ -38,12 +40,27 @@ func (tf *TwitterFlow) process(tweetID string, message *core.Message, bot core.I media, err := tf.mf.CreateMedia(tweetID, message.Sender) if err != nil { if strings.HasPrefix(err.Error(), "Rate limit exceeded") { - return tf.handleTimeout(err, tweetID, message, bot) + err := tf.handleTimeout(err, tweetID, message, bot) + if strings.HasPrefix(err.Error(), "twitter api timeout") { + sent, err := bot.SendText(err.Error(), message) + if err != nil { + return err + } + tf.timeoutReplies[*message] = *sent + } } return err } - return tf.handleMedia(media, message, bot) + err = tf.handleMedia(media, message, bot) + if err == nil { + if sent, ok := tf.timeoutReplies[*message]; ok { + _ = bot.Delete(&sent) + delete(tf.timeoutReplies, *message) + } + return bot.Delete(message) + } + return err } func (tf *TwitterFlow) handleMedia(media []*core.Media, message *core.Message, bot core.IBot) error { @@ -83,7 +100,9 @@ func (tf *TwitterFlow) handleTimeout(err error, tweetID string, message *core.Me time.Sleep(time.Duration(timeout) * time.Second) tf.process(tweetID, message, bot) }() - return nil // TODO: is it ok? + minutes := timeout / 60 + seconds := timeout % 60 + return fmt.Errorf("twitter api timeout %d min %d sec", minutes, seconds) } func (tf *TwitterFlow) fallbackToUploading(media *core.Media, bot core.IBot) error { From 9306b2a99d1b62ceb1bd2603e82d492589c3cb8c Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Tue, 15 Jun 2021 21:27:29 +0300 Subject: [PATCH 032/439] Move image download logic as separate dependency --- api/helpers.go | 14 +++++++++ api/telebot.go | 36 ++++++++++++++++-------- core/file.go | 6 ++++ core/image.go | 39 +++----------------------- core/{file_loader.go => networking.go} | 6 +++- pullanusbot.go | 3 +- use_cases/image_flow.go | 11 +++++--- 7 files changed, 62 insertions(+), 53 deletions(-) create mode 100644 api/helpers.go rename core/{file_loader.go => networking.go} (64%) diff --git a/api/helpers.go b/api/helpers.go new file mode 100644 index 0000000..91cdcae --- /dev/null +++ b/api/helpers.go @@ -0,0 +1,14 @@ +package api + +import "math/rand" + +var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") + +// RandStringRunes returns a random n-length string +func RandStringRunes(n int) string { + b := make([]rune, n) + for i := range b { + b[i] = letterRunes[rand.Intn(len(letterRunes))] + } + return string(b) +} diff --git a/api/telebot.go b/api/telebot.go index 9b143e6..196e71d 100644 --- a/api/telebot.go +++ b/api/telebot.go @@ -79,18 +79,7 @@ func CreateTelebot(token string, logger core.ILogger) *Telebot { bot.Handle(tb.OnPhoto, func(m *tb.Message) { - image := core.CreateImage(m.Photo.FileID, func() (string, error) { - path := path.Join(os.TempDir(), m.Photo.File.UniqueID+".jpg") - err := bot.Download(&m.Photo.File, path) - if err != nil { - logger.Error(err) - return "", err - } - - logger.Infof("image %s downloaded to %s", m.Photo.UniqueID, path) - return path, nil - }) - defer image.Dispose() + image := core.CreateImage(m.Photo.FileID, m.Photo.FileURL) for _, h := range telebot.imageHandlers { h.HandleImage(&image, makeMessage(m), &TelebotAdapter{m, telebot}) @@ -100,6 +89,22 @@ func CreateTelebot(token string, logger core.ILogger) *Telebot { return telebot } +func (t *Telebot) Download(image *core.Image) (*core.File, error) { + //TODO: potential race condition + file := tb.FromURL(image.FileURL) + file.FileID = image.ID + name := RandStringRunes(4) + ".jpg" + path := path.Join(os.TempDir(), name) + err := t.bot.Download(&file, path) + if err != nil { + t.logger.Error(err) + return nil, err + } + + t.logger.Infof("image %s downloaded to %s", file.UniqueID, path) + return makeFile(name, path), nil +} + func (t *Telebot) AddHandler(handlers ...interface{}) { switch h := handlers[0].(type) { case core.IDocumentHandler: @@ -144,3 +149,10 @@ func makeMessage(m *tb.Message) *core.Message { Text: m.Text, } } + +func makeFile(name string, path string) *core.File { + return &core.File{ + Name: name, + Path: path, + } +} diff --git a/core/file.go b/core/file.go index c760b51..e249695 100644 --- a/core/file.go +++ b/core/file.go @@ -1,6 +1,12 @@ package core +import "os" + type File struct { Name string Path string } + +func (f *File) Dispose() error { + return os.Remove(f.Path) +} diff --git a/core/image.go b/core/image.go index aaf7d72..24e9f5e 100644 --- a/core/image.go +++ b/core/image.go @@ -1,42 +1,11 @@ package core -import ( - "os" - "path" - "sync" -) - -func CreateImage(id string, dl func() (string, error)) Image { - return Image{ID: id, dl: dl} +func CreateImage(id string, fileURL string) Image { + return Image{ID: id, FileURL: fileURL} } type Image struct { File - ID string - - dl func() (string, error) - mutex sync.Mutex -} - -func (i *Image) Download() error { - if _, err := os.Stat(i.File.Path); err == nil { - return nil - } - - i.mutex.Lock() - defer i.mutex.Unlock() - - filepath, err := i.dl() - if err != nil { - return nil - } - - i.File.Name = path.Base(filepath) - i.File.Path = filepath - - return nil -} - -func (i *Image) Dispose() error { - return os.Remove(i.File.Path) + ID string + FileURL string } diff --git a/core/file_loader.go b/core/networking.go similarity index 64% rename from core/file_loader.go rename to core/networking.go index 2b94457..d4b957d 100644 --- a/core/file_loader.go +++ b/core/networking.go @@ -6,4 +6,8 @@ type IFileDownloader interface { type IFileUploader interface { Upload(*File) (URL, error) -} \ No newline at end of file +} + +type IImageDownloader interface { + Download(image *Image) (*File, error) +} diff --git a/pullanusbot.go b/pullanusbot.go index 7d46e32..12b9ba3 100644 --- a/pullanusbot.go +++ b/pullanusbot.go @@ -39,7 +39,8 @@ func main() { telebot.AddHandler(link_flow) file_uploader := api.CreateTelegraphAPI() - image_flow := use_cases.CreateImageFlow(logger, file_uploader) + //TODO: image_downloader := api.CreateTelebotImageDownloader() + image_flow := use_cases.CreateImageFlow(logger, file_uploader, telebot) telebot.AddHandler(image_flow) publisher_flow := use_cases.CreatePublisherFlow(logger) diff --git a/use_cases/image_flow.go b/use_cases/image_flow.go index 34bab40..e581170 100644 --- a/use_cases/image_flow.go +++ b/use_cases/image_flow.go @@ -2,13 +2,14 @@ package use_cases import "github.com/ailinykh/pullanusbot/v2/core" -func CreateImageFlow(l core.ILogger, fu core.IFileUploader) *ImageFlow { - return &ImageFlow{l, fu} +func CreateImageFlow(l core.ILogger, fu core.IFileUploader, id core.IImageDownloader) *ImageFlow { + return &ImageFlow{l, fu, id} } type ImageFlow struct { l core.ILogger fu core.IFileUploader + id core.IImageDownloader } // IImageHandler @@ -17,12 +18,14 @@ func (f *ImageFlow) HandleImage(image *core.Image, message *core.Message, bot co return nil } - err := image.Download() + file, err := f.id.Download(image) if err != nil { return err } + //TODO: memory management + defer file.Dispose() - url, err := f.fu.Upload(&image.File) + url, err := f.fu.Upload(file) if err != nil { return err } From a63a613a87ca847953c69822ab1493c0ee1cc7b9 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Tue, 15 Jun 2021 22:30:45 +0300 Subject: [PATCH 033/439] Some logs added --- api/telebot_game.go | 1 + 1 file changed, 1 insertion(+) diff --git a/api/telebot_game.go b/api/telebot_game.go index 844898f..6c9729d 100644 --- a/api/telebot_game.go +++ b/api/telebot_game.go @@ -29,6 +29,7 @@ func (t *Telebot) SetupGame(g use_cases.GameFlow) { mutex.Lock() defer mutex.Unlock() + t.logger.Infof("playing game in chat %d", m.Chat.ID) messages := g.Play(makeUser(m), makeStorage(m, t)) if len(messages) > 1 { for _, msg := range messages { From 9f77c800ca74a53bf030d4ecb9d129b318276c21 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Tue, 15 Jun 2021 22:40:52 +0300 Subject: [PATCH 034/439] README.md --- README.md | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ Readme.MD | 12 ------------ 2 files changed, 54 insertions(+), 12 deletions(-) create mode 100644 README.md delete mode 100644 Readme.MD diff --git a/README.md b/README.md new file mode 100644 index 0000000..a197c9d --- /dev/null +++ b/README.md @@ -0,0 +1,54 @@ +# pullanusbot + +[![Build Status](https://github.com/ailinykh/pullanusbot/workflows/build/badge.svg)](https://github.com/ailinykh/pullanusbot/actions?query=workflow%3Abuild) +[![Go Report Card](https://goreportcard.com/badge/github.com/ailinykh/pullanusbot)](https://goreportcard.com/report/github.com/ailinykh/pullanusbot) +![GitHub](https://img.shields.io/github/license/ailinykh/pullanusbot.svg) + +This bot helps your telegram chat to consume content in more native way + +Let's say somebody sends a link to the webm video: + +![bot-webm-video](https://user-images.githubusercontent.com/939390/95298451-c7757100-0884-11eb-9140-4c6474959720.gif) + +Or a video file as a document: + +![bot-video-convert](https://user-images.githubusercontent.com/939390/95298623-07d4ef00-0885-11eb-92e4-b3c2015f7ecc.gif) + +It's even support links to twitter videos + +![bot-twitter-video](https://user-images.githubusercontent.com/939390/95298730-3783f700-0885-11eb-9650-b0c04e40aa2f.gif) + +... and images! + +![bot-twitter-images](https://user-images.githubusercontent.com/939390/95298790-4cf92100-0885-11eb-8bb2-8adbc91f5b23.gif) + +## how to run + +Setup environment + +```shell +brew install go ffmpeg youtube-dl +``` +clone repo + +```shell +git clone https://github.com/ailinykh/pullanusbot.git +cd pullanusbot +``` + +install go dependencies +```shell +go mod download +``` +obtain bot token from [@BotFather](https://t.me/BotFather) and your telegram ID via `/info` command from [@pullanusbot](https://t.me/pullanusbot) + +```shell +echo "export BOT_TOKEN:=12345678:XXXXXXXXxxxxxxxxXXXXXXXXxxxxxxxxXXX" > .env +echo "export ADMIN_CHAT_ID:=123456789" >> .env +``` + +and run! + +```shell +make +``` diff --git a/Readme.MD b/Readme.MD deleted file mode 100644 index 0381ac2..0000000 --- a/Readme.MD +++ /dev/null @@ -1,12 +0,0 @@ -# pullanusbot - -## TODO: - -- [ ] Setup environment -- [ ] Write tests -- [ ] Implement `/pidorules` command -- [ ] Implement `/pidoreg` command -- [ ] Implement `/pidorstats` command -- [ ] Implement `/pidorall` command -- [ ] Implement `/pidorme` command -- [ ] Setup autodeploy \ No newline at end of file From 4c03b48b0c0e55d4cc305d23b953567faf27bd93 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Tue, 15 Jun 2021 23:10:25 +0300 Subject: [PATCH 035/439] Extendend migration and lazy database connection init --- infrastructure/game_storage.go | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/infrastructure/game_storage.go b/infrastructure/game_storage.go index e16ec87..503c570 100644 --- a/infrastructure/game_storage.go +++ b/infrastructure/game_storage.go @@ -10,15 +10,29 @@ import ( "gorm.io/gorm/logger" ) +var conn *gorm.DB + func CreateGameStorage(gameID int64, factory IPlayerFactory) GameStorage { - dbFile := path.Join(".", "pullanusbot.db") - conn, err := gorm.Open(sqlite.Open(dbFile+"?cache=shared"), &gorm.Config{ - Logger: logger.Default.LogMode(logger.Error), - }) - if err != nil { - log.Fatal(err) + if conn == nil { + dbFile := path.Join(".", "pullanusbot.db") + var err error + conn, err = gorm.Open(sqlite.Open(dbFile+"?cache=shared"), &gorm.Config{ + Logger: logger.Default.LogMode(logger.Error), + }) + if err != nil { + log.Fatal(err) + } + + if conn.Migrator().HasTable(&Player{}) && conn.Migrator().HasColumn(&Player{}, "chat_id") { + log.Println("Extendend migration") + conn.Migrator().RenameColumn(&Player{}, "chat_id", "game_id") + conn.Migrator().RenameTable("faggot_entries", "faggot_rounds") + conn.Migrator().RenameColumn(&Round{}, "chat_id", "game_id") + } else { + log.Println("Default migration") + conn.AutoMigrate(&Player{}, &Round{}) + } } - conn.AutoMigrate(&Player{}, &Round{}) s := GameStorage{conn, gameID, factory} return s From 409debf9d848fbefc68cb7625c89fec8dc15ab99 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Tue, 15 Jun 2021 23:19:45 +0300 Subject: [PATCH 036/439] Setup CI --- .circleci/config.yml | 43 ++++++++++++++++++++++++++ .github/workflows/build.yml | 61 +++++++++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+) create mode 100644 .circleci/config.yml create mode 100644 .github/workflows/build.yml diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..df0a568 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,43 @@ +version: 2 +jobs: + build: + branches: + only: master + docker: + - image: circleci/golang + environment: + - DEP_VERSION: 0.5.0 + - IMAGE_NAME: pullanusbot + working_directory: /go/src/github.com/ailinykh/pullanusbot + steps: + - checkout + - run: + name: Setup environment + command: | + if [ ! -d vendor ]; then + curl -L -s https://github.com/golang/dep/releases/download/v${DEP_VERSION}/dep-linux-amd64 -o /go/bin/dep + chmod +x /go/bin/dep + /go/bin/dep ensure + fi + echo 'export TAG=0.1.${CIRCLE_BUILD_NUM}' >> $BASH_ENV + - run: + name: Run tests + command: go test -race -v -coverprofile=coverage.txt -covermode=atomic + - run: + name: Push coverage results + command: bash <(curl -s https://codecov.io/bash) + - setup_remote_docker: + docker_layer_caching: true + - run: + name: Build docker image + command: | + docker build -t ailinykh/$IMAGE_NAME:$TAG . + echo $DOCKER_PWD | docker login -u $DOCKER_LOGIN --password-stdin + docker push ailinykh/$IMAGE_NAME:$TAG + - run: + name: Deploy app to Digital Ocean Server via Docker + command: ssh -o StrictHostKeyChecking=no root@proxy.ailinykh.com "/bin/bash ./pullanusbot/deploy_app.sh $TAG" + - save_cache: + key: gopkg-{{ .Branch }}-{{ checksum "Gopkg.lock" }} + paths: + - vendor \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..aa9e617 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,61 @@ +name: build +on: + push: + branches: + - master + +jobs: + build: + name: Build + runs-on: ubuntu-latest + env: + ACTIONS_ALLOW_UNSECURE_COMMANDS: true + steps: + + - name: Set up Go 1.15 + uses: actions/setup-go@v1 + with: + go-version: 1.15 + id: go + + - name: Check out code into the Go module directory + uses: actions/checkout@v1 + + - name: Git LFS setup + run: git lfs pull + + - name: Build + run: make build + + - name: Test + run: make test + + - name: Coverage + run: bash <(curl -s https://codecov.io/bash) + + + - name: Generate build number + uses: einaregilsson/build-number@v2 + with: + token: ${{secrets.github_token}} + + - name: Setup tag + run: echo "::set-env name=TAG::0.5.$BUILD_NUMBER" + + - name: Build docker container + run: | + docker build -t ${{ github.repository }}:$TAG . + echo $DOCKER_PWD | docker login -u $DOCKER_LOGIN --password-stdin + docker push ${{ github.repository }}:$TAG + env: + DOCKER_LOGIN: ${{ secrets.DOCKER_LOGIN }} + DOCKER_PWD: ${{ secrets.DOCKER_PWD }} + + - name: Deploy app + run: | + echo "$SSH_IDENTITY_KEY" > identity + chmod 600 identity + ssh -i identity -o StrictHostKeyChecking=no root@proxy.ailinykh.com "/bin/bash ./docker/pullanusbot/deploy_app.sh $TAG" + env: + SSH_IDENTITY_KEY: ${{ secrets.SSH_IDENTITY_KEY }} + From 96a6c224fcdeb4d3f94039f04ddc59a8ba9df2f0 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Tue, 15 Jun 2021 23:35:14 +0300 Subject: [PATCH 037/439] Docker setup --- .docker/supervisord.conf | 13 +++++++++++++ .docker/telegram-bot-api | 3 +++ .dockerignore | 1 + Dockerfile | 22 ++++++++++++++++++++++ 4 files changed, 39 insertions(+) create mode 100644 .docker/supervisord.conf create mode 100755 .docker/telegram-bot-api create mode 100644 .dockerignore create mode 100644 Dockerfile diff --git a/.docker/supervisord.conf b/.docker/supervisord.conf new file mode 100644 index 0000000..93fc81c --- /dev/null +++ b/.docker/supervisord.conf @@ -0,0 +1,13 @@ +[supervisord] +nodaemon=true +loglevel=debug + +[program:telegram-bot-api] +command=telegram-bot-api --api-id=%(ENV_API_ID)s --api-hash=%(ENV_API_HASH)s --local + +[program:pullanusbot] +command=pullanusbot + +[program:sshd] +command=/usr/sbin/sshd -D +autorestart=true \ No newline at end of file diff --git a/.docker/telegram-bot-api b/.docker/telegram-bot-api new file mode 100755 index 0000000..4d11db7 --- /dev/null +++ b/.docker/telegram-bot-api @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:38935e47c90cb92299615e8b14116cf6c03d8a61bfe20a73c7645bddde9aca5c +size 32967920 diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..6320cd2 --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +data \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..8c7d7c3 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,22 @@ +FROM golang:stretch as builder +WORKDIR /go/src/github.com/ailinykh/pullanusbot +# cache dependencies first +COPY go.mod go.sum ./ +RUN go mod download +# now build +ADD . . +RUN CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -a -installsuffix cgo -ldflags '-extldflags "-static"' + +FROM jrottenberg/ffmpeg:4.1-alpine +RUN apk update && apk add tzdata python3 supervisor openssh --no-cache && \ + ssh-keygen -f /etc/ssh/ssh_host_rsa_key -N '' -t rsa && \ + ssh-keygen -f /etc/ssh/ssh_host_dsa_key -N '' -t dsa && \ + wget https://yt-dl.org/downloads/latest/youtube-dl -O /usr/local/bin/youtube-dl && chmod a+rx /usr/local/bin/youtube-dl + +COPY --from=builder /go/src/github.com/ailinykh/pullanusbot/pullanusbot /usr/local/bin/pullanusbot +COPY .docker/telegram-bot-api /usr/local/bin/telegram-bot-api +COPY .docker/supervisord.conf /etc/supervisord.conf + +WORKDIR /usr/local/share +VOLUME [ "pullanusbot-data" ] +ENTRYPOINT supervisord -c /etc/supervisord.conf \ No newline at end of file From b93fc25b03a7ecb10c25d37dc6364208bb59fd92 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Tue, 15 Jun 2021 23:50:33 +0300 Subject: [PATCH 038/439] Respect working directory --- pullanusbot.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/pullanusbot.go b/pullanusbot.go index 12b9ba3..0897586 100644 --- a/pullanusbot.go +++ b/pullanusbot.go @@ -3,6 +3,7 @@ package main import ( "math/rand" "os" + "path" "time" "github.com/ailinykh/pullanusbot/v2/api" @@ -51,7 +52,8 @@ func main() { } func createLogger() (core.ILogger, func()) { - lf, err := os.OpenFile("pullanusbot.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0660) + logFilePath := path.Join(getWorkingDir(), "pullanusbot.log") + lf, err := os.OpenFile(logFilePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0660) if err != nil { panic(err) } @@ -63,3 +65,11 @@ func createLogger() (core.ILogger, func()) { } return l, close } + +func getWorkingDir() string { + workingDir := os.Getenv("WORKING_DIR") + if len(workingDir) == 0 { + return "pullanusbot-data" + } + return workingDir +} From d60e69731ba7594c151f0abe55233354c4428dcf Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Tue, 15 Jun 2021 23:58:50 +0300 Subject: [PATCH 039/439] Respect working dir twice --- infrastructure/game_storage.go | 12 +++++++++++- pullanusbot.go | 1 + 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/infrastructure/game_storage.go b/infrastructure/game_storage.go index 503c570..a2a7bda 100644 --- a/infrastructure/game_storage.go +++ b/infrastructure/game_storage.go @@ -2,6 +2,7 @@ package infrastructure import ( "log" + "os" "path" "github.com/ailinykh/pullanusbot/v2/core" @@ -14,7 +15,7 @@ var conn *gorm.DB func CreateGameStorage(gameID int64, factory IPlayerFactory) GameStorage { if conn == nil { - dbFile := path.Join(".", "pullanusbot.db") + dbFile := path.Join(getWorkingDir(), "pullanusbot.db") var err error conn, err = gorm.Open(sqlite.Open(dbFile+"?cache=shared"), &gorm.Config{ Logger: logger.Default.LogMode(logger.Error), @@ -82,3 +83,12 @@ func (s *GameStorage) AddRound(round *core.Round) error { s.conn.Create(&dbRound) return nil } + +//TODO: duplicated code +func getWorkingDir() string { + workingDir := os.Getenv("WORKING_DIR") + if len(workingDir) == 0 { + return "pullanusbot-data" + } + return workingDir +} diff --git a/pullanusbot.go b/pullanusbot.go index 0897586..99ccc88 100644 --- a/pullanusbot.go +++ b/pullanusbot.go @@ -66,6 +66,7 @@ func createLogger() (core.ILogger, func()) { return l, close } +//TODO: duplicated code func getWorkingDir() string { workingDir := os.Getenv("WORKING_DIR") if len(workingDir) == 0 { From 9336357b3f621e8eca14bbe17bc57edc075daa53 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Wed, 16 Jun 2021 00:14:39 +0300 Subject: [PATCH 040/439] Sort game results by username in case of equal scores --- use_cases/faggot_game.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/use_cases/faggot_game.go b/use_cases/faggot_game.go index 80c231c..c9cdd39 100644 --- a/use_cases/faggot_game.go +++ b/use_cases/faggot_game.go @@ -113,6 +113,9 @@ func (flow *GameFlow) Stats(storage core.IGameStorage) string { } sort.Slice(entries, func(i, j int) bool { + if entries[i].Score == entries[j].Score { + return entries[i].Player.Username > entries[j].Player.Username + } return entries[i].Score > entries[j].Score }) From ac486871cc9951cb9a16634c33bcf1db75152751 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Fri, 18 Jun 2021 10:48:14 +0300 Subject: [PATCH 041/439] Delete original message after video processed --- use_cases/link_flow.go | 1 + 1 file changed, 1 insertion(+) diff --git a/use_cases/link_flow.go b/use_cases/link_flow.go index 445a140..ee37131 100644 --- a/use_cases/link_flow.go +++ b/use_cases/link_flow.go @@ -50,6 +50,7 @@ func (lf *LinkFlow) processLink(message *core.Message, bot core.IBot) error { lf.l.Errorf("%s. Fallback to uploading", err) return lf.sendByUploading(media, bot) } + return bot.Delete(message) case "video/webm": vf, err := lf.downloadMedia(media) if err != nil { From 1500309194a34ebcdf3fcd0e998e9d0e905336d9 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Fri, 18 Jun 2021 16:18:27 +0300 Subject: [PATCH 042/439] rename use_cases to usecases to conform go naming convention --- api/telebot_game.go | 4 +-- pullanusbot.go | 32 ++++++++++----------- {use_cases => usecases}/faggot_game.go | 10 ++++++- {use_cases => usecases}/faggot_game_test.go | 2 +- {use_cases => usecases}/faggot_stat.go | 4 ++- {use_cases => usecases}/image_flow.go | 6 ++-- {use_cases => usecases}/link_flow.go | 16 ++++++++--- {use_cases => usecases}/publisher_flow.go | 8 ++++-- {use_cases => usecases}/twitter_flow.go | 6 ++-- {use_cases => usecases}/video_flow.go | 13 +++++---- 10 files changed, 64 insertions(+), 37 deletions(-) rename {use_cases => usecases}/faggot_game.go (93%) rename {use_cases => usecases}/faggot_game_test.go (99%) rename {use_cases => usecases}/faggot_stat.go (71%) rename {use_cases => usecases}/image_flow.go (78%) rename {use_cases => usecases}/link_flow.go (88%) rename {use_cases => usecases}/publisher_flow.go (90%) rename {use_cases => usecases}/twitter_flow.go (95%) rename {use_cases => usecases}/video_flow.go (90%) diff --git a/api/telebot_game.go b/api/telebot_game.go index 6c9729d..eda555c 100644 --- a/api/telebot_game.go +++ b/api/telebot_game.go @@ -7,12 +7,12 @@ import ( "github.com/ailinykh/pullanusbot/v2/core" "github.com/ailinykh/pullanusbot/v2/infrastructure" - "github.com/ailinykh/pullanusbot/v2/use_cases" + "github.com/ailinykh/pullanusbot/v2/usecases" tb "gopkg.in/tucnak/telebot.v2" ) -func (t *Telebot) SetupGame(g use_cases.GameFlow) { +func (t *Telebot) SetupGame(g usecases.GameFlow) { t.bot.Handle("/pidorules", func(m *tb.Message) { text := g.Rules() t.bot.Send(m.Chat, text, &tb.SendOptions{ParseMode: tb.ModeHTML}) diff --git a/pullanusbot.go b/pullanusbot.go index 99ccc88..e13e7a4 100644 --- a/pullanusbot.go +++ b/pullanusbot.go @@ -9,7 +9,7 @@ import ( "github.com/ailinykh/pullanusbot/v2/api" "github.com/ailinykh/pullanusbot/v2/core" "github.com/ailinykh/pullanusbot/v2/infrastructure" - "github.com/ailinykh/pullanusbot/v2/use_cases" + "github.com/ailinykh/pullanusbot/v2/usecases" "github.com/google/logger" ) @@ -22,31 +22,31 @@ func main() { telebot := api.CreateTelebot(os.Getenv("BOT_TOKEN"), logger) localizer := infrastructure.GameLocalizer{} - game := use_cases.CreateGameFlow(localizer) + game := usecases.CreateGameFlow(localizer) telebot.SetupGame(game) telebot.SetupInfo() converter := infrastructure.CreateFfmpegConverter() - video_flow := use_cases.CreateVideoFlow(logger, converter, converter) - telebot.AddHandler(video_flow) + videoFlow := usecases.CreateVideoFlow(logger, converter, converter) + telebot.AddHandler(videoFlow) - file_downloader := infrastructure.CreateFileDownloader() - twitter_api := api.CreateTwitterAPI() - twitter_flow := use_cases.CreateTwitterFlow(logger, twitter_api, file_downloader, converter) - telebot.AddHandler(twitter_flow) + fileDownloader := infrastructure.CreateFileDownloader() + twitterAPI := api.CreateTwitterAPI() + twitterFlow := usecases.CreateTwitterFlow(logger, twitterAPI, fileDownloader, converter) + telebot.AddHandler(twitterFlow) - link_flow := use_cases.CreateLinkFlow(logger, file_downloader, converter, converter) - telebot.AddHandler(link_flow) + linkFlow := usecases.CreateLinkFlow(logger, fileDownloader, converter, converter) + telebot.AddHandler(linkFlow) - file_uploader := api.CreateTelegraphAPI() + fileUploader := api.CreateTelegraphAPI() //TODO: image_downloader := api.CreateTelebotImageDownloader() - image_flow := use_cases.CreateImageFlow(logger, file_uploader, telebot) - telebot.AddHandler(image_flow) + imageFlow := usecases.CreateImageFlow(logger, fileUploader, telebot) + telebot.AddHandler(imageFlow) - publisher_flow := use_cases.CreatePublisherFlow(logger) - telebot.AddHandler(publisher_flow) - telebot.AddHandler("/loh666", publisher_flow) + publisherFlow := usecases.CreatePublisherFlow(logger) + telebot.AddHandler(publisherFlow) + telebot.AddHandler("/loh666", publisherFlow) // Start endless loop telebot.Run() } diff --git a/use_cases/faggot_game.go b/usecases/faggot_game.go similarity index 93% rename from use_cases/faggot_game.go rename to usecases/faggot_game.go index c9cdd39..f84144a 100644 --- a/use_cases/faggot_game.go +++ b/usecases/faggot_game.go @@ -1,4 +1,4 @@ -package use_cases +package usecases import ( "fmt" @@ -11,18 +11,22 @@ import ( "github.com/ailinykh/pullanusbot/v2/core" ) +// CreateGameFlow is a simple GameFlow factory func CreateGameFlow(l core.ILocalizer) GameFlow { return GameFlow{l} } +// GameFlow represents faggot game logic type GameFlow struct { l core.ILocalizer } +// Rules of the game func (flow *GameFlow) Rules() string { return flow.l.I18n("faggot_rules") } +// Add a new player to game func (flow *GameFlow) Add(player *core.User, storage core.IGameStorage) string { players, _ := storage.GetPlayers() for _, p := range players { @@ -40,6 +44,7 @@ func (flow *GameFlow) Add(player *core.User, storage core.IGameStorage) string { return flow.l.I18n("faggot_added_to_game") } +// Play game func (flow *GameFlow) Play(player *core.User, storage core.IGameStorage) []string { players, _ := storage.GetPlayers() switch len(players) { @@ -85,6 +90,7 @@ func (flow *GameFlow) Play(player *core.User, storage core.IGameStorage) []strin return phrases } +// All statistics for all time func (flow *GameFlow) All(storage core.IGameStorage) string { entries, _ := flow.getStat(storage) messages := []string{flow.l.I18n("faggot_all_top"), ""} @@ -96,6 +102,7 @@ func (flow *GameFlow) All(storage core.IGameStorage) string { return strings.Join(messages, "\n") } +// Stats returns current year statistics func (flow *GameFlow) Stats(storage core.IGameStorage) string { year := strconv.Itoa(time.Now().Year()) rounds, _ := storage.GetRounds() @@ -128,6 +135,7 @@ func (flow *GameFlow) Stats(storage core.IGameStorage) string { return strings.Join(messages, "\n") } +// Me returns your personal statistics func (flow *GameFlow) Me(player *core.User, storage core.IGameStorage) string { entries, _ := flow.getStat(storage) score := 0 diff --git a/use_cases/faggot_game_test.go b/usecases/faggot_game_test.go similarity index 99% rename from use_cases/faggot_game_test.go rename to usecases/faggot_game_test.go index 3fec90d..f7b2628 100644 --- a/use_cases/faggot_game_test.go +++ b/usecases/faggot_game_test.go @@ -1,4 +1,4 @@ -package use_cases +package usecases import ( "errors" diff --git a/use_cases/faggot_stat.go b/usecases/faggot_stat.go similarity index 71% rename from use_cases/faggot_stat.go rename to usecases/faggot_stat.go index f15f0ad..7854e09 100644 --- a/use_cases/faggot_stat.go +++ b/usecases/faggot_stat.go @@ -1,14 +1,16 @@ -package use_cases +package usecases import ( "github.com/ailinykh/pullanusbot/v2/core" ) +// Stat represents game statistics type Stat struct { Player *core.User Score int } +// Find player by username in current stat func Find(a []Stat, username string) int { for i, n := range a { if username == n.Player.Username { diff --git a/use_cases/image_flow.go b/usecases/image_flow.go similarity index 78% rename from use_cases/image_flow.go rename to usecases/image_flow.go index e581170..55ef766 100644 --- a/use_cases/image_flow.go +++ b/usecases/image_flow.go @@ -1,18 +1,20 @@ -package use_cases +package usecases import "github.com/ailinykh/pullanusbot/v2/core" +// CreateImageFlow is a basic ImageFlow factory func CreateImageFlow(l core.ILogger, fu core.IFileUploader, id core.IImageDownloader) *ImageFlow { return &ImageFlow{l, fu, id} } +// ImageFlow represents convert image to hotlink logic type ImageFlow struct { l core.ILogger fu core.IFileUploader id core.IImageDownloader } -// IImageHandler +// HandleImage is a core.IImageHandler protocol implementation func (f *ImageFlow) HandleImage(image *core.Image, message *core.Message, bot core.IBot) error { if !message.IsPrivate { return nil diff --git a/use_cases/link_flow.go b/usecases/link_flow.go similarity index 88% rename from use_cases/link_flow.go rename to usecases/link_flow.go index ee37131..1013f4f 100644 --- a/use_cases/link_flow.go +++ b/usecases/link_flow.go @@ -1,4 +1,4 @@ -package use_cases +package usecases import ( "fmt" @@ -10,10 +10,12 @@ import ( "github.com/ailinykh/pullanusbot/v2/core" ) +// CreateLinkFlow is a basic LinkFlow factory func CreateLinkFlow(l core.ILogger, fd core.IFileDownloader, vff core.IVideoFileFactory, vfc core.IVideoFileConverter) *LinkFlow { return &LinkFlow{l, fd, vff, vfc} } +// LinkFlow represents convert hotlink to video file logic type LinkFlow struct { l core.ILogger fd core.IFileDownloader @@ -21,7 +23,7 @@ type LinkFlow struct { vfc core.IVideoFileConverter } -// core.ITextHandler +// HandleText is a core.ITextHandler protocol implementation func (lf *LinkFlow) HandleText(message *core.Message, bot core.IBot) error { r := regexp.MustCompile(`^http(\S+)$`) if r.MatchString(message.Text) { @@ -48,7 +50,10 @@ func (lf *LinkFlow) processLink(message *core.Message, bot core.IBot) error { if err != nil { lf.l.Errorf("%s. Fallback to uploading", err) - return lf.sendByUploading(media, bot) + err := lf.sendByUploading(media, bot) + if err != nil { + return err + } } return bot.Delete(message) case "video/webm": @@ -66,7 +71,10 @@ func (lf *LinkFlow) processLink(message *core.Message, bot core.IBot) error { defer vfc.Dispose() _, err = bot.SendVideoFile(vfc, media.Caption) - return err + if err != nil { + return err + } + return bot.Delete(message) case "text/html; charset=utf-8": default: lf.l.Warningf("Unsupported content type: %s", resp.Header["Content-Type"]) diff --git a/use_cases/publisher_flow.go b/usecases/publisher_flow.go similarity index 90% rename from use_cases/publisher_flow.go rename to usecases/publisher_flow.go index 3a1ecde..61cd9e8 100644 --- a/use_cases/publisher_flow.go +++ b/usecases/publisher_flow.go @@ -1,4 +1,4 @@ -package use_cases +package usecases import ( "os" @@ -8,6 +8,7 @@ import ( "github.com/ailinykh/pullanusbot/v2/core" ) +// CreatePublisherFlow is a basic PublisherFlow factory func CreatePublisherFlow(l core.ILogger) *PublisherFlow { chatID, err := strconv.ParseInt(os.Getenv("PUBLISER_CHAT_ID"), 10, 64) if err != nil { @@ -28,6 +29,7 @@ func CreatePublisherFlow(l core.ILogger) *PublisherFlow { return &publisher } +// PublisherFlow represents last sent image keeper logic type PublisherFlow struct { l core.ILogger @@ -47,7 +49,7 @@ type msgSource struct { bot core.IBot } -// core.IImageHandler +// HandleImage is a core.IImageHandler protocol implementation func (p *PublisherFlow) HandleImage(image *core.Image, message *core.Message, bot core.IBot) error { if message.ChatID == p.chatID && message.Sender.Username == p.username { p.imageChan <- imgSource{image.ID, bot} @@ -56,7 +58,7 @@ func (p *PublisherFlow) HandleImage(image *core.Image, message *core.Message, bo return nil } -// core.ICommandHandler +// HandleCommand is a core.ICommandHandler protocol implementation func (p *PublisherFlow) HandleCommand(message *core.Message, bot core.IBot) error { if message.ChatID == p.chatID { p.requestChan <- msgSource{*message, bot} diff --git a/use_cases/twitter_flow.go b/usecases/twitter_flow.go similarity index 95% rename from use_cases/twitter_flow.go rename to usecases/twitter_flow.go index b1880a2..79163e5 100644 --- a/use_cases/twitter_flow.go +++ b/usecases/twitter_flow.go @@ -1,4 +1,4 @@ -package use_cases +package usecases import ( "errors" @@ -13,10 +13,12 @@ import ( "github.com/ailinykh/pullanusbot/v2/core" ) +// CreateTwitterFlow is a basic TwitterFlow factory func CreateTwitterFlow(l core.ILogger, mf core.IMediaFactory, fd core.IFileDownloader, vff core.IVideoFileFactory) *TwitterFlow { return &TwitterFlow{l, mf, fd, vff, make(map[core.Message]core.Message)} } +// TwitterFlow represents tweet processing logic type TwitterFlow struct { l core.ILogger mf core.IMediaFactory @@ -25,7 +27,7 @@ type TwitterFlow struct { timeoutReplies map[core.Message]core.Message } -// core.ITextHandler +// HandleText is a core.ITextHandler protocol implementation func (tf *TwitterFlow) HandleText(message *core.Message, bot core.IBot) error { r := regexp.MustCompile(`twitter\.com.+/(\d+)\S*$`) match := r.FindStringSubmatch(message.Text) diff --git a/use_cases/video_flow.go b/usecases/video_flow.go similarity index 90% rename from use_cases/video_flow.go rename to usecases/video_flow.go index 1342ac9..36dc508 100644 --- a/use_cases/video_flow.go +++ b/usecases/video_flow.go @@ -1,4 +1,4 @@ -package use_cases +package usecases import ( "fmt" @@ -8,16 +8,19 @@ import ( "github.com/ailinykh/pullanusbot/v2/core" ) +// CreateVideoFlow is a basic VideoFlow factory +func CreateVideoFlow(l core.ILogger, f core.IVideoFileFactory, c core.IVideoFileConverter) *VideoFlow { + return &VideoFlow{c, f, l} +} + +// VideoFlow represents convert file to video logic type VideoFlow struct { c core.IVideoFileConverter f core.IVideoFileFactory l core.ILogger } -func CreateVideoFlow(l core.ILogger, f core.IVideoFileFactory, c core.IVideoFileConverter) *VideoFlow { - return &VideoFlow{c, f, l} -} - +// HandleDocument is a core.IDocumentHandler protocol implementation func (f *VideoFlow) HandleDocument(document *core.Document, b core.IBot) error { vf, err := f.f.CreateVideoFile(document.FilePath) if err != nil { From 4dc7325541c0daaa67c99b9c170c74e57d72409d Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Fri, 18 Jun 2021 16:38:26 +0300 Subject: [PATCH 043/439] core package updated for golint --- core/bot.go | 1 + core/document.go | 1 + core/file.go | 2 ++ core/game_storage.go | 1 + core/handlers.go | 4 ++++ core/image.go | 2 ++ core/localizer.go | 1 + core/logger.go | 1 + core/media.go | 5 +++++ core/media_loader.go | 2 ++ core/message.go | 1 + core/networking.go | 3 +++ core/round.go | 1 + core/user.go | 1 + core/video_file.go | 2 ++ core/video_file_converter.go | 1 + core/video_file_factory.go | 1 + 17 files changed, 30 insertions(+) diff --git a/core/bot.go b/core/bot.go index 8a10332..f1e84af 100644 --- a/core/bot.go +++ b/core/bot.go @@ -1,5 +1,6 @@ package core +// IBot represents abstract bot interface type IBot interface { Delete(*Message) error SendText(string, ...interface{}) (*Message, error) diff --git a/core/document.go b/core/document.go index df882b2..36eb72c 100644 --- a/core/document.go +++ b/core/document.go @@ -1,5 +1,6 @@ package core +// Document ... type Document struct { Author string FileName string diff --git a/core/file.go b/core/file.go index e249695..8b93e11 100644 --- a/core/file.go +++ b/core/file.go @@ -2,11 +2,13 @@ package core import "os" +// File ... type File struct { Name string Path string } +// Dispose for filesystem cleanup func (f *File) Dispose() error { return os.Remove(f.Path) } diff --git a/core/game_storage.go b/core/game_storage.go index 02c3631..12ddead 100644 --- a/core/game_storage.go +++ b/core/game_storage.go @@ -1,5 +1,6 @@ package core +// IGameStorage is an abstract interface for game players and results handling type IGameStorage interface { GetPlayers() ([]*User, error) GetRounds() ([]*Round, error) diff --git a/core/handlers.go b/core/handlers.go index d231c50..a66b47a 100644 --- a/core/handlers.go +++ b/core/handlers.go @@ -1,17 +1,21 @@ package core +// ICommandHandler responds to text messages started from forwardslash type ICommandHandler interface { HandleCommand(*Message, IBot) error } +// IDocumentHandler responds to documents sent in chah type IDocumentHandler interface { HandleDocument(*Document, IBot) error } +// ITextHandler responds to all the text messages type ITextHandler interface { HandleText(*Message, IBot) error } +// IImageHandler responds to images type IImageHandler interface { HandleImage(*Image, *Message, IBot) error } diff --git a/core/image.go b/core/image.go index 24e9f5e..f3b79d1 100644 --- a/core/image.go +++ b/core/image.go @@ -1,9 +1,11 @@ package core +// CreateImage is an Image factory func CreateImage(id string, fileURL string) Image { return Image{ID: id, FileURL: fileURL} } +// Image represents remote image file that can be also downloaded type Image struct { File ID string diff --git a/core/localizer.go b/core/localizer.go index aa2facd..f1e8508 100644 --- a/core/localizer.go +++ b/core/localizer.go @@ -1,5 +1,6 @@ package core +// ILocalizer for localization type ILocalizer interface { I18n(string, ...interface{}) string AllKeys() []string diff --git a/core/logger.go b/core/logger.go index ea4abc9..79b6344 100644 --- a/core/logger.go +++ b/core/logger.go @@ -1,5 +1,6 @@ package core +// ILogger for logging type ILogger interface { Error(...interface{}) Errorf(string, ...interface{}) diff --git a/core/media.go b/core/media.go index e15a2f8..5e4a625 100644 --- a/core/media.go +++ b/core/media.go @@ -1,13 +1,18 @@ package core +// MediaType ... type MediaType int const ( + // Video media type Video MediaType = iota + // Photo media type Photo + // Text media type Text ) +// Media ... type Media struct { URL string Caption string diff --git a/core/media_loader.go b/core/media_loader.go index 282ba5f..adfa072 100644 --- a/core/media_loader.go +++ b/core/media_loader.go @@ -1,7 +1,9 @@ package core +// URL ... type URL = string +// IMediaFactory creates Media from URL type IMediaFactory interface { CreateMedia(URL, *User) ([]*Media, error) } diff --git a/core/message.go b/core/message.go index b0b0242..5895a89 100644 --- a/core/message.go +++ b/core/message.go @@ -1,5 +1,6 @@ package core +// Message from chat type Message struct { ID int ChatID int64 diff --git a/core/networking.go b/core/networking.go index d4b957d..469d31f 100644 --- a/core/networking.go +++ b/core/networking.go @@ -1,13 +1,16 @@ package core +// IFileDownloader turns URL to File type IFileDownloader interface { Download(URL) (*File, error) } +// IFileUploader turns File to URL type IFileUploader interface { Upload(*File) (URL, error) } +// IImageDownloader download Image to disk type IImageDownloader interface { Download(image *Image) (*File, error) } diff --git a/core/round.go b/core/round.go index 2a36e5f..0e852b9 100644 --- a/core/round.go +++ b/core/round.go @@ -1,5 +1,6 @@ package core +// Round is a single game result type Round struct { Day string Winner *User diff --git a/core/user.go b/core/user.go index aa2164b..f624554 100644 --- a/core/user.go +++ b/core/user.go @@ -1,5 +1,6 @@ package core +// User ... type User struct { Username string } diff --git a/core/video_file.go b/core/video_file.go index f9a70e7..ab67bfb 100644 --- a/core/video_file.go +++ b/core/video_file.go @@ -2,6 +2,7 @@ package core import "os" +// VideoFile ... type VideoFile struct { File Width int @@ -12,6 +13,7 @@ type VideoFile struct { ThumbPath string } +// Dispose to cleanup filesystem func (vf *VideoFile) Dispose() { os.Remove(vf.Path) os.Remove(vf.ThumbPath) diff --git a/core/video_file_converter.go b/core/video_file_converter.go index 6124d6f..b4921ed 100644 --- a/core/video_file_converter.go +++ b/core/video_file_converter.go @@ -1,5 +1,6 @@ package core +// IVideoFileConverter convert VideoFile with specified bitrate type IVideoFileConverter interface { Convert(*VideoFile, int) (*VideoFile, error) } diff --git a/core/video_file_factory.go b/core/video_file_factory.go index 16f3a18..c645cf0 100644 --- a/core/video_file_factory.go +++ b/core/video_file_factory.go @@ -1,5 +1,6 @@ package core +// IVideoFileFactory retreives video file parameters from file on disk type IVideoFileFactory interface { CreateVideoFile(path string) (*VideoFile, error) } From c4841b4b80c69b10b123b128f451410b7f1e3d30 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Fri, 18 Jun 2021 16:48:30 +0300 Subject: [PATCH 044/439] infrastructure fixed for golint --- infrastructure/ffmpeg_converter.go | 5 ++++- infrastructure/file_downloader.go | 4 +++- infrastructure/game_localizer.go | 3 +++ infrastructure/game_storage.go | 10 ++++++++-- infrastructure/player.go | 2 ++ infrastructure/player_factory.go | 1 + infrastructure/round.go | 2 ++ 7 files changed, 23 insertions(+), 4 deletions(-) diff --git a/infrastructure/ffmpeg_converter.go b/infrastructure/ffmpeg_converter.go index 3f09ead..1eb2d8c 100644 --- a/infrastructure/ffmpeg_converter.go +++ b/infrastructure/ffmpeg_converter.go @@ -12,13 +12,15 @@ import ( "github.com/ailinykh/pullanusbot/v2/core" ) +// CreateFfmpegConverter is a basic FfmpegConverter factory func CreateFfmpegConverter() *FfmpegConverter { return &FfmpegConverter{} } +// FfmpegConverter implements core.IVideoFileConverter and core.IVideoFileFactory using ffmpeg type FfmpegConverter struct{} -// core.IVideoFileConverter +// Convert is a core.IVideoFileConverter interface implementation func (c *FfmpegConverter) Convert(vf *core.VideoFile, bitrate int) (*core.VideoFile, error) { convertedVideoFilePath := path.Join(os.TempDir(), vf.Name+"_converted.mp4") cmd := fmt.Sprintf(`ffmpeg -y -i "%s" -pix_fmt yuv420p -vf "scale=trunc(iw/2)*2:trunc(ih/2)*2" "%s"`, vf.Path, convertedVideoFilePath) @@ -34,6 +36,7 @@ func (c *FfmpegConverter) Convert(vf *core.VideoFile, bitrate int) (*core.VideoF return c.CreateVideoFile(convertedVideoFilePath) } +// CreateVideoFile is a core.IVideoFileFactory interface implementation func (c *FfmpegConverter) CreateVideoFile(path string) (*core.VideoFile, error) { ffprobe, err := c.getFFProbe(path) if err != nil { diff --git a/infrastructure/file_downloader.go b/infrastructure/file_downloader.go index 153bdd2..b50831a 100644 --- a/infrastructure/file_downloader.go +++ b/infrastructure/file_downloader.go @@ -9,13 +9,15 @@ import ( "github.com/ailinykh/pullanusbot/v2/core" ) +// CreateFileDownloader is a default FileDownloader factory func CreateFileDownloader() *FileDownloader { return &FileDownloader{} } +// FileDownloader is a default implementation for core.IFileDownloader type FileDownloader struct{} -// core.IFileDownloader +// Download is a core.IFileDownloader interface implementation func (FileDownloader) Download(url core.URL) (*core.File, error) { name := path.Base(url) path := path.Join(os.TempDir(), name) diff --git a/infrastructure/game_localizer.go b/infrastructure/game_localizer.go index 121ec57..9774023 100644 --- a/infrastructure/game_localizer.go +++ b/infrastructure/game_localizer.go @@ -5,6 +5,7 @@ import ( "runtime" ) +// GameLocalizer for faggot game type GameLocalizer struct{} var ru = map[string]string{ @@ -89,6 +90,7 @@ var ru = map[string]string{ "faggot_me": "%s, ты был(а) пидором дня — %d раз!", } +// I18n is a core.ILocalizer implementation func (l GameLocalizer) I18n(key string, args ...interface{}) string { if val, ok := ru[key]; ok { @@ -99,6 +101,7 @@ func (l GameLocalizer) I18n(key string, args ...interface{}) string { return fmt.Sprintf("%s:%d KEY_MISSED:\"%s\"", file, line, key) } +// AllKeys is a core.ILocalizer implementation func (l GameLocalizer) AllKeys() []string { keys := make([]string, 0, len(ru)) for k := range ru { diff --git a/infrastructure/game_storage.go b/infrastructure/game_storage.go index a2a7bda..8d7262e 100644 --- a/infrastructure/game_storage.go +++ b/infrastructure/game_storage.go @@ -13,6 +13,7 @@ import ( var conn *gorm.DB +// CreateGameStorage is a default GameStorage factory func CreateGameStorage(gameID int64, factory IPlayerFactory) GameStorage { if conn == nil { dbFile := path.Join(getWorkingDir(), "pullanusbot.db") @@ -39,22 +40,25 @@ func CreateGameStorage(gameID int64, factory IPlayerFactory) GameStorage { return s } +// GameStorage implements core.IGameStorage interface type GameStorage struct { conn *gorm.DB gameID int64 playerFactory IPlayerFactory } -func (db *GameStorage) GetPlayers() ([]*core.User, error) { +// GetPlayers is a core.IGameStorage interface implementation +func (s *GameStorage) GetPlayers() ([]*core.User, error) { var dbPlayers []Player var corePlayers []*core.User - db.conn.Where("game_id = ?", db.gameID).Find(&dbPlayers) + s.conn.Where("game_id = ?", s.gameID).Find(&dbPlayers) for _, p := range dbPlayers { corePlayers = append(corePlayers, &core.User{Username: p.Username}) } return corePlayers, nil } +// GetRounds is a core.IGameStorage interface implementation func (s *GameStorage) GetRounds() ([]*core.Round, error) { var dbRounds []Round var coreRounds []*core.Round @@ -66,12 +70,14 @@ func (s *GameStorage) GetRounds() ([]*core.Round, error) { return coreRounds, nil } +// AddPlayer is a core.IGameStorage interface implementation func (s *GameStorage) AddPlayer(player *core.User) error { dbPlayer := s.playerFactory.CreatePlayer(player.Username) s.conn.Create(&dbPlayer) return nil } +// AddRound is a core.IGameStorage interface implementation func (s *GameStorage) AddRound(round *core.Round) error { player := s.playerFactory.CreatePlayer(round.Winner.Username) dbRound := Round{ diff --git a/infrastructure/player.go b/infrastructure/player.go index 80c62e0..87f4b4b 100644 --- a/infrastructure/player.go +++ b/infrastructure/player.go @@ -1,5 +1,6 @@ package infrastructure +// Player that can be persistent on disk type Player struct { GameID int64 `gorm:"primaryKey"` UserID int `gorm:"primaryKey"` @@ -9,6 +10,7 @@ type Player struct { LanguageCode string } +// TableName gorm API func (Player) TableName() string { return "faggot_players" } diff --git a/infrastructure/player_factory.go b/infrastructure/player_factory.go index c9fe6b7..e08ae14 100644 --- a/infrastructure/player_factory.go +++ b/infrastructure/player_factory.go @@ -1,5 +1,6 @@ package infrastructure +// IPlayerFactory creates infrastructure Player representation type IPlayerFactory interface { CreatePlayer(string) Player } diff --git a/infrastructure/round.go b/infrastructure/round.go index d69d934..8e484a4 100644 --- a/infrastructure/round.go +++ b/infrastructure/round.go @@ -1,5 +1,6 @@ package infrastructure +// Round that can be persistent on disk type Round struct { GameID int64 UserID int @@ -7,6 +8,7 @@ type Round struct { Username string } +// TableName gorm API func (Round) TableName() string { return "faggot_rounds" } From f7e494074ffe5b1cd29924da687e7c401d42cabf Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Fri, 18 Jun 2021 17:03:21 +0300 Subject: [PATCH 045/439] api fixes for golint --- api/telebot.go | 11 ++++++++--- api/telebot_adapter.go | 9 +++++++++ api/telebot_game.go | 1 + api/telebot_info.go | 1 + api/telegraph.go | 4 +++- api/tweet.go | 8 ++++++++ api/twitter.go | 6 ++++-- api/video_file_handler.go | 1 + 8 files changed, 35 insertions(+), 6 deletions(-) diff --git a/api/telebot.go b/api/telebot.go index 196e71d..b9aa401 100644 --- a/api/telebot.go +++ b/api/telebot.go @@ -11,6 +11,7 @@ import ( tb "gopkg.in/tucnak/telebot.v2" ) +// Telebot is a telegram API type Telebot struct { bot *tb.Bot logger core.ILogger @@ -20,6 +21,7 @@ type Telebot struct { imageHandlers []core.IImageHandler } +// CreateTelebot is a default Telebot factory func CreateTelebot(token string, logger core.ILogger) *Telebot { poller := tb.NewMiddlewarePoller(&tb.LongPoller{Timeout: 10 * time.Second}, func(upd *tb.Update) bool { return true @@ -89,6 +91,7 @@ func CreateTelebot(token string, logger core.ILogger) *Telebot { return telebot } +// Download is a core.IImageDownloader interface implementation func (t *Telebot) Download(image *core.Image) (*core.File, error) { //TODO: potential race condition file := tb.FromURL(image.FileURL) @@ -105,8 +108,9 @@ func (t *Telebot) Download(image *core.Image) (*core.File, error) { return makeFile(name, path), nil } -func (t *Telebot) AddHandler(handlers ...interface{}) { - switch h := handlers[0].(type) { +// AddHandler register object as one of core.Handler's +func (t *Telebot) AddHandler(handler ...interface{}) { + switch h := handler[0].(type) { case core.IDocumentHandler: t.documentHandlers = append(t.documentHandlers, h) case core.ITextHandler: @@ -115,7 +119,7 @@ func (t *Telebot) AddHandler(handlers ...interface{}) { t.imageHandlers = append(t.imageHandlers, h) case string: t.registerCommand(h) - if handler, ok := handlers[1].(core.ICommandHandler); ok { + if handler, ok := handler[1].(core.ICommandHandler); ok { t.bot.Handle(h, func(m *tb.Message) { handler.HandleCommand(makeMessage(m), &TelebotAdapter{m, t}) }) @@ -127,6 +131,7 @@ func (t *Telebot) AddHandler(handlers ...interface{}) { } } +// Run bot loop func (t *Telebot) Run() { t.bot.Start() } diff --git a/api/telebot_adapter.go b/api/telebot_adapter.go index 79ab441..d997067 100644 --- a/api/telebot_adapter.go +++ b/api/telebot_adapter.go @@ -6,11 +6,13 @@ import ( tb "gopkg.in/tucnak/telebot.v2" ) +// TelebotAdapter combines Telebot and core.IBot type TelebotAdapter struct { m *tb.Message t *Telebot } +// SendText is a core.IBot interface implementation func (a *TelebotAdapter) SendText(text string, params ...interface{}) (*core.Message, error) { opts := tb.SendOptions{ParseMode: tb.ModeHTML, DisableWebPagePreview: true} for _, param := range params { @@ -25,16 +27,19 @@ func (a *TelebotAdapter) SendText(text string, params ...interface{}) (*core.Mes return makeMessage(sent), err } +// Delete is a core.IBot interface implementation func (a *TelebotAdapter) Delete(message *core.Message) error { return a.t.bot.Delete(&tb.Message{ID: message.ID, Chat: &tb.Chat{ID: message.ChatID}}) } +// SendImage is a core.IBot interface implementation func (a *TelebotAdapter) SendImage(image *core.Image) (*core.Message, error) { photo := &tb.Photo{File: tb.File{FileID: image.ID}} sent, err := a.t.bot.Send(a.m.Chat, photo) return makeMessage(sent), err } +// SendAlbum is a core.IBot interface implementation func (a *TelebotAdapter) SendAlbum(images []*core.Image) ([]*core.Message, error) { album := tb.Album{} for _, i := range images { @@ -50,6 +55,7 @@ func (a *TelebotAdapter) SendAlbum(images []*core.Image) ([]*core.Message, error return messages, err } +// SendMedia is a core.IBot interface implementation func (a *TelebotAdapter) SendMedia(media *core.Media) (*core.Message, error) { var sent *tb.Message var err error @@ -71,6 +77,7 @@ func (a *TelebotAdapter) SendMedia(media *core.Media) (*core.Message, error) { return makeMessage(sent), err } +// SendPhotoAlbum is a core.IBot interface implementation func (a *TelebotAdapter) SendPhotoAlbum(medias []*core.Media) ([]*core.Message, error) { var photo *tb.Photo var album = tb.Album{} @@ -92,6 +99,7 @@ func (a *TelebotAdapter) SendPhotoAlbum(medias []*core.Media) ([]*core.Message, return messages, err } +// SendVideoFile is a core.IBot interface implementation func (a *TelebotAdapter) SendVideoFile(vf *core.VideoFile, caption string) (*core.Message, error) { video := makeVideoFile(vf, caption) a.t.bot.Notify(a.m.Chat, tb.UploadingVideo) @@ -103,6 +111,7 @@ func (a *TelebotAdapter) SendVideoFile(vf *core.VideoFile, caption string) (*cor return makeMessage(sent), err } +// CreatePlayer is a core.IPlayerFactory interface implementation func (a *TelebotAdapter) CreatePlayer(string) infrastructure.Player { return infrastructure.Player{ GameID: a.m.Chat.ID, diff --git a/api/telebot_game.go b/api/telebot_game.go index eda555c..9e5117c 100644 --- a/api/telebot_game.go +++ b/api/telebot_game.go @@ -12,6 +12,7 @@ import ( tb "gopkg.in/tucnak/telebot.v2" ) +// SetupGame ... func (t *Telebot) SetupGame(g usecases.GameFlow) { t.bot.Handle("/pidorules", func(m *tb.Message) { text := g.Rules() diff --git a/api/telebot_info.go b/api/telebot_info.go index 11d1bbb..d98e880 100644 --- a/api/telebot_info.go +++ b/api/telebot_info.go @@ -7,6 +7,7 @@ import ( tb "gopkg.in/tucnak/telebot.v2" ) +// SetupInfo ... func (t *Telebot) SetupInfo() { t.bot.Handle("/info", func(m *tb.Message) { info := []string{ diff --git a/api/telegraph.go b/api/telegraph.go index 469e87b..b09598c 100644 --- a/api/telegraph.go +++ b/api/telegraph.go @@ -12,10 +12,12 @@ import ( "github.com/ailinykh/pullanusbot/v2/core" ) +// CreateTelegraphAPI is a default Telegraph factory func CreateTelegraphAPI() *Telegraph { return &Telegraph{} } +// Telegraph uploads files to telegra.ph type Telegraph struct { } @@ -23,7 +25,7 @@ type telegraphImage struct { Src string `json:"src"` } -// IFileUploader +// Upload is a core.IFileUploader interface implementation func (t *Telegraph) Upload(file *core.File) (core.URL, error) { fd, err := os.Open(file.Path) if err != nil { diff --git a/api/tweet.go b/api/tweet.go index 4eb01e5..14ec81f 100644 --- a/api/tweet.go +++ b/api/tweet.go @@ -1,5 +1,6 @@ package api +// Tweet is a twitter api representation of a single tweet type Tweet struct { ID string `json:"id_str"` FullText string `json:"full_text"` @@ -10,16 +11,19 @@ type Tweet struct { Errors []Error `json:"errors,omitempty"` } +// User ... type User struct { Name string `json:"name"` ScreenName string `json:"screen_name"` } +// Entity ... type Entity struct { Urls []URL `json:"urls,omitempty"` Media []Media `json:"media"` } +// Media ... type Media struct { MediaURL string `json:"media_url"` MediaURLHTTPS string `json:"media_url_https"` @@ -27,14 +31,17 @@ type Media struct { VideoInfo VideoInfo `json:"video_info,omitempty"` } +// URL ... type URL struct { ExpandedURL string `json:"expanded_url"` } +// VideoInfo ... type VideoInfo struct { Variants []VideoInfoVariant `json:"variants"` } +// Error ... type Error struct { Message string `json:"message"` Code int `json:"code"` @@ -50,6 +57,7 @@ func (info *VideoInfo) best() VideoInfoVariant { return variant } +// VideoInfoVariant ... type VideoInfoVariant struct { Bitrate int `json:"bitrate"` ContentType string `json:"content_type"` diff --git a/api/twitter.go b/api/twitter.go index e2db4f2..609ab11 100644 --- a/api/twitter.go +++ b/api/twitter.go @@ -11,10 +11,12 @@ import ( "github.com/ailinykh/pullanusbot/v2/core" ) +// CreateTwitterAPI is a default Twitter factory func CreateTwitterAPI() *Twitter { return &Twitter{} } +// Twitter API type Twitter struct{} func (Twitter) get(tweetID string) (*Tweet, error) { @@ -38,14 +40,14 @@ func (Twitter) get(tweetID string) (*Tweet, error) { if len(tweet.Errors) > 0 { if tweet.Errors[0].Code == 88 { // "Rate limit exceeded 88" return nil, errors.New(tweet.Errors[0].Message + " " + res.Header["X-Rate-Limit-Reset"][0]) - } else { - return nil, errors.New(tweet.Errors[0].Message) } + return nil, errors.New(tweet.Errors[0].Message) } return &tweet, err } +// CreateMedia is a core.IMediaFactory interface implementation func (t *Twitter) CreateMedia(tweetID string, author *core.User) ([]*core.Media, error) { tweet, err := t.get(tweetID) if err != nil { diff --git a/api/video_file_handler.go b/api/video_file_handler.go index 6364efe..803369c 100644 --- a/api/video_file_handler.go +++ b/api/video_file_handler.go @@ -2,6 +2,7 @@ package api import "github.com/ailinykh/pullanusbot/v2/core" +// IVdeoFileHandler interface for processing VideoFiles type IVdeoFileHandler interface { HandleVideoFile(*core.VideoFile, core.IBot) error } From 3c39c5827b5c6e188b42218f2570cef104a58d45 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Fri, 18 Jun 2021 17:04:00 +0300 Subject: [PATCH 046/439] License added --- LICENSE | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..985f3f8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 indes + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. From 823160156d0c4938426bc13bf027fb054b0a83ee Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Fri, 18 Jun 2021 17:14:52 +0300 Subject: [PATCH 047/439] publisher misspelling fixed --- usecases/publisher_flow.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/usecases/publisher_flow.go b/usecases/publisher_flow.go index 61cd9e8..9109b07 100644 --- a/usecases/publisher_flow.go +++ b/usecases/publisher_flow.go @@ -10,12 +10,12 @@ import ( // CreatePublisherFlow is a basic PublisherFlow factory func CreatePublisherFlow(l core.ILogger) *PublisherFlow { - chatID, err := strconv.ParseInt(os.Getenv("PUBLISER_CHAT_ID"), 10, 64) + chatID, err := strconv.ParseInt(os.Getenv("PUBLISHER_CHAT_ID"), 10, 64) if err != nil { chatID = 0 } - username := os.Getenv("PUBLISER_USERNAME") + username := os.Getenv("PUBLISHER_USERNAME") publisher := PublisherFlow{ l: l, From cd001d55e7c7bfb4af0751ffcfed29dc612e4e01 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Fri, 18 Jun 2021 17:25:43 +0300 Subject: [PATCH 048/439] ADMIN_CHET_ID not necessary anymore --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index a197c9d..e418575 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,6 @@ obtain bot token from [@BotFather](https://t.me/BotFather) and your telegram ID ```shell echo "export BOT_TOKEN:=12345678:XXXXXXXXxxxxxxxxXXXXXXXXxxxxxxxxXXX" > .env -echo "export ADMIN_CHAT_ID:=123456789" >> .env ``` and run! From 27a73339574f1551bd7dd30af9cecd3bdd2dd864 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Fri, 18 Jun 2021 20:43:08 +0300 Subject: [PATCH 049/439] Create gorm.DB on top level of application --- api/telebot_game.go | 17 +++++++------ infrastructure/game_storage.go | 45 +++++++--------------------------- pullanusbot.go | 17 +++++++++++-- 3 files changed, 33 insertions(+), 46 deletions(-) diff --git a/api/telebot_game.go b/api/telebot_game.go index 9e5117c..dffb7a7 100644 --- a/api/telebot_game.go +++ b/api/telebot_game.go @@ -8,19 +8,20 @@ import ( "github.com/ailinykh/pullanusbot/v2/core" "github.com/ailinykh/pullanusbot/v2/infrastructure" "github.com/ailinykh/pullanusbot/v2/usecases" + "gorm.io/gorm" tb "gopkg.in/tucnak/telebot.v2" ) // SetupGame ... -func (t *Telebot) SetupGame(g usecases.GameFlow) { +func (t *Telebot) SetupGame(g usecases.GameFlow, conn *gorm.DB) { t.bot.Handle("/pidorules", func(m *tb.Message) { text := g.Rules() t.bot.Send(m.Chat, text, &tb.SendOptions{ParseMode: tb.ModeHTML}) }) t.bot.Handle("/pidoreg", func(m *tb.Message) { - text := g.Add(makeUser(m), makeStorage(m, t)) + text := g.Add(makeUser(m), makeStorage(conn, m, t)) t.bot.Send(m.Chat, text, &tb.SendOptions{ParseMode: tb.ModeHTML}) }) @@ -31,7 +32,7 @@ func (t *Telebot) SetupGame(g usecases.GameFlow) { defer mutex.Unlock() t.logger.Infof("playing game in chat %d", m.Chat.ID) - messages := g.Play(makeUser(m), makeStorage(m, t)) + messages := g.Play(makeUser(m), makeStorage(conn, m, t)) if len(messages) > 1 { for _, msg := range messages { t.bot.Send(m.Chat, msg, &tb.SendOptions{ParseMode: tb.ModeHTML}) @@ -44,17 +45,17 @@ func (t *Telebot) SetupGame(g usecases.GameFlow) { }) t.bot.Handle("/pidorall", func(m *tb.Message) { - text := g.All(makeStorage(m, t)) + text := g.All(makeStorage(conn, m, t)) t.bot.Send(m.Chat, text, &tb.SendOptions{ParseMode: tb.ModeHTML}) }) t.bot.Handle("/pidorstats", func(m *tb.Message) { - text := g.Stats(makeStorage(m, t)) + text := g.Stats(makeStorage(conn, m, t)) t.bot.Send(m.Chat, text, &tb.SendOptions{ParseMode: tb.ModeHTML}) }) t.bot.Handle("/pidorme", func(m *tb.Message) { - text := g.Me(makeUser(m), makeStorage(m, t)) + text := g.Me(makeUser(m), makeStorage(conn, m, t)) t.bot.Send(m.Chat, text, &tb.SendOptions{ParseMode: tb.ModeHTML}) }) } @@ -63,7 +64,7 @@ func makeUser(m *tb.Message) *core.User { return &core.User{Username: m.Sender.Username} } -func makeStorage(m *tb.Message, t *Telebot) core.IGameStorage { - storage := infrastructure.CreateGameStorage(m.Chat.ID, &TelebotAdapter{m, t}) +func makeStorage(conn *gorm.DB, m *tb.Message, t *Telebot) core.IGameStorage { + storage := infrastructure.CreateGameStorage(conn, m.Chat.ID, &TelebotAdapter{m, t}, t.logger) return &storage } diff --git a/infrastructure/game_storage.go b/infrastructure/game_storage.go index 8d7262e..729726f 100644 --- a/infrastructure/game_storage.go +++ b/infrastructure/game_storage.go @@ -1,41 +1,23 @@ package infrastructure import ( - "log" - "os" - "path" - "github.com/ailinykh/pullanusbot/v2/core" - "gorm.io/driver/sqlite" "gorm.io/gorm" - "gorm.io/gorm/logger" ) var conn *gorm.DB // CreateGameStorage is a default GameStorage factory -func CreateGameStorage(gameID int64, factory IPlayerFactory) GameStorage { - if conn == nil { - dbFile := path.Join(getWorkingDir(), "pullanusbot.db") - var err error - conn, err = gorm.Open(sqlite.Open(dbFile+"?cache=shared"), &gorm.Config{ - Logger: logger.Default.LogMode(logger.Error), - }) - if err != nil { - log.Fatal(err) - } - - if conn.Migrator().HasTable(&Player{}) && conn.Migrator().HasColumn(&Player{}, "chat_id") { - log.Println("Extendend migration") - conn.Migrator().RenameColumn(&Player{}, "chat_id", "game_id") - conn.Migrator().RenameTable("faggot_entries", "faggot_rounds") - conn.Migrator().RenameColumn(&Round{}, "chat_id", "game_id") - } else { - log.Println("Default migration") - conn.AutoMigrate(&Player{}, &Round{}) - } +func CreateGameStorage(conn *gorm.DB, gameID int64, factory IPlayerFactory, logger core.ILogger) GameStorage { + if conn.Migrator().HasTable(&Player{}) && conn.Migrator().HasColumn(&Player{}, "chat_id") { + logger.Info("Extendend migration") + conn.Migrator().RenameColumn(&Player{}, "chat_id", "game_id") + conn.Migrator().RenameTable("faggot_entries", "faggot_rounds") + conn.Migrator().RenameColumn(&Round{}, "chat_id", "game_id") + } else { + logger.Info("Default migration") + conn.AutoMigrate(&Player{}, &Round{}) } - s := GameStorage{conn, gameID, factory} return s } @@ -89,12 +71,3 @@ func (s *GameStorage) AddRound(round *core.Round) error { s.conn.Create(&dbRound) return nil } - -//TODO: duplicated code -func getWorkingDir() string { - workingDir := os.Getenv("WORKING_DIR") - if len(workingDir) == 0 { - return "pullanusbot-data" - } - return workingDir -} diff --git a/pullanusbot.go b/pullanusbot.go index e13e7a4..6b8eb9e 100644 --- a/pullanusbot.go +++ b/pullanusbot.go @@ -11,6 +11,9 @@ import ( "github.com/ailinykh/pullanusbot/v2/infrastructure" "github.com/ailinykh/pullanusbot/v2/usecases" "github.com/google/logger" + "gorm.io/driver/sqlite" + "gorm.io/gorm" + loger "gorm.io/gorm/logger" ) func main() { @@ -23,7 +26,7 @@ func main() { localizer := infrastructure.GameLocalizer{} game := usecases.CreateGameFlow(localizer) - telebot.SetupGame(game) + telebot.SetupGame(game, createDatabaseConnection()) telebot.SetupInfo() @@ -66,7 +69,17 @@ func createLogger() (core.ILogger, func()) { return l, close } -//TODO: duplicated code +func createDatabaseConnection() *gorm.DB { + dbFile := path.Join(getWorkingDir(), "pullanusbot.db") + conn, err := gorm.Open(sqlite.Open(dbFile+"?cache=shared"), &gorm.Config{ + Logger: loger.Default.LogMode(loger.Error), + }) + if err != nil { + logger.Fatal(err) + } + return conn +} + func getWorkingDir() string { workingDir := os.Getenv("WORKING_DIR") if len(workingDir) == 0 { From f574bb8cbf699b292a2266bfa2856b4b12b2eada Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Fri, 18 Jun 2021 22:15:45 +0300 Subject: [PATCH 050/439] Add .circleci/config.yml From 7622a7129bf5eb6e1edade8f8f9087790f516acc Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Fri, 18 Jun 2021 22:25:48 +0300 Subject: [PATCH 051/439] small code refactoring --- api/telebot_game.go | 3 +-- infrastructure/game_storage.go | 7 ++----- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/api/telebot_game.go b/api/telebot_game.go index dffb7a7..3f2d808 100644 --- a/api/telebot_game.go +++ b/api/telebot_game.go @@ -65,6 +65,5 @@ func makeUser(m *tb.Message) *core.User { } func makeStorage(conn *gorm.DB, m *tb.Message, t *Telebot) core.IGameStorage { - storage := infrastructure.CreateGameStorage(conn, m.Chat.ID, &TelebotAdapter{m, t}, t.logger) - return &storage + return infrastructure.CreateGameStorage(conn, m.Chat.ID, &TelebotAdapter{m, t}, t.logger) } diff --git a/infrastructure/game_storage.go b/infrastructure/game_storage.go index 729726f..06ec48b 100644 --- a/infrastructure/game_storage.go +++ b/infrastructure/game_storage.go @@ -5,10 +5,8 @@ import ( "gorm.io/gorm" ) -var conn *gorm.DB - // CreateGameStorage is a default GameStorage factory -func CreateGameStorage(conn *gorm.DB, gameID int64, factory IPlayerFactory, logger core.ILogger) GameStorage { +func CreateGameStorage(conn *gorm.DB, gameID int64, factory IPlayerFactory, logger core.ILogger) *GameStorage { if conn.Migrator().HasTable(&Player{}) && conn.Migrator().HasColumn(&Player{}, "chat_id") { logger.Info("Extendend migration") conn.Migrator().RenameColumn(&Player{}, "chat_id", "game_id") @@ -18,8 +16,7 @@ func CreateGameStorage(conn *gorm.DB, gameID int64, factory IPlayerFactory, logg logger.Info("Default migration") conn.AutoMigrate(&Player{}, &Round{}) } - s := GameStorage{conn, gameID, factory} - return s + return &GameStorage{conn, gameID, factory} } // GameStorage implements core.IGameStorage interface From e491e9bbca0ae1dc47ba4932c0a29b1722dbfe88 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Fri, 18 Jun 2021 22:26:16 +0300 Subject: [PATCH 052/439] remove ciecleci config since it not needed --- .circleci/config.yml | 43 ------------------------------------------- 1 file changed, 43 deletions(-) delete mode 100644 .circleci/config.yml diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index df0a568..0000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,43 +0,0 @@ -version: 2 -jobs: - build: - branches: - only: master - docker: - - image: circleci/golang - environment: - - DEP_VERSION: 0.5.0 - - IMAGE_NAME: pullanusbot - working_directory: /go/src/github.com/ailinykh/pullanusbot - steps: - - checkout - - run: - name: Setup environment - command: | - if [ ! -d vendor ]; then - curl -L -s https://github.com/golang/dep/releases/download/v${DEP_VERSION}/dep-linux-amd64 -o /go/bin/dep - chmod +x /go/bin/dep - /go/bin/dep ensure - fi - echo 'export TAG=0.1.${CIRCLE_BUILD_NUM}' >> $BASH_ENV - - run: - name: Run tests - command: go test -race -v -coverprofile=coverage.txt -covermode=atomic - - run: - name: Push coverage results - command: bash <(curl -s https://codecov.io/bash) - - setup_remote_docker: - docker_layer_caching: true - - run: - name: Build docker image - command: | - docker build -t ailinykh/$IMAGE_NAME:$TAG . - echo $DOCKER_PWD | docker login -u $DOCKER_LOGIN --password-stdin - docker push ailinykh/$IMAGE_NAME:$TAG - - run: - name: Deploy app to Digital Ocean Server via Docker - command: ssh -o StrictHostKeyChecking=no root@proxy.ailinykh.com "/bin/bash ./pullanusbot/deploy_app.sh $TAG" - - save_cache: - key: gopkg-{{ .Branch }}-{{ checksum "Gopkg.lock" }} - paths: - - vendor \ No newline at end of file From a829fafde59fdb3782f339bd71c6685dca3faf36 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Sat, 19 Jun 2021 18:42:50 +0300 Subject: [PATCH 053/439] More plain game flow design --- api/telebot.go | 22 ++- api/telebot_adapter.go | 13 -- api/telebot_game.go | 69 ---------- core/game_storage.go | 8 +- core/user.go | 6 +- infrastructure/game_storage.go | 62 ++++++--- infrastructure/player_factory.go | 6 - pullanusbot.go | 13 +- usecases/faggot_game.go | 88 +++++++----- usecases/faggot_game_test.go | 225 +++++++++++++++---------------- 10 files changed, 244 insertions(+), 268 deletions(-) delete mode 100644 api/telebot_game.go delete mode 100644 infrastructure/player_factory.go diff --git a/api/telebot.go b/api/telebot.go index b9aa401..1422933 100644 --- a/api/telebot.go +++ b/api/telebot.go @@ -119,12 +119,16 @@ func (t *Telebot) AddHandler(handler ...interface{}) { t.imageHandlers = append(t.imageHandlers, h) case string: t.registerCommand(h) - if handler, ok := handler[1].(core.ICommandHandler); ok { + if ch, ok := handler[1].(core.ICommandHandler); ok { t.bot.Handle(h, func(m *tb.Message) { - handler.HandleCommand(makeMessage(m), &TelebotAdapter{m, t}) + ch.HandleCommand(makeMessage(m), &TelebotAdapter{m, t}) + }) + } else if f, ok := handler[1].(func(*core.Message, core.IBot) error); ok { + t.bot.Handle(h, func(m *tb.Message) { + f(makeMessage(m), &TelebotAdapter{m, t}) }) } else { - panic("interface must implement core.ICommandHandler") + panic("interface must implement core.ICommandHandler or func(*core.Message, core.IBot) error") } default: panic(fmt.Sprintf("something wrong with %s", h)) @@ -150,11 +154,21 @@ func makeMessage(m *tb.Message) *core.Message { ID: m.ID, ChatID: m.Chat.ID, IsPrivate: m.Private(), - Sender: &core.User{Username: m.Sender.Username}, + Sender: makeUser(m.Sender), Text: m.Text, } } +func makeUser(u *tb.User) *core.User { + return &core.User{ + ID: u.ID, + FirstName: u.FirstName, + LastName: u.LastName, + Username: u.Username, + LanguageCode: u.LanguageCode, + } +} + func makeFile(name string, path string) *core.File { return &core.File{ Name: name, diff --git a/api/telebot_adapter.go b/api/telebot_adapter.go index d997067..19b4b40 100644 --- a/api/telebot_adapter.go +++ b/api/telebot_adapter.go @@ -2,7 +2,6 @@ package api import ( "github.com/ailinykh/pullanusbot/v2/core" - "github.com/ailinykh/pullanusbot/v2/infrastructure" tb "gopkg.in/tucnak/telebot.v2" ) @@ -110,15 +109,3 @@ func (a *TelebotAdapter) SendVideoFile(vf *core.VideoFile, caption string) (*cor } return makeMessage(sent), err } - -// CreatePlayer is a core.IPlayerFactory interface implementation -func (a *TelebotAdapter) CreatePlayer(string) infrastructure.Player { - return infrastructure.Player{ - GameID: a.m.Chat.ID, - UserID: a.m.Sender.ID, - FirstName: a.m.Sender.FirstName, - LastName: a.m.Sender.LastName, - Username: a.m.Sender.Username, - LanguageCode: a.m.Sender.LanguageCode, - } -} diff --git a/api/telebot_game.go b/api/telebot_game.go deleted file mode 100644 index 3f2d808..0000000 --- a/api/telebot_game.go +++ /dev/null @@ -1,69 +0,0 @@ -package api - -import ( - "math/rand" - "sync" - "time" - - "github.com/ailinykh/pullanusbot/v2/core" - "github.com/ailinykh/pullanusbot/v2/infrastructure" - "github.com/ailinykh/pullanusbot/v2/usecases" - "gorm.io/gorm" - - tb "gopkg.in/tucnak/telebot.v2" -) - -// SetupGame ... -func (t *Telebot) SetupGame(g usecases.GameFlow, conn *gorm.DB) { - t.bot.Handle("/pidorules", func(m *tb.Message) { - text := g.Rules() - t.bot.Send(m.Chat, text, &tb.SendOptions{ParseMode: tb.ModeHTML}) - }) - - t.bot.Handle("/pidoreg", func(m *tb.Message) { - text := g.Add(makeUser(m), makeStorage(conn, m, t)) - t.bot.Send(m.Chat, text, &tb.SendOptions{ParseMode: tb.ModeHTML}) - }) - - var mutex sync.Mutex - - t.bot.Handle("/pidor", func(m *tb.Message) { - mutex.Lock() - defer mutex.Unlock() - - t.logger.Infof("playing game in chat %d", m.Chat.ID) - messages := g.Play(makeUser(m), makeStorage(conn, m, t)) - if len(messages) > 1 { - for _, msg := range messages { - t.bot.Send(m.Chat, msg, &tb.SendOptions{ParseMode: tb.ModeHTML}) - r := rand.Intn(3) + 1 - time.Sleep(time.Duration(r) * time.Second) - } - } else { - t.bot.Send(m.Chat, messages[0], &tb.SendOptions{ParseMode: tb.ModeHTML}) - } - }) - - t.bot.Handle("/pidorall", func(m *tb.Message) { - text := g.All(makeStorage(conn, m, t)) - t.bot.Send(m.Chat, text, &tb.SendOptions{ParseMode: tb.ModeHTML}) - }) - - t.bot.Handle("/pidorstats", func(m *tb.Message) { - text := g.Stats(makeStorage(conn, m, t)) - t.bot.Send(m.Chat, text, &tb.SendOptions{ParseMode: tb.ModeHTML}) - }) - - t.bot.Handle("/pidorme", func(m *tb.Message) { - text := g.Me(makeUser(m), makeStorage(conn, m, t)) - t.bot.Send(m.Chat, text, &tb.SendOptions{ParseMode: tb.ModeHTML}) - }) -} - -func makeUser(m *tb.Message) *core.User { - return &core.User{Username: m.Sender.Username} -} - -func makeStorage(conn *gorm.DB, m *tb.Message, t *Telebot) core.IGameStorage { - return infrastructure.CreateGameStorage(conn, m.Chat.ID, &TelebotAdapter{m, t}, t.logger) -} diff --git a/core/game_storage.go b/core/game_storage.go index 12ddead..e2fa271 100644 --- a/core/game_storage.go +++ b/core/game_storage.go @@ -2,8 +2,8 @@ package core // IGameStorage is an abstract interface for game players and results handling type IGameStorage interface { - GetPlayers() ([]*User, error) - GetRounds() ([]*Round, error) - AddPlayer(*User) error - AddRound(*Round) error + GetPlayers(int64) ([]*User, error) + GetRounds(int64) ([]*Round, error) + AddPlayer(int64, *User) error + AddRound(int64, *Round) error } diff --git a/core/user.go b/core/user.go index f624554..a2e579b 100644 --- a/core/user.go +++ b/core/user.go @@ -2,5 +2,9 @@ package core // User ... type User struct { - Username string + ID int + FirstName string + LastName string + Username string + LanguageCode string } diff --git a/infrastructure/game_storage.go b/infrastructure/game_storage.go index 06ec48b..be2f7c0 100644 --- a/infrastructure/game_storage.go +++ b/infrastructure/game_storage.go @@ -6,7 +6,7 @@ import ( ) // CreateGameStorage is a default GameStorage factory -func CreateGameStorage(conn *gorm.DB, gameID int64, factory IPlayerFactory, logger core.ILogger) *GameStorage { +func CreateGameStorage(conn *gorm.DB, logger core.ILogger) *GameStorage { if conn.Migrator().HasTable(&Player{}) && conn.Migrator().HasColumn(&Player{}, "chat_id") { logger.Info("Extendend migration") conn.Migrator().RenameColumn(&Player{}, "chat_id", "game_id") @@ -16,55 +16,79 @@ func CreateGameStorage(conn *gorm.DB, gameID int64, factory IPlayerFactory, logg logger.Info("Default migration") conn.AutoMigrate(&Player{}, &Round{}) } - return &GameStorage{conn, gameID, factory} + return &GameStorage{conn} } // GameStorage implements core.IGameStorage interface type GameStorage struct { - conn *gorm.DB - gameID int64 - playerFactory IPlayerFactory + conn *gorm.DB } // GetPlayers is a core.IGameStorage interface implementation -func (s *GameStorage) GetPlayers() ([]*core.User, error) { +func (s *GameStorage) GetPlayers(gameID int64) ([]*core.User, error) { var dbPlayers []Player var corePlayers []*core.User - s.conn.Where("game_id = ?", s.gameID).Find(&dbPlayers) + s.conn.Where("game_id = ?", gameID).Find(&dbPlayers) for _, p := range dbPlayers { - corePlayers = append(corePlayers, &core.User{Username: p.Username}) + user := makeUser(p) + corePlayers = append(corePlayers, user) } return corePlayers, nil } // GetRounds is a core.IGameStorage interface implementation -func (s *GameStorage) GetRounds() ([]*core.Round, error) { +func (s *GameStorage) GetRounds(gameID int64) ([]*core.Round, error) { + players, err := s.GetPlayers(gameID) + if err != nil { + return nil, err + } var dbRounds []Round var coreRounds []*core.Round - s.conn.Where("game_id = ?", s.gameID).Find(&dbRounds) + s.conn.Where("game_id = ?", gameID).Find(&dbRounds) for _, r := range dbRounds { - player := &core.User{Username: r.Username} - coreRounds = append(coreRounds, &core.Round{Day: r.Day, Winner: player}) + for _, p := range players { + if p.Username == r.Username { + coreRounds = append(coreRounds, &core.Round{Day: r.Day, Winner: p}) + break + } + } } return coreRounds, nil } // AddPlayer is a core.IGameStorage interface implementation -func (s *GameStorage) AddPlayer(player *core.User) error { - dbPlayer := s.playerFactory.CreatePlayer(player.Username) - s.conn.Create(&dbPlayer) +func (s *GameStorage) AddPlayer(gameID int64, user *core.User) error { + player := Player{ + GameID: gameID, + UserID: user.ID, + FirstName: user.FirstName, + LastName: user.LastName, + Username: user.Username, + LanguageCode: user.LanguageCode, + } + player.GameID = gameID + s.conn.Create(&player) return nil } // AddRound is a core.IGameStorage interface implementation -func (s *GameStorage) AddRound(round *core.Round) error { - player := s.playerFactory.CreatePlayer(round.Winner.Username) +func (s *GameStorage) AddRound(gameID int64, round *core.Round) error { dbRound := Round{ - GameID: s.gameID, - UserID: player.UserID, + GameID: gameID, + UserID: round.Winner.ID, Day: round.Day, Username: round.Winner.Username, } s.conn.Create(&dbRound) return nil } + +func makeUser(player Player) *core.User { + return &core.User{ + ID: player.UserID, + FirstName: player.FirstName, + LastName: player.LastName, + Username: player.Username, + LanguageCode: player.LanguageCode, + } +} diff --git a/infrastructure/player_factory.go b/infrastructure/player_factory.go deleted file mode 100644 index e08ae14..0000000 --- a/infrastructure/player_factory.go +++ /dev/null @@ -1,6 +0,0 @@ -package infrastructure - -// IPlayerFactory creates infrastructure Player representation -type IPlayerFactory interface { - CreatePlayer(string) Player -} diff --git a/pullanusbot.go b/pullanusbot.go index 6b8eb9e..91f6eef 100644 --- a/pullanusbot.go +++ b/pullanusbot.go @@ -23,12 +23,17 @@ func main() { defer close() telebot := api.CreateTelebot(os.Getenv("BOT_TOKEN"), logger) + telebot.SetupInfo() localizer := infrastructure.GameLocalizer{} - game := usecases.CreateGameFlow(localizer) - telebot.SetupGame(game, createDatabaseConnection()) - - telebot.SetupInfo() + gameStorade := infrastructure.CreateGameStorage(createDatabaseConnection(), logger) + gameFlow := usecases.CreateGameFlow(localizer, gameStorade) + telebot.AddHandler("/pidorules", gameFlow.Rules) + telebot.AddHandler("/pidoreg", gameFlow.Add) + telebot.AddHandler("/pidor", gameFlow.Play) + telebot.AddHandler("/pidorstats", gameFlow.Stats) + telebot.AddHandler("/pidorall", gameFlow.All) + telebot.AddHandler("/pidorme", gameFlow.Me) converter := infrastructure.CreateFfmpegConverter() videoFlow := usecases.CreateVideoFlow(logger, converter, converter) diff --git a/usecases/faggot_game.go b/usecases/faggot_game.go index f84144a..cb7ee51 100644 --- a/usecases/faggot_game.go +++ b/usecases/faggot_game.go @@ -6,69 +6,80 @@ import ( "sort" "strconv" "strings" + "sync" "time" "github.com/ailinykh/pullanusbot/v2/core" ) // CreateGameFlow is a simple GameFlow factory -func CreateGameFlow(l core.ILocalizer) GameFlow { - return GameFlow{l} +func CreateGameFlow(l core.ILocalizer, s core.IGameStorage) GameFlow { + return GameFlow{l, s} } // GameFlow represents faggot game logic type GameFlow struct { l core.ILocalizer + s core.IGameStorage } // Rules of the game -func (flow *GameFlow) Rules() string { - return flow.l.I18n("faggot_rules") +func (flow *GameFlow) Rules(message *core.Message, bot core.IBot) error { + _, err := bot.SendText(flow.l.I18n("faggot_rules")) + return err } // Add a new player to game -func (flow *GameFlow) Add(player *core.User, storage core.IGameStorage) string { - players, _ := storage.GetPlayers() +func (flow *GameFlow) Add(message *core.Message, bot core.IBot) error { + players, _ := flow.s.GetPlayers(message.ChatID) for _, p := range players { - if p == player { - return flow.l.I18n("faggot_already_in_game") + if p.ID == message.Sender.ID { + _, err := bot.SendText(flow.l.I18n("faggot_already_in_game")) + return err } } - err := storage.AddPlayer(player) - + err := flow.s.AddPlayer(message.ChatID, message.Sender) if err != nil { - return "Unexpected error" + return err } - return flow.l.I18n("faggot_added_to_game") + _, err = bot.SendText(flow.l.I18n("faggot_added_to_game")) + return err } +var mutex sync.Mutex + // Play game -func (flow *GameFlow) Play(player *core.User, storage core.IGameStorage) []string { - players, _ := storage.GetPlayers() +func (flow *GameFlow) Play(message *core.Message, bot core.IBot) error { + mutex.Lock() + defer mutex.Unlock() + + players, _ := flow.s.GetPlayers(message.ChatID) switch len(players) { case 0: - return []string{flow.l.I18n("faggot_no_players", player.Username)} + _, err := bot.SendText(flow.l.I18n("faggot_no_players", message.Sender.Username)) + return err case 1: - return []string{flow.l.I18n("faggot_not_enough_players")} + _, err := bot.SendText(flow.l.I18n("faggot_not_enough_players")) + return err } - games, _ := storage.GetRounds() + games, _ := flow.s.GetRounds(message.ChatID) loc, _ := time.LoadLocation("Europe/Zurich") day := time.Now().In(loc).Format("2006-01-02") for _, r := range games { if r.Day == day { - return []string{flow.l.I18n("faggot_winner_known", r.Winner.Username)} + _, err := bot.SendText(flow.l.I18n("faggot_winner_known", r.Winner.Username)) + return err } } winner := players[rand.Intn(len(players))] round := &core.Round{Day: day, Winner: winner} - storage.AddRound(round) + flow.s.AddRound(message.ChatID, round) - phrases := make([]string, 0, 4) for i := 0; i <= 3; i++ { templates := []string{} for _, key := range flow.l.AllKeys() { @@ -84,28 +95,35 @@ func (flow *GameFlow) Play(player *core.User, storage core.IGameStorage) []strin phrase = flow.l.I18n(template, "@"+winner.Username) } - phrases = append(phrases, phrase) + _, err := bot.SendText(phrase) + if err != nil { + //TODO: logger? + } + + r := rand.Intn(3) + 1 + time.Sleep(time.Duration(r) * time.Second) } - return phrases + return nil } // All statistics for all time -func (flow *GameFlow) All(storage core.IGameStorage) string { - entries, _ := flow.getStat(storage) +func (flow *GameFlow) All(message *core.Message, bot core.IBot) error { + entries, _ := flow.getStat(message) messages := []string{flow.l.I18n("faggot_all_top"), ""} for i, e := range entries { message := flow.l.I18n("faggot_all_entry", i+1, e.Player.Username, e.Score) messages = append(messages, message) } messages = append(messages, "", flow.l.I18n("faggot_all_bottom", len(entries))) - return strings.Join(messages, "\n") + _, err := bot.SendText(strings.Join(messages, "\n")) + return err } // Stats returns current year statistics -func (flow *GameFlow) Stats(storage core.IGameStorage) string { +func (flow *GameFlow) Stats(message *core.Message, bot core.IBot) error { year := strconv.Itoa(time.Now().Year()) - rounds, _ := storage.GetRounds() + rounds, _ := flow.s.GetRounds(message.ChatID) entries := []Stat{} for _, r := range rounds { @@ -132,24 +150,26 @@ func (flow *GameFlow) Stats(storage core.IGameStorage) string { messages = append(messages, message) } messages = append(messages, "", flow.l.I18n("faggot_stats_bottom", len(entries))) - return strings.Join(messages, "\n") + _, err := bot.SendText(strings.Join(messages, "\n")) + return err } // Me returns your personal statistics -func (flow *GameFlow) Me(player *core.User, storage core.IGameStorage) string { - entries, _ := flow.getStat(storage) +func (flow *GameFlow) Me(message *core.Message, bot core.IBot) error { + entries, _ := flow.getStat(message) score := 0 for _, e := range entries { - if e.Player == player { + if e.Player.ID == message.Sender.ID { score = e.Score } } - return flow.l.I18n("faggot_me", player.Username, score) + _, err := bot.SendText(flow.l.I18n("faggot_me", message.Sender.Username, score)) + return err } -func (flow *GameFlow) getStat(storage core.IGameStorage) ([]Stat, error) { +func (flow *GameFlow) getStat(message *core.Message) ([]Stat, error) { entries := []Stat{} - rounds, err := storage.GetRounds() + rounds, err := flow.s.GetRounds(message.ChatID) if err != nil { return nil, err diff --git a/usecases/faggot_game_test.go b/usecases/faggot_game_test.go index f7b2628..066e88c 100644 --- a/usecases/faggot_game_test.go +++ b/usecases/faggot_game_test.go @@ -1,7 +1,6 @@ package usecases import ( - "errors" "fmt" "strconv" "strings" @@ -13,95 +12,84 @@ import ( ) func Test_RulesCommand_DeliversRules(t *testing.T) { - game, _, l := makeSUT(LocalizerDict{"faggot_rules": "Game rules:"}) - expected := l.I18n("faggot_rules") - rules := game.Rules() + game, bot, _ := makeSUT(LocalizerDict{"faggot_rules": "Game rules:"}) + message := makeMessage(1, "Faggot") - assert.Equal(t, rules, expected) -} - -func Test_RulesCommand_DeliversRulesInDifferentTranslations(t *testing.T) { - game, _, l := makeSUT(LocalizerDict{"faggot_rules": "Правила игры:"}) - expected := l.I18n("faggot_rules") - rules := game.Rules() + game.Rules(message, bot) - assert.Equal(t, rules, expected) -} - -func Test_Add_ReturnsErrorOnStorageError(t *testing.T) { - game, storage, _ := makeSUT() - storage.err = errors.New("Unexpected error") - player := &core.User{Username: "Faggot"} - message := game.Add(player, storage) - - assert.Equal(t, message, storage.err.Error()) + assert.Equal(t, bot.messages[0], "Game rules:") } func Test_Add_AppendsPlayerInGameOnlyOnce(t *testing.T) { - game, storage, localizer := makeSUT(LocalizerDict{ + game, bot, storage := makeSUT(LocalizerDict{ "faggot_added_to_game": "Player added", "faggot_already_in_game": "Player already in game", }) - player := &core.User{Username: "Faggot"} + message := makeMessage(1, "Faggot") - message := game.Add(player, storage) + game.Add(message, bot) - assert.Equal(t, storage.players, []*core.User{player}) - assert.Equal(t, message, localizer.I18n("faggot_added_to_game")) + assert.Equal(t, storage.players, []*core.User{message.Sender}) + assert.Equal(t, bot.messages[0], "Player added") - message = game.Add(player, storage) + game.Add(message, bot) - assert.Equal(t, storage.players, []*core.User{player}) - assert.Equal(t, message, localizer.I18n("faggot_already_in_game")) + assert.Equal(t, storage.players, []*core.User{message.Sender}) + assert.Equal(t, bot.messages[1], "Player already in game") } func Test_Play_RespondsWithNoPlayers(t *testing.T) { - game, storage, localizer := makeSUT(LocalizerDict{ + game, bot, _ := makeSUT(LocalizerDict{ "faggot_no_players": "Nobody in game. So you win, %s!", }) - player := &core.User{Username: "Faggot"} - messages := game.Play(player, storage) - expected := []string{localizer.I18n("faggot_no_players", player.Username)} - assert.Equal(t, messages, expected) + message := makeMessage(1, "Faggot") + + game.Play(message, bot) + + assert.Equal(t, bot.messages[0], "Nobody in game. So you win, Faggot!") } func Test_Play_RespondsNotEnoughPlayers(t *testing.T) { - game, storage, localizer := makeSUT(LocalizerDict{ + game, bot, _ := makeSUT(LocalizerDict{ "faggot_not_enough_players": "Not enough players", }) - player := &core.User{Username: "Faggot"} - game.Add(player, storage) + message := makeMessage(1, "Faggot") - messages := game.Play(player, storage) - expected := []string{localizer.I18n("faggot_not_enough_players")} - assert.Equal(t, messages, expected) + game.Add(message, bot) + game.Play(message, bot) + + assert.Equal(t, bot.messages[1], "Not enough players") } func Test_Play_RespondsWinnerAlreadyKnown(t *testing.T) { - game, storage, localizer := makeSUT(LocalizerDict{ + game, bot, storage := makeSUT(LocalizerDict{ "faggot_game_0_0": "0", "faggot_game_1_0": "1", "faggot_game_2_0": "2", "faggot_game_3_0": "3 %s", "faggot_winner_known": "Winner already known %s", }) - player1 := &core.User{Username: "Faggot1"} - player2 := &core.User{Username: "Faggot2"} - game.Add(player1, storage) - game.Add(player2, storage) - - messages := game.Play(player1, storage) - expected := []string{"0", "1", "2", fmt.Sprintf("3 @%s", storage.rounds[0].Winner.Username)} - assert.Equal(t, messages, expected) - - messages = game.Play(player1, storage) - expected = []string{localizer.I18n("faggot_winner_known", storage.rounds[0].Winner.Username)} - assert.Equal(t, messages, expected) + m1 := makeMessage(1, "Faggot1") + m2 := makeMessage(2, "Faggot2") + + game.Add(m1, bot) + game.Add(m2, bot) + game.Play(m1, bot) + + winner := storage.rounds[0].Winner.Username + assert.Equal(t, bot.messages[2], "0") + assert.Equal(t, bot.messages[3], "1") + assert.Equal(t, bot.messages[4], "2") + assert.Equal(t, bot.messages[5], fmt.Sprintf("3 @%s", winner)) + + game.Play(m1, bot) + + assert.Equal(t, bot.messages[6], fmt.Sprintf("Winner already known %s", winner)) } func Test_Stats_RespondsWithDescendingResultsForCurrentYear(t *testing.T) { year := strconv.Itoa(time.Now().Year()) - game, storage, _ := makeSUT(LocalizerDict{ + game, bot, storage := makeSUT(LocalizerDict{ "faggot_stats_top": "top", "faggot_stats_entry": "index:%d,player:%s,scores:%d", "faggot_stats_bottom": "total_players:%d", @@ -117,26 +105,26 @@ func Test_Stats_RespondsWithDescendingResultsForCurrentYear(t *testing.T) { "total_players:3", } - player1 := &core.User{Username: "Faggot1"} - player2 := &core.User{Username: "Faggot2"} - player3 := &core.User{Username: "Faggot3"} + m1 := makeMessage(1, "Faggot1") + m2 := makeMessage(2, "Faggot2") + m3 := makeMessage(3, "Faggot3") storage.rounds = []*core.Round{ - {Day: year + "-01-01", Winner: player2}, - {Day: "2020-01-02", Winner: player3}, - {Day: year + "-01-02", Winner: player3}, - {Day: year + "-01-03", Winner: player3}, - {Day: year + "-01-04", Winner: player3}, - {Day: year + "-01-05", Winner: player1}, - {Day: year + "-01-06", Winner: player1}, + {Day: year + "-01-01", Winner: m2.Sender}, + {Day: "2020-01-02", Winner: m3.Sender}, + {Day: year + "-01-02", Winner: m3.Sender}, + {Day: year + "-01-03", Winner: m3.Sender}, + {Day: year + "-01-04", Winner: m3.Sender}, + {Day: year + "-01-05", Winner: m1.Sender}, + {Day: year + "-01-06", Winner: m1.Sender}, } - message := game.Stats(storage) - assert.Equal(t, strings.Split(message, "\n"), expected) + game.Stats(m1, bot) + assert.Equal(t, strings.Split(bot.messages[0], "\n"), expected) } func Test_All_RespondsWithDescendingResultsForAllTime(t *testing.T) { - game, storage, _ := makeSUT(LocalizerDict{ + game, bot, storage := makeSUT(LocalizerDict{ "faggot_all_top": "top", "faggot_all_entry": "index:%d,player:%s,scores:%d", "faggot_all_bottom": "total_players:%d", @@ -152,51 +140,61 @@ func Test_All_RespondsWithDescendingResultsForAllTime(t *testing.T) { "total_players:3", } - player1 := &core.User{Username: "Faggot1"} - player2 := &core.User{Username: "Faggot2"} - player3 := &core.User{Username: "Faggot3"} + m1 := makeMessage(1, "Faggot1") + m2 := makeMessage(2, "Faggot2") + m3 := makeMessage(3, "Faggot3") storage.rounds = []*core.Round{ - {Day: "2021-01-01", Winner: player2}, - {Day: "2020-01-02", Winner: player3}, - {Day: "2020-01-02", Winner: player3}, - {Day: "2021-01-03", Winner: player3}, - {Day: "2021-01-04", Winner: player3}, - {Day: "2021-01-05", Winner: player1}, - {Day: "2021-01-06", Winner: player1}, + {Day: "2021-01-01", Winner: m2.Sender}, + {Day: "2020-01-02", Winner: m3.Sender}, + {Day: "2020-01-02", Winner: m3.Sender}, + {Day: "2021-01-03", Winner: m3.Sender}, + {Day: "2021-01-04", Winner: m3.Sender}, + {Day: "2021-01-05", Winner: m1.Sender}, + {Day: "2021-01-06", Winner: m1.Sender}, } - message := game.All(storage) - assert.Equal(t, strings.Split(message, "\n"), expected) + game.All(m1, bot) + assert.Equal(t, strings.Split(bot.messages[0], "\n"), expected) } func Test_Me_RespondsWithPersonalStat(t *testing.T) { - game, storage, localizer := makeSUT(LocalizerDict{ + game, bot, storage := makeSUT(LocalizerDict{ "faggot_me": "username:%s,scores:%d", }) - player1 := &core.User{Username: "Faggot1"} - player2 := &core.User{Username: "Faggot2"} + m1 := makeMessage(1, "Faggot1") + m2 := makeMessage(2, "Faggot2") storage.rounds = []*core.Round{ - {Day: "2021-01-01", Winner: player2}, - {Day: "2021-01-05", Winner: player1}, - {Day: "2021-01-06", Winner: player1}, + {Day: "2021-01-01", Winner: m2.Sender}, + {Day: "2021-01-05", Winner: m1.Sender}, + {Day: "2021-01-06", Winner: m1.Sender}, } - var message string - message = game.Me(player1, storage) - assert.Equal(t, message, localizer.I18n("faggot_me", player1.Username, 2)) + game.Me(m1, bot) + assert.Equal(t, bot.messages[0], fmt.Sprintf("username:%s,scores:%d", m1.Sender.Username, 2)) - message = game.Me(player2, storage) - assert.Equal(t, message, localizer.I18n("faggot_me", player2.Username, 1)) + game.Me(m2, bot) + assert.Equal(t, bot.messages[1], fmt.Sprintf("username:%s,scores:%d", m2.Sender.Username, 1)) } // Helpers -func makeSUT(args ...interface{}) (*GameFlow, *GameStorageMock, *LocalizerMock) { +func makeMessage(id int, username string) *core.Message { + player := &core.User{ + ID: id, + FirstName: "FirstName" + fmt.Sprint(id), + LastName: "LastName" + fmt.Sprint(id), + Username: username, + } + return &core.Message{ID: 0, Sender: player} +} + +func makeSUT(args ...interface{}) (*GameFlow, *BotMock, *GameStorageMock) { dict := LocalizerDict{} storage := &GameStorageMock{players: []*core.User{}} + bot := &BotMock{} for _, arg := range args { switch opt := arg.(type) { @@ -206,8 +204,8 @@ func makeSUT(args ...interface{}) (*GameFlow, *GameStorageMock, *LocalizerMock) } l := &LocalizerMock{dict: dict} - game := &GameFlow{l} - return game, storage, l + game := &GameFlow{l, storage} + return game, bot, storage } // LocalizerMock @@ -238,39 +236,38 @@ func (l *LocalizerMock) AllKeys() []string { type GameStorageMock struct { players []*core.User rounds []*core.Round - err error } -func (s *GameStorageMock) AddPlayer(player *core.User) error { - if s.err != nil { - return s.err - } - +func (s *GameStorageMock) AddPlayer(gameID int64, player *core.User) error { s.players = append(s.players, player) return nil } -func (s *GameStorageMock) GetPlayers() ([]*core.User, error) { - if s.err != nil { - return []*core.User{}, s.err - } - +func (s *GameStorageMock) GetPlayers(gameID int64) ([]*core.User, error) { return s.players, nil } -func (s *GameStorageMock) AddRound(round *core.Round) error { - if s.err != nil { - return s.err - } - +func (s *GameStorageMock) AddRound(gameID int64, round *core.Round) error { s.rounds = append(s.rounds, round) return nil } -func (s *GameStorageMock) GetRounds() ([]*core.Round, error) { - if s.err != nil { - return []*core.Round{}, s.err - } - +func (s *GameStorageMock) GetRounds(gameID int64) ([]*core.Round, error) { return s.rounds, nil } + +type BotMock struct { + messages []string +} + +func (BotMock) Delete(*core.Message) error { return nil } +func (BotMock) SendImage(*core.Image) (*core.Message, error) { return nil, nil } +func (BotMock) SendAlbum([]*core.Image) ([]*core.Message, error) { return nil, nil } +func (BotMock) SendMedia(*core.Media) (*core.Message, error) { return nil, nil } +func (BotMock) SendPhotoAlbum([]*core.Media) ([]*core.Message, error) { return nil, nil } +func (BotMock) SendVideoFile(*core.VideoFile, string) (*core.Message, error) { return nil, nil } + +func (b *BotMock) SendText(text string, args ...interface{}) (*core.Message, error) { + b.messages = append(b.messages, text) + return nil, nil +} From ad6ecefb7a78bc4bc89d5d460a9df0ccb1dd8422 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Sat, 19 Jun 2021 18:43:22 +0300 Subject: [PATCH 054/439] Run tests with -race flag --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 00b7746..8b189d9 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ run: build ./pullanusbot test: - go test ./... -coverprofile=cover.txt + go test ./... -coverprofile=cover.txt -race build: clean *.go go build . From 9df50a1a093244d2dfea87e570979f9552eb4352 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Sat, 19 Jun 2021 19:03:44 +0300 Subject: [PATCH 055/439] Database connection refactoring --- infrastructure/game_storage.go | 13 ++++++++++--- pullanusbot.go | 17 ++--------------- 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/infrastructure/game_storage.go b/infrastructure/game_storage.go index be2f7c0..27a481a 100644 --- a/infrastructure/game_storage.go +++ b/infrastructure/game_storage.go @@ -2,18 +2,25 @@ package infrastructure import ( "github.com/ailinykh/pullanusbot/v2/core" + "gorm.io/driver/sqlite" "gorm.io/gorm" + "gorm.io/gorm/logger" ) // CreateGameStorage is a default GameStorage factory -func CreateGameStorage(conn *gorm.DB, logger core.ILogger) *GameStorage { +func CreateGameStorage(dbFile string) *GameStorage { + conn, err := gorm.Open(sqlite.Open(dbFile+"?cache=shared"), &gorm.Config{ + Logger: logger.Default.LogMode(logger.Error), + }) + if err != nil { + panic(err) + } + if conn.Migrator().HasTable(&Player{}) && conn.Migrator().HasColumn(&Player{}, "chat_id") { - logger.Info("Extendend migration") conn.Migrator().RenameColumn(&Player{}, "chat_id", "game_id") conn.Migrator().RenameTable("faggot_entries", "faggot_rounds") conn.Migrator().RenameColumn(&Round{}, "chat_id", "game_id") } else { - logger.Info("Default migration") conn.AutoMigrate(&Player{}, &Round{}) } return &GameStorage{conn} diff --git a/pullanusbot.go b/pullanusbot.go index 91f6eef..b1ad9f5 100644 --- a/pullanusbot.go +++ b/pullanusbot.go @@ -11,9 +11,6 @@ import ( "github.com/ailinykh/pullanusbot/v2/infrastructure" "github.com/ailinykh/pullanusbot/v2/usecases" "github.com/google/logger" - "gorm.io/driver/sqlite" - "gorm.io/gorm" - loger "gorm.io/gorm/logger" ) func main() { @@ -26,7 +23,8 @@ func main() { telebot.SetupInfo() localizer := infrastructure.GameLocalizer{} - gameStorade := infrastructure.CreateGameStorage(createDatabaseConnection(), logger) + dbFile := path.Join(getWorkingDir(), "pullanusbot.db") + gameStorade := infrastructure.CreateGameStorage(dbFile) gameFlow := usecases.CreateGameFlow(localizer, gameStorade) telebot.AddHandler("/pidorules", gameFlow.Rules) telebot.AddHandler("/pidoreg", gameFlow.Add) @@ -74,17 +72,6 @@ func createLogger() (core.ILogger, func()) { return l, close } -func createDatabaseConnection() *gorm.DB { - dbFile := path.Join(getWorkingDir(), "pullanusbot.db") - conn, err := gorm.Open(sqlite.Open(dbFile+"?cache=shared"), &gorm.Config{ - Logger: loger.Default.LogMode(loger.Error), - }) - if err != nil { - logger.Fatal(err) - } - return conn -} - func getWorkingDir() string { workingDir := os.Getenv("WORKING_DIR") if len(workingDir) == 0 { From c54440b45a2389e3174d1a5ab00fb2958480979d Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Sat, 19 Jun 2021 22:32:54 +0300 Subject: [PATCH 056/439] ICommandHandler removed since it is not flexible enough --- api/telebot.go | 8 ++------ core/handlers.go | 5 ----- pullanusbot.go | 2 +- usecases/publisher_flow.go | 3 +-- 4 files changed, 4 insertions(+), 14 deletions(-) diff --git a/api/telebot.go b/api/telebot.go index 1422933..e5e320f 100644 --- a/api/telebot.go +++ b/api/telebot.go @@ -119,16 +119,12 @@ func (t *Telebot) AddHandler(handler ...interface{}) { t.imageHandlers = append(t.imageHandlers, h) case string: t.registerCommand(h) - if ch, ok := handler[1].(core.ICommandHandler); ok { - t.bot.Handle(h, func(m *tb.Message) { - ch.HandleCommand(makeMessage(m), &TelebotAdapter{m, t}) - }) - } else if f, ok := handler[1].(func(*core.Message, core.IBot) error); ok { + if f, ok := handler[1].(func(*core.Message, core.IBot) error); ok { t.bot.Handle(h, func(m *tb.Message) { f(makeMessage(m), &TelebotAdapter{m, t}) }) } else { - panic("interface must implement core.ICommandHandler or func(*core.Message, core.IBot) error") + panic("interface must implement func(*core.Message, core.IBot) error") } default: panic(fmt.Sprintf("something wrong with %s", h)) diff --git a/core/handlers.go b/core/handlers.go index a66b47a..d84e0f3 100644 --- a/core/handlers.go +++ b/core/handlers.go @@ -1,10 +1,5 @@ package core -// ICommandHandler responds to text messages started from forwardslash -type ICommandHandler interface { - HandleCommand(*Message, IBot) error -} - // IDocumentHandler responds to documents sent in chah type IDocumentHandler interface { HandleDocument(*Document, IBot) error diff --git a/pullanusbot.go b/pullanusbot.go index b1ad9f5..512d0cc 100644 --- a/pullanusbot.go +++ b/pullanusbot.go @@ -52,7 +52,7 @@ func main() { publisherFlow := usecases.CreatePublisherFlow(logger) telebot.AddHandler(publisherFlow) - telebot.AddHandler("/loh666", publisherFlow) + telebot.AddHandler("/loh666", publisherFlow.HandleRequest) // Start endless loop telebot.Run() } diff --git a/usecases/publisher_flow.go b/usecases/publisher_flow.go index 9109b07..db02c21 100644 --- a/usecases/publisher_flow.go +++ b/usecases/publisher_flow.go @@ -58,8 +58,7 @@ func (p *PublisherFlow) HandleImage(image *core.Image, message *core.Message, bo return nil } -// HandleCommand is a core.ICommandHandler protocol implementation -func (p *PublisherFlow) HandleCommand(message *core.Message, bot core.IBot) error { +func (p *PublisherFlow) HandleRequest(message *core.Message, bot core.IBot) error { if message.ChatID == p.chatID { p.requestChan <- msgSource{*message, bot} } From da9572826d1b4ad9d8edc6d7ca5c0d26b2ae2547 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Mon, 21 Jun 2021 00:27:47 +0300 Subject: [PATCH 057/439] Remove redundant file disposing --- usecases/link_flow.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/usecases/link_flow.go b/usecases/link_flow.go index 1013f4f..a09ebd2 100644 --- a/usecases/link_flow.go +++ b/usecases/link_flow.go @@ -102,8 +102,6 @@ func (lf *LinkFlow) downloadMedia(media *core.Media) (*core.VideoFile, error) { return nil, err } - defer os.Remove(file.Path) - stat, err := os.Stat(file.Path) if err != nil { return nil, err From a15ec5abcf5d1dc31824d07863c9a7cef6dd716c Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Mon, 21 Jun 2021 12:27:48 +0300 Subject: [PATCH 058/439] Do not delete original message in telebot_adapter --- api/telebot_adapter.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/api/telebot_adapter.go b/api/telebot_adapter.go index 19b4b40..967123f 100644 --- a/api/telebot_adapter.go +++ b/api/telebot_adapter.go @@ -103,9 +103,9 @@ func (a *TelebotAdapter) SendVideoFile(vf *core.VideoFile, caption string) (*cor video := makeVideoFile(vf, caption) a.t.bot.Notify(a.m.Chat, tb.UploadingVideo) sent, err := video.Send(a.t.bot, a.m.Chat, &tb.SendOptions{ParseMode: tb.ModeHTML}) - if err == nil { - a.t.logger.Infof("%s sent successfully", vf.Name) - a.t.bot.Delete(a.m) + if err != nil { + return nil, err } + a.t.logger.Infof("%s successfully sent", vf.Name) return makeMessage(sent), err } From 1a55c940a16d5b519466dc2fb5712bfc6b5805c2 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Mon, 21 Jun 2021 12:29:19 +0300 Subject: [PATCH 059/439] Rename twitter to twitter_api --- api/{twitter.go => twitter_api.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename api/{twitter.go => twitter_api.go} (100%) diff --git a/api/twitter.go b/api/twitter_api.go similarity index 100% rename from api/twitter.go rename to api/twitter_api.go From dd36f30815813f032e5f2d6d559d14e05704f987 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Mon, 21 Jun 2021 12:29:41 +0300 Subject: [PATCH 060/439] Add size to core.file --- core/file.go | 1 + 1 file changed, 1 insertion(+) diff --git a/core/file.go b/core/file.go index 8b93e11..a76a3fd 100644 --- a/core/file.go +++ b/core/file.go @@ -6,6 +6,7 @@ import "os" type File struct { Name string Path string + Size int64 } // Dispose for filesystem cleanup From e5918b7cccdd704b368251f2b8c8d7a9d4e28787 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Mon, 21 Jun 2021 12:29:59 +0300 Subject: [PATCH 061/439] Add duration to core.Media --- core/media.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/core/media.go b/core/media.go index 5e4a625..468bc67 100644 --- a/core/media.go +++ b/core/media.go @@ -14,7 +14,8 @@ const ( // Media ... type Media struct { - URL string - Caption string - Type MediaType + URL string + Caption string + Duration int + Type MediaType } From 9c07928d8a9caa8d7e6dff8f6fdf83b85016c951 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Mon, 21 Jun 2021 12:31:01 +0300 Subject: [PATCH 062/439] Add IVideoFileSplitter and default ffmpeg implementation --- core/video_file_splitter.go | 6 ++++++ infrastructure/ffmpeg_converter.go | 27 ++++++++++++++++++++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 core/video_file_splitter.go diff --git a/core/video_file_splitter.go b/core/video_file_splitter.go new file mode 100644 index 0000000..c036c3a --- /dev/null +++ b/core/video_file_splitter.go @@ -0,0 +1,6 @@ +package core + +// IVideoFileSplitter convert VideoFile with specified bitrate +type IVideoFileSplitter interface { + Split(*VideoFile, int) ([]*VideoFile, error) +} diff --git a/infrastructure/ffmpeg_converter.go b/infrastructure/ffmpeg_converter.go index 1eb2d8c..4bd856e 100644 --- a/infrastructure/ffmpeg_converter.go +++ b/infrastructure/ffmpeg_converter.go @@ -36,6 +36,31 @@ func (c *FfmpegConverter) Convert(vf *core.VideoFile, bitrate int) (*core.VideoF return c.CreateVideoFile(convertedVideoFilePath) } +// CreateVideoFile is a core.IVideoFileSplitter interface implementation +func (c *FfmpegConverter) Split(video *core.VideoFile, limit int) ([]*core.VideoFile, error) { + duration, n := 0, 0 + var videos = []*core.VideoFile{} + for duration < video.Duration { + nextFilePath := fmt.Sprintf("%s-%d.mp4", video.File.Path, n) + cmd := fmt.Sprintf(`ffmpeg -i %s -ss %d -fs %d %s`, video.File.Path, duration, limit, nextFilePath) + _, err := exec.Command("/bin/sh", "-c", cmd).CombinedOutput() + if err != nil { + return nil, err + } + + nextVideoFile, err := c.CreateVideoFile(nextFilePath) + if err != nil { + return nil, err + } + // defer nextVideoFile.Dispose() + + videos = append(videos, nextVideoFile) + duration += nextVideoFile.Duration + n++ + } + return videos, nil +} + // CreateVideoFile is a core.IVideoFileFactory interface implementation func (c *FfmpegConverter) CreateVideoFile(path string) (*core.VideoFile, error) { ffprobe, err := c.getFFProbe(path) @@ -73,7 +98,7 @@ func (c *FfmpegConverter) CreateVideoFile(path string) (*core.VideoFile, error) } return &core.VideoFile{ - File: core.File{Name: stat.Name(), Path: path}, + File: core.File{Name: stat.Name(), Path: path, Size: stat.Size()}, Width: stream.Width, Height: stream.Height, Bitrate: bitrate, From 4eccd5c6933c10e6b3904cc60770e319293bbe10 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Mon, 21 Jun 2021 12:31:58 +0300 Subject: [PATCH 063/439] youtube videos processing --- api/youtube.go | 83 +++++++++++++++++++++++++++++++++++++ api/youtube_api.go | 89 ++++++++++++++++++++++++++++++++++++++++ pullanusbot.go | 5 +++ usecases/youtube_flow.go | 85 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 262 insertions(+) create mode 100644 api/youtube.go create mode 100644 api/youtube_api.go create mode 100644 usecases/youtube_flow.go diff --git a/api/youtube.go b/api/youtube.go new file mode 100644 index 0000000..f0bb7e7 --- /dev/null +++ b/api/youtube.go @@ -0,0 +1,83 @@ +package api + +import ( + "errors" + "strings" +) + +// Video is a struct to handle youtube-dl's JSON output +type Video struct { + ID string `json:"id"` + Description string `json:"description"` + Duration int `json:"duration"` + Formats []*Format `json:"formats"` + Title string `json:"title"` + Thumbnail string `json:"thumbnail"` // might be .webp + Thumbnails []*Thumbnail `json:"thumbnails"` +} + +func (v Video) audioFormat() (*Format, error) { + for _, f := range v.Formats { + if f.FormatID == "140" { + return f, nil + } + } + + return nil, errors.New("140 not found for " + v.ID) +} + +// might be empty +func (v Video) availableFormats() []*Format { + rv := []*Format{} + for _, f := range v.Formats { + if f.Ext == "mp4" { // webm not friendly for iPhone + if f.VCodec != "none" && f.ACodec == "none" { + if strings.HasSuffix(f.FormatNote, "p") || strings.Contains(f.FormatNote, "DASH") { // skip 720p60 + rv = append(rv, f) + } + } + } + } + return rv +} + +func (v Video) formatByID(id string) (*Format, error) { + for _, f := range v.Formats { + if f.FormatID == id { + return f, nil + } + } + return nil, errors.New("can't find format with id " + id) +} + +func (v Video) thumb() *Thumbnail { + th := v.Thumbnails[0] + for _, t := range v.Thumbnails { + if !strings.Contains(t.URL, ".webp") { + th = t + } + } + return th +} + +// Format is a description of available formats for downloading +type Format struct { + Ext string `json:"ext"` + Filesize int `json:"filesize"` + Format string `json:"format"` + FormatID string `json:"format_id"` + FormatNote string `json:"format_note"` + Height int `json:"height"` + Width int `json:"width"` + VCodec string `json:"vcodec"` + ACodec string `json:"acodec"` +} + +// Thumbnail is a low resolution picture +type Thumbnail struct { + ID string `json:"id"` + Resolution string `json:"resolution"` + Width int `json:"width"` + Height int `json:"height"` + URL string `json:"url"` +} diff --git a/api/youtube_api.go b/api/youtube_api.go new file mode 100644 index 0000000..ede7c10 --- /dev/null +++ b/api/youtube_api.go @@ -0,0 +1,89 @@ +package api + +import ( + "encoding/json" + "fmt" + "os" + "os/exec" + "path" + + "github.com/ailinykh/pullanusbot/v2/core" +) + +func CreateYoutubeAPI(fd core.IFileDownloader) *YoutubeAPI { + return &YoutubeAPI{fd} +} + +type YoutubeAPI struct { + fd core.IFileDownloader +} + +// CreateMedia is a core.IMediaFactory interface implementation +func (y *YoutubeAPI) CreateMedia(url string, author *core.User) ([]*core.Media, error) { + video, err := y.getInfo(url) + if err != nil { + return nil, err + } + + return []*core.Media{ + { + URL: video.ID, + Caption: video.Title, + Duration: video.Duration, + Type: core.Video, + }, + }, nil +} + +// CreateVideoFile is a core.IVideoFileFactory interface implementation +func (y *YoutubeAPI) CreateVideoFile(youtubeID string) (*core.VideoFile, error) { + video, err := y.getInfo(youtubeID) + if err != nil { + return nil, err + } + + ytDlFormat := "134" + name := "youtube-" + youtubeID + "-" + ytDlFormat + ".mp4" + path := path.Join(os.TempDir(), name) + + cmd := fmt.Sprintf("youtube-dl -f %s+140 %s -o %s", ytDlFormat, youtubeID, path) + err = exec.Command("/bin/sh", "-c", cmd).Run() + if err != nil { + return nil, err + } + + thumb, err := y.fd.Download(video.thumb().URL) // will be disposed with VideoFile + if err != nil { + return nil, err + } + + format, err := video.formatByID(ytDlFormat) + if err != nil { + return nil, err + } + + return &core.VideoFile{ + File: core.File{Name: name, Path: path, Size: int64(format.Filesize)}, + Width: format.Width, + Height: format.Height, + Bitrate: 0, + Duration: video.Duration, + Codec: format.VCodec, + ThumbPath: thumb.Path, + }, nil +} + +func (YoutubeAPI) getInfo(url string) (*Video, error) { + cmd := fmt.Sprintf(`youtube-dl -j %s`, url) + out, err := exec.Command("/bin/sh", "-c", cmd).CombinedOutput() + if err != nil { + return nil, err + } + + var video Video + err = json.Unmarshal(out, &video) + if err != nil { + return nil, err + } + return &video, nil +} diff --git a/pullanusbot.go b/pullanusbot.go index 512d0cc..5c60286 100644 --- a/pullanusbot.go +++ b/pullanusbot.go @@ -53,6 +53,11 @@ func main() { publisherFlow := usecases.CreatePublisherFlow(logger) telebot.AddHandler(publisherFlow) telebot.AddHandler("/loh666", publisherFlow.HandleRequest) + + youtubeAPI := api.CreateYoutubeAPI(fileDownloader) + youtubeFlow := usecases.CreateYoutubeFlow(logger, youtubeAPI, youtubeAPI, converter) + telebot.AddHandler(youtubeFlow) + // Start endless loop telebot.Run() } diff --git a/usecases/youtube_flow.go b/usecases/youtube_flow.go new file mode 100644 index 0000000..c8377ee --- /dev/null +++ b/usecases/youtube_flow.go @@ -0,0 +1,85 @@ +package usecases + +import ( + "fmt" + "regexp" + "sync" + + "github.com/ailinykh/pullanusbot/v2/core" +) + +func CreateYoutubeFlow(l core.ILogger, mf core.IMediaFactory, vff core.IVideoFileFactory, vfs core.IVideoFileSplitter) *YoutubeFlow { + return &YoutubeFlow{l: l, mf: mf, vff: vff, vfs: vfs} +} + +type YoutubeFlow struct { + m sync.Mutex + l core.ILogger + mf core.IMediaFactory + vff core.IVideoFileFactory + vfs core.IVideoFileSplitter +} + +// HandleText is a core.ITextHandler protocol implementation +func (f *YoutubeFlow) HandleText(message *core.Message, bot core.IBot) error { + r := regexp.MustCompile(`https?:\/\/(www\.)?youtu[\.be|\.com]\S+`) + match := r.FindStringSubmatch(message.Text) + if len(match) > 0 { + return f.process(match[0], message, bot) + } + return nil +} + +func (f *YoutubeFlow) process(url string, message *core.Message, bot core.IBot) error { + f.m.Lock() + defer f.m.Unlock() + + f.l.Infof("processing youtube %s", url) + media, err := f.mf.CreateMedia(url, message.Sender) + if err != nil { + return err + } + + if !message.IsPrivate && media[0].Duration > 900 { + f.l.Infof("skip video in group chat due to duration %d", media[0].Duration) + return nil + } + + youtubeID, title := media[0].URL, media[0].Caption + f.l.Infof("downloading %s", youtubeID) + file, err := f.vff.CreateVideoFile(youtubeID) + if err != nil { + return err + } + defer file.Dispose() + + caption := fmt.Sprintf(`🔗 %s (by %s)`, youtubeID, title, message.Sender.Username) + _, err = bot.SendVideoFile(file, caption) + if err != nil { + f.l.Error("Can't send video: ", err) + if err.Error() == "telegram: Request Entity Too Large (400)" { + f.l.Info("Fallback to splitting") + files, err := f.vfs.Split(file, 50000000) + if err != nil { + return err + } + + for _, file := range files { + defer file.Dispose() + } + + for i, file := range files { + caption := fmt.Sprintf(`🔗 [%d/%d] %s (by %s)`, youtubeID, i+1, len(files), title, message.Sender.Username) + _, err := bot.SendVideoFile(file, caption) + if err != nil { + return err + } + } + + f.l.Info("All parts successfully sent") + return bot.Delete(message) + } + return err + } + return bot.Delete(message) +} From b2fde16785936af9ba647c4ed31a5ff8b2480a4f Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Mon, 21 Jun 2021 13:27:48 +0300 Subject: [PATCH 064/439] Add logger to ffmpeg_converter --- infrastructure/ffmpeg.go | 35 +++++++++++++++ infrastructure/ffmpeg_converter.go | 68 +++++++++--------------------- pullanusbot.go | 2 +- 3 files changed, 57 insertions(+), 48 deletions(-) create mode 100644 infrastructure/ffmpeg.go diff --git a/infrastructure/ffmpeg.go b/infrastructure/ffmpeg.go new file mode 100644 index 0000000..f2c1183 --- /dev/null +++ b/infrastructure/ffmpeg.go @@ -0,0 +1,35 @@ +package infrastructure + +import "errors" + +type ffpResponse struct { + Streams []ffpStream `json:"streams"` + Format ffpFormat `json:"format"` +} + +type ffpStream struct { + Index int `json:"index"` + CodecName string `json:"codec_name"` + CodecType string `json:"codec_type"` + Width int `json:"width"` + Height int `json:"height"` + BitRate string `json:"bit_rate"` +} + +type ffpFormat struct { + Filename string `json:"filename"` + NbStreams int `json:"nb_streams"` + FormatName string `json:"format_name"` + FormatLongName string `json:"format_long_name"` + Duration string `json:"duration"` + Size string `json:"size"` +} + +func (f ffpResponse) getVideoStream() (ffpStream, error) { + for _, stream := range f.Streams { + if stream.CodecType == "video" { + return stream, nil + } + } + return ffpStream{}, errors.New("no video stream found") +} diff --git a/infrastructure/ffmpeg_converter.go b/infrastructure/ffmpeg_converter.go index 4bd856e..7f0b313 100644 --- a/infrastructure/ffmpeg_converter.go +++ b/infrastructure/ffmpeg_converter.go @@ -8,32 +8,37 @@ import ( "os/exec" "path" "strconv" + "strings" "github.com/ailinykh/pullanusbot/v2/core" ) // CreateFfmpegConverter is a basic FfmpegConverter factory -func CreateFfmpegConverter() *FfmpegConverter { - return &FfmpegConverter{} +func CreateFfmpegConverter(l core.ILogger) *FfmpegConverter { + return &FfmpegConverter{l} } // FfmpegConverter implements core.IVideoFileConverter and core.IVideoFileFactory using ffmpeg -type FfmpegConverter struct{} +type FfmpegConverter struct { + l core.ILogger +} // Convert is a core.IVideoFileConverter interface implementation func (c *FfmpegConverter) Convert(vf *core.VideoFile, bitrate int) (*core.VideoFile, error) { - convertedVideoFilePath := path.Join(os.TempDir(), vf.Name+"_converted.mp4") - cmd := fmt.Sprintf(`ffmpeg -y -i "%s" -pix_fmt yuv420p -vf "scale=trunc(iw/2)*2:trunc(ih/2)*2" "%s"`, vf.Path, convertedVideoFilePath) + path := path.Join(os.TempDir(), vf.Name+"_converted.mp4") + cmd := fmt.Sprintf(`ffmpeg -y -i "%s" -pix_fmt yuv420p -vf "scale=trunc(iw/2)*2:trunc(ih/2)*2" "%s"`, vf.Path, path) if bitrate > 0 { - cmd = fmt.Sprintf(`ffmpeg -v error -y -i "%s" -c:v libx264 -preset medium -b:v %dk -pass 1 -b:a 128k -f mp4 /dev/null && ffmpeg -y -i "%s" -c:v libx264 -preset medium -b:v %dk -pass 2 -b:a 128k "%s"`, vf.Path, bitrate/1024, vf.Path, bitrate/1024, convertedVideoFilePath) + cmd = fmt.Sprintf(`ffmpeg -v error -y -i "%s" -c:v libx264 -preset medium -b:v %dk -pass 1 -b:a 128k -f mp4 /dev/null && ffmpeg -y -i "%s" -c:v libx264 -preset medium -b:v %dk -pass 2 -b:a 128k "%s"`, vf.Path, bitrate/1024, vf.Path, bitrate/1024, path) } + c.l.Info(strings.ReplaceAll(cmd, os.TempDir(), "$TMPDIR/")) out, err := exec.Command("/bin/sh", "-c", cmd).CombinedOutput() if err != nil { - os.Remove(convertedVideoFilePath) - return nil, errors.New(string(out)) + os.Remove(path) + c.l.Error(out) + return nil, err } - return c.CreateVideoFile(convertedVideoFilePath) + return c.CreateVideoFile(path) } // CreateVideoFile is a core.IVideoFileSplitter interface implementation @@ -41,21 +46,22 @@ func (c *FfmpegConverter) Split(video *core.VideoFile, limit int) ([]*core.Video duration, n := 0, 0 var videos = []*core.VideoFile{} for duration < video.Duration { - nextFilePath := fmt.Sprintf("%s-%d.mp4", video.File.Path, n) - cmd := fmt.Sprintf(`ffmpeg -i %s -ss %d -fs %d %s`, video.File.Path, duration, limit, nextFilePath) + path := fmt.Sprintf("%s-%d.mp4", video.File.Path, n) + cmd := fmt.Sprintf(`ffmpeg -i %s -ss %d -fs %d %s`, video.File.Path, duration, limit, path) + c.l.Info(strings.ReplaceAll(cmd, os.TempDir(), "$TMPDIR/")) _, err := exec.Command("/bin/sh", "-c", cmd).CombinedOutput() if err != nil { return nil, err } - nextVideoFile, err := c.CreateVideoFile(nextFilePath) + file, err := c.CreateVideoFile(path) if err != nil { return nil, err } - // defer nextVideoFile.Dispose() + // defer file.Dispose() - videos = append(videos, nextVideoFile) - duration += nextVideoFile.Duration + videos = append(videos, file) + duration += file.Duration n++ } return videos, nil @@ -122,35 +128,3 @@ func (c *FfmpegConverter) getFFProbe(file string) (*ffpResponse, error) { return &resp, nil } - -type ffpResponse struct { - Streams []ffpStream `json:"streams"` - Format ffpFormat `json:"format"` -} - -type ffpStream struct { - Index int `json:"index"` - CodecName string `json:"codec_name"` - CodecType string `json:"codec_type"` - Width int `json:"width"` - Height int `json:"height"` - BitRate string `json:"bit_rate"` -} - -type ffpFormat struct { - Filename string `json:"filename"` - NbStreams int `json:"nb_streams"` - FormatName string `json:"format_name"` - FormatLongName string `json:"format_long_name"` - Duration string `json:"duration"` - Size string `json:"size"` -} - -func (f ffpResponse) getVideoStream() (ffpStream, error) { - for _, stream := range f.Streams { - if stream.CodecType == "video" { - return stream, nil - } - } - return ffpStream{}, errors.New("no video stream found") -} diff --git a/pullanusbot.go b/pullanusbot.go index 5c60286..2d9a301 100644 --- a/pullanusbot.go +++ b/pullanusbot.go @@ -33,7 +33,7 @@ func main() { telebot.AddHandler("/pidorall", gameFlow.All) telebot.AddHandler("/pidorme", gameFlow.Me) - converter := infrastructure.CreateFfmpegConverter() + converter := infrastructure.CreateFfmpegConverter(logger) videoFlow := usecases.CreateVideoFlow(logger, converter, converter) telebot.AddHandler(videoFlow) From 17f9fc989619fdb794e00e41403faa84d86c203c Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Mon, 21 Jun 2021 13:34:16 +0300 Subject: [PATCH 065/439] Report error from telebot --- api/telebot.go | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/api/telebot.go b/api/telebot.go index e5e320f..4dc6b40 100644 --- a/api/telebot.go +++ b/api/telebot.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path" + "strconv" "sync" "time" @@ -44,6 +45,7 @@ func CreateTelebot(token string, logger core.ILogger) *Telebot { err := h.HandleText(makeMessage(m), &TelebotAdapter{m, telebot}) if err != nil { logger.Error(err) + telebot.reportError(err) } } }) @@ -69,12 +71,16 @@ func CreateTelebot(token string, logger core.ILogger) *Telebot { defer os.Remove(path) for _, h := range telebot.documentHandlers { - h.HandleDocument(&core.Document{ + err := h.HandleDocument(&core.Document{ Author: m.Sender.Username, FileName: m.Document.FileName, FilePath: path, MIME: m.Document.MIME, }, &TelebotAdapter{m, telebot}) + if err != nil { + logger.Error(err) + telebot.reportError(err) + } } } }) @@ -84,7 +90,11 @@ func CreateTelebot(token string, logger core.ILogger) *Telebot { image := core.CreateImage(m.Photo.FileID, m.Photo.FileURL) for _, h := range telebot.imageHandlers { - h.HandleImage(&image, makeMessage(m), &TelebotAdapter{m, telebot}) + err := h.HandleImage(&image, makeMessage(m), &TelebotAdapter{m, telebot}) + if err != nil { + logger.Error(err) + telebot.reportError(err) + } } }) @@ -145,6 +155,14 @@ func (t *Telebot) registerCommand(command string) { t.commandHandlers = append(t.commandHandlers, command) } +func (t *Telebot) reportError(e error) { + chatID, err := strconv.ParseInt(os.Getenv("ADMIN_CHAT_ID"), 10, 64) + if err != nil { + return + } + t.bot.Send(&tb.Chat{ID: chatID}, e) +} + func makeMessage(m *tb.Message) *core.Message { return &core.Message{ ID: m.ID, From cfc9ce7d7bafb2381bc8f6c24678b8c54b2c528b Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Mon, 21 Jun 2021 14:22:51 +0300 Subject: [PATCH 066/439] Create /usr/bin/python symlink since youtube-dl says can't execute 'python': No such file or directory --- Dockerfile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 8c7d7c3..5abf594 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,7 +11,9 @@ FROM jrottenberg/ffmpeg:4.1-alpine RUN apk update && apk add tzdata python3 supervisor openssh --no-cache && \ ssh-keygen -f /etc/ssh/ssh_host_rsa_key -N '' -t rsa && \ ssh-keygen -f /etc/ssh/ssh_host_dsa_key -N '' -t dsa && \ - wget https://yt-dl.org/downloads/latest/youtube-dl -O /usr/local/bin/youtube-dl && chmod a+rx /usr/local/bin/youtube-dl + wget https://yt-dl.org/downloads/latest/youtube-dl -O /usr/local/bin/youtube-dl && \ + chmod a+rx /usr/local/bin/youtube-dl && \ + ln -s /usr/bin/python3 /usr/bin/python COPY --from=builder /go/src/github.com/ailinykh/pullanusbot/pullanusbot /usr/local/bin/pullanusbot COPY .docker/telegram-bot-api /usr/local/bin/telegram-bot-api From 60c141054059441c78030e58ebf6adb9e048efe5 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Mon, 21 Jun 2021 14:23:13 +0300 Subject: [PATCH 067/439] Send error as string in telebot report --- api/telebot.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/telebot.go b/api/telebot.go index 4dc6b40..6e8913a 100644 --- a/api/telebot.go +++ b/api/telebot.go @@ -160,7 +160,7 @@ func (t *Telebot) reportError(e error) { if err != nil { return } - t.bot.Send(&tb.Chat{ID: chatID}, e) + t.bot.Send(&tb.Chat{ID: chatID}, e.Error()) } func makeMessage(m *tb.Message) *core.Message { From 4baaaabd229b8dcf73c9aa94b52b1b485964b361 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Mon, 21 Jun 2021 18:14:43 +0300 Subject: [PATCH 068/439] Report error in telebot improved --- api/telebot.go | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/api/telebot.go b/api/telebot.go index 6e8913a..7242ac8 100644 --- a/api/telebot.go +++ b/api/telebot.go @@ -44,8 +44,8 @@ func CreateTelebot(token string, logger core.ILogger) *Telebot { for _, h := range telebot.textHandlers { err := h.HandleText(makeMessage(m), &TelebotAdapter{m, telebot}) if err != nil { - logger.Error(err) - telebot.reportError(err) + logger.Errorf("%T: %s", h, err) + telebot.reportError(m, err) } } }) @@ -78,8 +78,8 @@ func CreateTelebot(token string, logger core.ILogger) *Telebot { MIME: m.Document.MIME, }, &TelebotAdapter{m, telebot}) if err != nil { - logger.Error(err) - telebot.reportError(err) + logger.Errorf("%T: %s", h, err) + telebot.reportError(m, err) } } } @@ -92,8 +92,8 @@ func CreateTelebot(token string, logger core.ILogger) *Telebot { for _, h := range telebot.imageHandlers { err := h.HandleImage(&image, makeMessage(m), &TelebotAdapter{m, telebot}) if err != nil { - logger.Error(err) - telebot.reportError(err) + logger.Errorf("%T: %s", h, err) + telebot.reportError(m, err) } } }) @@ -155,12 +155,14 @@ func (t *Telebot) registerCommand(command string) { t.commandHandlers = append(t.commandHandlers, command) } -func (t *Telebot) reportError(e error) { +func (t *Telebot) reportError(m *tb.Message, e error) { chatID, err := strconv.ParseInt(os.Getenv("ADMIN_CHAT_ID"), 10, 64) if err != nil { return } - t.bot.Send(&tb.Chat{ID: chatID}, e.Error()) + chat := &tb.Chat{ID: chatID} + t.bot.Forward(chat, m) + t.bot.Send(chat, e.Error()) } func makeMessage(m *tb.Message) *core.Message { From 80cf260057e0bbd3a653ac51f95c4ffbca9f256e Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Mon, 21 Jun 2021 18:20:08 +0300 Subject: [PATCH 069/439] Rename VideoFile to just Video --- api/telebot_adapter.go | 12 ++++++------ api/twitter_api.go | 8 ++++---- api/video_file_factory.go | 2 +- api/video_file_handler.go | 4 ++-- api/youtube_api.go | 10 +++++----- core/bot.go | 2 +- core/media.go | 6 +++--- core/{video_file.go => video.go} | 6 +++--- core/video_converter.go | 6 ++++++ core/video_factory.go | 6 ++++++ core/video_file_converter.go | 6 ------ core/video_file_factory.go | 6 ------ core/video_file_splitter.go | 6 ------ core/video_splitter.go | 6 ++++++ infrastructure/ffmpeg_converter.go | 22 +++++++++++----------- usecases/faggot_game_test.go | 12 ++++++------ usecases/link_flow.go | 14 +++++++------- usecases/twitter_flow.go | 10 +++++----- usecases/video_flow.go | 14 +++++++------- usecases/youtube_flow.go | 12 ++++++------ 20 files changed, 85 insertions(+), 85 deletions(-) rename core/{video_file.go => video.go} (74%) create mode 100644 core/video_converter.go create mode 100644 core/video_factory.go delete mode 100644 core/video_file_converter.go delete mode 100644 core/video_file_factory.go delete mode 100644 core/video_file_splitter.go create mode 100644 core/video_splitter.go diff --git a/api/telebot_adapter.go b/api/telebot_adapter.go index 967123f..531eaa2 100644 --- a/api/telebot_adapter.go +++ b/api/telebot_adapter.go @@ -59,17 +59,17 @@ func (a *TelebotAdapter) SendMedia(media *core.Media) (*core.Message, error) { var sent *tb.Message var err error switch media.Type { - case core.Photo: + case core.TPhoto: file := &tb.Photo{File: tb.FromURL(media.URL)} file.Caption = media.Caption a.t.bot.Notify(a.m.Chat, tb.UploadingPhoto) sent, err = a.t.bot.Send(a.m.Chat, file, &tb.SendOptions{ParseMode: tb.ModeHTML}) - case core.Video: + case core.TVideo: file := &tb.Video{File: tb.FromURL(media.URL)} file.Caption = media.Caption a.t.bot.Notify(a.m.Chat, tb.UploadingVideo) sent, err = a.t.bot.Send(a.m.Chat, file, &tb.SendOptions{ParseMode: tb.ModeHTML}) - case core.Text: + case core.TText: sent, err = a.t.bot.Send(a.m.Chat, media.Caption, &tb.SendOptions{ParseMode: tb.ModeHTML}) } @@ -98,9 +98,9 @@ func (a *TelebotAdapter) SendPhotoAlbum(medias []*core.Media) ([]*core.Message, return messages, err } -// SendVideoFile is a core.IBot interface implementation -func (a *TelebotAdapter) SendVideoFile(vf *core.VideoFile, caption string) (*core.Message, error) { - video := makeVideoFile(vf, caption) +// SendVideo is a core.IBot interface implementation +func (a *TelebotAdapter) SendVideo(vf *core.Video, caption string) (*core.Message, error) { + video := makeVideo(vf, caption) a.t.bot.Notify(a.m.Chat, tb.UploadingVideo) sent, err := video.Send(a.t.bot, a.m.Chat, &tb.SendOptions{ParseMode: tb.ModeHTML}) if err != nil { diff --git a/api/twitter_api.go b/api/twitter_api.go index 609ab11..2e9ae0a 100644 --- a/api/twitter_api.go +++ b/api/twitter_api.go @@ -63,12 +63,12 @@ func (t *Twitter) CreateMedia(tweetID string, author *core.User) ([]*core.Media, switch len(media) { case 0: - return []*core.Media{{URL: "", Caption: t.makeCaption(author.Username, tweet), Type: core.Text}}, nil + return []*core.Media{{URL: "", Caption: t.makeCaption(author.Username, tweet), Type: core.TText}}, nil case 1: if media[0].Type == "video" || media[0].Type == "animated_gif" { - return []*core.Media{{URL: media[0].VideoInfo.best().URL, Caption: t.makeCaption(author.Username, tweet), Type: core.Video}}, nil + return []*core.Media{{URL: media[0].VideoInfo.best().URL, Caption: t.makeCaption(author.Username, tweet), Type: core.TVideo}}, nil } else if media[0].Type == "photo" { - return []*core.Media{{URL: media[0].MediaURL, Caption: t.makeCaption(author.Username, tweet), Type: core.Photo}}, nil + return []*core.Media{{URL: media[0].MediaURL, Caption: t.makeCaption(author.Username, tweet), Type: core.TPhoto}}, nil } else { return nil, errors.New("Unknown type: " + media[0].Type) } @@ -76,7 +76,7 @@ func (t *Twitter) CreateMedia(tweetID string, author *core.User) ([]*core.Media, // t.sendAlbum(media, tweet, m) medias := []*core.Media{} for _, m := range media { - medias = append(medias, &core.Media{URL: m.MediaURL, Caption: t.makeCaption(author.Username, tweet), Type: core.Photo}) + medias = append(medias, &core.Media{URL: m.MediaURL, Caption: t.makeCaption(author.Username, tweet), Type: core.TPhoto}) } return medias, nil } diff --git a/api/video_file_factory.go b/api/video_file_factory.go index c8223ea..8259cae 100644 --- a/api/video_file_factory.go +++ b/api/video_file_factory.go @@ -5,7 +5,7 @@ import ( tb "gopkg.in/tucnak/telebot.v2" ) -func makeVideoFile(vf *core.VideoFile, caption string) tb.Video { +func makeVideo(vf *core.Video, caption string) tb.Video { video := tb.Video{File: tb.FromDisk(vf.Path)} video.Width = vf.Width video.Height = vf.Height diff --git a/api/video_file_handler.go b/api/video_file_handler.go index 803369c..4ca1bb6 100644 --- a/api/video_file_handler.go +++ b/api/video_file_handler.go @@ -2,7 +2,7 @@ package api import "github.com/ailinykh/pullanusbot/v2/core" -// IVdeoFileHandler interface for processing VideoFiles +// IVdeoFileHandler interface for processing Videos type IVdeoFileHandler interface { - HandleVideoFile(*core.VideoFile, core.IBot) error + HandleVideo(*core.Video, core.IBot) error } diff --git a/api/youtube_api.go b/api/youtube_api.go index ede7c10..e0729f2 100644 --- a/api/youtube_api.go +++ b/api/youtube_api.go @@ -30,13 +30,13 @@ func (y *YoutubeAPI) CreateMedia(url string, author *core.User) ([]*core.Media, URL: video.ID, Caption: video.Title, Duration: video.Duration, - Type: core.Video, + Type: core.TVideo, }, }, nil } -// CreateVideoFile is a core.IVideoFileFactory interface implementation -func (y *YoutubeAPI) CreateVideoFile(youtubeID string) (*core.VideoFile, error) { +// CreateVideo is a core.IVideoFactory interface implementation +func (y *YoutubeAPI) CreateVideo(youtubeID string) (*core.Video, error) { video, err := y.getInfo(youtubeID) if err != nil { return nil, err @@ -52,7 +52,7 @@ func (y *YoutubeAPI) CreateVideoFile(youtubeID string) (*core.VideoFile, error) return nil, err } - thumb, err := y.fd.Download(video.thumb().URL) // will be disposed with VideoFile + thumb, err := y.fd.Download(video.thumb().URL) // will be disposed with Video if err != nil { return nil, err } @@ -62,7 +62,7 @@ func (y *YoutubeAPI) CreateVideoFile(youtubeID string) (*core.VideoFile, error) return nil, err } - return &core.VideoFile{ + return &core.Video{ File: core.File{Name: name, Path: path, Size: int64(format.Filesize)}, Width: format.Width, Height: format.Height, diff --git a/core/bot.go b/core/bot.go index f1e84af..27d982e 100644 --- a/core/bot.go +++ b/core/bot.go @@ -8,5 +8,5 @@ type IBot interface { SendAlbum([]*Image) ([]*Message, error) SendMedia(*Media) (*Message, error) SendPhotoAlbum([]*Media) ([]*Message, error) - SendVideoFile(*VideoFile, string) (*Message, error) + SendVideo(*Video, string) (*Message, error) } diff --git a/core/media.go b/core/media.go index 468bc67..fe7e45c 100644 --- a/core/media.go +++ b/core/media.go @@ -5,11 +5,11 @@ type MediaType int const ( // Video media type - Video MediaType = iota + TVideo MediaType = iota // Photo media type - Photo + TPhoto // Text media type - Text + TText ) // Media ... diff --git a/core/video_file.go b/core/video.go similarity index 74% rename from core/video_file.go rename to core/video.go index ab67bfb..ba8ec50 100644 --- a/core/video_file.go +++ b/core/video.go @@ -2,8 +2,8 @@ package core import "os" -// VideoFile ... -type VideoFile struct { +// Video ... +type Video struct { File Width int Height int @@ -14,7 +14,7 @@ type VideoFile struct { } // Dispose to cleanup filesystem -func (vf *VideoFile) Dispose() { +func (vf *Video) Dispose() { os.Remove(vf.Path) os.Remove(vf.ThumbPath) } diff --git a/core/video_converter.go b/core/video_converter.go new file mode 100644 index 0000000..1477e5a --- /dev/null +++ b/core/video_converter.go @@ -0,0 +1,6 @@ +package core + +// IVideoConverter convert Video with specified bitrate +type IVideoConverter interface { + Convert(*Video, int) (*Video, error) +} diff --git a/core/video_factory.go b/core/video_factory.go new file mode 100644 index 0000000..51a59d7 --- /dev/null +++ b/core/video_factory.go @@ -0,0 +1,6 @@ +package core + +// IVideoFactory retreives video file parameters from file on disk +type IVideoFactory interface { + CreateVideo(path string) (*Video, error) +} diff --git a/core/video_file_converter.go b/core/video_file_converter.go deleted file mode 100644 index b4921ed..0000000 --- a/core/video_file_converter.go +++ /dev/null @@ -1,6 +0,0 @@ -package core - -// IVideoFileConverter convert VideoFile with specified bitrate -type IVideoFileConverter interface { - Convert(*VideoFile, int) (*VideoFile, error) -} diff --git a/core/video_file_factory.go b/core/video_file_factory.go deleted file mode 100644 index c645cf0..0000000 --- a/core/video_file_factory.go +++ /dev/null @@ -1,6 +0,0 @@ -package core - -// IVideoFileFactory retreives video file parameters from file on disk -type IVideoFileFactory interface { - CreateVideoFile(path string) (*VideoFile, error) -} diff --git a/core/video_file_splitter.go b/core/video_file_splitter.go deleted file mode 100644 index c036c3a..0000000 --- a/core/video_file_splitter.go +++ /dev/null @@ -1,6 +0,0 @@ -package core - -// IVideoFileSplitter convert VideoFile with specified bitrate -type IVideoFileSplitter interface { - Split(*VideoFile, int) ([]*VideoFile, error) -} diff --git a/core/video_splitter.go b/core/video_splitter.go new file mode 100644 index 0000000..9d4bef3 --- /dev/null +++ b/core/video_splitter.go @@ -0,0 +1,6 @@ +package core + +// IVideoSplitter convert Video with specified bitrate +type IVideoSplitter interface { + Split(*Video, int) ([]*Video, error) +} diff --git a/infrastructure/ffmpeg_converter.go b/infrastructure/ffmpeg_converter.go index 7f0b313..86eb03f 100644 --- a/infrastructure/ffmpeg_converter.go +++ b/infrastructure/ffmpeg_converter.go @@ -18,13 +18,13 @@ func CreateFfmpegConverter(l core.ILogger) *FfmpegConverter { return &FfmpegConverter{l} } -// FfmpegConverter implements core.IVideoFileConverter and core.IVideoFileFactory using ffmpeg +// FfmpegConverter implements core.IVideoConverter and core.IVideoFactory using ffmpeg type FfmpegConverter struct { l core.ILogger } -// Convert is a core.IVideoFileConverter interface implementation -func (c *FfmpegConverter) Convert(vf *core.VideoFile, bitrate int) (*core.VideoFile, error) { +// Convert is a core.IVideoConverter interface implementation +func (c *FfmpegConverter) Convert(vf *core.Video, bitrate int) (*core.Video, error) { path := path.Join(os.TempDir(), vf.Name+"_converted.mp4") cmd := fmt.Sprintf(`ffmpeg -y -i "%s" -pix_fmt yuv420p -vf "scale=trunc(iw/2)*2:trunc(ih/2)*2" "%s"`, vf.Path, path) if bitrate > 0 { @@ -38,13 +38,13 @@ func (c *FfmpegConverter) Convert(vf *core.VideoFile, bitrate int) (*core.VideoF return nil, err } - return c.CreateVideoFile(path) + return c.CreateVideo(path) } -// CreateVideoFile is a core.IVideoFileSplitter interface implementation -func (c *FfmpegConverter) Split(video *core.VideoFile, limit int) ([]*core.VideoFile, error) { +// CreateVideo is a core.IVideoSplitter interface implementation +func (c *FfmpegConverter) Split(video *core.Video, limit int) ([]*core.Video, error) { duration, n := 0, 0 - var videos = []*core.VideoFile{} + var videos = []*core.Video{} for duration < video.Duration { path := fmt.Sprintf("%s-%d.mp4", video.File.Path, n) cmd := fmt.Sprintf(`ffmpeg -i %s -ss %d -fs %d %s`, video.File.Path, duration, limit, path) @@ -54,7 +54,7 @@ func (c *FfmpegConverter) Split(video *core.VideoFile, limit int) ([]*core.Video return nil, err } - file, err := c.CreateVideoFile(path) + file, err := c.CreateVideo(path) if err != nil { return nil, err } @@ -67,8 +67,8 @@ func (c *FfmpegConverter) Split(video *core.VideoFile, limit int) ([]*core.Video return videos, nil } -// CreateVideoFile is a core.IVideoFileFactory interface implementation -func (c *FfmpegConverter) CreateVideoFile(path string) (*core.VideoFile, error) { +// CreateVideo is a core.IVideoFactory interface implementation +func (c *FfmpegConverter) CreateVideo(path string) (*core.Video, error) { ffprobe, err := c.getFFProbe(path) if err != nil { return nil, err @@ -103,7 +103,7 @@ func (c *FfmpegConverter) CreateVideoFile(path string) (*core.VideoFile, error) return nil, err } - return &core.VideoFile{ + return &core.Video{ File: core.File{Name: stat.Name(), Path: path, Size: stat.Size()}, Width: stream.Width, Height: stream.Height, diff --git a/usecases/faggot_game_test.go b/usecases/faggot_game_test.go index 066e88c..c211ebd 100644 --- a/usecases/faggot_game_test.go +++ b/usecases/faggot_game_test.go @@ -260,12 +260,12 @@ type BotMock struct { messages []string } -func (BotMock) Delete(*core.Message) error { return nil } -func (BotMock) SendImage(*core.Image) (*core.Message, error) { return nil, nil } -func (BotMock) SendAlbum([]*core.Image) ([]*core.Message, error) { return nil, nil } -func (BotMock) SendMedia(*core.Media) (*core.Message, error) { return nil, nil } -func (BotMock) SendPhotoAlbum([]*core.Media) ([]*core.Message, error) { return nil, nil } -func (BotMock) SendVideoFile(*core.VideoFile, string) (*core.Message, error) { return nil, nil } +func (BotMock) Delete(*core.Message) error { return nil } +func (BotMock) SendImage(*core.Image) (*core.Message, error) { return nil, nil } +func (BotMock) SendAlbum([]*core.Image) ([]*core.Message, error) { return nil, nil } +func (BotMock) SendMedia(*core.Media) (*core.Message, error) { return nil, nil } +func (BotMock) SendPhotoAlbum([]*core.Media) ([]*core.Message, error) { return nil, nil } +func (BotMock) SendVideo(*core.Video, string) (*core.Message, error) { return nil, nil } func (b *BotMock) SendText(text string, args ...interface{}) (*core.Message, error) { b.messages = append(b.messages, text) diff --git a/usecases/link_flow.go b/usecases/link_flow.go index a09ebd2..7ba6288 100644 --- a/usecases/link_flow.go +++ b/usecases/link_flow.go @@ -11,7 +11,7 @@ import ( ) // CreateLinkFlow is a basic LinkFlow factory -func CreateLinkFlow(l core.ILogger, fd core.IFileDownloader, vff core.IVideoFileFactory, vfc core.IVideoFileConverter) *LinkFlow { +func CreateLinkFlow(l core.ILogger, fd core.IFileDownloader, vff core.IVideoFactory, vfc core.IVideoConverter) *LinkFlow { return &LinkFlow{l, fd, vff, vfc} } @@ -19,8 +19,8 @@ func CreateLinkFlow(l core.ILogger, fd core.IFileDownloader, vff core.IVideoFile type LinkFlow struct { l core.ILogger fd core.IFileDownloader - vff core.IVideoFileFactory - vfc core.IVideoFileConverter + vff core.IVideoFactory + vfc core.IVideoConverter } // HandleText is a core.ITextHandler protocol implementation @@ -70,7 +70,7 @@ func (lf *LinkFlow) processLink(message *core.Message, bot core.IBot) error { } defer vfc.Dispose() - _, err = bot.SendVideoFile(vfc, media.Caption) + _, err = bot.SendVideo(vfc, media.Caption) if err != nil { return err } @@ -91,11 +91,11 @@ func (lf *LinkFlow) sendByUploading(media *core.Media, bot core.IBot) error { return err } defer vf.Dispose() - _, err = bot.SendVideoFile(vf, media.Caption) + _, err = bot.SendVideo(vf, media.Caption) return err } -func (lf *LinkFlow) downloadMedia(media *core.Media) (*core.VideoFile, error) { +func (lf *LinkFlow) downloadMedia(media *core.Media) (*core.Video, error) { file, err := lf.fd.Download(media.URL) if err != nil { lf.l.Errorf("video download error: %v", err) @@ -109,7 +109,7 @@ func (lf *LinkFlow) downloadMedia(media *core.Media) (*core.VideoFile, error) { lf.l.Infof("File downloaded: %s %0.2fMB", file.Name, float64(stat.Size())/1024/1024) - vf, err := lf.vff.CreateVideoFile(file.Path) + vf, err := lf.vff.CreateVideo(file.Path) if err != nil { lf.l.Errorf("Can't create video file for %s, %v", file.Path, err) return nil, err diff --git a/usecases/twitter_flow.go b/usecases/twitter_flow.go index 79163e5..ec76791 100644 --- a/usecases/twitter_flow.go +++ b/usecases/twitter_flow.go @@ -14,7 +14,7 @@ import ( ) // CreateTwitterFlow is a basic TwitterFlow factory -func CreateTwitterFlow(l core.ILogger, mf core.IMediaFactory, fd core.IFileDownloader, vff core.IVideoFileFactory) *TwitterFlow { +func CreateTwitterFlow(l core.ILogger, mf core.IMediaFactory, fd core.IFileDownloader, vff core.IVideoFactory) *TwitterFlow { return &TwitterFlow{l, mf, fd, vff, make(map[core.Message]core.Message)} } @@ -23,7 +23,7 @@ type TwitterFlow struct { l core.ILogger mf core.IMediaFactory fd core.IFileDownloader - vff core.IVideoFileFactory + vff core.IVideoFactory timeoutReplies map[core.Message]core.Message } @@ -71,7 +71,7 @@ func (tf *TwitterFlow) handleMedia(media []*core.Media, message *core.Message, b return errors.New("unexpected 0 media count") case 1: _, err := bot.SendMedia(media[0]) - if err != nil && media[0].Type == core.Video { + if err != nil && media[0].Type == core.TVideo { if strings.Contains(err.Error(), "failed to get HTTP URL content") || strings.Contains(err.Error(), "wrong file identifier/HTTP URL specified") { return tf.fallbackToUploading(media[0], bot) } @@ -125,12 +125,12 @@ func (tf *TwitterFlow) fallbackToUploading(media *core.Media, bot core.IBot) err tf.l.Infof("File downloaded: %s %0.2fMB", file.Name, float64(stat.Size())/1024/1024) - vf, err := tf.vff.CreateVideoFile(file.Path) + vf, err := tf.vff.CreateVideo(file.Path) if err != nil { tf.l.Errorf("Can't create video file for %s, %v", file.Path, err) return err } defer vf.Dispose() - _, err = bot.SendVideoFile(vf, media.Caption) + _, err = bot.SendVideo(vf, media.Caption) return err } diff --git a/usecases/video_flow.go b/usecases/video_flow.go index 36dc508..f49f51d 100644 --- a/usecases/video_flow.go +++ b/usecases/video_flow.go @@ -9,20 +9,20 @@ import ( ) // CreateVideoFlow is a basic VideoFlow factory -func CreateVideoFlow(l core.ILogger, f core.IVideoFileFactory, c core.IVideoFileConverter) *VideoFlow { +func CreateVideoFlow(l core.ILogger, f core.IVideoFactory, c core.IVideoConverter) *VideoFlow { return &VideoFlow{c, f, l} } // VideoFlow represents convert file to video logic type VideoFlow struct { - c core.IVideoFileConverter - f core.IVideoFileFactory + c core.IVideoConverter + f core.IVideoFactory l core.ILogger } // HandleDocument is a core.IDocumentHandler protocol implementation func (f *VideoFlow) HandleDocument(document *core.Document, b core.IBot) error { - vf, err := f.f.CreateVideoFile(document.FilePath) + vf, err := f.f.CreateVideo(document.FilePath) if err != nil { f.l.Error(err) b.SendText(err.Error()) @@ -43,7 +43,7 @@ func (f *VideoFlow) HandleDocument(document *core.Document, b core.IBot) error { fi1, _ := os.Stat(vf.Path) fi2, _ := os.Stat(cvf.Path) caption := fmt.Sprintf("%s (by %s)\nOriginal size: %.2f MB (%d kb/s)\nConverted size: %.2f MB (%d kb/s)", vf.Name, document.Author, float32(fi1.Size())/1048576, vf.Bitrate/1024, float32(fi2.Size())/1048576, cvf.Bitrate/1024) - _, err = b.SendVideoFile(cvf, caption) + _, err = b.SendVideo(cvf, caption) return err } @@ -56,12 +56,12 @@ func (f *VideoFlow) HandleDocument(document *core.Document, b core.IBot) error { } defer cvf.Dispose() caption := fmt.Sprintf("%s (by %s)", vf.Name, document.Author) - _, err = b.SendVideoFile(cvf, caption) + _, err = b.SendVideo(cvf, caption) return err } f.l.Infof("No need to convert %s", vf.Name) caption := fmt.Sprintf("%s (by %s)", vf.Name, document.Author) - _, err = b.SendVideoFile(vf, caption) + _, err = b.SendVideo(vf, caption) return err } diff --git a/usecases/youtube_flow.go b/usecases/youtube_flow.go index c8377ee..8fe9521 100644 --- a/usecases/youtube_flow.go +++ b/usecases/youtube_flow.go @@ -8,7 +8,7 @@ import ( "github.com/ailinykh/pullanusbot/v2/core" ) -func CreateYoutubeFlow(l core.ILogger, mf core.IMediaFactory, vff core.IVideoFileFactory, vfs core.IVideoFileSplitter) *YoutubeFlow { +func CreateYoutubeFlow(l core.ILogger, mf core.IMediaFactory, vff core.IVideoFactory, vfs core.IVideoSplitter) *YoutubeFlow { return &YoutubeFlow{l: l, mf: mf, vff: vff, vfs: vfs} } @@ -16,8 +16,8 @@ type YoutubeFlow struct { m sync.Mutex l core.ILogger mf core.IMediaFactory - vff core.IVideoFileFactory - vfs core.IVideoFileSplitter + vff core.IVideoFactory + vfs core.IVideoSplitter } // HandleText is a core.ITextHandler protocol implementation @@ -47,14 +47,14 @@ func (f *YoutubeFlow) process(url string, message *core.Message, bot core.IBot) youtubeID, title := media[0].URL, media[0].Caption f.l.Infof("downloading %s", youtubeID) - file, err := f.vff.CreateVideoFile(youtubeID) + file, err := f.vff.CreateVideo(youtubeID) if err != nil { return err } defer file.Dispose() caption := fmt.Sprintf(`🔗 %s (by %s)`, youtubeID, title, message.Sender.Username) - _, err = bot.SendVideoFile(file, caption) + _, err = bot.SendVideo(file, caption) if err != nil { f.l.Error("Can't send video: ", err) if err.Error() == "telegram: Request Entity Too Large (400)" { @@ -70,7 +70,7 @@ func (f *YoutubeFlow) process(url string, message *core.Message, bot core.IBot) for i, file := range files { caption := fmt.Sprintf(`🔗 [%d/%d] %s (by %s)`, youtubeID, i+1, len(files), title, message.Sender.Username) - _, err := bot.SendVideoFile(file, caption) + _, err := bot.SendVideo(file, caption) if err != nil { return err } From 392a1ca87002d615b56e8f12209ab6c48807c2b4 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Mon, 21 Jun 2021 20:22:49 +0300 Subject: [PATCH 070/439] Add logger into youtube api --- api/youtube_api.go | 16 ++++++++++++---- infrastructure/ffmpeg_converter.go | 5 +++-- pullanusbot.go | 2 +- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/api/youtube_api.go b/api/youtube_api.go index e0729f2..c551189 100644 --- a/api/youtube_api.go +++ b/api/youtube_api.go @@ -10,11 +10,12 @@ import ( "github.com/ailinykh/pullanusbot/v2/core" ) -func CreateYoutubeAPI(fd core.IFileDownloader) *YoutubeAPI { - return &YoutubeAPI{fd} +func CreateYoutubeAPI(l core.ILogger, fd core.IFileDownloader) *YoutubeAPI { + return &YoutubeAPI{l, fd} } type YoutubeAPI struct { + l core.ILogger fd core.IFileDownloader } @@ -42,13 +43,19 @@ func (y *YoutubeAPI) CreateVideo(youtubeID string) (*core.Video, error) { return nil, err } + // formats := video.availableFormats() + // for _, f := range formats { + // y.l.Info(f) + // } + ytDlFormat := "134" name := "youtube-" + youtubeID + "-" + ytDlFormat + ".mp4" path := path.Join(os.TempDir(), name) cmd := fmt.Sprintf("youtube-dl -f %s+140 %s -o %s", ytDlFormat, youtubeID, path) - err = exec.Command("/bin/sh", "-c", cmd).Run() + out, err := exec.Command("/bin/sh", "-c", cmd).CombinedOutput() if err != nil { + y.l.Error(string(out)) return nil, err } @@ -73,7 +80,7 @@ func (y *YoutubeAPI) CreateVideo(youtubeID string) (*core.Video, error) { }, nil } -func (YoutubeAPI) getInfo(url string) (*Video, error) { +func (y *YoutubeAPI) getInfo(url string) (*Video, error) { cmd := fmt.Sprintf(`youtube-dl -j %s`, url) out, err := exec.Command("/bin/sh", "-c", cmd).CombinedOutput() if err != nil { @@ -83,6 +90,7 @@ func (YoutubeAPI) getInfo(url string) (*Video, error) { var video Video err = json.Unmarshal(out, &video) if err != nil { + y.l.Error(string(out)) return nil, err } return &video, nil diff --git a/infrastructure/ffmpeg_converter.go b/infrastructure/ffmpeg_converter.go index 86eb03f..b1c0c17 100644 --- a/infrastructure/ffmpeg_converter.go +++ b/infrastructure/ffmpeg_converter.go @@ -34,7 +34,7 @@ func (c *FfmpegConverter) Convert(vf *core.Video, bitrate int) (*core.Video, err out, err := exec.Command("/bin/sh", "-c", cmd).CombinedOutput() if err != nil { os.Remove(path) - c.l.Error(out) + c.l.Error(string(out)) return nil, err } @@ -49,8 +49,9 @@ func (c *FfmpegConverter) Split(video *core.Video, limit int) ([]*core.Video, er path := fmt.Sprintf("%s-%d.mp4", video.File.Path, n) cmd := fmt.Sprintf(`ffmpeg -i %s -ss %d -fs %d %s`, video.File.Path, duration, limit, path) c.l.Info(strings.ReplaceAll(cmd, os.TempDir(), "$TMPDIR/")) - _, err := exec.Command("/bin/sh", "-c", cmd).CombinedOutput() + out, err := exec.Command("/bin/sh", "-c", cmd).CombinedOutput() if err != nil { + c.l.Error(string(out)) return nil, err } diff --git a/pullanusbot.go b/pullanusbot.go index 2d9a301..9378a79 100644 --- a/pullanusbot.go +++ b/pullanusbot.go @@ -54,7 +54,7 @@ func main() { telebot.AddHandler(publisherFlow) telebot.AddHandler("/loh666", publisherFlow.HandleRequest) - youtubeAPI := api.CreateYoutubeAPI(fileDownloader) + youtubeAPI := api.CreateYoutubeAPI(logger, fileDownloader) youtubeFlow := usecases.CreateYoutubeFlow(logger, youtubeAPI, youtubeAPI, converter) telebot.AddHandler(youtubeFlow) From 604c30f5a305e4496041872dc2e93ac7bfd8c100 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Mon, 21 Jun 2021 22:20:44 +0300 Subject: [PATCH 071/439] Do not use hardcoded video formats in youtube api --- api/youtube.go | 2 +- api/youtube_api.go | 49 +++++++++++++++++++++++++++++++--------------- 2 files changed, 34 insertions(+), 17 deletions(-) diff --git a/api/youtube.go b/api/youtube.go index f0bb7e7..a9dd152 100644 --- a/api/youtube.go +++ b/api/youtube.go @@ -31,7 +31,7 @@ func (v Video) availableFormats() []*Format { rv := []*Format{} for _, f := range v.Formats { if f.Ext == "mp4" { // webm not friendly for iPhone - if f.VCodec != "none" && f.ACodec == "none" { + if f.VCodec != "none" && f.ACodec == "none" { // got zero Filesize if no audio if strings.HasSuffix(f.FormatNote, "p") || strings.Contains(f.FormatNote, "DASH") { // skip 720p60 rv = append(rv, f) } diff --git a/api/youtube_api.go b/api/youtube_api.go index c551189..0ce8224 100644 --- a/api/youtube_api.go +++ b/api/youtube_api.go @@ -6,6 +6,7 @@ import ( "os" "os/exec" "path" + "strings" "github.com/ailinykh/pullanusbot/v2/core" ) @@ -43,16 +44,16 @@ func (y *YoutubeAPI) CreateVideo(youtubeID string) (*core.Video, error) { return nil, err } - // formats := video.availableFormats() - // for _, f := range formats { - // y.l.Info(f) - // } + vf, af, err := y.getFormats(video) + if err != nil { + return nil, err + } - ytDlFormat := "134" - name := "youtube-" + youtubeID + "-" + ytDlFormat + ".mp4" + name := fmt.Sprintf("youtube-%s-%s-%s.mp4", youtubeID, vf.FormatID, af.FormatID) path := path.Join(os.TempDir(), name) - cmd := fmt.Sprintf("youtube-dl -f %s+140 %s -o %s", ytDlFormat, youtubeID, path) + cmd := fmt.Sprintf("youtube-dl -f %s+%s %s -o %s", vf.FormatID, af.FormatID, youtubeID, path) + y.l.Info(strings.ReplaceAll(cmd, os.TempDir(), "$TMPDIR/")) out, err := exec.Command("/bin/sh", "-c", cmd).CombinedOutput() if err != nil { y.l.Error(string(out)) @@ -64,18 +65,13 @@ func (y *YoutubeAPI) CreateVideo(youtubeID string) (*core.Video, error) { return nil, err } - format, err := video.formatByID(ytDlFormat) - if err != nil { - return nil, err - } - return &core.Video{ - File: core.File{Name: name, Path: path, Size: int64(format.Filesize)}, - Width: format.Width, - Height: format.Height, + File: core.File{Name: name, Path: path, Size: int64(vf.Filesize)}, + Width: vf.Width, + Height: vf.Height, Bitrate: 0, Duration: video.Duration, - Codec: format.VCodec, + Codec: vf.VCodec, ThumbPath: thumb.Path, }, nil } @@ -95,3 +91,24 @@ func (y *YoutubeAPI) getInfo(url string) (*Video, error) { } return &video, nil } + +func (y *YoutubeAPI) getFormats(video *Video) (*Format, *Format, error) { + af, err := video.audioFormat() + if err != nil { + return nil, nil, err + } + + vf, err := video.formatByID("134") + if err != nil { + formats := video.availableFormats() + for _, f := range formats { + y.l.Info(f) + } + if len(formats) == 0 { + return nil, nil, err + } + vf = formats[len(formats)-1] + } + + return vf, af, nil +} From 7ba0fb0f46b4cf8cc06c343174a0f651ea68ed85 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Mon, 21 Jun 2021 22:25:07 +0300 Subject: [PATCH 072/439] =?UTF-8?q?Replace=20=F0=9F=8E=9E=20with=20?= =?UTF-8?q?=F0=9F=94=97=20and=20vise=20versa?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- usecases/link_flow.go | 2 +- usecases/youtube_flow.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/usecases/link_flow.go b/usecases/link_flow.go index 7ba6288..43c1d55 100644 --- a/usecases/link_flow.go +++ b/usecases/link_flow.go @@ -41,7 +41,7 @@ func (lf *LinkFlow) processLink(message *core.Message, bot core.IBot) error { } media := &core.Media{URL: resp.Request.URL.String()} - media.Caption = fmt.Sprintf(`🎞 %s (by %s)`, message.Text, path.Base(resp.Request.URL.Path), message.Sender.Username) + media.Caption = fmt.Sprintf(`🔗 %s (by %s)`, message.Text, path.Base(resp.Request.URL.Path), message.Sender.Username) switch resp.Header["Content-Type"][0] { case "video/mp4": diff --git a/usecases/youtube_flow.go b/usecases/youtube_flow.go index 8fe9521..57bb8c2 100644 --- a/usecases/youtube_flow.go +++ b/usecases/youtube_flow.go @@ -53,7 +53,7 @@ func (f *YoutubeFlow) process(url string, message *core.Message, bot core.IBot) } defer file.Dispose() - caption := fmt.Sprintf(`🔗 %s (by %s)`, youtubeID, title, message.Sender.Username) + caption := fmt.Sprintf(`🎞 %s (by %s)`, youtubeID, title, message.Sender.Username) _, err = bot.SendVideo(file, caption) if err != nil { f.l.Error("Can't send video: ", err) @@ -69,7 +69,7 @@ func (f *YoutubeFlow) process(url string, message *core.Message, bot core.IBot) } for i, file := range files { - caption := fmt.Sprintf(`🔗 [%d/%d] %s (by %s)`, youtubeID, i+1, len(files), title, message.Sender.Username) + caption := fmt.Sprintf(`🎞 [%d/%d] %s (by %s)`, youtubeID, i+1, len(files), title, message.Sender.Username) _, err := bot.SendVideo(file, caption) if err != nil { return err From bf59ab152bba3caf7a3dbdadc856cb12b45164a8 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Tue, 22 Jun 2021 11:21:25 +0300 Subject: [PATCH 073/439] More flexible regular expression --- usecases/youtube_flow.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/usecases/youtube_flow.go b/usecases/youtube_flow.go index 57bb8c2..93755de 100644 --- a/usecases/youtube_flow.go +++ b/usecases/youtube_flow.go @@ -22,7 +22,7 @@ type YoutubeFlow struct { // HandleText is a core.ITextHandler protocol implementation func (f *YoutubeFlow) HandleText(message *core.Message, bot core.IBot) error { - r := regexp.MustCompile(`https?:\/\/(www\.)?youtu[\.be|\.com]\S+`) + r := regexp.MustCompile(`https?:\/\/(www\.|m\.)?youtu[\.be|\.com]\S+`) match := r.FindStringSubmatch(message.Text) if len(match) > 0 { return f.process(match[0], message, bot) From 2ea7c807358d65960293309ab4154457a0b7cd79 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Tue, 22 Jun 2021 11:22:53 +0300 Subject: [PATCH 074/439] Return string(out) instead of just error. Maybe I'm wrong --- api/youtube_api.go | 9 +++++---- infrastructure/ffmpeg_converter.go | 8 ++++---- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/api/youtube_api.go b/api/youtube_api.go index 0ce8224..81191fc 100644 --- a/api/youtube_api.go +++ b/api/youtube_api.go @@ -2,6 +2,7 @@ package api import ( "encoding/json" + "errors" "fmt" "os" "os/exec" @@ -56,8 +57,8 @@ func (y *YoutubeAPI) CreateVideo(youtubeID string) (*core.Video, error) { y.l.Info(strings.ReplaceAll(cmd, os.TempDir(), "$TMPDIR/")) out, err := exec.Command("/bin/sh", "-c", cmd).CombinedOutput() if err != nil { - y.l.Error(string(out)) - return nil, err + y.l.Error(err) + return nil, errors.New(string(out)) } thumb, err := y.fd.Download(video.thumb().URL) // will be disposed with Video @@ -80,13 +81,13 @@ func (y *YoutubeAPI) getInfo(url string) (*Video, error) { cmd := fmt.Sprintf(`youtube-dl -j %s`, url) out, err := exec.Command("/bin/sh", "-c", cmd).CombinedOutput() if err != nil { - return nil, err + y.l.Error(err) + return nil, errors.New(string(out)) } var video Video err = json.Unmarshal(out, &video) if err != nil { - y.l.Error(string(out)) return nil, err } return &video, nil diff --git a/infrastructure/ffmpeg_converter.go b/infrastructure/ffmpeg_converter.go index b1c0c17..543eeb0 100644 --- a/infrastructure/ffmpeg_converter.go +++ b/infrastructure/ffmpeg_converter.go @@ -34,8 +34,8 @@ func (c *FfmpegConverter) Convert(vf *core.Video, bitrate int) (*core.Video, err out, err := exec.Command("/bin/sh", "-c", cmd).CombinedOutput() if err != nil { os.Remove(path) - c.l.Error(string(out)) - return nil, err + c.l.Error(err) + return nil, errors.New(string(out)) } return c.CreateVideo(path) @@ -51,8 +51,8 @@ func (c *FfmpegConverter) Split(video *core.Video, limit int) ([]*core.Video, er c.l.Info(strings.ReplaceAll(cmd, os.TempDir(), "$TMPDIR/")) out, err := exec.Command("/bin/sh", "-c", cmd).CombinedOutput() if err != nil { - c.l.Error(string(out)) - return nil, err + c.l.Error(err) + return nil, errors.New(string(out)) } file, err := c.CreateVideo(path) From e3c8056d76164db55e6483fcb48247a152855fbd Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Wed, 23 Jun 2021 11:27:56 +0300 Subject: [PATCH 075/439] Remove original message with document --- api/telebot.go | 11 +++++------ core/document.go | 6 ++---- core/handlers.go | 2 +- usecases/video_flow.go | 23 +++++++++++++---------- 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/api/telebot.go b/api/telebot.go index 7242ac8..bee91e9 100644 --- a/api/telebot.go +++ b/api/telebot.go @@ -5,6 +5,7 @@ import ( "os" "path" "strconv" + "strings" "sync" "time" @@ -67,16 +68,14 @@ func CreateTelebot(token string, logger core.ILogger) *Telebot { return } - logger.Infof("Downloaded to %s", path) + logger.Infof("Downloaded to %s", strings.ReplaceAll(path, os.TempDir(), "$TMPDIR/")) defer os.Remove(path) for _, h := range telebot.documentHandlers { err := h.HandleDocument(&core.Document{ - Author: m.Sender.Username, - FileName: m.Document.FileName, - FilePath: path, - MIME: m.Document.MIME, - }, &TelebotAdapter{m, telebot}) + File: core.File{Name: m.Document.FileName, Path: path}, + MIME: m.Document.MIME, + }, makeMessage(m), &TelebotAdapter{m, telebot}) if err != nil { logger.Errorf("%T: %s", h, err) telebot.reportError(m, err) diff --git a/core/document.go b/core/document.go index 36eb72c..83f4bf7 100644 --- a/core/document.go +++ b/core/document.go @@ -2,8 +2,6 @@ package core // Document ... type Document struct { - Author string - FileName string - FilePath string - MIME string + File + MIME string } diff --git a/core/handlers.go b/core/handlers.go index d84e0f3..e0c99c5 100644 --- a/core/handlers.go +++ b/core/handlers.go @@ -2,7 +2,7 @@ package core // IDocumentHandler responds to documents sent in chah type IDocumentHandler interface { - HandleDocument(*Document, IBot) error + HandleDocument(*Document, *Message, IBot) error } // ITextHandler responds to all the text messages diff --git a/usecases/video_flow.go b/usecases/video_flow.go index f49f51d..63ed551 100644 --- a/usecases/video_flow.go +++ b/usecases/video_flow.go @@ -21,11 +21,11 @@ type VideoFlow struct { } // HandleDocument is a core.IDocumentHandler protocol implementation -func (f *VideoFlow) HandleDocument(document *core.Document, b core.IBot) error { - vf, err := f.f.CreateVideo(document.FilePath) +func (f *VideoFlow) HandleDocument(document *core.Document, message *core.Message, bot core.IBot) error { + vf, err := f.f.CreateVideo(document.File.Path) if err != nil { f.l.Error(err) - b.SendText(err.Error()) + bot.SendText(err.Error()) return err } defer vf.Dispose() @@ -42,8 +42,8 @@ func (f *VideoFlow) HandleDocument(document *core.Document, b core.IBot) error { defer cvf.Dispose() fi1, _ := os.Stat(vf.Path) fi2, _ := os.Stat(cvf.Path) - caption := fmt.Sprintf("%s (by %s)\nOriginal size: %.2f MB (%d kb/s)\nConverted size: %.2f MB (%d kb/s)", vf.Name, document.Author, float32(fi1.Size())/1048576, vf.Bitrate/1024, float32(fi2.Size())/1048576, cvf.Bitrate/1024) - _, err = b.SendVideo(cvf, caption) + caption := fmt.Sprintf("%s (by %s)\nOriginal size: %.2f MB (%d kb/s)\nConverted size: %.2f MB (%d kb/s)", vf.Name, message.Sender.Username, float32(fi1.Size())/1048576, vf.Bitrate/1024, float32(fi2.Size())/1048576, cvf.Bitrate/1024) + _, err = bot.SendVideo(cvf, caption) return err } @@ -55,13 +55,16 @@ func (f *VideoFlow) HandleDocument(document *core.Document, b core.IBot) error { return err } defer cvf.Dispose() - caption := fmt.Sprintf("%s (by %s)", vf.Name, document.Author) - _, err = b.SendVideo(cvf, caption) + caption := fmt.Sprintf("%s (by %s)", vf.Name, message.Sender.Username) + _, err = bot.SendVideo(cvf, caption) return err } f.l.Infof("No need to convert %s", vf.Name) - caption := fmt.Sprintf("%s (by %s)", vf.Name, document.Author) - _, err = b.SendVideo(vf, caption) - return err + caption := fmt.Sprintf("%s (by %s)", vf.Name, message.Sender.Username) + _, err = bot.SendVideo(vf, caption) + if err != nil { + return err + } + return bot.Delete(message) } From 5c3b461fa1d06150319536226280e653b9d45b20 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Wed, 23 Jun 2021 18:07:28 +0300 Subject: [PATCH 076/439] Handle error on media sent --- api/telebot_adapter.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/api/telebot_adapter.go b/api/telebot_adapter.go index 531eaa2..3a382fe 100644 --- a/api/telebot_adapter.go +++ b/api/telebot_adapter.go @@ -73,6 +73,9 @@ func (a *TelebotAdapter) SendMedia(media *core.Media) (*core.Message, error) { sent, err = a.t.bot.Send(a.m.Chat, media.Caption, &tb.SendOptions{ParseMode: tb.ModeHTML}) } + if err != nil { + return nil, err + } return makeMessage(sent), err } From e969cfc4faf705c9ad154ed64c3641b5acb85042 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Wed, 23 Jun 2021 18:15:37 +0300 Subject: [PATCH 077/439] Do not return error on twitter API timeout --- usecases/twitter_flow.go | 1 + 1 file changed, 1 insertion(+) diff --git a/usecases/twitter_flow.go b/usecases/twitter_flow.go index ec76791..0466673 100644 --- a/usecases/twitter_flow.go +++ b/usecases/twitter_flow.go @@ -49,6 +49,7 @@ func (tf *TwitterFlow) process(tweetID string, message *core.Message, bot core.I return err } tf.timeoutReplies[*message] = *sent + return nil } } return err From 1b98708365b4a035120186f47e013b8b6127b3ff Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Wed, 23 Jun 2021 20:36:21 +0300 Subject: [PATCH 078/439] extract youtube ID since it can start with dash --- api/youtube_api.go | 12 ++++++------ usecases/youtube_flow.go | 30 +++++++++++++++++++----------- 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/api/youtube_api.go b/api/youtube_api.go index 81191fc..c696916 100644 --- a/api/youtube_api.go +++ b/api/youtube_api.go @@ -39,8 +39,8 @@ func (y *YoutubeAPI) CreateMedia(url string, author *core.User) ([]*core.Media, } // CreateVideo is a core.IVideoFactory interface implementation -func (y *YoutubeAPI) CreateVideo(youtubeID string) (*core.Video, error) { - video, err := y.getInfo(youtubeID) +func (y *YoutubeAPI) CreateVideo(id string) (*core.Video, error) { + video, err := y.getInfo(id) if err != nil { return nil, err } @@ -50,10 +50,10 @@ func (y *YoutubeAPI) CreateVideo(youtubeID string) (*core.Video, error) { return nil, err } - name := fmt.Sprintf("youtube-%s-%s-%s.mp4", youtubeID, vf.FormatID, af.FormatID) + name := fmt.Sprintf("youtube-%s-%s-%s.mp4", id, vf.FormatID, af.FormatID) path := path.Join(os.TempDir(), name) - cmd := fmt.Sprintf("youtube-dl -f %s+%s %s -o %s", vf.FormatID, af.FormatID, youtubeID, path) + cmd := fmt.Sprintf("youtube-dl -f %s+%s https://youtu.be/%s -o %s", vf.FormatID, af.FormatID, id, path) y.l.Info(strings.ReplaceAll(cmd, os.TempDir(), "$TMPDIR/")) out, err := exec.Command("/bin/sh", "-c", cmd).CombinedOutput() if err != nil { @@ -77,8 +77,8 @@ func (y *YoutubeAPI) CreateVideo(youtubeID string) (*core.Video, error) { }, nil } -func (y *YoutubeAPI) getInfo(url string) (*Video, error) { - cmd := fmt.Sprintf(`youtube-dl -j %s`, url) +func (y *YoutubeAPI) getInfo(id string) (*Video, error) { + cmd := fmt.Sprintf(`youtube-dl -j https://youtu.be/%s`, id) // id might start with dash, ex: -bdUoHZCf24 out, err := exec.Command("/bin/sh", "-c", cmd).CombinedOutput() if err != nil { y.l.Error(err) diff --git a/usecases/youtube_flow.go b/usecases/youtube_flow.go index 93755de..018266e 100644 --- a/usecases/youtube_flow.go +++ b/usecases/youtube_flow.go @@ -1,8 +1,10 @@ package usecases import ( + "errors" "fmt" "regexp" + "strings" "sync" "github.com/ailinykh/pullanusbot/v2/core" @@ -22,20 +24,26 @@ type YoutubeFlow struct { // HandleText is a core.ITextHandler protocol implementation func (f *YoutubeFlow) HandleText(message *core.Message, bot core.IBot) error { - r := regexp.MustCompile(`https?:\/\/(www\.|m\.)?youtu[\.be|\.com]\S+`) + r := regexp.MustCompile(`youtu\.?be(\.com)?\/(watch\?v=)?([\w\-_]+)`) match := r.FindStringSubmatch(message.Text) - if len(match) > 0 { - return f.process(match[0], message, bot) + if len(match) == 4 { + return f.process(match[3], message, bot) + } + if strings.Contains(message.Text, "youtu") { + for i, m := range match { + f.l.Info(i, " ", m) + } + return errors.New("possibble regexp mismatch: " + message.Text) } return nil } -func (f *YoutubeFlow) process(url string, message *core.Message, bot core.IBot) error { +func (f *YoutubeFlow) process(id string, message *core.Message, bot core.IBot) error { f.m.Lock() defer f.m.Unlock() - f.l.Infof("processing youtube %s", url) - media, err := f.mf.CreateMedia(url, message.Sender) + f.l.Infof("processing %s", id) + media, err := f.mf.CreateMedia(id, message.Sender) if err != nil { return err } @@ -45,15 +53,15 @@ func (f *YoutubeFlow) process(url string, message *core.Message, bot core.IBot) return nil } - youtubeID, title := media[0].URL, media[0].Caption - f.l.Infof("downloading %s", youtubeID) - file, err := f.vff.CreateVideo(youtubeID) + title := media[0].Caption + f.l.Infof("downloading %s", id) + file, err := f.vff.CreateVideo(id) if err != nil { return err } defer file.Dispose() - caption := fmt.Sprintf(`🎞 %s (by %s)`, youtubeID, title, message.Sender.Username) + caption := fmt.Sprintf(`🎞 %s (by %s)`, id, title, message.Sender.Username) _, err = bot.SendVideo(file, caption) if err != nil { f.l.Error("Can't send video: ", err) @@ -69,7 +77,7 @@ func (f *YoutubeFlow) process(url string, message *core.Message, bot core.IBot) } for i, file := range files { - caption := fmt.Sprintf(`🎞 [%d/%d] %s (by %s)`, youtubeID, i+1, len(files), title, message.Sender.Username) + caption := fmt.Sprintf(`🎞 [%d/%d] %s (by %s)`, id, i+1, len(files), title, message.Sender.Username) _, err := bot.SendVideo(file, caption) if err != nil { return err From de85e928a4bcbcb80b778f79a9b662d77472528f Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Wed, 23 Jun 2021 20:36:53 +0300 Subject: [PATCH 079/439] Send error report with page preview disabled --- api/telebot.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/api/telebot.go b/api/telebot.go index bee91e9..9fd110c 100644 --- a/api/telebot.go +++ b/api/telebot.go @@ -160,8 +160,9 @@ func (t *Telebot) reportError(m *tb.Message, e error) { return } chat := &tb.Chat{ID: chatID} - t.bot.Forward(chat, m) - t.bot.Send(chat, e.Error()) + opts := &tb.SendOptions{DisableWebPagePreview: true} + t.bot.Forward(chat, m, opts) + t.bot.Send(chat, e.Error(), opts) } func makeMessage(m *tb.Message) *core.Message { From 158c428c53a2455951d0a5e6f5dc938f695e4356 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Wed, 23 Jun 2021 21:19:40 +0300 Subject: [PATCH 080/439] Add width and height to core.Image --- api/telebot.go | 9 +++++++-- core/image.go | 7 ++----- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/api/telebot.go b/api/telebot.go index 9fd110c..f8c8d5d 100644 --- a/api/telebot.go +++ b/api/telebot.go @@ -86,10 +86,15 @@ func CreateTelebot(token string, logger core.ILogger) *Telebot { bot.Handle(tb.OnPhoto, func(m *tb.Message) { - image := core.CreateImage(m.Photo.FileID, m.Photo.FileURL) + image := &core.Image{ + ID: m.Photo.FileID, + FileURL: m.Photo.FileURL, + Width: m.Photo.Width, + Height: m.Photo.Height, + } for _, h := range telebot.imageHandlers { - err := h.HandleImage(&image, makeMessage(m), &TelebotAdapter{m, telebot}) + err := h.HandleImage(image, makeMessage(m), &TelebotAdapter{m, telebot}) if err != nil { logger.Errorf("%T: %s", h, err) telebot.reportError(m, err) diff --git a/core/image.go b/core/image.go index f3b79d1..39c47d2 100644 --- a/core/image.go +++ b/core/image.go @@ -1,13 +1,10 @@ package core -// CreateImage is an Image factory -func CreateImage(id string, fileURL string) Image { - return Image{ID: id, FileURL: fileURL} -} - // Image represents remote image file that can be also downloaded type Image struct { File ID string FileURL string + Width int + Height int } From 99fea28f9f4855749c36140ca40384e1f902d544 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Wed, 23 Jun 2021 21:59:26 +0300 Subject: [PATCH 081/439] Use core.Image as core.Video thumb --- api/video_file_factory.go | 6 +++- api/youtube_api.go | 21 +++++++----- core/video.go | 14 ++++---- infrastructure/ffmpeg_converter.go | 55 ++++++++++++++++++++---------- 4 files changed, 62 insertions(+), 34 deletions(-) diff --git a/api/video_file_factory.go b/api/video_file_factory.go index 8259cae..a4e44cc 100644 --- a/api/video_file_factory.go +++ b/api/video_file_factory.go @@ -12,6 +12,10 @@ func makeVideo(vf *core.Video, caption string) tb.Video { video.Caption = caption video.Duration = vf.Duration video.SupportsStreaming = true - video.Thumbnail = &tb.Photo{File: tb.FromDisk(vf.ThumbPath)} + video.Thumbnail = &tb.Photo{ + File: tb.FromDisk(vf.Thumb.Path), + Width: vf.Thumb.Width, + Height: vf.Thumb.Height, + } return video } diff --git a/api/youtube_api.go b/api/youtube_api.go index c696916..860bd9f 100644 --- a/api/youtube_api.go +++ b/api/youtube_api.go @@ -61,19 +61,24 @@ func (y *YoutubeAPI) CreateVideo(id string) (*core.Video, error) { return nil, errors.New(string(out)) } - thumb, err := y.fd.Download(video.thumb().URL) // will be disposed with Video + thumb := video.thumb() + file, err := y.fd.Download(thumb.URL) // will be disposed with Video if err != nil { return nil, err } return &core.Video{ - File: core.File{Name: name, Path: path, Size: int64(vf.Filesize)}, - Width: vf.Width, - Height: vf.Height, - Bitrate: 0, - Duration: video.Duration, - Codec: vf.VCodec, - ThumbPath: thumb.Path, + File: core.File{Name: name, Path: path, Size: int64(vf.Filesize)}, + Width: vf.Width, + Height: vf.Height, + Bitrate: 0, + Duration: video.Duration, + Codec: vf.VCodec, + Thumb: core.Image{ + File: *file, + Width: thumb.Width, + Height: thumb.Height, + }, }, nil } diff --git a/core/video.go b/core/video.go index ba8ec50..0c1d4c8 100644 --- a/core/video.go +++ b/core/video.go @@ -5,16 +5,16 @@ import "os" // Video ... type Video struct { File - Width int - Height int - Bitrate int - Duration int - Codec string - ThumbPath string + Width int + Height int + Bitrate int + Duration int + Codec string + Thumb Image } // Dispose to cleanup filesystem func (vf *Video) Dispose() { os.Remove(vf.Path) - os.Remove(vf.ThumbPath) + os.Remove(vf.Thumb.Path) } diff --git a/infrastructure/ffmpeg_converter.go b/infrastructure/ffmpeg_converter.go index 543eeb0..99fdfc1 100644 --- a/infrastructure/ffmpeg_converter.go +++ b/infrastructure/ffmpeg_converter.go @@ -80,23 +80,21 @@ func (c *FfmpegConverter) CreateVideo(path string) (*core.Video, error) { return nil, err } - bitrate, _ := strconv.Atoi(stream.BitRate) // empty for .gif + scale := "320:-1" + if stream.Width < stream.Height { + scale = "-1:320" + } - duration, err := strconv.ParseFloat(ffprobe.Format.Duration, 32) + thumb, err := c.createThumb(path, scale) if err != nil { return nil, err } - thumbpath := path + ".jpg" - scale := "320:-1" - if stream.Width < stream.Height { - scale = "-1:320" - } - cmd := fmt.Sprintf(`ffmpeg -v error -i "%s" -ss 00:00:01.000 -vframes 1 -filter:v scale="%s" "%s"`, path, scale, thumbpath) - out, err := exec.Command("/bin/sh", "-c", cmd).CombinedOutput() + bitrate, _ := strconv.Atoi(stream.BitRate) // empty for .gif + + duration, err := strconv.ParseFloat(ffprobe.Format.Duration, 32) if err != nil { - os.Remove(thumbpath) - return nil, errors.New(string(out)) + return nil, err } stat, err := os.Stat(path) @@ -105,13 +103,13 @@ func (c *FfmpegConverter) CreateVideo(path string) (*core.Video, error) { } return &core.Video{ - File: core.File{Name: stat.Name(), Path: path, Size: stat.Size()}, - Width: stream.Width, - Height: stream.Height, - Bitrate: bitrate, - Duration: int(duration), - Codec: stream.CodecName, - ThumbPath: thumbpath}, nil + File: core.File{Name: stat.Name(), Path: path, Size: stat.Size()}, + Width: stream.Width, + Height: stream.Height, + Bitrate: bitrate, + Duration: int(duration), + Codec: stream.CodecName, + Thumb: *thumb}, nil } func (c *FfmpegConverter) getFFProbe(file string) (*ffpResponse, error) { @@ -129,3 +127,24 @@ func (c *FfmpegConverter) getFFProbe(file string) (*ffpResponse, error) { return &resp, nil } + +func (c *FfmpegConverter) createThumb(videoPath string, scale string) (*core.Image, error) { + thumbPath := videoPath + ".jpg" + + cmd := fmt.Sprintf(`ffmpeg -v error -i "%s" -ss 00:00:01.000 -vframes 1 -filter:v scale="%s" "%s"`, videoPath, scale, thumbPath) + out, err := exec.Command("/bin/sh", "-c", cmd).CombinedOutput() + if err != nil { + return nil, errors.New(string(out)) + } + + ffprobe, err := c.getFFProbe(thumbPath) + if err != nil { + return nil, err + } + + return &core.Image{ + File: core.File{Name: path.Base(thumbPath), Path: thumbPath}, + Width: ffprobe.Streams[0].Width, + Height: ffprobe.Streams[0].Height, + }, nil +} From 16682e14840332cc5ef305d13b906f9b6aebacaf Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Wed, 23 Jun 2021 22:00:09 +0300 Subject: [PATCH 082/439] All rule in Makefile --- Makefile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 8b189d9..9eaed6c 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,9 @@ .PHONY: test run build clean -run: build +all: build run + +run: ./pullanusbot test: From 79bb9f25b367a915e9e1c6cdbec75e45f365dcc4 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Thu, 24 Jun 2021 10:03:16 +0300 Subject: [PATCH 083/439] Remove IVdeoFileHandler interface since it is not needed --- api/video_file_handler.go | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 api/video_file_handler.go diff --git a/api/video_file_handler.go b/api/video_file_handler.go deleted file mode 100644 index 4ca1bb6..0000000 --- a/api/video_file_handler.go +++ /dev/null @@ -1,8 +0,0 @@ -package api - -import "github.com/ailinykh/pullanusbot/v2/core" - -// IVdeoFileHandler interface for processing Videos -type IVdeoFileHandler interface { - HandleVideo(*core.Video, core.IBot) error -} From 68b237d33f5416e311b5ed1de81222f3cb1e69d8 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Thu, 24 Jun 2021 10:06:31 +0300 Subject: [PATCH 084/439] Specify video file name before send --- api/video_file_factory.go | 1 + 1 file changed, 1 insertion(+) diff --git a/api/video_file_factory.go b/api/video_file_factory.go index a4e44cc..c7d0338 100644 --- a/api/video_file_factory.go +++ b/api/video_file_factory.go @@ -7,6 +7,7 @@ import ( func makeVideo(vf *core.Video, caption string) tb.Video { video := tb.Video{File: tb.FromDisk(vf.Path)} + video.FileName = vf.File.Name video.Width = vf.Width video.Height = vf.Height video.Caption = caption From 21058d7068093b75c6c6b8c42f9c14e8e4c72b3b Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Thu, 24 Jun 2021 10:16:46 +0300 Subject: [PATCH 085/439] Specify path for file to download to avoid url paths as filenames --- api/youtube.go | 2 +- api/youtube_api.go | 10 ++++++---- core/networking.go | 2 +- infrastructure/file_downloader.go | 9 ++++----- usecases/link_flow.go | 3 ++- usecases/twitter_flow.go | 6 ++++-- 6 files changed, 18 insertions(+), 14 deletions(-) diff --git a/api/youtube.go b/api/youtube.go index a9dd152..76480ff 100644 --- a/api/youtube.go +++ b/api/youtube.go @@ -53,7 +53,7 @@ func (v Video) formatByID(id string) (*Format, error) { func (v Video) thumb() *Thumbnail { th := v.Thumbnails[0] for _, t := range v.Thumbnails { - if !strings.Contains(t.URL, ".webp") { + if !strings.Contains(t.URL, ".webp") && t.Height < 320 && t.Width < 320 { th = t } } diff --git a/api/youtube_api.go b/api/youtube_api.go index 860bd9f..86a7b27 100644 --- a/api/youtube_api.go +++ b/api/youtube_api.go @@ -51,9 +51,9 @@ func (y *YoutubeAPI) CreateVideo(id string) (*core.Video, error) { } name := fmt.Sprintf("youtube-%s-%s-%s.mp4", id, vf.FormatID, af.FormatID) - path := path.Join(os.TempDir(), name) + videoPath := path.Join(os.TempDir(), name) - cmd := fmt.Sprintf("youtube-dl -f %s+%s https://youtu.be/%s -o %s", vf.FormatID, af.FormatID, id, path) + cmd := fmt.Sprintf("youtube-dl -f %s+%s https://youtu.be/%s -o %s", vf.FormatID, af.FormatID, id, videoPath) y.l.Info(strings.ReplaceAll(cmd, os.TempDir(), "$TMPDIR/")) out, err := exec.Command("/bin/sh", "-c", cmd).CombinedOutput() if err != nil { @@ -62,13 +62,15 @@ func (y *YoutubeAPI) CreateVideo(id string) (*core.Video, error) { } thumb := video.thumb() - file, err := y.fd.Download(thumb.URL) // will be disposed with Video + thumbPath := path.Join(os.TempDir(), name+".jpg") + y.l.Infof("downloading thumb %s", thumb.URL) + file, err := y.fd.Download(thumb.URL, thumbPath) // will be disposed with Video if err != nil { return nil, err } return &core.Video{ - File: core.File{Name: name, Path: path, Size: int64(vf.Filesize)}, + File: core.File{Name: name, Path: videoPath, Size: int64(vf.Filesize)}, Width: vf.Width, Height: vf.Height, Bitrate: 0, diff --git a/core/networking.go b/core/networking.go index 469d31f..c967470 100644 --- a/core/networking.go +++ b/core/networking.go @@ -2,7 +2,7 @@ package core // IFileDownloader turns URL to File type IFileDownloader interface { - Download(URL) (*File, error) + Download(URL, string) (*File, error) } // IFileUploader turns File to URL diff --git a/infrastructure/file_downloader.go b/infrastructure/file_downloader.go index b50831a..b1d227f 100644 --- a/infrastructure/file_downloader.go +++ b/infrastructure/file_downloader.go @@ -18,9 +18,8 @@ func CreateFileDownloader() *FileDownloader { type FileDownloader struct{} // Download is a core.IFileDownloader interface implementation -func (FileDownloader) Download(url core.URL) (*core.File, error) { - name := path.Base(url) - path := path.Join(os.TempDir(), name) +func (FileDownloader) Download(url core.URL, filepath string) (*core.File, error) { + name := path.Base(filepath) // Get the data resp, err := http.Get(url) if err != nil { @@ -29,7 +28,7 @@ func (FileDownloader) Download(url core.URL) (*core.File, error) { defer resp.Body.Close() // Create the file - out, err := os.Create(path) + out, err := os.Create(filepath) if err != nil { return nil, err } @@ -37,5 +36,5 @@ func (FileDownloader) Download(url core.URL) (*core.File, error) { // Write the body to file _, err = io.Copy(out, resp.Body) - return &core.File{Name: name, Path: path}, err + return &core.File{Name: name, Path: filepath}, err } diff --git a/usecases/link_flow.go b/usecases/link_flow.go index 43c1d55..5e0d46b 100644 --- a/usecases/link_flow.go +++ b/usecases/link_flow.go @@ -96,7 +96,8 @@ func (lf *LinkFlow) sendByUploading(media *core.Media, bot core.IBot) error { } func (lf *LinkFlow) downloadMedia(media *core.Media) (*core.Video, error) { - file, err := lf.fd.Download(media.URL) + mediaPath := path.Join(os.TempDir(), path.Base(media.URL)) + file, err := lf.fd.Download(media.URL, mediaPath) if err != nil { lf.l.Errorf("video download error: %v", err) return nil, err diff --git a/usecases/twitter_flow.go b/usecases/twitter_flow.go index 0466673..e324866 100644 --- a/usecases/twitter_flow.go +++ b/usecases/twitter_flow.go @@ -5,6 +5,7 @@ import ( "fmt" "math" "os" + "path" "regexp" "strconv" "strings" @@ -111,13 +112,14 @@ func (tf *TwitterFlow) handleTimeout(err error, tweetID string, message *core.Me func (tf *TwitterFlow) fallbackToUploading(media *core.Media, bot core.IBot) error { // Try to upload file to telegram tf.l.Info("Sending by uploading") - file, err := tf.fd.Download(media.URL) + mediaPath := path.Join(os.TempDir(), path.Base(media.URL)) + file, err := tf.fd.Download(media.URL, mediaPath) if err != nil { tf.l.Errorf("video download error: %v", err) return err } - defer os.Remove(file.Path) + defer file.Dispose() stat, err := os.Stat(file.Path) if err != nil { From bbe796119605f4c78eb352ff68a1c35a479e3e62 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Fri, 25 Jun 2021 22:35:27 +0300 Subject: [PATCH 086/439] Rollback supervisord, telegram-bot-api and ssh stuff from Dockerfile --- Dockerfile | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/Dockerfile b/Dockerfile index 5abf594..f122775 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,17 +8,11 @@ ADD . . RUN CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -a -installsuffix cgo -ldflags '-extldflags "-static"' FROM jrottenberg/ffmpeg:4.1-alpine -RUN apk update && apk add tzdata python3 supervisor openssh --no-cache && \ - ssh-keygen -f /etc/ssh/ssh_host_rsa_key -N '' -t rsa && \ - ssh-keygen -f /etc/ssh/ssh_host_dsa_key -N '' -t dsa && \ +RUN apk update && apk add tzdata python3 --no-cache && \ wget https://yt-dl.org/downloads/latest/youtube-dl -O /usr/local/bin/youtube-dl && \ chmod a+rx /usr/local/bin/youtube-dl && \ ln -s /usr/bin/python3 /usr/bin/python - -COPY --from=builder /go/src/github.com/ailinykh/pullanusbot/pullanusbot /usr/local/bin/pullanusbot -COPY .docker/telegram-bot-api /usr/local/bin/telegram-bot-api -COPY .docker/supervisord.conf /etc/supervisord.conf - +COPY --from=builder /go/src/github.com/ailinykh/pullanusbot/pullanusbot . WORKDIR /usr/local/share VOLUME [ "pullanusbot-data" ] -ENTRYPOINT supervisord -c /etc/supervisord.conf \ No newline at end of file +ENTRYPOINT /go/bin/pullanusbot \ No newline at end of file From 15d23bdd44ed496b55e2b8290615e786da02c64c Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Fri, 25 Jun 2021 22:35:48 +0300 Subject: [PATCH 087/439] Add multiple tags when building docker image --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index aa9e617..0e2484c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -44,7 +44,7 @@ jobs: - name: Build docker container run: | - docker build -t ${{ github.repository }}:$TAG . + docker build -t ${{ github.repository }}:$TAG -t latest . echo $DOCKER_PWD | docker login -u $DOCKER_LOGIN --password-stdin docker push ${{ github.repository }}:$TAG env: From cccfc330d0771c4bd1c1a20588b7a360e94d65bc Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Fri, 25 Jun 2021 22:42:39 +0300 Subject: [PATCH 088/439] Attempt to push multiple tags on success build --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0e2484c..61c2599 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -44,9 +44,9 @@ jobs: - name: Build docker container run: | - docker build -t ${{ github.repository }}:$TAG -t latest . + docker build -t ${{ github.repository }}:$TAG -t ${{ github.repository }}:latest . echo $DOCKER_PWD | docker login -u $DOCKER_LOGIN --password-stdin - docker push ${{ github.repository }}:$TAG + docker push ${{ github.repository }}:$TAG ${{ github.repository }}:latest env: DOCKER_LOGIN: ${{ secrets.DOCKER_LOGIN }} DOCKER_PWD: ${{ secrets.DOCKER_PWD }} From 64517db89d74cb88484392feebbb92c944ca4271 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Fri, 25 Jun 2021 22:50:07 +0300 Subject: [PATCH 089/439] "docker push" requires exactly 1 argument. --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 61c2599..0be973a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -46,7 +46,7 @@ jobs: run: | docker build -t ${{ github.repository }}:$TAG -t ${{ github.repository }}:latest . echo $DOCKER_PWD | docker login -u $DOCKER_LOGIN --password-stdin - docker push ${{ github.repository }}:$TAG ${{ github.repository }}:latest + docker push --all-tags env: DOCKER_LOGIN: ${{ secrets.DOCKER_LOGIN }} DOCKER_PWD: ${{ secrets.DOCKER_PWD }} From 39e5b158f9910c629d5d0bc42ea88848d590dd27 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Fri, 25 Jun 2021 22:50:36 +0300 Subject: [PATCH 090/439] Fix coverage report --- .gitignore | 2 +- Makefile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index f52b52d..850426b 100644 --- a/.gitignore +++ b/.gitignore @@ -10,7 +10,7 @@ *.so *.dylib *.db -cover.txt +coverage.txt .env # Test binary, built with `go test -c` diff --git a/Makefile b/Makefile index 9eaed6c..2293ac3 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ run: ./pullanusbot test: - go test ./... -coverprofile=cover.txt -race + go test ./... -coverprofile=coverage.txt -race -covermode=atomic build: clean *.go go build . From 0441d0b1bdf39f5bf6c38633fc46029937bdba47 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Fri, 25 Jun 2021 22:55:50 +0300 Subject: [PATCH 091/439] "docker push" requires exactly 1 argument x2 --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0be973a..91d3f7d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -46,7 +46,7 @@ jobs: run: | docker build -t ${{ github.repository }}:$TAG -t ${{ github.repository }}:latest . echo $DOCKER_PWD | docker login -u $DOCKER_LOGIN --password-stdin - docker push --all-tags + docker push --all-tags ${{ github.repository }} env: DOCKER_LOGIN: ${{ secrets.DOCKER_LOGIN }} DOCKER_PWD: ${{ secrets.DOCKER_PWD }} From 835372b658772f52505ffd3eb5b898500887c208 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Fri, 25 Jun 2021 23:29:59 +0300 Subject: [PATCH 092/439] Remove volume from Dockerfile --- Dockerfile | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index f122775..9cd04a7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,7 +12,6 @@ RUN apk update && apk add tzdata python3 --no-cache && \ wget https://yt-dl.org/downloads/latest/youtube-dl -O /usr/local/bin/youtube-dl && \ chmod a+rx /usr/local/bin/youtube-dl && \ ln -s /usr/bin/python3 /usr/bin/python -COPY --from=builder /go/src/github.com/ailinykh/pullanusbot/pullanusbot . +COPY --from=builder /go/src/github.com/ailinykh/pullanusbot/pullanusbot /usr/local/bin/pullanusbot WORKDIR /usr/local/share -VOLUME [ "pullanusbot-data" ] -ENTRYPOINT /go/bin/pullanusbot \ No newline at end of file +ENTRYPOINT pullanusbot \ No newline at end of file From ea756c79f5e3c19da825d8ada840f61f9ca8b315 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Fri, 25 Jun 2021 23:47:19 +0300 Subject: [PATCH 093/439] Use absolute path for volume --- Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile b/Dockerfile index 9cd04a7..ade1f1d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,4 +14,5 @@ RUN apk update && apk add tzdata python3 --no-cache && \ ln -s /usr/bin/python3 /usr/bin/python COPY --from=builder /go/src/github.com/ailinykh/pullanusbot/pullanusbot /usr/local/bin/pullanusbot WORKDIR /usr/local/share +VOLUME [ "/usr/local/share/pullanusbot-data" ] ENTRYPOINT pullanusbot \ No newline at end of file From 949ed6888701d0813924980b586f1490f28594ea Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Fri, 25 Jun 2021 23:47:34 +0300 Subject: [PATCH 094/439] Add /proxy command --- pullanusbot.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pullanusbot.go b/pullanusbot.go index 9378a79..131cf55 100644 --- a/pullanusbot.go +++ b/pullanusbot.go @@ -58,6 +58,10 @@ func main() { youtubeFlow := usecases.CreateYoutubeFlow(logger, youtubeAPI, youtubeAPI, converter) telebot.AddHandler(youtubeFlow) + telebot.AddHandler("/proxy", func(m *core.Message, bot core.IBot) error { + _, err := bot.SendText("tg://proxy?server=proxy.ailinykh.com&port=443&secret=dd71ce3b5bf1b7015dc62a76dc244c5aec") + return err + }) // Start endless loop telebot.Run() } From f8a127c3cd89e82dbf9612d19569cc7da59b74c1 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Fri, 25 Jun 2021 23:48:16 +0300 Subject: [PATCH 095/439] Add docker-compose files for dev and prod environment --- .docker/dev/docker-compose.yaml | 11 +++++++++++ .docker/prod/docker-compose.yaml | 11 +++++++++++ 2 files changed, 22 insertions(+) create mode 100644 .docker/dev/docker-compose.yaml create mode 100644 .docker/prod/docker-compose.yaml diff --git a/.docker/dev/docker-compose.yaml b/.docker/dev/docker-compose.yaml new file mode 100644 index 0000000..1466f54 --- /dev/null +++ b/.docker/dev/docker-compose.yaml @@ -0,0 +1,11 @@ +version: '2' +services: + bot: + build: ../.. + container_name: pullanusbot + environment: + BOT_TOKEN: 12345678:XXXXXXXXxxxxxxxxXXXXXXXXxxxxxxxxXXX + ADMIN_CHAT_ID: 1488 + volumes: + - ./pullanusbot-data:/usr/local/share/pullanusbot-data + restart: always \ No newline at end of file diff --git a/.docker/prod/docker-compose.yaml b/.docker/prod/docker-compose.yaml new file mode 100644 index 0000000..be371cc --- /dev/null +++ b/.docker/prod/docker-compose.yaml @@ -0,0 +1,11 @@ +version: '2' +services: + bot: + image: ailinykh/pullanusbot + container_name: pullanusbot + environment: + BOT_TOKEN: 12345678:XXXXXXXXxxxxxxxxXXXXXXXXxxxxxxxxXXX + ADMIN_CHAT_ID: 1488 + volumes: + - ./pullanusbot-data:/usr/local/share/pullanusbot-data + restart: always \ No newline at end of file From 22ce49d6e93ef57a5315f8488779690c536452ce Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Sat, 26 Jun 2021 07:58:53 +0300 Subject: [PATCH 096/439] [GitHub] At 80% of Git LFS data quota for ailinykh --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 91d3f7d..739656c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -21,8 +21,8 @@ jobs: - name: Check out code into the Go module directory uses: actions/checkout@v1 - - name: Git LFS setup - run: git lfs pull + # - name: Git LFS setup + # run: git lfs pull - name: Build run: make build From 43c4177d62df747129189b6290817b21534a8488 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Sat, 26 Jun 2021 08:10:09 +0300 Subject: [PATCH 097/439] Game must be available only for group chats --- usecases/faggot_game.go | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/usecases/faggot_game.go b/usecases/faggot_game.go index cb7ee51..bea25da 100644 --- a/usecases/faggot_game.go +++ b/usecases/faggot_game.go @@ -25,12 +25,20 @@ type GameFlow struct { // Rules of the game func (flow *GameFlow) Rules(message *core.Message, bot core.IBot) error { + if message.IsPrivate { + _, err := bot.SendText(flow.l.I18n("faggot_not_available_for_private")) + return err + } _, err := bot.SendText(flow.l.I18n("faggot_rules")) return err } // Add a new player to game func (flow *GameFlow) Add(message *core.Message, bot core.IBot) error { + if message.IsPrivate { + _, err := bot.SendText(flow.l.I18n("faggot_not_available_for_private")) + return err + } players, _ := flow.s.GetPlayers(message.ChatID) for _, p := range players { if p.ID == message.Sender.ID { @@ -52,6 +60,10 @@ var mutex sync.Mutex // Play game func (flow *GameFlow) Play(message *core.Message, bot core.IBot) error { + if message.IsPrivate { + _, err := bot.SendText(flow.l.I18n("faggot_not_available_for_private")) + return err + } mutex.Lock() defer mutex.Unlock() @@ -109,6 +121,11 @@ func (flow *GameFlow) Play(message *core.Message, bot core.IBot) error { // All statistics for all time func (flow *GameFlow) All(message *core.Message, bot core.IBot) error { + if message.IsPrivate { + _, err := bot.SendText(flow.l.I18n("faggot_not_available_for_private")) + return err + } + entries, _ := flow.getStat(message) messages := []string{flow.l.I18n("faggot_all_top"), ""} for i, e := range entries { @@ -122,6 +139,11 @@ func (flow *GameFlow) All(message *core.Message, bot core.IBot) error { // Stats returns current year statistics func (flow *GameFlow) Stats(message *core.Message, bot core.IBot) error { + if message.IsPrivate { + _, err := bot.SendText(flow.l.I18n("faggot_not_available_for_private")) + return err + } + year := strconv.Itoa(time.Now().Year()) rounds, _ := flow.s.GetRounds(message.ChatID) entries := []Stat{} @@ -156,6 +178,11 @@ func (flow *GameFlow) Stats(message *core.Message, bot core.IBot) error { // Me returns your personal statistics func (flow *GameFlow) Me(message *core.Message, bot core.IBot) error { + if message.IsPrivate { + _, err := bot.SendText(flow.l.I18n("faggot_not_available_for_private")) + return err + } + entries, _ := flow.getStat(message) score := 0 for _, e := range entries { From b221a71cfd0e670cf233888d0b5c618d2f9dcc7a Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Fri, 2 Jul 2021 16:54:08 +0300 Subject: [PATCH 098/439] Add ability to enable web page preview --- api/telebot_adapter.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/api/telebot_adapter.go b/api/telebot_adapter.go index 3a382fe..5b235e8 100644 --- a/api/telebot_adapter.go +++ b/api/telebot_adapter.go @@ -18,6 +18,8 @@ func (a *TelebotAdapter) SendText(text string, params ...interface{}) (*core.Mes switch m := param.(type) { case *core.Message: opts.ReplyTo = &tb.Message{ID: m.ID} + case bool: + opts.DisableWebPagePreview = m default: break } From 25889dffc6d16851d41faf49c0dc79549047a33c Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Fri, 2 Jul 2021 16:54:28 +0300 Subject: [PATCH 099/439] i do not care --- pullanusbot.go | 3 +++ usecases/i_do_not_care.go | 22 ++++++++++++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 usecases/i_do_not_care.go diff --git a/pullanusbot.go b/pullanusbot.go index 131cf55..7addd05 100644 --- a/pullanusbot.go +++ b/pullanusbot.go @@ -62,6 +62,9 @@ func main() { _, err := bot.SendText("tg://proxy?server=proxy.ailinykh.com&port=443&secret=dd71ce3b5bf1b7015dc62a76dc244c5aec") return err }) + + iDoNotCare := usecases.CreateIDoNotCare() + telebot.AddHandler(iDoNotCare) // Start endless loop telebot.Run() } diff --git a/usecases/i_do_not_care.go b/usecases/i_do_not_care.go new file mode 100644 index 0000000..746fde2 --- /dev/null +++ b/usecases/i_do_not_care.go @@ -0,0 +1,22 @@ +package usecases + +import ( + "strings" + + "github.com/ailinykh/pullanusbot/v2/core" +) + +func CreateIDoNotCare() *IDoNotCare { + return &IDoNotCare{} +} + +type IDoNotCare struct{} + +// HandleText is a core.ITextHandler protocol implementation +func (IDoNotCare) HandleText(message *core.Message, bot core.IBot) error { + if strings.Contains(message.Text, "мне всё равно coub") { + _, err := bot.SendText("https://coub.com/view/1ov5oi", false) + return err + } + return nil +} From 98b7689c55494f39fdfaa7f750962e6c6362131e Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Fri, 2 Jul 2021 17:52:42 +0300 Subject: [PATCH 100/439] i do not care without coub --- usecases/i_do_not_care.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/usecases/i_do_not_care.go b/usecases/i_do_not_care.go index 746fde2..4f86514 100644 --- a/usecases/i_do_not_care.go +++ b/usecases/i_do_not_care.go @@ -14,7 +14,7 @@ type IDoNotCare struct{} // HandleText is a core.ITextHandler protocol implementation func (IDoNotCare) HandleText(message *core.Message, bot core.IBot) error { - if strings.Contains(message.Text, "мне всё равно coub") { + if strings.Contains(message.Text, "мне всё равно") { _, err := bot.SendText("https://coub.com/view/1ov5oi", false) return err } From f3fa23e12c46d78ab52cd14c50ab92b3f4dad03d Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Sun, 11 Jul 2021 20:52:46 +0300 Subject: [PATCH 101/439] I do not care about case sensitivity --- usecases/i_do_not_care.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/usecases/i_do_not_care.go b/usecases/i_do_not_care.go index 4f86514..32002ba 100644 --- a/usecases/i_do_not_care.go +++ b/usecases/i_do_not_care.go @@ -14,7 +14,7 @@ type IDoNotCare struct{} // HandleText is a core.ITextHandler protocol implementation func (IDoNotCare) HandleText(message *core.Message, bot core.IBot) error { - if strings.Contains(message.Text, "мне всё равно") { + if strings.Contains(strings.ToLower(message.Text), "мне всё равно") { _, err := bot.SendText("https://coub.com/view/1ov5oi", false) return err } From cfa6b578d403eac706acf53951d4b9e0367485fd Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Sun, 11 Jul 2021 20:58:47 +0300 Subject: [PATCH 102/439] Remove original document on success --- usecases/video_flow.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/usecases/video_flow.go b/usecases/video_flow.go index 63ed551..23335a0 100644 --- a/usecases/video_flow.go +++ b/usecases/video_flow.go @@ -44,7 +44,11 @@ func (f *VideoFlow) HandleDocument(document *core.Document, message *core.Messag fi2, _ := os.Stat(cvf.Path) caption := fmt.Sprintf("%s (by %s)\nOriginal size: %.2f MB (%d kb/s)\nConverted size: %.2f MB (%d kb/s)", vf.Name, message.Sender.Username, float32(fi1.Size())/1048576, vf.Bitrate/1024, float32(fi2.Size())/1048576, cvf.Bitrate/1024) _, err = bot.SendVideo(cvf, caption) - return err + if err != nil { + f.l.Error(err) + return err + } + return bot.Delete(message) } if vf.Codec != "h264" { @@ -57,7 +61,11 @@ func (f *VideoFlow) HandleDocument(document *core.Document, message *core.Messag defer cvf.Dispose() caption := fmt.Sprintf("%s (by %s)", vf.Name, message.Sender.Username) _, err = bot.SendVideo(cvf, caption) - return err + if err != nil { + f.l.Error(err) + return err + } + return bot.Delete(message) } f.l.Infof("No need to convert %s", vf.Name) From 82267d3ff6c181d2b90d04f2a1c29a22a05ee5e2 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Wed, 28 Jul 2021 08:24:31 +0300 Subject: [PATCH 103/439] use caption as message text for document case --- api/telebot.go | 6 +++++- usecases/video_flow.go | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/api/telebot.go b/api/telebot.go index f8c8d5d..bf2f492 100644 --- a/api/telebot.go +++ b/api/telebot.go @@ -171,12 +171,16 @@ func (t *Telebot) reportError(m *tb.Message, e error) { } func makeMessage(m *tb.Message) *core.Message { + text := m.Text + if m.Document != nil { + text = m.Caption + } return &core.Message{ ID: m.ID, ChatID: m.Chat.ID, IsPrivate: m.Private(), Sender: makeUser(m.Sender), - Text: m.Text, + Text: text, } } diff --git a/usecases/video_flow.go b/usecases/video_flow.go index 23335a0..c104ea9 100644 --- a/usecases/video_flow.go +++ b/usecases/video_flow.go @@ -59,7 +59,7 @@ func (f *VideoFlow) HandleDocument(document *core.Document, message *core.Messag return err } defer cvf.Dispose() - caption := fmt.Sprintf("%s (by %s)", vf.Name, message.Sender.Username) + caption := fmt.Sprintf("%s (by %s)\n%s", vf.Name, message.Sender.Username, message.Text) _, err = bot.SendVideo(cvf, caption) if err != nil { f.l.Error(err) From 195c16b55ead9595dd1106132206cd955235c006 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Wed, 28 Jul 2021 08:48:45 +0300 Subject: [PATCH 104/439] more logs needed --- infrastructure/ffmpeg_converter.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/infrastructure/ffmpeg_converter.go b/infrastructure/ffmpeg_converter.go index 99fdfc1..4d234d7 100644 --- a/infrastructure/ffmpeg_converter.go +++ b/infrastructure/ffmpeg_converter.go @@ -57,6 +57,7 @@ func (c *FfmpegConverter) Split(video *core.Video, limit int) ([]*core.Video, er file, err := c.CreateVideo(path) if err != nil { + c.l.Error(err) return nil, err } // defer file.Dispose() @@ -72,11 +73,13 @@ func (c *FfmpegConverter) Split(video *core.Video, limit int) ([]*core.Video, er func (c *FfmpegConverter) CreateVideo(path string) (*core.Video, error) { ffprobe, err := c.getFFProbe(path) if err != nil { + c.l.Error(err) return nil, err } stream, err := ffprobe.getVideoStream() if err != nil { + c.l.Error(err) return nil, err } @@ -87,6 +90,7 @@ func (c *FfmpegConverter) CreateVideo(path string) (*core.Video, error) { thumb, err := c.createThumb(path, scale) if err != nil { + c.l.Error(err) return nil, err } @@ -94,11 +98,13 @@ func (c *FfmpegConverter) CreateVideo(path string) (*core.Video, error) { duration, err := strconv.ParseFloat(ffprobe.Format.Duration, 32) if err != nil { + c.l.Error(err) return nil, err } stat, err := os.Stat(path) if err != nil { + c.l.Error(err) return nil, err } @@ -116,12 +122,14 @@ func (c *FfmpegConverter) getFFProbe(file string) (*ffpResponse, error) { cmd := fmt.Sprintf(`ffprobe -v error -of json -show_streams -show_format "%s"`, file) out, err := exec.Command("/bin/sh", "-c", cmd).CombinedOutput() if err != nil { + c.l.Error(err) return nil, errors.New(string(out)) } var resp ffpResponse err = json.Unmarshal(out, &resp) if err != nil { + c.l.Error(err) return nil, err } @@ -134,11 +142,13 @@ func (c *FfmpegConverter) createThumb(videoPath string, scale string) (*core.Ima cmd := fmt.Sprintf(`ffmpeg -v error -i "%s" -ss 00:00:01.000 -vframes 1 -filter:v scale="%s" "%s"`, videoPath, scale, thumbPath) out, err := exec.Command("/bin/sh", "-c", cmd).CombinedOutput() if err != nil { + c.l.Error(err) return nil, errors.New(string(out)) } ffprobe, err := c.getFFProbe(thumbPath) if err != nil { + c.l.Error(err) return nil, err } From 0d2c378f7ec3c1d046ea1be928d07feb26b9404f Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Wed, 28 Jul 2021 09:54:30 +0300 Subject: [PATCH 105/439] respect users without usernames on faggot game --- core/user.go | 7 +++++++ usecases/faggot_game.go | 10 +++++++--- usecases/faggot_game_test.go | 21 +++++++++++++++++++++ 3 files changed, 35 insertions(+), 3 deletions(-) diff --git a/core/user.go b/core/user.go index a2e579b..6aae83e 100644 --- a/core/user.go +++ b/core/user.go @@ -8,3 +8,10 @@ type User struct { Username string LanguageCode string } + +func (u *User) DisplayName() string { + if len(u.Username) == 0 { + return u.FirstName + " " + u.LastName + } + return u.Username +} diff --git a/usecases/faggot_game.go b/usecases/faggot_game.go index bea25da..f9b2811 100644 --- a/usecases/faggot_game.go +++ b/usecases/faggot_game.go @@ -104,7 +104,11 @@ func (flow *GameFlow) Play(message *core.Message, bot core.IBot) error { if i == 3 { // TODO: implementation detail leaked - phrase = flow.l.I18n(template, "@"+winner.Username) + if len(winner.Username) == 0 { + phrase = flow.l.I18n(template, fmt.Sprintf(`%s %s`, winner.ID, winner.FirstName, winner.LastName)) + } else { + phrase = flow.l.I18n(template, "@"+winner.Username) + } } _, err := bot.SendText(phrase) @@ -129,7 +133,7 @@ func (flow *GameFlow) All(message *core.Message, bot core.IBot) error { entries, _ := flow.getStat(message) messages := []string{flow.l.I18n("faggot_all_top"), ""} for i, e := range entries { - message := flow.l.I18n("faggot_all_entry", i+1, e.Player.Username, e.Score) + message := flow.l.I18n("faggot_all_entry", i+1, e.Player.DisplayName(), e.Score) messages = append(messages, message) } messages = append(messages, "", flow.l.I18n("faggot_all_bottom", len(entries))) @@ -168,7 +172,7 @@ func (flow *GameFlow) Stats(message *core.Message, bot core.IBot) error { messages := []string{flow.l.I18n("faggot_stats_top"), ""} for i, e := range entries { - message := flow.l.I18n("faggot_stats_entry", i+1, e.Player.Username, e.Score) + message := flow.l.I18n("faggot_stats_entry", i+1, e.Player.DisplayName(), e.Score) messages = append(messages, message) } messages = append(messages, "", flow.l.I18n("faggot_stats_bottom", len(entries))) diff --git a/usecases/faggot_game_test.go b/usecases/faggot_game_test.go index c211ebd..fe66f15 100644 --- a/usecases/faggot_game_test.go +++ b/usecases/faggot_game_test.go @@ -61,6 +61,27 @@ func Test_Play_RespondsNotEnoughPlayers(t *testing.T) { assert.Equal(t, bot.messages[1], "Not enough players") } +func Test_Play_RespondsWithCurrentGameResult(t *testing.T) { + game, bot, storage := makeSUT(LocalizerDict{ + "faggot_game_0_0": "0", + "faggot_game_1_0": "1", + "faggot_game_2_0": "2", + "faggot_game_3_0": "%s", + }) + m1 := makeMessage(1, "") + m2 := makeMessage(2, "") + + game.Add(m1, bot) + game.Add(m2, bot) + game.Play(m1, bot) + + winner := storage.rounds[0].Winner + phrase := fmt.Sprintf(`%s %s`, winner.ID, winner.FirstName, winner.LastName) + assert.Equal(t, "0", bot.messages[2]) + assert.Equal(t, "1", bot.messages[3]) + assert.Equal(t, "2", bot.messages[4]) + assert.Equal(t, phrase, bot.messages[5]) +} func Test_Play_RespondsWinnerAlreadyKnown(t *testing.T) { game, bot, storage := makeSUT(LocalizerDict{ "faggot_game_0_0": "0", From ecf6e4a3e7c076ae55dfb8c504d36cfa90777e51 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Wed, 28 Jul 2021 10:34:21 +0300 Subject: [PATCH 106/439] update ffmpeg version --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index ade1f1d..9e89ad7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,7 +7,7 @@ RUN go mod download ADD . . RUN CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -a -installsuffix cgo -ldflags '-extldflags "-static"' -FROM jrottenberg/ffmpeg:4.1-alpine +FROM jrottenberg/ffmpeg:4.3-alpine RUN apk update && apk add tzdata python3 --no-cache && \ wget https://yt-dl.org/downloads/latest/youtube-dl -O /usr/local/bin/youtube-dl && \ chmod a+rx /usr/local/bin/youtube-dl && \ From 4bb9ad271ac31dfb7a668c351b29432c33048340 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Wed, 28 Jul 2021 12:21:24 +0300 Subject: [PATCH 107/439] replace Username with DisplayName in faggot game --- usecases/faggot_game.go | 10 +++++----- usecases/faggot_stat.go | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/usecases/faggot_game.go b/usecases/faggot_game.go index f9b2811..ecaefd3 100644 --- a/usecases/faggot_game.go +++ b/usecases/faggot_game.go @@ -70,7 +70,7 @@ func (flow *GameFlow) Play(message *core.Message, bot core.IBot) error { players, _ := flow.s.GetPlayers(message.ChatID) switch len(players) { case 0: - _, err := bot.SendText(flow.l.I18n("faggot_no_players", message.Sender.Username)) + _, err := bot.SendText(flow.l.I18n("faggot_no_players", message.Sender.DisplayName())) return err case 1: _, err := bot.SendText(flow.l.I18n("faggot_not_enough_players")) @@ -83,7 +83,7 @@ func (flow *GameFlow) Play(message *core.Message, bot core.IBot) error { for _, r := range games { if r.Day == day { - _, err := bot.SendText(flow.l.I18n("faggot_winner_known", r.Winner.Username)) + _, err := bot.SendText(flow.l.I18n("faggot_winner_known", r.Winner.DisplayName())) return err } } @@ -154,7 +154,7 @@ func (flow *GameFlow) Stats(message *core.Message, bot core.IBot) error { for _, r := range rounds { if strings.HasPrefix(r.Day, year) { - index := Find(entries, r.Winner.Username) + index := Find(entries, r.Winner.ID) if index == -1 { entries = append(entries, Stat{Player: r.Winner, Score: 1}) } else { @@ -194,7 +194,7 @@ func (flow *GameFlow) Me(message *core.Message, bot core.IBot) error { score = e.Score } } - _, err := bot.SendText(flow.l.I18n("faggot_me", message.Sender.Username, score)) + _, err := bot.SendText(flow.l.I18n("faggot_me", message.Sender.DisplayName(), score)) return err } @@ -207,7 +207,7 @@ func (flow *GameFlow) getStat(message *core.Message) ([]Stat, error) { } for _, r := range rounds { - index := Find(entries, r.Winner.Username) + index := Find(entries, r.Winner.ID) if index == -1 { entries = append(entries, Stat{Player: r.Winner, Score: 1}) } else { diff --git a/usecases/faggot_stat.go b/usecases/faggot_stat.go index 7854e09..13322f9 100644 --- a/usecases/faggot_stat.go +++ b/usecases/faggot_stat.go @@ -11,9 +11,9 @@ type Stat struct { } // Find player by username in current stat -func Find(a []Stat, username string) int { +func Find(a []Stat, id int) int { for i, n := range a { - if username == n.Player.Username { + if id == n.Player.ID { return i } } From 415fbb50fefbe23f413c9b2a5f83a7c6b903c982 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Wed, 28 Jul 2021 15:23:43 +0300 Subject: [PATCH 108/439] more logs for ffmpeg --- infrastructure/ffmpeg_converter.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/infrastructure/ffmpeg_converter.go b/infrastructure/ffmpeg_converter.go index 4d234d7..20ebd72 100644 --- a/infrastructure/ffmpeg_converter.go +++ b/infrastructure/ffmpeg_converter.go @@ -71,6 +71,7 @@ func (c *FfmpegConverter) Split(video *core.Video, limit int) ([]*core.Video, er // CreateVideo is a core.IVideoFactory interface implementation func (c *FfmpegConverter) CreateVideo(path string) (*core.Video, error) { + c.l.Infof("create video: %s", strings.ReplaceAll(path, os.TempDir(), "$TMPDIR/")) ffprobe, err := c.getFFProbe(path) if err != nil { c.l.Error(err) @@ -120,6 +121,7 @@ func (c *FfmpegConverter) CreateVideo(path string) (*core.Video, error) { func (c *FfmpegConverter) getFFProbe(file string) (*ffpResponse, error) { cmd := fmt.Sprintf(`ffprobe -v error -of json -show_streams -show_format "%s"`, file) + c.l.Info(strings.ReplaceAll(cmd, os.TempDir(), "$TMPDIR/")) out, err := exec.Command("/bin/sh", "-c", cmd).CombinedOutput() if err != nil { c.l.Error(err) @@ -140,6 +142,7 @@ func (c *FfmpegConverter) createThumb(videoPath string, scale string) (*core.Ima thumbPath := videoPath + ".jpg" cmd := fmt.Sprintf(`ffmpeg -v error -i "%s" -ss 00:00:01.000 -vframes 1 -filter:v scale="%s" "%s"`, videoPath, scale, thumbPath) + c.l.Info(strings.ReplaceAll(cmd, os.TempDir(), "$TMPDIR/")) out, err := exec.Command("/bin/sh", "-c", cmd).CombinedOutput() if err != nil { c.l.Error(err) From 9a1750d31bc0e359bfdde9b7723bb85da9f0b8d6 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Wed, 28 Jul 2021 17:05:36 +0300 Subject: [PATCH 109/439] during split of large videos the last piece might be shorter than one second --- infrastructure/ffmpeg_converter.go | 35 ++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/infrastructure/ffmpeg_converter.go b/infrastructure/ffmpeg_converter.go index 20ebd72..9c29e4a 100644 --- a/infrastructure/ffmpeg_converter.go +++ b/infrastructure/ffmpeg_converter.go @@ -26,9 +26,9 @@ type FfmpegConverter struct { // Convert is a core.IVideoConverter interface implementation func (c *FfmpegConverter) Convert(vf *core.Video, bitrate int) (*core.Video, error) { path := path.Join(os.TempDir(), vf.Name+"_converted.mp4") - cmd := fmt.Sprintf(`ffmpeg -y -i "%s" -pix_fmt yuv420p -vf "scale=trunc(iw/2)*2:trunc(ih/2)*2" "%s"`, vf.Path, path) + cmd := fmt.Sprintf(`ffmpeg -v error -y -i "%s" -pix_fmt yuv420p -vf "scale=trunc(iw/2)*2:trunc(ih/2)*2" "%s"`, vf.Path, path) if bitrate > 0 { - cmd = fmt.Sprintf(`ffmpeg -v error -y -i "%s" -c:v libx264 -preset medium -b:v %dk -pass 1 -b:a 128k -f mp4 /dev/null && ffmpeg -y -i "%s" -c:v libx264 -preset medium -b:v %dk -pass 2 -b:a 128k "%s"`, vf.Path, bitrate/1024, vf.Path, bitrate/1024, path) + cmd = fmt.Sprintf(`ffmpeg -v error -y -i "%s" -c:v libx264 -preset medium -b:v %dk -pass 1 -b:a 128k -f mp4 /dev/null && ffmpeg -v error -y -i "%s" -c:v libx264 -preset medium -b:v %dk -pass 2 -b:a 128k "%s"`, vf.Path, bitrate/1024, vf.Path, bitrate/1024, path) } c.l.Info(strings.ReplaceAll(cmd, os.TempDir(), "$TMPDIR/")) out, err := exec.Command("/bin/sh", "-c", cmd).CombinedOutput() @@ -47,7 +47,7 @@ func (c *FfmpegConverter) Split(video *core.Video, limit int) ([]*core.Video, er var videos = []*core.Video{} for duration < video.Duration { path := fmt.Sprintf("%s-%d.mp4", video.File.Path, n) - cmd := fmt.Sprintf(`ffmpeg -i %s -ss %d -fs %d %s`, video.File.Path, duration, limit, path) + cmd := fmt.Sprintf(`ffmpeg -v error -y -i %s -ss %d -fs %d %s`, video.File.Path, duration, limit, path) c.l.Info(strings.ReplaceAll(cmd, os.TempDir(), "$TMPDIR/")) out, err := exec.Command("/bin/sh", "-c", cmd).CombinedOutput() if err != nil { @@ -58,7 +58,15 @@ func (c *FfmpegConverter) Split(video *core.Video, limit int) ([]*core.Video, er file, err := c.CreateVideo(path) if err != nil { c.l.Error(err) - return nil, err + os.Remove(path) + if err.Error() == "file is too short" { + // the last piece might be shorter than a second + // example: https://youtu.be/1MLRCczBKn8 + duration += 1 + continue + } else { + return nil, err + } } // defer file.Dispose() @@ -78,6 +86,17 @@ func (c *FfmpegConverter) CreateVideo(path string) (*core.Video, error) { return nil, err } + duration, err := strconv.ParseFloat(ffprobe.Format.Duration, 32) + if err != nil { + c.l.Error(err) + return nil, err + } + + if duration < 1 { + c.l.Errorf("expected duration at least 1 second, got %f", duration) + return nil, errors.New("file is too short") + } + stream, err := ffprobe.getVideoStream() if err != nil { c.l.Error(err) @@ -97,12 +116,6 @@ func (c *FfmpegConverter) CreateVideo(path string) (*core.Video, error) { bitrate, _ := strconv.Atoi(stream.BitRate) // empty for .gif - duration, err := strconv.ParseFloat(ffprobe.Format.Duration, 32) - if err != nil { - c.l.Error(err) - return nil, err - } - stat, err := os.Stat(path) if err != nil { c.l.Error(err) @@ -141,7 +154,7 @@ func (c *FfmpegConverter) getFFProbe(file string) (*ffpResponse, error) { func (c *FfmpegConverter) createThumb(videoPath string, scale string) (*core.Image, error) { thumbPath := videoPath + ".jpg" - cmd := fmt.Sprintf(`ffmpeg -v error -i "%s" -ss 00:00:01.000 -vframes 1 -filter:v scale="%s" "%s"`, videoPath, scale, thumbPath) + cmd := fmt.Sprintf(`ffmpeg -v error -y -i "%s" -ss 00:00:01.000 -vframes 1 -filter:v scale="%s" "%s"`, videoPath, scale, thumbPath) c.l.Info(strings.ReplaceAll(cmd, os.TempDir(), "$TMPDIR/")) out, err := exec.Command("/bin/sh", "-c", cmd).CombinedOutput() if err != nil { From 7082a91fdb0fce0d8c0a1d63880b98f5e7529bdb Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Sat, 7 Aug 2021 22:14:26 +0300 Subject: [PATCH 110/439] Extract twitter timeout logic into separate decorator --- pullanusbot.go | 3 +- usecases/twitter_flow.go | 54 +++------------------------- usecases/twitter_timeout.go | 72 +++++++++++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+), 50 deletions(-) create mode 100644 usecases/twitter_timeout.go diff --git a/pullanusbot.go b/pullanusbot.go index 7addd05..8e88c34 100644 --- a/pullanusbot.go +++ b/pullanusbot.go @@ -40,7 +40,8 @@ func main() { fileDownloader := infrastructure.CreateFileDownloader() twitterAPI := api.CreateTwitterAPI() twitterFlow := usecases.CreateTwitterFlow(logger, twitterAPI, fileDownloader, converter) - telebot.AddHandler(twitterFlow) + twitterTimeout := usecases.CreateTwitterTimeout(logger, twitterFlow) + telebot.AddHandler(twitterTimeout) linkFlow := usecases.CreateLinkFlow(logger, fileDownloader, converter, converter) telebot.AddHandler(linkFlow) diff --git a/usecases/twitter_flow.go b/usecases/twitter_flow.go index e324866..3b4c410 100644 --- a/usecases/twitter_flow.go +++ b/usecases/twitter_flow.go @@ -2,30 +2,25 @@ package usecases import ( "errors" - "fmt" - "math" "os" "path" "regexp" - "strconv" "strings" - "time" "github.com/ailinykh/pullanusbot/v2/core" ) // CreateTwitterFlow is a basic TwitterFlow factory func CreateTwitterFlow(l core.ILogger, mf core.IMediaFactory, fd core.IFileDownloader, vff core.IVideoFactory) *TwitterFlow { - return &TwitterFlow{l, mf, fd, vff, make(map[core.Message]core.Message)} + return &TwitterFlow{l, mf, fd, vff} } // TwitterFlow represents tweet processing logic type TwitterFlow struct { - l core.ILogger - mf core.IMediaFactory - fd core.IFileDownloader - vff core.IVideoFactory - timeoutReplies map[core.Message]core.Message + l core.ILogger + mf core.IMediaFactory + fd core.IFileDownloader + vff core.IVideoFactory } // HandleText is a core.ITextHandler protocol implementation @@ -42,26 +37,11 @@ func (tf *TwitterFlow) process(tweetID string, message *core.Message, bot core.I tf.l.Infof("processing tweet %s", tweetID) media, err := tf.mf.CreateMedia(tweetID, message.Sender) if err != nil { - if strings.HasPrefix(err.Error(), "Rate limit exceeded") { - err := tf.handleTimeout(err, tweetID, message, bot) - if strings.HasPrefix(err.Error(), "twitter api timeout") { - sent, err := bot.SendText(err.Error(), message) - if err != nil { - return err - } - tf.timeoutReplies[*message] = *sent - return nil - } - } return err } err = tf.handleMedia(media, message, bot) if err == nil { - if sent, ok := tf.timeoutReplies[*message]; ok { - _ = bot.Delete(&sent) - delete(tf.timeoutReplies, *message) - } return bot.Delete(message) } return err @@ -85,30 +65,6 @@ func (tf *TwitterFlow) handleMedia(media []*core.Media, message *core.Message, b } } -func (tf *TwitterFlow) handleTimeout(err error, tweetID string, message *core.Message, bot core.IBot) error { - r := regexp.MustCompile(`(\-?\d+)$`) - match := r.FindStringSubmatch(err.Error()) - if len(match) < 2 { - return errors.New("rate limit not found") - } - - limit, err := strconv.ParseInt(match[1], 10, 64) - if err != nil { - return err - } - - timeout := limit - time.Now().Unix() - tf.l.Infof("Twitter api timeout %d seconds", timeout) - timeout = int64(math.Max(float64(timeout), 1)) // Twitter api timeout might be negative - go func() { - time.Sleep(time.Duration(timeout) * time.Second) - tf.process(tweetID, message, bot) - }() - minutes := timeout / 60 - seconds := timeout % 60 - return fmt.Errorf("twitter api timeout %d min %d sec", minutes, seconds) -} - func (tf *TwitterFlow) fallbackToUploading(media *core.Media, bot core.IBot) error { // Try to upload file to telegram tf.l.Info("Sending by uploading") diff --git a/usecases/twitter_timeout.go b/usecases/twitter_timeout.go new file mode 100644 index 0000000..2729649 --- /dev/null +++ b/usecases/twitter_timeout.go @@ -0,0 +1,72 @@ +package usecases + +import ( + "errors" + "fmt" + "math" + "regexp" + "strconv" + "strings" + "time" + + "github.com/ailinykh/pullanusbot/v2/core" +) + +// CreateTwitterFlow is a basic TwitterFlow factory +func CreateTwitterTimeout(l core.ILogger, tf *TwitterFlow) *TwitterTimeout { + return &TwitterTimeout{l, tf, make(map[core.Message]core.Message)} +} + +// TwitterTimeout is a decorator for TwitterFlow to handle API timeouts gracefully +type TwitterTimeout struct { + l core.ILogger + tf *TwitterFlow + replies map[core.Message]core.Message +} + +// HandleText is a core.ITextHandler protocol implementation +func (tt *TwitterTimeout) HandleText(message *core.Message, bot core.IBot) error { + err := tt.tf.HandleText(message, bot) + if err != nil { + if strings.HasPrefix(err.Error(), "Rate limit exceeded") { + err := tt.handleTimeout(err, message, bot) + if strings.HasPrefix(err.Error(), "twitter api timeout") { + sent, err := bot.SendText(err.Error(), message) + if err != nil { + return err + } + tt.replies[*message] = *sent + return nil + } + } + tt.l.Error(err) + } else if sent, ok := tt.replies[*message]; ok { + _ = bot.Delete(&sent) + delete(tt.replies, *message) + } + return err +} + +func (tt *TwitterTimeout) handleTimeout(err error, message *core.Message, bot core.IBot) error { + r := regexp.MustCompile(`(\-?\d+)$`) + match := r.FindStringSubmatch(err.Error()) + if len(match) < 2 { + return errors.New("rate limit not found") + } + + limit, err := strconv.ParseInt(match[1], 10, 64) + if err != nil { + return err + } + + timeout := limit - time.Now().Unix() + tt.l.Infof("Twitter api timeout %d seconds", timeout) + timeout = int64(math.Max(float64(timeout), 1)) // Twitter api timeout might be negative + go func() { + time.Sleep(time.Duration(timeout) * time.Second) + tt.HandleText(message, bot) + }() + minutes := timeout / 60 + seconds := timeout % 60 + return fmt.Errorf("twitter api timeout %d min %d sec", minutes, seconds) +} From 6f40897f7bc036c3c02cbd3bb70c57a7449c1c17 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Sun, 8 Aug 2021 13:18:23 +0300 Subject: [PATCH 111/439] twitter extended error logging --- usecases/twitter_flow.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/usecases/twitter_flow.go b/usecases/twitter_flow.go index 3b4c410..c300268 100644 --- a/usecases/twitter_flow.go +++ b/usecases/twitter_flow.go @@ -37,14 +37,17 @@ func (tf *TwitterFlow) process(tweetID string, message *core.Message, bot core.I tf.l.Infof("processing tweet %s", tweetID) media, err := tf.mf.CreateMedia(tweetID, message.Sender) if err != nil { + tf.l.Error(err) return err } err = tf.handleMedia(media, message, bot) - if err == nil { - return bot.Delete(message) + if err != nil { + tf.l.Error(err) + return err } - return err + + return bot.Delete(message) } func (tf *TwitterFlow) handleMedia(media []*core.Media, message *core.Message, bot core.IBot) error { From 308f3f5ec235fe72fa73be81d1fc6afe4efde111 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Sun, 8 Aug 2021 13:33:19 +0300 Subject: [PATCH 112/439] rename video_file_factory to telebot_factory since it is domain specific --- api/telebot_adapter.go | 2 +- api/{video_file_factory.go => telebot_factory.go} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename api/{video_file_factory.go => telebot_factory.go} (88%) diff --git a/api/telebot_adapter.go b/api/telebot_adapter.go index 5b235e8..df639bd 100644 --- a/api/telebot_adapter.go +++ b/api/telebot_adapter.go @@ -105,7 +105,7 @@ func (a *TelebotAdapter) SendPhotoAlbum(medias []*core.Media) ([]*core.Message, // SendVideo is a core.IBot interface implementation func (a *TelebotAdapter) SendVideo(vf *core.Video, caption string) (*core.Message, error) { - video := makeVideo(vf, caption) + video := makeTbVideo(vf, caption) a.t.bot.Notify(a.m.Chat, tb.UploadingVideo) sent, err := video.Send(a.t.bot, a.m.Chat, &tb.SendOptions{ParseMode: tb.ModeHTML}) if err != nil { diff --git a/api/video_file_factory.go b/api/telebot_factory.go similarity index 88% rename from api/video_file_factory.go rename to api/telebot_factory.go index c7d0338..1116de5 100644 --- a/api/video_file_factory.go +++ b/api/telebot_factory.go @@ -5,7 +5,7 @@ import ( tb "gopkg.in/tucnak/telebot.v2" ) -func makeVideo(vf *core.Video, caption string) tb.Video { +func makeTbVideo(vf *core.Video, caption string) tb.Video { video := tb.Video{File: tb.FromDisk(vf.Path)} video.FileName = vf.File.Name video.Width = vf.Width From d827fe0561b7c38d976ee26e2dbd8984d2133b8d Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Sun, 8 Aug 2021 15:46:28 +0300 Subject: [PATCH 113/439] Extend SendImage method with "caption" field --- api/telebot_adapter.go | 9 ++++++--- api/telebot_factory.go | 13 +++++++++++-- core/bot.go | 2 +- usecases/publisher_flow.go | 2 +- 4 files changed, 19 insertions(+), 7 deletions(-) diff --git a/api/telebot_adapter.go b/api/telebot_adapter.go index df639bd..4325929 100644 --- a/api/telebot_adapter.go +++ b/api/telebot_adapter.go @@ -34,9 +34,12 @@ func (a *TelebotAdapter) Delete(message *core.Message) error { } // SendImage is a core.IBot interface implementation -func (a *TelebotAdapter) SendImage(image *core.Image) (*core.Message, error) { - photo := &tb.Photo{File: tb.File{FileID: image.ID}} - sent, err := a.t.bot.Send(a.m.Chat, photo) +func (a *TelebotAdapter) SendImage(image *core.Image, caption string) (*core.Message, error) { + photo := makeTbPhoto(image, caption) + sent, err := photo.Send(a.t.bot, a.m.Chat, &tb.SendOptions{ParseMode: tb.ModeHTML}) + if err != nil { + return nil, err + } return makeMessage(sent), err } diff --git a/api/telebot_factory.go b/api/telebot_factory.go index 1116de5..47c05f9 100644 --- a/api/telebot_factory.go +++ b/api/telebot_factory.go @@ -5,7 +5,7 @@ import ( tb "gopkg.in/tucnak/telebot.v2" ) -func makeTbVideo(vf *core.Video, caption string) tb.Video { +func makeTbVideo(vf *core.Video, caption string) *tb.Video { video := tb.Video{File: tb.FromDisk(vf.Path)} video.FileName = vf.File.Name video.Width = vf.Width @@ -18,5 +18,14 @@ func makeTbVideo(vf *core.Video, caption string) tb.Video { Width: vf.Thumb.Width, Height: vf.Thumb.Height, } - return video + return &video +} + +func makeTbPhoto(image *core.Image, caption string) *tb.Photo { + photo := &tb.Photo{File: tb.FromDisk(image.File.Path)} + if len(image.ID) > 0 { + photo = &tb.Photo{File: tb.File{FileID: image.ID}} + } + photo.Caption = caption + return photo } diff --git a/core/bot.go b/core/bot.go index 27d982e..f9bfd26 100644 --- a/core/bot.go +++ b/core/bot.go @@ -4,7 +4,7 @@ package core type IBot interface { Delete(*Message) error SendText(string, ...interface{}) (*Message, error) - SendImage(*Image) (*Message, error) + SendImage(*Image, string) (*Message, error) SendAlbum([]*Image) ([]*Message, error) SendMedia(*Media) (*Message, error) SendPhotoAlbum([]*Media) ([]*Message, error) diff --git a/usecases/publisher_flow.go b/usecases/publisher_flow.go index db02c21..868507b 100644 --- a/usecases/publisher_flow.go +++ b/usecases/publisher_flow.go @@ -102,7 +102,7 @@ func (p *PublisherFlow) runLoop() { } case 1: p.l.Info("have one actual photo") - sent, err := ms.bot.SendImage(&core.Image{ID: photos[0]}) + sent, err := ms.bot.SendImage(&core.Image{ID: photos[0]}, "") if err != nil { p.l.Error(err) } else { From 0704ca56e8f5d5528d44d3d1fe58bf2e7be53ad4 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Sun, 8 Aug 2021 16:15:20 +0300 Subject: [PATCH 114/439] fix failed tests --- usecases/faggot_game_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/usecases/faggot_game_test.go b/usecases/faggot_game_test.go index fe66f15..5abe1b5 100644 --- a/usecases/faggot_game_test.go +++ b/usecases/faggot_game_test.go @@ -282,7 +282,7 @@ type BotMock struct { } func (BotMock) Delete(*core.Message) error { return nil } -func (BotMock) SendImage(*core.Image) (*core.Message, error) { return nil, nil } +func (BotMock) SendImage(*core.Image, string) (*core.Message, error) { return nil, nil } func (BotMock) SendAlbum([]*core.Image) ([]*core.Message, error) { return nil, nil } func (BotMock) SendMedia(*core.Media) (*core.Message, error) { return nil, nil } func (BotMock) SendPhotoAlbum([]*core.Media) ([]*core.Message, error) { return nil, nil } From ff1f8684fc6b059fb62c77cb5264fcff9e2aadef Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Mon, 9 Aug 2021 23:44:35 +0300 Subject: [PATCH 115/439] Fallback to media upload for image as for video --- usecases/twitter_flow.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/usecases/twitter_flow.go b/usecases/twitter_flow.go index c300268..6d1b543 100644 --- a/usecases/twitter_flow.go +++ b/usecases/twitter_flow.go @@ -56,7 +56,7 @@ func (tf *TwitterFlow) handleMedia(media []*core.Media, message *core.Message, b return errors.New("unexpected 0 media count") case 1: _, err := bot.SendMedia(media[0]) - if err != nil && media[0].Type == core.TVideo { + if err != nil { if strings.Contains(err.Error(), "failed to get HTTP URL content") || strings.Contains(err.Error(), "wrong file identifier/HTTP URL specified") { return tf.fallbackToUploading(media[0], bot) } @@ -74,7 +74,7 @@ func (tf *TwitterFlow) fallbackToUploading(media *core.Media, bot core.IBot) err mediaPath := path.Join(os.TempDir(), path.Base(media.URL)) file, err := tf.fd.Download(media.URL, mediaPath) if err != nil { - tf.l.Errorf("video download error: %v", err) + tf.l.Errorf("file download error: %v", err) return err } @@ -87,6 +87,12 @@ func (tf *TwitterFlow) fallbackToUploading(media *core.Media, bot core.IBot) err tf.l.Infof("File downloaded: %s %0.2fMB", file.Name, float64(stat.Size())/1024/1024) + if media.Type == core.TPhoto { + image := &core.Image{File: *file} + _, err := bot.SendImage(image, media.Caption) + return err + } + // else vf, err := tf.vff.CreateVideo(file.Path) if err != nil { tf.l.Errorf("Can't create video file for %s, %v", file.Path, err) From b3295d1b88d892a177b759a81cffa8d86406a020 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Tue, 10 Aug 2021 11:28:38 +0300 Subject: [PATCH 116/439] TwitterFlow refactoring - move parsing loginc into TwitterParser --- pullanusbot.go | 3 ++- usecases/twitter_flow.go | 13 +--------- usecases/twitter_parser.go | 25 ++++++++++++++++++++ usecases/twitter_timeout.go | 47 ++++++++++++++++++++----------------- 4 files changed, 53 insertions(+), 35 deletions(-) create mode 100644 usecases/twitter_parser.go diff --git a/pullanusbot.go b/pullanusbot.go index 8e88c34..d3d39f9 100644 --- a/pullanusbot.go +++ b/pullanusbot.go @@ -41,7 +41,8 @@ func main() { twitterAPI := api.CreateTwitterAPI() twitterFlow := usecases.CreateTwitterFlow(logger, twitterAPI, fileDownloader, converter) twitterTimeout := usecases.CreateTwitterTimeout(logger, twitterFlow) - telebot.AddHandler(twitterTimeout) + twitterParser := usecases.CreateTwitterParser(twitterTimeout) + telebot.AddHandler(twitterParser) linkFlow := usecases.CreateLinkFlow(logger, fileDownloader, converter, converter) telebot.AddHandler(linkFlow) diff --git a/usecases/twitter_flow.go b/usecases/twitter_flow.go index 6d1b543..ae57eb5 100644 --- a/usecases/twitter_flow.go +++ b/usecases/twitter_flow.go @@ -4,7 +4,6 @@ import ( "errors" "os" "path" - "regexp" "strings" "github.com/ailinykh/pullanusbot/v2/core" @@ -23,17 +22,7 @@ type TwitterFlow struct { vff core.IVideoFactory } -// HandleText is a core.ITextHandler protocol implementation -func (tf *TwitterFlow) HandleText(message *core.Message, bot core.IBot) error { - r := regexp.MustCompile(`twitter\.com.+/(\d+)\S*$`) - match := r.FindStringSubmatch(message.Text) - if len(match) < 2 { - return nil // no tweet id found - } - return tf.process(match[1], message, bot) -} - -func (tf *TwitterFlow) process(tweetID string, message *core.Message, bot core.IBot) error { +func (tf *TwitterFlow) HandleTweet(tweetID string, message *core.Message, bot core.IBot) error { tf.l.Infof("processing tweet %s", tweetID) media, err := tf.mf.CreateMedia(tweetID, message.Sender) if err != nil { diff --git a/usecases/twitter_parser.go b/usecases/twitter_parser.go new file mode 100644 index 0000000..409c777 --- /dev/null +++ b/usecases/twitter_parser.go @@ -0,0 +1,25 @@ +package usecases + +import ( + "regexp" + + "github.com/ailinykh/pullanusbot/v2/core" +) + +func CreateTwitterParser(tt *TwitterTimeout) *TwitterParser { + return &TwitterParser{tt} +} + +type TwitterParser struct { + tt *TwitterTimeout +} + +// HandleText is a core.ITextHandler protocol implementation +func (tp *TwitterParser) HandleText(message *core.Message, bot core.IBot) error { + r := regexp.MustCompile(`twitter\.com.+/(\d+)\S*$`) + match := r.FindStringSubmatch(message.Text) + if len(match) < 2 { + return nil // no tweet id found + } + return tp.tt.HandleTweet(match[1], message, bot) +} diff --git a/usecases/twitter_timeout.go b/usecases/twitter_timeout.go index 2729649..f200dc5 100644 --- a/usecases/twitter_timeout.go +++ b/usecases/twitter_timeout.go @@ -24,20 +24,29 @@ type TwitterTimeout struct { replies map[core.Message]core.Message } -// HandleText is a core.ITextHandler protocol implementation -func (tt *TwitterTimeout) HandleText(message *core.Message, bot core.IBot) error { - err := tt.tf.HandleText(message, bot) +func (tt *TwitterTimeout) HandleTweet(tweetID string, message *core.Message, bot core.IBot) error { + err := tt.tf.HandleTweet(tweetID, message, bot) if err != nil { if strings.HasPrefix(err.Error(), "Rate limit exceeded") { - err := tt.handleTimeout(err, message, bot) - if strings.HasPrefix(err.Error(), "twitter api timeout") { - sent, err := bot.SendText(err.Error(), message) - if err != nil { - return err - } - tt.replies[*message] = *sent - return nil + timeout, err := tt.parseTimeout(err) + if err != nil { + return err } + + go func() { + time.Sleep(time.Duration(timeout) * time.Second) + tt.HandleTweet(tweetID, message, bot) + }() + + minutes := timeout / 60 + seconds := timeout % 60 + reply := fmt.Sprintf("twitter api timeout %d min %d sec", minutes, seconds) + sent, err := bot.SendText(reply, message) + if err != nil { + return err + } + tt.replies[*message] = *sent + return nil } tt.l.Error(err) } else if sent, ok := tt.replies[*message]; ok { @@ -47,26 +56,20 @@ func (tt *TwitterTimeout) HandleText(message *core.Message, bot core.IBot) error return err } -func (tt *TwitterTimeout) handleTimeout(err error, message *core.Message, bot core.IBot) error { +func (tt *TwitterTimeout) parseTimeout(err error) (int64, error) { r := regexp.MustCompile(`(\-?\d+)$`) match := r.FindStringSubmatch(err.Error()) if len(match) < 2 { - return errors.New("rate limit not found") + return 0, errors.New("rate limit not found") } limit, err := strconv.ParseInt(match[1], 10, 64) if err != nil { - return err + return 0, err } timeout := limit - time.Now().Unix() tt.l.Infof("Twitter api timeout %d seconds", timeout) - timeout = int64(math.Max(float64(timeout), 1)) // Twitter api timeout might be negative - go func() { - time.Sleep(time.Duration(timeout) * time.Second) - tt.HandleText(message, bot) - }() - minutes := timeout / 60 - seconds := timeout % 60 - return fmt.Errorf("twitter api timeout %d min %d sec", minutes, seconds) + timeout = int64(math.Max(float64(timeout), 2)) // Twitter api timeout might be negative + return timeout, nil } From d6c41d4fc7dd836d9906742f2b1f5e3f91801963 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Tue, 10 Aug 2021 18:22:58 +0300 Subject: [PATCH 117/439] Add more generic ITweetHandler protocol for combining all the Twitter-related stuff --- usecases/twitter_flow.go | 5 +++++ usecases/twitter_parser.go | 8 ++++---- usecases/twitter_timeout.go | 9 +++++---- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/usecases/twitter_flow.go b/usecases/twitter_flow.go index ae57eb5..d192d33 100644 --- a/usecases/twitter_flow.go +++ b/usecases/twitter_flow.go @@ -9,6 +9,10 @@ import ( "github.com/ailinykh/pullanusbot/v2/core" ) +type ITweetHandler interface { + HandleTweet(string, *core.Message, core.IBot) error +} + // CreateTwitterFlow is a basic TwitterFlow factory func CreateTwitterFlow(l core.ILogger, mf core.IMediaFactory, fd core.IFileDownloader, vff core.IVideoFactory) *TwitterFlow { return &TwitterFlow{l, mf, fd, vff} @@ -22,6 +26,7 @@ type TwitterFlow struct { vff core.IVideoFactory } +// HandleTweet is a ITweetHandler protocol implementation func (tf *TwitterFlow) HandleTweet(tweetID string, message *core.Message, bot core.IBot) error { tf.l.Infof("processing tweet %s", tweetID) media, err := tf.mf.CreateMedia(tweetID, message.Sender) diff --git a/usecases/twitter_parser.go b/usecases/twitter_parser.go index 409c777..eea5953 100644 --- a/usecases/twitter_parser.go +++ b/usecases/twitter_parser.go @@ -6,12 +6,12 @@ import ( "github.com/ailinykh/pullanusbot/v2/core" ) -func CreateTwitterParser(tt *TwitterTimeout) *TwitterParser { - return &TwitterParser{tt} +func CreateTwitterParser(th ITweetHandler) *TwitterParser { + return &TwitterParser{th} } type TwitterParser struct { - tt *TwitterTimeout + th ITweetHandler } // HandleText is a core.ITextHandler protocol implementation @@ -21,5 +21,5 @@ func (tp *TwitterParser) HandleText(message *core.Message, bot core.IBot) error if len(match) < 2 { return nil // no tweet id found } - return tp.tt.HandleTweet(match[1], message, bot) + return tp.th.HandleTweet(match[1], message, bot) } diff --git a/usecases/twitter_timeout.go b/usecases/twitter_timeout.go index f200dc5..3023648 100644 --- a/usecases/twitter_timeout.go +++ b/usecases/twitter_timeout.go @@ -13,19 +13,20 @@ import ( ) // CreateTwitterFlow is a basic TwitterFlow factory -func CreateTwitterTimeout(l core.ILogger, tf *TwitterFlow) *TwitterTimeout { - return &TwitterTimeout{l, tf, make(map[core.Message]core.Message)} +func CreateTwitterTimeout(l core.ILogger, th ITweetHandler) *TwitterTimeout { + return &TwitterTimeout{l, th, make(map[core.Message]core.Message)} } // TwitterTimeout is a decorator for TwitterFlow to handle API timeouts gracefully type TwitterTimeout struct { l core.ILogger - tf *TwitterFlow + th ITweetHandler replies map[core.Message]core.Message } +// HandleTweet is a ITweetHandler protocol implementation func (tt *TwitterTimeout) HandleTweet(tweetID string, message *core.Message, bot core.IBot) error { - err := tt.tf.HandleTweet(tweetID, message, bot) + err := tt.th.HandleTweet(tweetID, message, bot) if err != nil { if strings.HasPrefix(err.Error(), "Rate limit exceeded") { timeout, err := tt.parseTimeout(err) From 614a77f5b96e60543195d9d69e1bc531c3d7ad64 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Tue, 10 Aug 2021 18:45:02 +0300 Subject: [PATCH 118/439] export GO_ENV=testing to be able to detect testing enviromnemt in source code --- Makefile | 2 +- usecases/faggot_game.go | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 2293ac3..caa454c 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ run: ./pullanusbot test: - go test ./... -coverprofile=coverage.txt -race -covermode=atomic + GO_ENV=testing go test ./... -v -coverprofile=coverage.txt -race -covermode=atomic build: clean *.go go build . diff --git a/usecases/faggot_game.go b/usecases/faggot_game.go index ecaefd3..2ff23b4 100644 --- a/usecases/faggot_game.go +++ b/usecases/faggot_game.go @@ -3,6 +3,7 @@ package usecases import ( "fmt" "math/rand" + "os" "sort" "strconv" "strings" @@ -116,8 +117,10 @@ func (flow *GameFlow) Play(message *core.Message, bot core.IBot) error { //TODO: logger? } - r := rand.Intn(3) + 1 - time.Sleep(time.Duration(r) * time.Second) + if os.Getenv("GO_ENV") != "testing" { + r := rand.Intn(3) + 1 + time.Sleep(time.Duration(r) * time.Second) + } } return nil From 0674581ef04281a404cdb36dbee13fbcc14d5069 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Tue, 10 Aug 2021 20:00:27 +0300 Subject: [PATCH 119/439] typo fixed --- pullanusbot.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pullanusbot.go b/pullanusbot.go index d3d39f9..9049809 100644 --- a/pullanusbot.go +++ b/pullanusbot.go @@ -24,8 +24,8 @@ func main() { localizer := infrastructure.GameLocalizer{} dbFile := path.Join(getWorkingDir(), "pullanusbot.db") - gameStorade := infrastructure.CreateGameStorage(dbFile) - gameFlow := usecases.CreateGameFlow(localizer, gameStorade) + gameStorage := infrastructure.CreateGameStorage(dbFile) + gameFlow := usecases.CreateGameFlow(localizer, gameStorage) telebot.AddHandler("/pidorules", gameFlow.Rules) telebot.AddHandler("/pidoreg", gameFlow.Add) telebot.AddHandler("/pidor", gameFlow.Play) From 8bd875d2eae2a7e93c6a67ed6e662e98b2b5e3a8 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Tue, 10 Aug 2021 23:10:50 +0300 Subject: [PATCH 120/439] Move test files into separate usecases_test package --- usecases/faggot_game.go | 4 ++-- usecases/faggot_game_test.go | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/usecases/faggot_game.go b/usecases/faggot_game.go index 2ff23b4..6517055 100644 --- a/usecases/faggot_game.go +++ b/usecases/faggot_game.go @@ -14,8 +14,8 @@ import ( ) // CreateGameFlow is a simple GameFlow factory -func CreateGameFlow(l core.ILocalizer, s core.IGameStorage) GameFlow { - return GameFlow{l, s} +func CreateGameFlow(l core.ILocalizer, s core.IGameStorage) *GameFlow { + return &GameFlow{l, s} } // GameFlow represents faggot game logic diff --git a/usecases/faggot_game_test.go b/usecases/faggot_game_test.go index 5abe1b5..ebb007b 100644 --- a/usecases/faggot_game_test.go +++ b/usecases/faggot_game_test.go @@ -1,4 +1,4 @@ -package usecases +package usecases_test import ( "fmt" @@ -8,6 +8,7 @@ import ( "time" "github.com/ailinykh/pullanusbot/v2/core" + "github.com/ailinykh/pullanusbot/v2/usecases" "github.com/stretchr/testify/assert" ) @@ -212,7 +213,7 @@ func makeMessage(id int, username string) *core.Message { return &core.Message{ID: 0, Sender: player} } -func makeSUT(args ...interface{}) (*GameFlow, *BotMock, *GameStorageMock) { +func makeSUT(args ...interface{}) (*usecases.GameFlow, *BotMock, *GameStorageMock) { dict := LocalizerDict{} storage := &GameStorageMock{players: []*core.User{}} bot := &BotMock{} @@ -225,7 +226,7 @@ func makeSUT(args ...interface{}) (*GameFlow, *BotMock, *GameStorageMock) { } l := &LocalizerMock{dict: dict} - game := &GameFlow{l, storage} + game := usecases.CreateGameFlow(l, storage) return game, bot, storage } From efc87b47234c208fd3d1dc4f70093514290a3d7c Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Wed, 11 Aug 2021 09:33:29 +0300 Subject: [PATCH 121/439] Rename makeMessage to makeGameMessage since it game related --- usecases/faggot_game_test.go | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/usecases/faggot_game_test.go b/usecases/faggot_game_test.go index ebb007b..ebd9f80 100644 --- a/usecases/faggot_game_test.go +++ b/usecases/faggot_game_test.go @@ -14,7 +14,7 @@ import ( func Test_RulesCommand_DeliversRules(t *testing.T) { game, bot, _ := makeSUT(LocalizerDict{"faggot_rules": "Game rules:"}) - message := makeMessage(1, "Faggot") + message := makeGameMessage(1, "Faggot") game.Rules(message, bot) @@ -26,7 +26,7 @@ func Test_Add_AppendsPlayerInGameOnlyOnce(t *testing.T) { "faggot_added_to_game": "Player added", "faggot_already_in_game": "Player already in game", }) - message := makeMessage(1, "Faggot") + message := makeGameMessage(1, "Faggot") game.Add(message, bot) @@ -43,7 +43,7 @@ func Test_Play_RespondsWithNoPlayers(t *testing.T) { game, bot, _ := makeSUT(LocalizerDict{ "faggot_no_players": "Nobody in game. So you win, %s!", }) - message := makeMessage(1, "Faggot") + message := makeGameMessage(1, "Faggot") game.Play(message, bot) @@ -54,7 +54,7 @@ func Test_Play_RespondsNotEnoughPlayers(t *testing.T) { game, bot, _ := makeSUT(LocalizerDict{ "faggot_not_enough_players": "Not enough players", }) - message := makeMessage(1, "Faggot") + message := makeGameMessage(1, "Faggot") game.Add(message, bot) game.Play(message, bot) @@ -69,8 +69,8 @@ func Test_Play_RespondsWithCurrentGameResult(t *testing.T) { "faggot_game_2_0": "2", "faggot_game_3_0": "%s", }) - m1 := makeMessage(1, "") - m2 := makeMessage(2, "") + m1 := makeGameMessage(1, "") + m2 := makeGameMessage(2, "") game.Add(m1, bot) game.Add(m2, bot) @@ -91,8 +91,8 @@ func Test_Play_RespondsWinnerAlreadyKnown(t *testing.T) { "faggot_game_3_0": "3 %s", "faggot_winner_known": "Winner already known %s", }) - m1 := makeMessage(1, "Faggot1") - m2 := makeMessage(2, "Faggot2") + m1 := makeGameMessage(1, "Faggot1") + m2 := makeGameMessage(2, "Faggot2") game.Add(m1, bot) game.Add(m2, bot) @@ -127,9 +127,9 @@ func Test_Stats_RespondsWithDescendingResultsForCurrentYear(t *testing.T) { "total_players:3", } - m1 := makeMessage(1, "Faggot1") - m2 := makeMessage(2, "Faggot2") - m3 := makeMessage(3, "Faggot3") + m1 := makeGameMessage(1, "Faggot1") + m2 := makeGameMessage(2, "Faggot2") + m3 := makeGameMessage(3, "Faggot3") storage.rounds = []*core.Round{ {Day: year + "-01-01", Winner: m2.Sender}, @@ -162,9 +162,9 @@ func Test_All_RespondsWithDescendingResultsForAllTime(t *testing.T) { "total_players:3", } - m1 := makeMessage(1, "Faggot1") - m2 := makeMessage(2, "Faggot2") - m3 := makeMessage(3, "Faggot3") + m1 := makeGameMessage(1, "Faggot1") + m2 := makeGameMessage(2, "Faggot2") + m3 := makeGameMessage(3, "Faggot3") storage.rounds = []*core.Round{ {Day: "2021-01-01", Winner: m2.Sender}, @@ -185,8 +185,8 @@ func Test_Me_RespondsWithPersonalStat(t *testing.T) { "faggot_me": "username:%s,scores:%d", }) - m1 := makeMessage(1, "Faggot1") - m2 := makeMessage(2, "Faggot2") + m1 := makeGameMessage(1, "Faggot1") + m2 := makeGameMessage(2, "Faggot2") storage.rounds = []*core.Round{ {Day: "2021-01-01", Winner: m2.Sender}, @@ -203,7 +203,7 @@ func Test_Me_RespondsWithPersonalStat(t *testing.T) { // Helpers -func makeMessage(id int, username string) *core.Message { +func makeGameMessage(id int, username string) *core.Message { player := &core.User{ ID: id, FirstName: "FirstName" + fmt.Sprint(id), From c4bb579188ded75fa82c3b03486b0acc0f409f6f Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Wed, 11 Aug 2021 10:50:17 +0300 Subject: [PATCH 122/439] Process multiple twitter links --- usecases/twitter_parser.go | 15 ++++++--- usecases/twitter_parser_test.go | 56 +++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 5 deletions(-) create mode 100644 usecases/twitter_parser_test.go diff --git a/usecases/twitter_parser.go b/usecases/twitter_parser.go index eea5953..76b64ea 100644 --- a/usecases/twitter_parser.go +++ b/usecases/twitter_parser.go @@ -16,10 +16,15 @@ type TwitterParser struct { // HandleText is a core.ITextHandler protocol implementation func (tp *TwitterParser) HandleText(message *core.Message, bot core.IBot) error { - r := regexp.MustCompile(`twitter\.com.+/(\d+)\S*$`) - match := r.FindStringSubmatch(message.Text) - if len(match) < 2 { - return nil // no tweet id found + r := regexp.MustCompile(`twitter\.com\S+/(\d+)`) + match := r.FindAllStringSubmatch(message.Text, -1) + + for _, m := range match { + err := tp.th.HandleTweet(m[1], message, bot) + if err != nil { + return err + } } - return tp.th.HandleTweet(match[1], message, bot) + + return nil } diff --git a/usecases/twitter_parser_test.go b/usecases/twitter_parser_test.go new file mode 100644 index 0000000..c6a4755 --- /dev/null +++ b/usecases/twitter_parser_test.go @@ -0,0 +1,56 @@ +package usecases_test + +import ( + "testing" + + "github.com/ailinykh/pullanusbot/v2/core" + "github.com/ailinykh/pullanusbot/v2/usecases" + "github.com/stretchr/testify/assert" +) + +func Test_HandleText_NotFoundAnyLinkByDefault(t *testing.T) { + handler := &FakeTweetHandler{[]string{}} + parser := usecases.CreateTwitterParser(handler) + m := makeTweetMessage("a message without any links") + bot := &BotMock{} + + parser.HandleText(m, bot) + + assert.Equal(t, []string{}, handler.tweets) +} + +func Test_HandleText_FoundTweetLink(t *testing.T) { + handler := &FakeTweetHandler{[]string{}} + parser := usecases.CreateTwitterParser(handler) + m := makeTweetMessage("a message with https://twitter.com/status/username/123456") + bot := &BotMock{} + + parser.HandleText(m, bot) + + assert.Equal(t, []string{"123456"}, handler.tweets) +} + +func Test_HandleText_FoundMultipleTweetLinks(t *testing.T) { + handler := &FakeTweetHandler{[]string{}} + parser := usecases.CreateTwitterParser(handler) + m := makeTweetMessage("a message with https://twitter.com/status/username/123456 and https://twitter.com/status/username/789010 and some text") + bot := &BotMock{} + + parser.HandleText(m, bot) + + assert.Equal(t, []string{"123456", "789010"}, handler.tweets) +} + +func makeTweetMessage(text string) *core.Message { + return &core.Message{ID: 0, Text: text} +} + +type FakeTweetHandler struct { + tweets []string +} + +// HandleTweet is a ITweetHandler protocol implementation +func (fth *FakeTweetHandler) HandleTweet(tweetID string, message *core.Message, bot core.IBot) error { + fth.tweets = append(fth.tweets, tweetID) + return nil +} From 63f23bd209ffcadc1fdcb4669db2d016c394b46e Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Wed, 11 Aug 2021 22:56:14 +0300 Subject: [PATCH 123/439] Rename messages to sentMessages and add removedMessages to BotMock --- usecases/faggot_game_test.go | 47 ++++++++++++++++++++---------------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/usecases/faggot_game_test.go b/usecases/faggot_game_test.go index ebd9f80..e8328ee 100644 --- a/usecases/faggot_game_test.go +++ b/usecases/faggot_game_test.go @@ -18,7 +18,7 @@ func Test_RulesCommand_DeliversRules(t *testing.T) { game.Rules(message, bot) - assert.Equal(t, bot.messages[0], "Game rules:") + assert.Equal(t, bot.sentMessages[0], "Game rules:") } func Test_Add_AppendsPlayerInGameOnlyOnce(t *testing.T) { @@ -31,12 +31,12 @@ func Test_Add_AppendsPlayerInGameOnlyOnce(t *testing.T) { game.Add(message, bot) assert.Equal(t, storage.players, []*core.User{message.Sender}) - assert.Equal(t, bot.messages[0], "Player added") + assert.Equal(t, bot.sentMessages[0], "Player added") game.Add(message, bot) assert.Equal(t, storage.players, []*core.User{message.Sender}) - assert.Equal(t, bot.messages[1], "Player already in game") + assert.Equal(t, bot.sentMessages[1], "Player already in game") } func Test_Play_RespondsWithNoPlayers(t *testing.T) { @@ -47,7 +47,7 @@ func Test_Play_RespondsWithNoPlayers(t *testing.T) { game.Play(message, bot) - assert.Equal(t, bot.messages[0], "Nobody in game. So you win, Faggot!") + assert.Equal(t, bot.sentMessages[0], "Nobody in game. So you win, Faggot!") } func Test_Play_RespondsNotEnoughPlayers(t *testing.T) { @@ -59,7 +59,7 @@ func Test_Play_RespondsNotEnoughPlayers(t *testing.T) { game.Add(message, bot) game.Play(message, bot) - assert.Equal(t, bot.messages[1], "Not enough players") + assert.Equal(t, bot.sentMessages[1], "Not enough players") } func Test_Play_RespondsWithCurrentGameResult(t *testing.T) { @@ -78,10 +78,10 @@ func Test_Play_RespondsWithCurrentGameResult(t *testing.T) { winner := storage.rounds[0].Winner phrase := fmt.Sprintf(`%s %s`, winner.ID, winner.FirstName, winner.LastName) - assert.Equal(t, "0", bot.messages[2]) - assert.Equal(t, "1", bot.messages[3]) - assert.Equal(t, "2", bot.messages[4]) - assert.Equal(t, phrase, bot.messages[5]) + assert.Equal(t, "0", bot.sentMessages[2]) + assert.Equal(t, "1", bot.sentMessages[3]) + assert.Equal(t, "2", bot.sentMessages[4]) + assert.Equal(t, phrase, bot.sentMessages[5]) } func Test_Play_RespondsWinnerAlreadyKnown(t *testing.T) { game, bot, storage := makeSUT(LocalizerDict{ @@ -99,14 +99,14 @@ func Test_Play_RespondsWinnerAlreadyKnown(t *testing.T) { game.Play(m1, bot) winner := storage.rounds[0].Winner.Username - assert.Equal(t, bot.messages[2], "0") - assert.Equal(t, bot.messages[3], "1") - assert.Equal(t, bot.messages[4], "2") - assert.Equal(t, bot.messages[5], fmt.Sprintf("3 @%s", winner)) + assert.Equal(t, bot.sentMessages[2], "0") + assert.Equal(t, bot.sentMessages[3], "1") + assert.Equal(t, bot.sentMessages[4], "2") + assert.Equal(t, bot.sentMessages[5], fmt.Sprintf("3 @%s", winner)) game.Play(m1, bot) - assert.Equal(t, bot.messages[6], fmt.Sprintf("Winner already known %s", winner)) + assert.Equal(t, bot.sentMessages[6], fmt.Sprintf("Winner already known %s", winner)) } func Test_Stats_RespondsWithDescendingResultsForCurrentYear(t *testing.T) { @@ -142,7 +142,7 @@ func Test_Stats_RespondsWithDescendingResultsForCurrentYear(t *testing.T) { } game.Stats(m1, bot) - assert.Equal(t, strings.Split(bot.messages[0], "\n"), expected) + assert.Equal(t, strings.Split(bot.sentMessages[0], "\n"), expected) } func Test_All_RespondsWithDescendingResultsForAllTime(t *testing.T) { @@ -177,7 +177,7 @@ func Test_All_RespondsWithDescendingResultsForAllTime(t *testing.T) { } game.All(m1, bot) - assert.Equal(t, strings.Split(bot.messages[0], "\n"), expected) + assert.Equal(t, strings.Split(bot.sentMessages[0], "\n"), expected) } func Test_Me_RespondsWithPersonalStat(t *testing.T) { @@ -195,10 +195,10 @@ func Test_Me_RespondsWithPersonalStat(t *testing.T) { } game.Me(m1, bot) - assert.Equal(t, bot.messages[0], fmt.Sprintf("username:%s,scores:%d", m1.Sender.Username, 2)) + assert.Equal(t, bot.sentMessages[0], fmt.Sprintf("username:%s,scores:%d", m1.Sender.Username, 2)) game.Me(m2, bot) - assert.Equal(t, bot.messages[1], fmt.Sprintf("username:%s,scores:%d", m2.Sender.Username, 1)) + assert.Equal(t, bot.sentMessages[1], fmt.Sprintf("username:%s,scores:%d", m2.Sender.Username, 1)) } // Helpers @@ -279,17 +279,22 @@ func (s *GameStorageMock) GetRounds(gameID int64) ([]*core.Round, error) { } type BotMock struct { - messages []string + sentMessages []string + removedMessages []string } -func (BotMock) Delete(*core.Message) error { return nil } func (BotMock) SendImage(*core.Image, string) (*core.Message, error) { return nil, nil } func (BotMock) SendAlbum([]*core.Image) ([]*core.Message, error) { return nil, nil } func (BotMock) SendMedia(*core.Media) (*core.Message, error) { return nil, nil } func (BotMock) SendPhotoAlbum([]*core.Media) ([]*core.Message, error) { return nil, nil } func (BotMock) SendVideo(*core.Video, string) (*core.Message, error) { return nil, nil } +func (b *BotMock) Delete(message *core.Message) error { + b.removedMessages = append(b.removedMessages, message.Text) + return nil +} + func (b *BotMock) SendText(text string, args ...interface{}) (*core.Message, error) { - b.messages = append(b.messages, text) + b.sentMessages = append(b.sentMessages, text) return nil, nil } From 1f70ce3bb55bc54ba03f5b0e51353764c76a3036 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Wed, 11 Aug 2021 22:58:43 +0300 Subject: [PATCH 124/439] TwitterParser: delete original message only if it matches link --- usecases/twitter_flow.go | 9 +++++--- usecases/twitter_parser.go | 4 ++-- usecases/twitter_parser_test.go | 41 +++++++++++++++++++++++++-------- usecases/twitter_timeout.go | 6 ++--- 4 files changed, 43 insertions(+), 17 deletions(-) diff --git a/usecases/twitter_flow.go b/usecases/twitter_flow.go index d192d33..88a063f 100644 --- a/usecases/twitter_flow.go +++ b/usecases/twitter_flow.go @@ -10,7 +10,7 @@ import ( ) type ITweetHandler interface { - HandleTweet(string, *core.Message, core.IBot) error + HandleTweet(string, *core.Message, core.IBot, bool) error } // CreateTwitterFlow is a basic TwitterFlow factory @@ -27,7 +27,7 @@ type TwitterFlow struct { } // HandleTweet is a ITweetHandler protocol implementation -func (tf *TwitterFlow) HandleTweet(tweetID string, message *core.Message, bot core.IBot) error { +func (tf *TwitterFlow) HandleTweet(tweetID string, message *core.Message, bot core.IBot, deleteOriginal bool) error { tf.l.Infof("processing tweet %s", tweetID) media, err := tf.mf.CreateMedia(tweetID, message.Sender) if err != nil { @@ -41,7 +41,10 @@ func (tf *TwitterFlow) HandleTweet(tweetID string, message *core.Message, bot co return err } - return bot.Delete(message) + if deleteOriginal { + return bot.Delete(message) + } + return nil } func (tf *TwitterFlow) handleMedia(media []*core.Media, message *core.Message, bot core.IBot) error { diff --git a/usecases/twitter_parser.go b/usecases/twitter_parser.go index 76b64ea..9531c3d 100644 --- a/usecases/twitter_parser.go +++ b/usecases/twitter_parser.go @@ -16,11 +16,11 @@ type TwitterParser struct { // HandleText is a core.ITextHandler protocol implementation func (tp *TwitterParser) HandleText(message *core.Message, bot core.IBot) error { - r := regexp.MustCompile(`twitter\.com\S+/(\d+)`) + r := regexp.MustCompile(`https://twitter\.com\S+/(\d+)\S*`) match := r.FindAllStringSubmatch(message.Text, -1) for _, m := range match { - err := tp.th.HandleTweet(m[1], message, bot) + err := tp.th.HandleTweet(m[1], message, bot, message.Text == m[0]) if err != nil { return err } diff --git a/usecases/twitter_parser_test.go b/usecases/twitter_parser_test.go index c6a4755..c11239b 100644 --- a/usecases/twitter_parser_test.go +++ b/usecases/twitter_parser_test.go @@ -20,10 +20,8 @@ func Test_HandleText_NotFoundAnyLinkByDefault(t *testing.T) { } func Test_HandleText_FoundTweetLink(t *testing.T) { - handler := &FakeTweetHandler{[]string{}} - parser := usecases.CreateTwitterParser(handler) + parser, handler, bot := makeTwitterSUT() m := makeTweetMessage("a message with https://twitter.com/status/username/123456") - bot := &BotMock{} parser.HandleText(m, bot) @@ -31,16 +29,38 @@ func Test_HandleText_FoundTweetLink(t *testing.T) { } func Test_HandleText_FoundMultipleTweetLinks(t *testing.T) { - handler := &FakeTweetHandler{[]string{}} - parser := usecases.CreateTwitterParser(handler) - m := makeTweetMessage("a message with https://twitter.com/status/username/123456 and https://twitter.com/status/username/789010 and some text") - bot := &BotMock{} - + parser, handler, bot := makeTwitterSUT() + m := makeTweetMessage("a message with https://twitter.com/username/status/123456 and https://twitter.com/username/status/789010 and some text") parser.HandleText(m, bot) assert.Equal(t, []string{"123456", "789010"}, handler.tweets) } +func Test_HandleText_RemovesOriginalMessageInCaseOfFullMatch(t *testing.T) { + parser, _, bot := makeTwitterSUT() + m := makeTweetMessage("https://twitter.com/username/status/123456") + + parser.HandleText(m, bot) + + assert.Equal(t, []string{"https://twitter.com/username/status/123456"}, bot.removedMessages) +} + +func Test_HandleText_DoesNotRemoveOriginalMessage(t *testing.T) { + parser, _, bot := makeTwitterSUT() + m := makeTweetMessage("https://twitter.com/username/status/123456 and some other text") + + parser.HandleText(m, bot) + + assert.Equal(t, []string{}, bot.removedMessages) +} + +func makeTwitterSUT() (*usecases.TwitterParser, *FakeTweetHandler, *BotMock) { + handler := &FakeTweetHandler{[]string{}} + parser := usecases.CreateTwitterParser(handler) + bot := &BotMock{[]string{}, []string{}} + return parser, handler, bot +} + func makeTweetMessage(text string) *core.Message { return &core.Message{ID: 0, Text: text} } @@ -50,7 +70,10 @@ type FakeTweetHandler struct { } // HandleTweet is a ITweetHandler protocol implementation -func (fth *FakeTweetHandler) HandleTweet(tweetID string, message *core.Message, bot core.IBot) error { +func (fth *FakeTweetHandler) HandleTweet(tweetID string, message *core.Message, bot core.IBot, deleteOriginal bool) error { fth.tweets = append(fth.tweets, tweetID) + if deleteOriginal { + return bot.Delete(message) + } return nil } diff --git a/usecases/twitter_timeout.go b/usecases/twitter_timeout.go index 3023648..30cfd0f 100644 --- a/usecases/twitter_timeout.go +++ b/usecases/twitter_timeout.go @@ -25,8 +25,8 @@ type TwitterTimeout struct { } // HandleTweet is a ITweetHandler protocol implementation -func (tt *TwitterTimeout) HandleTweet(tweetID string, message *core.Message, bot core.IBot) error { - err := tt.th.HandleTweet(tweetID, message, bot) +func (tt *TwitterTimeout) HandleTweet(tweetID string, message *core.Message, bot core.IBot, deleteOriginal bool) error { + err := tt.th.HandleTweet(tweetID, message, bot, deleteOriginal) if err != nil { if strings.HasPrefix(err.Error(), "Rate limit exceeded") { timeout, err := tt.parseTimeout(err) @@ -36,7 +36,7 @@ func (tt *TwitterTimeout) HandleTweet(tweetID string, message *core.Message, bot go func() { time.Sleep(time.Duration(timeout) * time.Second) - tt.HandleTweet(tweetID, message, bot) + tt.HandleTweet(tweetID, message, bot, deleteOriginal) }() minutes := timeout / 60 From b479b7f9304ae71953353aedb499b73c61530bf8 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Thu, 12 Aug 2021 22:36:25 +0300 Subject: [PATCH 125/439] Core: use thumb Image as reference in Video struct --- api/youtube_api.go | 2 +- core/video.go | 2 +- infrastructure/ffmpeg_converter.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/api/youtube_api.go b/api/youtube_api.go index 86a7b27..6721ad5 100644 --- a/api/youtube_api.go +++ b/api/youtube_api.go @@ -76,7 +76,7 @@ func (y *YoutubeAPI) CreateVideo(id string) (*core.Video, error) { Bitrate: 0, Duration: video.Duration, Codec: vf.VCodec, - Thumb: core.Image{ + Thumb: &core.Image{ File: *file, Width: thumb.Width, Height: thumb.Height, diff --git a/core/video.go b/core/video.go index 0c1d4c8..7e0f9bb 100644 --- a/core/video.go +++ b/core/video.go @@ -10,7 +10,7 @@ type Video struct { Bitrate int Duration int Codec string - Thumb Image + Thumb *Image } // Dispose to cleanup filesystem diff --git a/infrastructure/ffmpeg_converter.go b/infrastructure/ffmpeg_converter.go index 9c29e4a..53d98af 100644 --- a/infrastructure/ffmpeg_converter.go +++ b/infrastructure/ffmpeg_converter.go @@ -129,7 +129,7 @@ func (c *FfmpegConverter) CreateVideo(path string) (*core.Video, error) { Bitrate: bitrate, Duration: int(duration), Codec: stream.CodecName, - Thumb: *thumb}, nil + Thumb: thumb}, nil } func (c *FfmpegConverter) getFFProbe(file string) (*ffpResponse, error) { From b3a125d7791773de5e64f23cb38dc960e7c99229 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Thu, 12 Aug 2021 22:37:30 +0300 Subject: [PATCH 126/439] Make Video.Thumb optional --- api/telebot_factory.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/api/telebot_factory.go b/api/telebot_factory.go index 47c05f9..536d9bc 100644 --- a/api/telebot_factory.go +++ b/api/telebot_factory.go @@ -13,10 +13,12 @@ func makeTbVideo(vf *core.Video, caption string) *tb.Video { video.Caption = caption video.Duration = vf.Duration video.SupportsStreaming = true - video.Thumbnail = &tb.Photo{ - File: tb.FromDisk(vf.Thumb.Path), - Width: vf.Thumb.Width, - Height: vf.Thumb.Height, + if vf.Thumb != nil { + video.Thumbnail = &tb.Photo{ + File: tb.FromDisk(vf.Thumb.Path), + Width: vf.Thumb.Width, + Height: vf.Thumb.Height, + } } return &video } From d58f99bf4fbd8be6b1e6a95c95ede77b0a616f58 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Thu, 12 Aug 2021 23:13:41 +0300 Subject: [PATCH 127/439] Add GetCodec method to VideoConverter --- core/video_converter.go | 1 + infrastructure/ffmpeg_converter.go | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/core/video_converter.go b/core/video_converter.go index 1477e5a..5c7622b 100644 --- a/core/video_converter.go +++ b/core/video_converter.go @@ -2,5 +2,6 @@ package core // IVideoConverter convert Video with specified bitrate type IVideoConverter interface { + GetCodec(string) string Convert(*Video, int) (*Video, error) } diff --git a/infrastructure/ffmpeg_converter.go b/infrastructure/ffmpeg_converter.go index 53d98af..1f7e867 100644 --- a/infrastructure/ffmpeg_converter.go +++ b/infrastructure/ffmpeg_converter.go @@ -41,6 +41,23 @@ func (c *FfmpegConverter) Convert(vf *core.Video, bitrate int) (*core.Video, err return c.CreateVideo(path) } +// GetCodec is a core.IVideoConverter interface implementation +func (c *FfmpegConverter) GetCodec(path string) string { + ffprobe, err := c.getFFProbe(path) + if err != nil { + c.l.Error(err) + return "unknown" + } + + stream, err := ffprobe.getVideoStream() + if err != nil { + c.l.Error(err) + return "unknown" + } + + return stream.CodecName +} + // CreateVideo is a core.IVideoSplitter interface implementation func (c *FfmpegConverter) Split(video *core.Video, limit int) ([]*core.Video, error) { duration, n := 0, 0 From cbc6e6d78a2b78f2111a5c6ef18cc7b0c02de132 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Thu, 12 Aug 2021 23:15:45 +0300 Subject: [PATCH 128/439] Check mp4 file codec for h264 before uploading --- usecases/link_flow.go | 56 +++++++++++++++++++++++++++---------------- 1 file changed, 35 insertions(+), 21 deletions(-) diff --git a/usecases/link_flow.go b/usecases/link_flow.go index 5e0d46b..788ca23 100644 --- a/usecases/link_flow.go +++ b/usecases/link_flow.go @@ -46,40 +46,54 @@ func (lf *LinkFlow) processLink(message *core.Message, bot core.IBot) error { switch resp.Header["Content-Type"][0] { case "video/mp4": lf.l.Infof("found mp4 file %s", message.Text) - _, err := bot.SendMedia(media) - if err != nil { - lf.l.Errorf("%s. Fallback to uploading", err) - err := lf.sendByUploading(media, bot) + codec := lf.vfc.GetCodec(media.URL) + if codec != "h264" { + lf.l.Warningf("expected h264 codec, but got %s", codec) + err := lf.sendByConverting(media, bot) if err != nil { return err } + } else { + _, err = bot.SendMedia(media) + if err != nil { + lf.l.Errorf("%s. Fallback to uploading", err) + err := lf.sendByUploading(media, bot) + if err != nil { + return err + } + } } - return bot.Delete(message) case "video/webm": - vf, err := lf.downloadMedia(media) - if err != nil { - return err - } - defer vf.Dispose() - - vfc, err := lf.vfc.Convert(vf, 0) + err := lf.sendByConverting(media, bot) if err != nil { - lf.l.Errorf("cant convert video file: %v", err) return err } - defer vfc.Dispose() - - _, err = bot.SendVideo(vfc, media.Caption) - if err != nil { - return err - } - return bot.Delete(message) case "text/html; charset=utf-8": + return nil default: lf.l.Warningf("Unsupported content type: %s", resp.Header["Content-Type"]) + return nil } - return nil + return bot.Delete(message) +} + +func (lf *LinkFlow) sendByConverting(media *core.Media, bot core.IBot) error { + vf, err := lf.downloadMedia(media) + if err != nil { + return err + } + defer vf.Dispose() + + vfc, err := lf.vfc.Convert(vf, 0) + if err != nil { + lf.l.Errorf("cant convert video file: %v", err) + return err + } + defer vfc.Dispose() + + _, err = bot.SendVideo(vfc, media.Caption) + return err } func (lf *LinkFlow) sendByUploading(media *core.Media, bot core.IBot) error { From 6b92b1e6bb230bdcf1b9a9ac8f07c8f5dd683cc2 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Fri, 13 Aug 2021 09:41:01 +0300 Subject: [PATCH 129/439] Remove redundant code from TwitterFlow --- usecases/twitter_flow.go | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/usecases/twitter_flow.go b/usecases/twitter_flow.go index 88a063f..74f515f 100644 --- a/usecases/twitter_flow.go +++ b/usecases/twitter_flow.go @@ -77,12 +77,7 @@ func (tf *TwitterFlow) fallbackToUploading(media *core.Media, bot core.IBot) err defer file.Dispose() - stat, err := os.Stat(file.Path) - if err != nil { - return err - } - - tf.l.Infof("File downloaded: %s %0.2fMB", file.Name, float64(stat.Size())/1024/1024) + tf.l.Infof("File downloaded: %s %0.2fMB", file.Name, float64(file.Size)/1024/1024) if media.Type == core.TPhoto { image := &core.Image{File: *file} From d0ae034b2965691c773762737d05278d2ef1c3b8 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Fri, 13 Aug 2021 09:41:50 +0300 Subject: [PATCH 130/439] Revert "Make Video.Thumb optional" This reverts commit b3a125d7791773de5e64f23cb38dc960e7c99229. --- api/telebot_factory.go | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/api/telebot_factory.go b/api/telebot_factory.go index 536d9bc..47c05f9 100644 --- a/api/telebot_factory.go +++ b/api/telebot_factory.go @@ -13,12 +13,10 @@ func makeTbVideo(vf *core.Video, caption string) *tb.Video { video.Caption = caption video.Duration = vf.Duration video.SupportsStreaming = true - if vf.Thumb != nil { - video.Thumbnail = &tb.Photo{ - File: tb.FromDisk(vf.Thumb.Path), - Width: vf.Thumb.Width, - Height: vf.Thumb.Height, - } + video.Thumbnail = &tb.Photo{ + File: tb.FromDisk(vf.Thumb.Path), + Width: vf.Thumb.Width, + Height: vf.Thumb.Height, } return &video } From a213a4d8df3bc2a04562bfc7a3634b8e384d93e1 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Fri, 13 Aug 2021 09:45:49 +0300 Subject: [PATCH 131/439] Rename Twitter to TwitterAPI --- api/twitter_api.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/api/twitter_api.go b/api/twitter_api.go index 2e9ae0a..c1a930e 100644 --- a/api/twitter_api.go +++ b/api/twitter_api.go @@ -12,14 +12,14 @@ import ( ) // CreateTwitterAPI is a default Twitter factory -func CreateTwitterAPI() *Twitter { - return &Twitter{} +func CreateTwitterAPI() *TwitterAPI { + return &TwitterAPI{} } // Twitter API -type Twitter struct{} +type TwitterAPI struct{} -func (Twitter) get(tweetID string) (*Tweet, error) { +func (TwitterAPI) get(tweetID string) (*Tweet, error) { client := http.DefaultClient req, _ := http.NewRequest("GET", fmt.Sprintf("https://api.twitter.com/1.1/statuses/show.json?id=%s&tweet_mode=extended", tweetID), nil) req.Header.Add("Authorization", "Bearer AAAAAAAAAAAAAAAAAAAAAPYXBAAAAAAACLXUNDekMxqa8h%2F40K4moUkGsoc%3DTYfbDKbT3jJPCEVnMYqilB28NHfOPqkca3qaAxGfsyKCs0wRbw") @@ -48,7 +48,7 @@ func (Twitter) get(tweetID string) (*Tweet, error) { } // CreateMedia is a core.IMediaFactory interface implementation -func (t *Twitter) CreateMedia(tweetID string, author *core.User) ([]*core.Media, error) { +func (t *TwitterAPI) CreateMedia(tweetID string, author *core.User) ([]*core.Media, error) { tweet, err := t.get(tweetID) if err != nil { return nil, err @@ -82,7 +82,7 @@ func (t *Twitter) CreateMedia(tweetID string, author *core.User) ([]*core.Media, } } -func (Twitter) makeCaption(author string, tweet *Tweet) string { +func (TwitterAPI) makeCaption(author string, tweet *Tweet) string { re := regexp.MustCompile(`\s?http\S+$`) text := re.ReplaceAllString(tweet.FullText, "") return fmt.Sprintf("🐦 %s (by %s)\n%s", tweet.User.ScreenName, tweet.ID, tweet.User.Name, author, text) From 1ee9f7f0848a70da6dbed1861673ee4c7d1224e6 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Fri, 13 Aug 2021 09:52:52 +0300 Subject: [PATCH 132/439] Move BotMock into a separate common_test file --- usecases/common_test.go | 24 ++++++++++++++++++++++++ usecases/faggot_game_test.go | 21 --------------------- 2 files changed, 24 insertions(+), 21 deletions(-) create mode 100644 usecases/common_test.go diff --git a/usecases/common_test.go b/usecases/common_test.go new file mode 100644 index 0000000..f21933f --- /dev/null +++ b/usecases/common_test.go @@ -0,0 +1,24 @@ +package usecases_test + +import "github.com/ailinykh/pullanusbot/v2/core" + +type BotMock struct { + sentMessages []string + removedMessages []string +} + +func (BotMock) SendImage(*core.Image, string) (*core.Message, error) { return nil, nil } +func (BotMock) SendAlbum([]*core.Image) ([]*core.Message, error) { return nil, nil } +func (BotMock) SendMedia(*core.Media) (*core.Message, error) { return nil, nil } +func (BotMock) SendPhotoAlbum([]*core.Media) ([]*core.Message, error) { return nil, nil } +func (BotMock) SendVideo(*core.Video, string) (*core.Message, error) { return nil, nil } + +func (b *BotMock) Delete(message *core.Message) error { + b.removedMessages = append(b.removedMessages, message.Text) + return nil +} + +func (b *BotMock) SendText(text string, args ...interface{}) (*core.Message, error) { + b.sentMessages = append(b.sentMessages, text) + return nil, nil +} diff --git a/usecases/faggot_game_test.go b/usecases/faggot_game_test.go index e8328ee..45c4f60 100644 --- a/usecases/faggot_game_test.go +++ b/usecases/faggot_game_test.go @@ -277,24 +277,3 @@ func (s *GameStorageMock) AddRound(gameID int64, round *core.Round) error { func (s *GameStorageMock) GetRounds(gameID int64) ([]*core.Round, error) { return s.rounds, nil } - -type BotMock struct { - sentMessages []string - removedMessages []string -} - -func (BotMock) SendImage(*core.Image, string) (*core.Message, error) { return nil, nil } -func (BotMock) SendAlbum([]*core.Image) ([]*core.Message, error) { return nil, nil } -func (BotMock) SendMedia(*core.Media) (*core.Message, error) { return nil, nil } -func (BotMock) SendPhotoAlbum([]*core.Media) ([]*core.Message, error) { return nil, nil } -func (BotMock) SendVideo(*core.Video, string) (*core.Message, error) { return nil, nil } - -func (b *BotMock) Delete(message *core.Message) error { - b.removedMessages = append(b.removedMessages, message.Text) - return nil -} - -func (b *BotMock) SendText(text string, args ...interface{}) (*core.Message, error) { - b.sentMessages = append(b.sentMessages, text) - return nil, nil -} From 11e5f6f67b9e65104756a28de27a5045e7740915 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Fri, 13 Aug 2021 09:53:43 +0300 Subject: [PATCH 133/439] Rename BotMock to FakeBot --- usecases/common_test.go | 16 ++++++++-------- usecases/faggot_game_test.go | 4 ++-- usecases/twitter_parser_test.go | 6 +++--- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/usecases/common_test.go b/usecases/common_test.go index f21933f..baed8d4 100644 --- a/usecases/common_test.go +++ b/usecases/common_test.go @@ -2,23 +2,23 @@ package usecases_test import "github.com/ailinykh/pullanusbot/v2/core" -type BotMock struct { +type FakeBot struct { sentMessages []string removedMessages []string } -func (BotMock) SendImage(*core.Image, string) (*core.Message, error) { return nil, nil } -func (BotMock) SendAlbum([]*core.Image) ([]*core.Message, error) { return nil, nil } -func (BotMock) SendMedia(*core.Media) (*core.Message, error) { return nil, nil } -func (BotMock) SendPhotoAlbum([]*core.Media) ([]*core.Message, error) { return nil, nil } -func (BotMock) SendVideo(*core.Video, string) (*core.Message, error) { return nil, nil } +func (FakeBot) SendImage(*core.Image, string) (*core.Message, error) { return nil, nil } +func (FakeBot) SendAlbum([]*core.Image) ([]*core.Message, error) { return nil, nil } +func (FakeBot) SendMedia(*core.Media) (*core.Message, error) { return nil, nil } +func (FakeBot) SendPhotoAlbum([]*core.Media) ([]*core.Message, error) { return nil, nil } +func (FakeBot) SendVideo(*core.Video, string) (*core.Message, error) { return nil, nil } -func (b *BotMock) Delete(message *core.Message) error { +func (b *FakeBot) Delete(message *core.Message) error { b.removedMessages = append(b.removedMessages, message.Text) return nil } -func (b *BotMock) SendText(text string, args ...interface{}) (*core.Message, error) { +func (b *FakeBot) SendText(text string, args ...interface{}) (*core.Message, error) { b.sentMessages = append(b.sentMessages, text) return nil, nil } diff --git a/usecases/faggot_game_test.go b/usecases/faggot_game_test.go index 45c4f60..2e932ae 100644 --- a/usecases/faggot_game_test.go +++ b/usecases/faggot_game_test.go @@ -213,10 +213,10 @@ func makeGameMessage(id int, username string) *core.Message { return &core.Message{ID: 0, Sender: player} } -func makeSUT(args ...interface{}) (*usecases.GameFlow, *BotMock, *GameStorageMock) { +func makeSUT(args ...interface{}) (*usecases.GameFlow, *FakeBot, *GameStorageMock) { dict := LocalizerDict{} storage := &GameStorageMock{players: []*core.User{}} - bot := &BotMock{} + bot := &FakeBot{} for _, arg := range args { switch opt := arg.(type) { diff --git a/usecases/twitter_parser_test.go b/usecases/twitter_parser_test.go index c11239b..a939699 100644 --- a/usecases/twitter_parser_test.go +++ b/usecases/twitter_parser_test.go @@ -12,7 +12,7 @@ func Test_HandleText_NotFoundAnyLinkByDefault(t *testing.T) { handler := &FakeTweetHandler{[]string{}} parser := usecases.CreateTwitterParser(handler) m := makeTweetMessage("a message without any links") - bot := &BotMock{} + bot := &FakeBot{} parser.HandleText(m, bot) @@ -54,10 +54,10 @@ func Test_HandleText_DoesNotRemoveOriginalMessage(t *testing.T) { assert.Equal(t, []string{}, bot.removedMessages) } -func makeTwitterSUT() (*usecases.TwitterParser, *FakeTweetHandler, *BotMock) { +func makeTwitterSUT() (*usecases.TwitterParser, *FakeTweetHandler, *FakeBot) { handler := &FakeTweetHandler{[]string{}} parser := usecases.CreateTwitterParser(handler) - bot := &BotMock{[]string{}, []string{}} + bot := &FakeBot{[]string{}, []string{}} return parser, handler, bot } From be7b57db2b0a172e788935a243582ba95599bd2d Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Fri, 13 Aug 2021 10:05:22 +0300 Subject: [PATCH 134/439] Test for TwitterParser error handling --- usecases/twitter_parser_test.go | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/usecases/twitter_parser_test.go b/usecases/twitter_parser_test.go index a939699..e5ca0b4 100644 --- a/usecases/twitter_parser_test.go +++ b/usecases/twitter_parser_test.go @@ -1,6 +1,7 @@ package usecases_test import ( + "errors" "testing" "github.com/ailinykh/pullanusbot/v2/core" @@ -9,10 +10,8 @@ import ( ) func Test_HandleText_NotFoundAnyLinkByDefault(t *testing.T) { - handler := &FakeTweetHandler{[]string{}} - parser := usecases.CreateTwitterParser(handler) + parser, handler, bot := makeTwitterSUT() m := makeTweetMessage("a message without any links") - bot := &FakeBot{} parser.HandleText(m, bot) @@ -54,8 +53,18 @@ func Test_HandleText_DoesNotRemoveOriginalMessage(t *testing.T) { assert.Equal(t, []string{}, bot.removedMessages) } +func Test_HandleText_ReturnsErrorOnError(t *testing.T) { + parser, handler, bot := makeTwitterSUT() + m := makeTweetMessage("a message with https://twitter.com/status/username/123456") + handler.err = errors.New("an error") + + err := parser.HandleText(m, bot) + + assert.Equal(t, "an error", err.Error()) +} + func makeTwitterSUT() (*usecases.TwitterParser, *FakeTweetHandler, *FakeBot) { - handler := &FakeTweetHandler{[]string{}} + handler := &FakeTweetHandler{[]string{}, nil} parser := usecases.CreateTwitterParser(handler) bot := &FakeBot{[]string{}, []string{}} return parser, handler, bot @@ -67,6 +76,7 @@ func makeTweetMessage(text string) *core.Message { type FakeTweetHandler struct { tweets []string + err error } // HandleTweet is a ITweetHandler protocol implementation @@ -75,5 +85,5 @@ func (fth *FakeTweetHandler) HandleTweet(tweetID string, message *core.Message, if deleteOriginal { return bot.Delete(message) } - return nil + return fth.err } From d1020700c530f67675a65613978a432c44b670fa Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Fri, 13 Aug 2021 16:33:46 +0300 Subject: [PATCH 135/439] Replace expected vs received in tests --- usecases/faggot_game_test.go | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/usecases/faggot_game_test.go b/usecases/faggot_game_test.go index 2e932ae..d946ebf 100644 --- a/usecases/faggot_game_test.go +++ b/usecases/faggot_game_test.go @@ -18,7 +18,7 @@ func Test_RulesCommand_DeliversRules(t *testing.T) { game.Rules(message, bot) - assert.Equal(t, bot.sentMessages[0], "Game rules:") + assert.Equal(t, "Game rules:", bot.sentMessages[0]) } func Test_Add_AppendsPlayerInGameOnlyOnce(t *testing.T) { @@ -31,12 +31,12 @@ func Test_Add_AppendsPlayerInGameOnlyOnce(t *testing.T) { game.Add(message, bot) assert.Equal(t, storage.players, []*core.User{message.Sender}) - assert.Equal(t, bot.sentMessages[0], "Player added") + assert.Equal(t, "Player added", bot.sentMessages[0]) game.Add(message, bot) assert.Equal(t, storage.players, []*core.User{message.Sender}) - assert.Equal(t, bot.sentMessages[1], "Player already in game") + assert.Equal(t, "Player already in game", bot.sentMessages[1]) } func Test_Play_RespondsWithNoPlayers(t *testing.T) { @@ -47,7 +47,7 @@ func Test_Play_RespondsWithNoPlayers(t *testing.T) { game.Play(message, bot) - assert.Equal(t, bot.sentMessages[0], "Nobody in game. So you win, Faggot!") + assert.Equal(t, "Nobody in game. So you win, Faggot!", bot.sentMessages[0]) } func Test_Play_RespondsNotEnoughPlayers(t *testing.T) { @@ -59,7 +59,7 @@ func Test_Play_RespondsNotEnoughPlayers(t *testing.T) { game.Add(message, bot) game.Play(message, bot) - assert.Equal(t, bot.sentMessages[1], "Not enough players") + assert.Equal(t, "Not enough players", bot.sentMessages[1]) } func Test_Play_RespondsWithCurrentGameResult(t *testing.T) { @@ -99,14 +99,14 @@ func Test_Play_RespondsWinnerAlreadyKnown(t *testing.T) { game.Play(m1, bot) winner := storage.rounds[0].Winner.Username - assert.Equal(t, bot.sentMessages[2], "0") - assert.Equal(t, bot.sentMessages[3], "1") - assert.Equal(t, bot.sentMessages[4], "2") - assert.Equal(t, bot.sentMessages[5], fmt.Sprintf("3 @%s", winner)) + assert.Equal(t, "0", bot.sentMessages[2]) + assert.Equal(t, "1", bot.sentMessages[3]) + assert.Equal(t, "2", bot.sentMessages[4]) + assert.Equal(t, fmt.Sprintf("3 @%s", winner), bot.sentMessages[5]) game.Play(m1, bot) - assert.Equal(t, bot.sentMessages[6], fmt.Sprintf("Winner already known %s", winner)) + assert.Equal(t, fmt.Sprintf("Winner already known %s", winner), bot.sentMessages[6]) } func Test_Stats_RespondsWithDescendingResultsForCurrentYear(t *testing.T) { @@ -142,7 +142,7 @@ func Test_Stats_RespondsWithDescendingResultsForCurrentYear(t *testing.T) { } game.Stats(m1, bot) - assert.Equal(t, strings.Split(bot.sentMessages[0], "\n"), expected) + assert.Equal(t, expected, strings.Split(bot.sentMessages[0], "\n")) } func Test_All_RespondsWithDescendingResultsForAllTime(t *testing.T) { @@ -177,7 +177,7 @@ func Test_All_RespondsWithDescendingResultsForAllTime(t *testing.T) { } game.All(m1, bot) - assert.Equal(t, strings.Split(bot.sentMessages[0], "\n"), expected) + assert.Equal(t, expected, strings.Split(bot.sentMessages[0], "\n")) } func Test_Me_RespondsWithPersonalStat(t *testing.T) { @@ -195,10 +195,10 @@ func Test_Me_RespondsWithPersonalStat(t *testing.T) { } game.Me(m1, bot) - assert.Equal(t, bot.sentMessages[0], fmt.Sprintf("username:%s,scores:%d", m1.Sender.Username, 2)) + assert.Equal(t, fmt.Sprintf("username:%s,scores:%d", m1.Sender.Username, 2), bot.sentMessages[0]) game.Me(m2, bot) - assert.Equal(t, bot.sentMessages[1], fmt.Sprintf("username:%s,scores:%d", m2.Sender.Username, 1)) + assert.Equal(t, fmt.Sprintf("username:%s,scores:%d", m2.Sender.Username, 1), bot.sentMessages[1]) } // Helpers From d848dd3e398c60db3ffaf47c5a17be8fb2de97c9 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Fri, 13 Aug 2021 16:40:28 +0300 Subject: [PATCH 136/439] Test all the game commands works for group chat only --- usecases/faggot_game_test.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/usecases/faggot_game_test.go b/usecases/faggot_game_test.go index d946ebf..5721a96 100644 --- a/usecases/faggot_game_test.go +++ b/usecases/faggot_game_test.go @@ -12,6 +12,22 @@ import ( "github.com/stretchr/testify/assert" ) +func Test_AllTheCommands_WorksOnlyInGroupChats(t *testing.T) { + game, bot, _ := makeSUT(LocalizerDict{"faggot_not_available_for_private": "group only"}) + message := makeGameMessage(1, "Faggot") + message.IsPrivate = true + + game.Rules(message, bot) + game.Add(message, bot) + game.Play(message, bot) + game.Stats(message, bot) + game.All(message, bot) + game.Me(message, bot) + + for _, m := range bot.sentMessages { + assert.Equal(t, "group only", m) + } +} func Test_RulesCommand_DeliversRules(t *testing.T) { game, bot, _ := makeSUT(LocalizerDict{"faggot_rules": "Game rules:"}) message := makeGameMessage(1, "Faggot") From b02f28b3b0858f0da37fc94ad827653c986ff402 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Fri, 13 Aug 2021 17:02:54 +0300 Subject: [PATCH 137/439] Redundant code removed --- usecases/link_flow.go | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/usecases/link_flow.go b/usecases/link_flow.go index 788ca23..cd2a3b6 100644 --- a/usecases/link_flow.go +++ b/usecases/link_flow.go @@ -117,12 +117,7 @@ func (lf *LinkFlow) downloadMedia(media *core.Media) (*core.Video, error) { return nil, err } - stat, err := os.Stat(file.Path) - if err != nil { - return nil, err - } - - lf.l.Infof("File downloaded: %s %0.2fMB", file.Name, float64(stat.Size())/1024/1024) + lf.l.Infof("File downloaded: %s %0.2fMB", file.Name, file.Size/1024/1024) vf, err := lf.vff.CreateVideo(file.Path) if err != nil { From 54337c15b73be0fe891326f278fd3e2a8faa9421 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Sat, 14 Aug 2021 10:43:13 +0300 Subject: [PATCH 138/439] Add codec to core.Media --- api/youtube_api.go | 6 ++++++ core/media.go | 1 + 2 files changed, 7 insertions(+) diff --git a/api/youtube_api.go b/api/youtube_api.go index 6721ad5..a632c07 100644 --- a/api/youtube_api.go +++ b/api/youtube_api.go @@ -28,11 +28,17 @@ func (y *YoutubeAPI) CreateMedia(url string, author *core.User) ([]*core.Media, return nil, err } + vf, _, err := y.getFormats(video) + if err != nil { + return nil, err + } + return []*core.Media{ { URL: video.ID, Caption: video.Title, Duration: video.Duration, + Codec: vf.VCodec, Type: core.TVideo, }, }, nil diff --git a/core/media.go b/core/media.go index fe7e45c..4071169 100644 --- a/core/media.go +++ b/core/media.go @@ -17,5 +17,6 @@ type Media struct { URL string Caption string Duration int + Codec string // only video Type MediaType } From d8da97bfc8789526cd038e5adb9c799bcc82c96f Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Sat, 14 Aug 2021 10:43:42 +0300 Subject: [PATCH 139/439] Extendend logging for SendMedia func --- api/telebot_adapter.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/api/telebot_adapter.go b/api/telebot_adapter.go index 4325929..3e5ea40 100644 --- a/api/telebot_adapter.go +++ b/api/telebot_adapter.go @@ -65,16 +65,19 @@ func (a *TelebotAdapter) SendMedia(media *core.Media) (*core.Message, error) { var err error switch media.Type { case core.TPhoto: + a.t.logger.Infof("sending media as photo: %v", media) file := &tb.Photo{File: tb.FromURL(media.URL)} file.Caption = media.Caption a.t.bot.Notify(a.m.Chat, tb.UploadingPhoto) sent, err = a.t.bot.Send(a.m.Chat, file, &tb.SendOptions{ParseMode: tb.ModeHTML}) case core.TVideo: + a.t.logger.Infof("sending media as video: %v", media) file := &tb.Video{File: tb.FromURL(media.URL)} file.Caption = media.Caption a.t.bot.Notify(a.m.Chat, tb.UploadingVideo) sent, err = a.t.bot.Send(a.m.Chat, file, &tb.SendOptions{ParseMode: tb.ModeHTML}) case core.TText: + a.t.logger.Infof("sending media as text: %v", media) sent, err = a.t.bot.Send(a.m.Chat, media.Caption, &tb.SendOptions{ParseMode: tb.ModeHTML}) } From d31fb76ad1e7088030cf5f190482dc8ee0743485 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Sat, 14 Aug 2021 12:44:12 +0300 Subject: [PATCH 140/439] Calculate filesize in FileDownloader --- infrastructure/file_downloader.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/infrastructure/file_downloader.go b/infrastructure/file_downloader.go index b1d227f..9b9b43d 100644 --- a/infrastructure/file_downloader.go +++ b/infrastructure/file_downloader.go @@ -36,5 +36,14 @@ func (FileDownloader) Download(url core.URL, filepath string) (*core.File, error // Write the body to file _, err = io.Copy(out, resp.Body) - return &core.File{Name: name, Path: filepath}, err + if err != nil { + return nil, err + } + + // Retreive file size + stat, err := os.Stat(filepath) + if err != nil { + return nil, err + } + return &core.File{Name: name, Path: filepath, Size: stat.Size()}, err } From f36f1c7539d62f02f6c4a49df785a651825371ed Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Sat, 14 Aug 2021 13:22:29 +0300 Subject: [PATCH 141/439] Add IHttpClient for ContentType retreiving --- api/http_client.go | 24 ++++++++++++++++++++++++ core/networking.go | 5 +++++ 2 files changed, 29 insertions(+) create mode 100644 api/http_client.go diff --git a/api/http_client.go b/api/http_client.go new file mode 100644 index 0000000..2ee7fa2 --- /dev/null +++ b/api/http_client.go @@ -0,0 +1,24 @@ +package api + +import ( + "net/http" + + "github.com/ailinykh/pullanusbot/v2/core" +) + +func CreateHttpClient() *HttpClient { + return &HttpClient{} +} + +type HttpClient struct{} + +// GetContentType is a core.IHttpClient interface implementation +func (HttpClient) GetContentType(url core.URL) (string, error) { + resp, err := http.Get(url) + + if err != nil { + return "", err + } + + return resp.Header["Content-Type"][0], nil +} diff --git a/core/networking.go b/core/networking.go index c967470..cf68d9c 100644 --- a/core/networking.go +++ b/core/networking.go @@ -14,3 +14,8 @@ type IFileUploader interface { type IImageDownloader interface { Download(image *Image) (*File, error) } + +// IHttpClient retreives remote content info +type IHttpClient interface { + GetContentType(URL) (string, error) +} From 193a9bde8a4a6a930021b7566d3d5c0d31b81365 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Sat, 14 Aug 2021 13:25:31 +0300 Subject: [PATCH 142/439] Add IMediaFactory implementation to ffmpeg_converter --- infrastructure/ffmpeg_converter.go | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/infrastructure/ffmpeg_converter.go b/infrastructure/ffmpeg_converter.go index 1f7e867..62953e2 100644 --- a/infrastructure/ffmpeg_converter.go +++ b/infrastructure/ffmpeg_converter.go @@ -58,6 +58,27 @@ func (c *FfmpegConverter) GetCodec(path string) string { return stream.CodecName } +// CreateMedia is a core.IMediaFactory interface implementation +func (c *FfmpegConverter) CreateMedia(url string, author *core.User) ([]*core.Media, error) { + ffprobe, err := c.getFFProbe(url) + if err != nil { + c.l.Error(err) + return nil, err + } + + stream, err := ffprobe.getVideoStream() + if err != nil { + c.l.Error(err) + return nil, err + } + + if ffprobe.Format.FormatName == "image2" { + return []*core.Media{{URL: url, Codec: stream.CodecName, Type: core.TPhoto}}, nil + } + + return []*core.Media{{URL: url, Codec: stream.CodecName, Type: core.TVideo}}, nil +} + // CreateVideo is a core.IVideoSplitter interface implementation func (c *FfmpegConverter) Split(video *core.Video, limit int) ([]*core.Video, error) { duration, n := 0, 0 @@ -109,8 +130,8 @@ func (c *FfmpegConverter) CreateVideo(path string) (*core.Video, error) { return nil, err } - if duration < 1 { - c.l.Errorf("expected duration at least 1 second, got %f", duration) + if duration < 2 { + c.l.Errorf("expected duration at least 2 seconds, got %f", duration) return nil, errors.New("file is too short") } From bd31afa7c947885538c2dd8ad9da741e53dde26f Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Sat, 14 Aug 2021 13:50:00 +0300 Subject: [PATCH 143/439] Add image processing to LinkFlow --- pullanusbot.go | 3 +- usecases/link_flow.go | 125 +++++++++++++++++++++++++++--------------- 2 files changed, 84 insertions(+), 44 deletions(-) diff --git a/pullanusbot.go b/pullanusbot.go index 9049809..076f32f 100644 --- a/pullanusbot.go +++ b/pullanusbot.go @@ -44,7 +44,8 @@ func main() { twitterParser := usecases.CreateTwitterParser(twitterTimeout) telebot.AddHandler(twitterParser) - linkFlow := usecases.CreateLinkFlow(logger, fileDownloader, converter, converter) + httpClient := api.CreateHttpClient() + linkFlow := usecases.CreateLinkFlow(logger, httpClient, converter, fileDownloader, converter, converter) telebot.AddHandler(linkFlow) fileUploader := api.CreateTelegraphAPI() diff --git a/usecases/link_flow.go b/usecases/link_flow.go index cd2a3b6..f70e223 100644 --- a/usecases/link_flow.go +++ b/usecases/link_flow.go @@ -2,22 +2,24 @@ package usecases import ( "fmt" - "net/http" "os" "path" "regexp" + "strings" "github.com/ailinykh/pullanusbot/v2/core" ) // CreateLinkFlow is a basic LinkFlow factory -func CreateLinkFlow(l core.ILogger, fd core.IFileDownloader, vff core.IVideoFactory, vfc core.IVideoConverter) *LinkFlow { - return &LinkFlow{l, fd, vff, vfc} +func CreateLinkFlow(l core.ILogger, hc core.IHttpClient, mf core.IMediaFactory, fd core.IFileDownloader, vff core.IVideoFactory, vfc core.IVideoConverter) *LinkFlow { + return &LinkFlow{l, hc, mf, fd, vff, vfc} } // LinkFlow represents convert hotlink to video file logic type LinkFlow struct { l core.ILogger + hc core.IHttpClient + mf core.IMediaFactory fd core.IFileDownloader vff core.IVideoFactory vfc core.IVideoConverter @@ -27,62 +29,97 @@ type LinkFlow struct { func (lf *LinkFlow) HandleText(message *core.Message, bot core.IBot) error { r := regexp.MustCompile(`^http(\S+)$`) if r.MatchString(message.Text) { - return lf.processLink(message, bot) + return lf.handleURL(message, bot) } return nil } -func (lf *LinkFlow) processLink(message *core.Message, bot core.IBot) error { - resp, err := http.Get(message.Text) - +func (lf *LinkFlow) handleURL(message *core.Message, bot core.IBot) error { + contentType, err := lf.hc.GetContentType(message.Text) if err != nil { lf.l.Error(err) return err } - media := &core.Media{URL: resp.Request.URL.String()} - media.Caption = fmt.Sprintf(`🔗 %s (by %s)`, message.Text, path.Base(resp.Request.URL.Path), message.Sender.Username) + if !strings.HasPrefix(contentType, "video") && !strings.HasPrefix(contentType, "image") { + return nil + } - switch resp.Header["Content-Type"][0] { - case "video/mp4": - lf.l.Infof("found mp4 file %s", message.Text) + media, err := lf.mf.CreateMedia(message.Text, message.Sender) + if err != nil { + lf.l.Error(err) + return err + } - codec := lf.vfc.GetCodec(media.URL) - if codec != "h264" { - lf.l.Warningf("expected h264 codec, but got %s", codec) - err := lf.sendByConverting(media, bot) + for _, m := range media { + switch m.Type { + case core.TPhoto: + err := lf.sendAsPhoto(m, message, bot) if err != nil { return err } - } else { - _, err = bot.SendMedia(media) + case core.TVideo: + err := lf.sendAsVideo(m, message, bot) if err != nil { - lf.l.Errorf("%s. Fallback to uploading", err) - err := lf.sendByUploading(media, bot) - if err != nil { - return err - } + return err } + case core.TText: + lf.l.Warningf("Unexpected %+v", m) } - case "video/webm": - err := lf.sendByConverting(media, bot) - if err != nil { + } + + return bot.Delete(message) +} + +func (lf *LinkFlow) sendAsPhoto(media *core.Media, message *core.Message, bot core.IBot) error { + lf.l.Infof("sending as photo: %s", media.URL) + media.Caption = fmt.Sprintf(`🖼 %s (by %s)`, media.URL, path.Base(media.URL), message.Sender.Username) + _, err := bot.SendMedia(media) + if err != nil { + lf.l.Error(err) + if strings.Contains(err.Error(), "failed to get HTTP URL content") || strings.Contains(err.Error(), "wrong file identifier/HTTP URL specified") { + file, err := lf.downloadMedia(media) + if err != nil { + return err + } + image := &core.Image{File: *file} + _, err = bot.SendImage(image, media.Caption) return err } - case "text/html; charset=utf-8": - return nil - default: - lf.l.Warningf("Unsupported content type: %s", resp.Header["Content-Type"]) - return nil } - return bot.Delete(message) + return err +} + +func (lf *LinkFlow) sendAsVideo(media *core.Media, message *core.Message, bot core.IBot) error { + lf.l.Infof("sending as video: %s", media.URL) + media.Caption = fmt.Sprintf(`🔗 %s (by %s)`, message.Text, path.Base(media.URL), message.Sender.Username) + + if media.Codec != "h264" { + lf.l.Warningf("expected h264 codec, but got %s", media.Codec) + return lf.sendByConverting(media, bot) + } + + _, err := bot.SendMedia(media) + if err != nil { + lf.l.Errorf("%s, fallback to uploading...", err) + return lf.sendByUploading(media, bot) + } + + return err } func (lf *LinkFlow) sendByConverting(media *core.Media, bot core.IBot) error { - vf, err := lf.downloadMedia(media) + lf.l.Info("sending by converting") + file, err := lf.downloadMedia(media) if err != nil { return err } + + vf, err := lf.vff.CreateVideo(file.Path) + if err != nil { + lf.l.Errorf("can't create video file for %s, %v", file.Path, err) + return err + } defer vf.Dispose() vfc, err := lf.vfc.Convert(vf, 0) @@ -98,18 +135,25 @@ func (lf *LinkFlow) sendByConverting(media *core.Media, bot core.IBot) error { func (lf *LinkFlow) sendByUploading(media *core.Media, bot core.IBot) error { // Try to upload file to telegram - lf.l.Info("Sending by uploading") + lf.l.Info("sending by uploading") + + file, err := lf.downloadMedia(media) + if err != nil { + return err + } - vf, err := lf.downloadMedia(media) + vf, err := lf.vff.CreateVideo(file.Path) if err != nil { + lf.l.Errorf("can't create video file for %s, %v", file.Path, err) return err } + defer vf.Dispose() _, err = bot.SendVideo(vf, media.Caption) return err } -func (lf *LinkFlow) downloadMedia(media *core.Media) (*core.Video, error) { +func (lf *LinkFlow) downloadMedia(media *core.Media) (*core.File, error) { mediaPath := path.Join(os.TempDir(), path.Base(media.URL)) file, err := lf.fd.Download(media.URL, mediaPath) if err != nil { @@ -117,12 +161,7 @@ func (lf *LinkFlow) downloadMedia(media *core.Media) (*core.Video, error) { return nil, err } - lf.l.Infof("File downloaded: %s %0.2fMB", file.Name, file.Size/1024/1024) + lf.l.Infof("file downloaded: %s %0.2fMB", file.Name, float64(file.Size)/1024/1024) - vf, err := lf.vff.CreateVideo(file.Path) - if err != nil { - lf.l.Errorf("Can't create video file for %s, %v", file.Path, err) - return nil, err - } - return vf, nil + return file, nil } From 82d798c797e9ff30b91ac78ca56371c802abc784 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Sun, 15 Aug 2021 18:41:12 +0300 Subject: [PATCH 144/439] Add new ISendMediaStrategy protocol --- core/media.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/media.go b/core/media.go index 4071169..a1f7f4e 100644 --- a/core/media.go +++ b/core/media.go @@ -20,3 +20,7 @@ type Media struct { Codec string // only video Type MediaType } + +type ISendMediaStrategy interface { + SendMedia([]*Media, IBot) error +} From 957bff9eb9145ac4151cdf5acc91ac04085f4545 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Sun, 15 Aug 2021 18:41:31 +0300 Subject: [PATCH 145/439] Add base ISendMediaStrategy implementation --- usecases/send_media_strategy.go | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 usecases/send_media_strategy.go diff --git a/usecases/send_media_strategy.go b/usecases/send_media_strategy.go new file mode 100644 index 0000000..3f0a026 --- /dev/null +++ b/usecases/send_media_strategy.go @@ -0,0 +1,33 @@ +package usecases + +import ( + "errors" + + "github.com/ailinykh/pullanusbot/v2/core" +) + +func CreateSendMediaStrategy(l core.ILogger) *SendMediaStrategy { + return &SendMediaStrategy{l} +} + +type SendMediaStrategy struct { + l core.ILogger +} + +// SendMedia is a core.ISendMediaStrategy interface implementation +func (sms *SendMediaStrategy) SendMedia(media []*core.Media, bot core.IBot) error { + switch len(media) { + case 0: + sms.l.Warning("Unexpected empty media") + case 1: + if media[0].Type == core.TVideo && media[0].Codec != "mp4" { + return errors.New("unexpected video codec " + media[0].Codec) + } + _, err := bot.SendMedia(media[0]) + return err + default: + _, err := bot.SendPhotoAlbum(media) + return err + } + return nil +} From 53297a8eef6ece27f3f3c2e11be1f16dd0777e52 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Sun, 15 Aug 2021 18:41:59 +0300 Subject: [PATCH 146/439] Add ISendMediaStrategy implementation with direct file uploading --- usecases/upload_media_strategy.go | 74 +++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 usecases/upload_media_strategy.go diff --git a/usecases/upload_media_strategy.go b/usecases/upload_media_strategy.go new file mode 100644 index 0000000..909da6b --- /dev/null +++ b/usecases/upload_media_strategy.go @@ -0,0 +1,74 @@ +package usecases + +import ( + "os" + "path" + "strings" + + "github.com/ailinykh/pullanusbot/v2/core" +) + +func CreateUploadMediaStrategy(l core.ILogger, sms core.ISendMediaStrategy, fd core.IFileDownloader, vf core.IVideoFactory, vc core.IVideoConverter) *UploadMediaStrategy { + return &UploadMediaStrategy{l, sms, fd, vf, vc} +} + +type UploadMediaStrategy struct { + l core.ILogger + sms core.ISendMediaStrategy + fd core.IFileDownloader + vf core.IVideoFactory + vc core.IVideoConverter +} + +// SendMedia is a core.ISendMediaStrategy interface implementation +func (ums *UploadMediaStrategy) SendMedia(media []*core.Media, bot core.IBot) error { + err := ums.sms.SendMedia(media, bot) + if err != nil { + ums.l.Error(err) + if strings.Contains(err.Error(), "failed to get HTTP URL content") || strings.Contains(err.Error(), "wrong file identifier/HTTP URL specified") { + return ums.fallbackToUploading(media[0], bot) + } + } + + return err +} + +func (ums *UploadMediaStrategy) fallbackToUploading(media *core.Media, bot core.IBot) error { + ums.l.Info("send by uploading") + file, err := ums.downloadMedia(media) + if err != nil { + return err + } + defer file.Dispose() + + switch media.Type { + case core.TText: + ums.l.Warning("unexpected media type") + case core.TPhoto: + image := &core.Image{File: *file} + _, err = bot.SendImage(image, media.Caption) + return err + case core.TVideo: + vf, err := ums.vf.CreateVideo(file.Path) + if err != nil { + ums.l.Errorf("can't create video file for %s, %v", file.Path, err) + return err + } + _, err = bot.SendVideo(vf, media.Caption) + return err + } + return err +} + +func (ums *UploadMediaStrategy) downloadMedia(media *core.Media) (*core.File, error) { + mediaPath := path.Join(os.TempDir(), path.Base(media.URL)) + file, err := ums.fd.Download(media.URL, mediaPath) + if err != nil { + ums.l.Errorf("video download error: %v", err) + return nil, err + } + + ums.l.Infof("file downloaded: %s %0.2fMB", file.Name, float64(file.Size)/1024/1024) + + return file, nil +} From a2a6d4fb913cd9e291b37533652006887d0d0dd3 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Sun, 15 Aug 2021 18:42:36 +0300 Subject: [PATCH 147/439] Add ISendMediaStrategy implementation by video file converting --- usecases/convert_media_strategy.go | 73 ++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 usecases/convert_media_strategy.go diff --git a/usecases/convert_media_strategy.go b/usecases/convert_media_strategy.go new file mode 100644 index 0000000..e87edf6 --- /dev/null +++ b/usecases/convert_media_strategy.go @@ -0,0 +1,73 @@ +package usecases + +import ( + "os" + "path" + "strings" + + "github.com/ailinykh/pullanusbot/v2/core" +) + +func CreateConvertMediaStrategy(l core.ILogger, sms core.ISendMediaStrategy, fd core.IFileDownloader, vf core.IVideoFactory, vc core.IVideoConverter) *ConvertMediaStrategy { + return &ConvertMediaStrategy{l, sms, fd, vf, vc} +} + +type ConvertMediaStrategy struct { + l core.ILogger + sms core.ISendMediaStrategy + fd core.IFileDownloader + vf core.IVideoFactory + vc core.IVideoConverter +} + +// SendMedia is a core.ISendMediaStrategy interface implementation +func (cms *ConvertMediaStrategy) SendMedia(media []*core.Media, bot core.IBot) error { + err := cms.sms.SendMedia(media, bot) + if err != nil { + cms.l.Error(err) + if strings.HasPrefix(err.Error(), "unexpected video codec") { + return cms.fallbackToConverting(media[0], bot) + } + } + + return err +} + +func (cms *ConvertMediaStrategy) fallbackToConverting(media *core.Media, bot core.IBot) error { + cms.l.Info("send by converting") + file, err := cms.downloadMedia(media) + if err != nil { + return err + } + defer file.Dispose() + + vf, err := cms.vf.CreateVideo(file.Path) + if err != nil { + cms.l.Errorf("can't create video file for %s, %v", file.Path, err) + return err + } + defer vf.Dispose() + + vfc, err := cms.vc.Convert(vf, 0) + if err != nil { + cms.l.Errorf("cant convert video file: %v", err) + return err + } + defer vfc.Dispose() + + _, err = bot.SendVideo(vfc, media.Caption) + return err +} + +func (cms *ConvertMediaStrategy) downloadMedia(media *core.Media) (*core.File, error) { + mediaPath := path.Join(os.TempDir(), path.Base(media.URL)) + file, err := cms.fd.Download(media.URL, mediaPath) + if err != nil { + cms.l.Errorf("video download error: %v", err) + return nil, err + } + + cms.l.Infof("file downloaded: %s %0.2fMB", file.Name, float64(file.Size)/1024/1024) + + return file, nil +} From 192d6a169d61e36227f91b9736aa6c2eb0b3e407 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Sun, 15 Aug 2021 18:43:22 +0300 Subject: [PATCH 148/439] Change link_flow send media strategy --- pullanusbot.go | 5 +- usecases/link_flow.go | 114 +++--------------------------------------- 2 files changed, 12 insertions(+), 107 deletions(-) diff --git a/pullanusbot.go b/pullanusbot.go index 076f32f..5cb78e0 100644 --- a/pullanusbot.go +++ b/pullanusbot.go @@ -45,7 +45,10 @@ func main() { telebot.AddHandler(twitterParser) httpClient := api.CreateHttpClient() - linkFlow := usecases.CreateLinkFlow(logger, httpClient, converter, fileDownloader, converter, converter) + remoteMediaSender := usecases.CreateSendMediaStrategy(logger) + localMediaSender := usecases.CreateUploadMediaStrategy(logger, remoteMediaSender, fileDownloader, converter, converter) + mp4MediaSender := usecases.CreateConvertMediaStrategy(logger, localMediaSender, fileDownloader, converter, converter) + linkFlow := usecases.CreateLinkFlow(logger, httpClient, converter, mp4MediaSender) telebot.AddHandler(linkFlow) fileUploader := api.CreateTelegraphAPI() diff --git a/usecases/link_flow.go b/usecases/link_flow.go index f70e223..38682e8 100644 --- a/usecases/link_flow.go +++ b/usecases/link_flow.go @@ -2,7 +2,6 @@ package usecases import ( "fmt" - "os" "path" "regexp" "strings" @@ -11,18 +10,16 @@ import ( ) // CreateLinkFlow is a basic LinkFlow factory -func CreateLinkFlow(l core.ILogger, hc core.IHttpClient, mf core.IMediaFactory, fd core.IFileDownloader, vff core.IVideoFactory, vfc core.IVideoConverter) *LinkFlow { - return &LinkFlow{l, hc, mf, fd, vff, vfc} +func CreateLinkFlow(l core.ILogger, hc core.IHttpClient, mf core.IMediaFactory, ms core.ISendMediaStrategy) *LinkFlow { + return &LinkFlow{l, hc, mf, ms} } -// LinkFlow represents convert hotlink to video file logic +// LinkFlow converts hotlink to video/photo attachment type LinkFlow struct { l core.ILogger hc core.IHttpClient mf core.IMediaFactory - fd core.IFileDownloader - vff core.IVideoFactory - vfc core.IVideoConverter + sms core.ISendMediaStrategy } // HandleText is a core.ITextHandler protocol implementation @@ -54,114 +51,19 @@ func (lf *LinkFlow) handleURL(message *core.Message, bot core.IBot) error { for _, m := range media { switch m.Type { case core.TPhoto: - err := lf.sendAsPhoto(m, message, bot) - if err != nil { - return err - } + m.Caption = fmt.Sprintf(`🖼 %s (by %s)`, m.URL, path.Base(m.URL), message.Sender.Username) case core.TVideo: - err := lf.sendAsVideo(m, message, bot) - if err != nil { - return err - } + m.Caption = fmt.Sprintf(`🔗 %s (by %s)`, m.URL, path.Base(m.URL), message.Sender.Username) case core.TText: lf.l.Warningf("Unexpected %+v", m) } } - return bot.Delete(message) -} - -func (lf *LinkFlow) sendAsPhoto(media *core.Media, message *core.Message, bot core.IBot) error { - lf.l.Infof("sending as photo: %s", media.URL) - media.Caption = fmt.Sprintf(`🖼 %s (by %s)`, media.URL, path.Base(media.URL), message.Sender.Username) - _, err := bot.SendMedia(media) + err = lf.sms.SendMedia(media, bot) if err != nil { lf.l.Error(err) - if strings.Contains(err.Error(), "failed to get HTTP URL content") || strings.Contains(err.Error(), "wrong file identifier/HTTP URL specified") { - file, err := lf.downloadMedia(media) - if err != nil { - return err - } - image := &core.Image{File: *file} - _, err = bot.SendImage(image, media.Caption) - return err - } - } - return err -} - -func (lf *LinkFlow) sendAsVideo(media *core.Media, message *core.Message, bot core.IBot) error { - lf.l.Infof("sending as video: %s", media.URL) - media.Caption = fmt.Sprintf(`🔗 %s (by %s)`, message.Text, path.Base(media.URL), message.Sender.Username) - - if media.Codec != "h264" { - lf.l.Warningf("expected h264 codec, but got %s", media.Codec) - return lf.sendByConverting(media, bot) - } - - _, err := bot.SendMedia(media) - if err != nil { - lf.l.Errorf("%s, fallback to uploading...", err) - return lf.sendByUploading(media, bot) - } - - return err -} - -func (lf *LinkFlow) sendByConverting(media *core.Media, bot core.IBot) error { - lf.l.Info("sending by converting") - file, err := lf.downloadMedia(media) - if err != nil { - return err - } - - vf, err := lf.vff.CreateVideo(file.Path) - if err != nil { - lf.l.Errorf("can't create video file for %s, %v", file.Path, err) - return err - } - defer vf.Dispose() - - vfc, err := lf.vfc.Convert(vf, 0) - if err != nil { - lf.l.Errorf("cant convert video file: %v", err) return err } - defer vfc.Dispose() - - _, err = bot.SendVideo(vfc, media.Caption) - return err -} - -func (lf *LinkFlow) sendByUploading(media *core.Media, bot core.IBot) error { - // Try to upload file to telegram - lf.l.Info("sending by uploading") - file, err := lf.downloadMedia(media) - if err != nil { - return err - } - - vf, err := lf.vff.CreateVideo(file.Path) - if err != nil { - lf.l.Errorf("can't create video file for %s, %v", file.Path, err) - return err - } - - defer vf.Dispose() - _, err = bot.SendVideo(vf, media.Caption) - return err -} - -func (lf *LinkFlow) downloadMedia(media *core.Media) (*core.File, error) { - mediaPath := path.Join(os.TempDir(), path.Base(media.URL)) - file, err := lf.fd.Download(media.URL, mediaPath) - if err != nil { - lf.l.Errorf("video download error: %v", err) - return nil, err - } - - lf.l.Infof("file downloaded: %s %0.2fMB", file.Name, float64(file.Size)/1024/1024) - - return file, nil + return bot.Delete(message) } From 5720ed001ef28355ea5160eda71c60c4874a0978 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Mon, 16 Aug 2021 00:18:23 +0300 Subject: [PATCH 149/439] Move codec check logic from cend_media_strategy to convert_media_strategy --- usecases/convert_media_strategy.go | 12 ++++-------- usecases/send_media_strategy.go | 5 ----- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/usecases/convert_media_strategy.go b/usecases/convert_media_strategy.go index e87edf6..07e16a9 100644 --- a/usecases/convert_media_strategy.go +++ b/usecases/convert_media_strategy.go @@ -3,7 +3,6 @@ package usecases import ( "os" "path" - "strings" "github.com/ailinykh/pullanusbot/v2/core" ) @@ -22,15 +21,12 @@ type ConvertMediaStrategy struct { // SendMedia is a core.ISendMediaStrategy interface implementation func (cms *ConvertMediaStrategy) SendMedia(media []*core.Media, bot core.IBot) error { - err := cms.sms.SendMedia(media, bot) - if err != nil { - cms.l.Error(err) - if strings.HasPrefix(err.Error(), "unexpected video codec") { - return cms.fallbackToConverting(media[0], bot) + for _, m := range media { + if m.Type == core.TVideo && media[0].Codec != "mp4" { + return cms.fallbackToConverting(m, bot) } } - - return err + return cms.sms.SendMedia(media, bot) } func (cms *ConvertMediaStrategy) fallbackToConverting(media *core.Media, bot core.IBot) error { diff --git a/usecases/send_media_strategy.go b/usecases/send_media_strategy.go index 3f0a026..b0ba26f 100644 --- a/usecases/send_media_strategy.go +++ b/usecases/send_media_strategy.go @@ -1,8 +1,6 @@ package usecases import ( - "errors" - "github.com/ailinykh/pullanusbot/v2/core" ) @@ -20,9 +18,6 @@ func (sms *SendMediaStrategy) SendMedia(media []*core.Media, bot core.IBot) erro case 0: sms.l.Warning("Unexpected empty media") case 1: - if media[0].Type == core.TVideo && media[0].Codec != "mp4" { - return errors.New("unexpected video codec " + media[0].Codec) - } _, err := bot.SendMedia(media[0]) return err default: From 2282fa58d6d056edf5892b4ff8edca658c9b35f5 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Mon, 16 Aug 2021 00:32:14 +0300 Subject: [PATCH 150/439] Using send media strategy in TwitterFlow --- pullanusbot.go | 6 ++-- usecases/twitter_flow.go | 71 +++------------------------------------- 2 files changed, 7 insertions(+), 70 deletions(-) diff --git a/pullanusbot.go b/pullanusbot.go index 5cb78e0..11ee104 100644 --- a/pullanusbot.go +++ b/pullanusbot.go @@ -38,15 +38,15 @@ func main() { telebot.AddHandler(videoFlow) fileDownloader := infrastructure.CreateFileDownloader() + remoteMediaSender := usecases.CreateSendMediaStrategy(logger) + localMediaSender := usecases.CreateUploadMediaStrategy(logger, remoteMediaSender, fileDownloader, converter, converter) twitterAPI := api.CreateTwitterAPI() - twitterFlow := usecases.CreateTwitterFlow(logger, twitterAPI, fileDownloader, converter) + twitterFlow := usecases.CreateTwitterFlow(logger, twitterAPI, localMediaSender) twitterTimeout := usecases.CreateTwitterTimeout(logger, twitterFlow) twitterParser := usecases.CreateTwitterParser(twitterTimeout) telebot.AddHandler(twitterParser) httpClient := api.CreateHttpClient() - remoteMediaSender := usecases.CreateSendMediaStrategy(logger) - localMediaSender := usecases.CreateUploadMediaStrategy(logger, remoteMediaSender, fileDownloader, converter, converter) mp4MediaSender := usecases.CreateConvertMediaStrategy(logger, localMediaSender, fileDownloader, converter, converter) linkFlow := usecases.CreateLinkFlow(logger, httpClient, converter, mp4MediaSender) telebot.AddHandler(linkFlow) diff --git a/usecases/twitter_flow.go b/usecases/twitter_flow.go index 74f515f..4f63421 100644 --- a/usecases/twitter_flow.go +++ b/usecases/twitter_flow.go @@ -1,11 +1,6 @@ package usecases import ( - "errors" - "os" - "path" - "strings" - "github.com/ailinykh/pullanusbot/v2/core" ) @@ -14,16 +9,15 @@ type ITweetHandler interface { } // CreateTwitterFlow is a basic TwitterFlow factory -func CreateTwitterFlow(l core.ILogger, mf core.IMediaFactory, fd core.IFileDownloader, vff core.IVideoFactory) *TwitterFlow { - return &TwitterFlow{l, mf, fd, vff} +func CreateTwitterFlow(l core.ILogger, mf core.IMediaFactory, sms core.ISendMediaStrategy) *TwitterFlow { + return &TwitterFlow{l, mf, sms} } // TwitterFlow represents tweet processing logic type TwitterFlow struct { l core.ILogger mf core.IMediaFactory - fd core.IFileDownloader - vff core.IVideoFactory + sms core.ISendMediaStrategy } // HandleTweet is a ITweetHandler protocol implementation @@ -35,62 +29,5 @@ func (tf *TwitterFlow) HandleTweet(tweetID string, message *core.Message, bot co return err } - err = tf.handleMedia(media, message, bot) - if err != nil { - tf.l.Error(err) - return err - } - - if deleteOriginal { - return bot.Delete(message) - } - return nil -} - -func (tf *TwitterFlow) handleMedia(media []*core.Media, message *core.Message, bot core.IBot) error { - switch len(media) { - case 0: - return errors.New("unexpected 0 media count") - case 1: - _, err := bot.SendMedia(media[0]) - if err != nil { - if strings.Contains(err.Error(), "failed to get HTTP URL content") || strings.Contains(err.Error(), "wrong file identifier/HTTP URL specified") { - return tf.fallbackToUploading(media[0], bot) - } - } - return err - default: - _, err := bot.SendPhotoAlbum(media) - return err - } -} - -func (tf *TwitterFlow) fallbackToUploading(media *core.Media, bot core.IBot) error { - // Try to upload file to telegram - tf.l.Info("Sending by uploading") - mediaPath := path.Join(os.TempDir(), path.Base(media.URL)) - file, err := tf.fd.Download(media.URL, mediaPath) - if err != nil { - tf.l.Errorf("file download error: %v", err) - return err - } - - defer file.Dispose() - - tf.l.Infof("File downloaded: %s %0.2fMB", file.Name, float64(file.Size)/1024/1024) - - if media.Type == core.TPhoto { - image := &core.Image{File: *file} - _, err := bot.SendImage(image, media.Caption) - return err - } - // else - vf, err := tf.vff.CreateVideo(file.Path) - if err != nil { - tf.l.Errorf("Can't create video file for %s, %v", file.Path, err) - return err - } - defer vf.Dispose() - _, err = bot.SendVideo(vf, media.Caption) - return err + return tf.sms.SendMedia(media, bot) } From 5e9c9be200bcd49d336d09ddb5deea148d719aba Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Mon, 16 Aug 2021 00:33:20 +0300 Subject: [PATCH 151/439] Rename mp4MediaSender to convertMediaSender --- pullanusbot.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pullanusbot.go b/pullanusbot.go index 11ee104..24776b0 100644 --- a/pullanusbot.go +++ b/pullanusbot.go @@ -47,8 +47,8 @@ func main() { telebot.AddHandler(twitterParser) httpClient := api.CreateHttpClient() - mp4MediaSender := usecases.CreateConvertMediaStrategy(logger, localMediaSender, fileDownloader, converter, converter) - linkFlow := usecases.CreateLinkFlow(logger, httpClient, converter, mp4MediaSender) + convertMediaSender := usecases.CreateConvertMediaStrategy(logger, localMediaSender, fileDownloader, converter, converter) + linkFlow := usecases.CreateLinkFlow(logger, httpClient, converter, convertMediaSender) telebot.AddHandler(linkFlow) fileUploader := api.CreateTelegraphAPI() From 9056d0cd980ee5109e7fd882d2ed8a5ff36f5d96 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Mon, 16 Aug 2021 10:07:38 +0300 Subject: [PATCH 152/439] Use multiple twitter tokens --- api/twitter_api.go | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/api/twitter_api.go b/api/twitter_api.go index c1a930e..9696c48 100644 --- a/api/twitter_api.go +++ b/api/twitter_api.go @@ -7,22 +7,41 @@ import ( "io/ioutil" "net/http" "regexp" + "strings" "github.com/ailinykh/pullanusbot/v2/core" ) // CreateTwitterAPI is a default Twitter factory func CreateTwitterAPI() *TwitterAPI { - return &TwitterAPI{} + return &TwitterAPI{[]string{ + "AAAAAAAAAAAAAAAAAAAAAPYXBAAAAAAACLXUNDekMxqa8h%2F40K4moUkGsoc%3DTYfbDKbT3jJPCEVnMYqilB28NHfOPqkca3qaAxGfsyKCs0wRbw", + "AAAAAAAAAAAAAAAAAAAAAPAh2AAAAAAAoInuXrJ%2BcqfgfR5PlJGnQsOniNY%3Dn9galDg4iUr7KyRAU47JGDbQz2q7sdwXRTkonzBX2uLxXRgNv0", + }} } // Twitter API -type TwitterAPI struct{} +type TwitterAPI struct { + tokens []string +} + +func (api *TwitterAPI) getTweetByID(tweetID string) (*Tweet, error) { + var tweet *Tweet + var err error + for _, t := range api.tokens { + fmt.Println(t) + tweet, err = api.getTweetByIdAndToken(tweetID, t) + if err == nil || !strings.HasPrefix(err.Error(), "Rate limit exceeded") { + return tweet, err + } + } + return tweet, err +} -func (TwitterAPI) get(tweetID string) (*Tweet, error) { +func (TwitterAPI) getTweetByIdAndToken(tweetID string, token string) (*Tweet, error) { client := http.DefaultClient req, _ := http.NewRequest("GET", fmt.Sprintf("https://api.twitter.com/1.1/statuses/show.json?id=%s&tweet_mode=extended", tweetID), nil) - req.Header.Add("Authorization", "Bearer AAAAAAAAAAAAAAAAAAAAAPYXBAAAAAAACLXUNDekMxqa8h%2F40K4moUkGsoc%3DTYfbDKbT3jJPCEVnMYqilB28NHfOPqkca3qaAxGfsyKCs0wRbw") + req.Header.Add("Authorization", "Bearer "+token) res, err := client.Do(req) if err != nil { return nil, err @@ -49,7 +68,7 @@ func (TwitterAPI) get(tweetID string) (*Tweet, error) { // CreateMedia is a core.IMediaFactory interface implementation func (t *TwitterAPI) CreateMedia(tweetID string, author *core.User) ([]*core.Media, error) { - tweet, err := t.get(tweetID) + tweet, err := t.getTweetByID(tweetID) if err != nil { return nil, err } @@ -66,11 +85,12 @@ func (t *TwitterAPI) CreateMedia(tweetID string, author *core.User) ([]*core.Med return []*core.Media{{URL: "", Caption: t.makeCaption(author.Username, tweet), Type: core.TText}}, nil case 1: if media[0].Type == "video" || media[0].Type == "animated_gif" { + //TODO: Codec ?? return []*core.Media{{URL: media[0].VideoInfo.best().URL, Caption: t.makeCaption(author.Username, tweet), Type: core.TVideo}}, nil } else if media[0].Type == "photo" { return []*core.Media{{URL: media[0].MediaURL, Caption: t.makeCaption(author.Username, tweet), Type: core.TPhoto}}, nil } else { - return nil, errors.New("Unknown type: " + media[0].Type) + return nil, errors.New("unexpected type: " + media[0].Type) } default: // t.sendAlbum(media, tweet, m) From 0cdcab01befca35cb7279f94fc88e84d3f2127a4 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Mon, 16 Aug 2021 10:34:07 +0300 Subject: [PATCH 153/439] Move media creation logic into a separate media factory --- api/twitter_api.go | 48 +---------------------------- api/twitter_media_factory.go | 59 ++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 47 deletions(-) create mode 100644 api/twitter_media_factory.go diff --git a/api/twitter_api.go b/api/twitter_api.go index 9696c48..d92c425 100644 --- a/api/twitter_api.go +++ b/api/twitter_api.go @@ -6,10 +6,7 @@ import ( "fmt" "io/ioutil" "net/http" - "regexp" "strings" - - "github.com/ailinykh/pullanusbot/v2/core" ) // CreateTwitterAPI is a default Twitter factory @@ -27,9 +24,8 @@ type TwitterAPI struct { func (api *TwitterAPI) getTweetByID(tweetID string) (*Tweet, error) { var tweet *Tweet - var err error + var err = errors.New("tokens not set") for _, t := range api.tokens { - fmt.Println(t) tweet, err = api.getTweetByIdAndToken(tweetID, t) if err == nil || !strings.HasPrefix(err.Error(), "Rate limit exceeded") { return tweet, err @@ -65,45 +61,3 @@ func (TwitterAPI) getTweetByIdAndToken(tweetID string, token string) (*Tweet, er return &tweet, err } - -// CreateMedia is a core.IMediaFactory interface implementation -func (t *TwitterAPI) CreateMedia(tweetID string, author *core.User) ([]*core.Media, error) { - tweet, err := t.getTweetByID(tweetID) - if err != nil { - return nil, err - } - - if len(tweet.ExtendedEntities.Media) == 0 && tweet.QuotedStatus != nil && len(tweet.QuotedStatus.ExtendedEntities.Media) > 0 { - tweet = tweet.QuotedStatus - // logger.Warningf("tweet media is empty, using QuotedStatus instead %s", tweet.ID) - } - - media := tweet.ExtendedEntities.Media - - switch len(media) { - case 0: - return []*core.Media{{URL: "", Caption: t.makeCaption(author.Username, tweet), Type: core.TText}}, nil - case 1: - if media[0].Type == "video" || media[0].Type == "animated_gif" { - //TODO: Codec ?? - return []*core.Media{{URL: media[0].VideoInfo.best().URL, Caption: t.makeCaption(author.Username, tweet), Type: core.TVideo}}, nil - } else if media[0].Type == "photo" { - return []*core.Media{{URL: media[0].MediaURL, Caption: t.makeCaption(author.Username, tweet), Type: core.TPhoto}}, nil - } else { - return nil, errors.New("unexpected type: " + media[0].Type) - } - default: - // t.sendAlbum(media, tweet, m) - medias := []*core.Media{} - for _, m := range media { - medias = append(medias, &core.Media{URL: m.MediaURL, Caption: t.makeCaption(author.Username, tweet), Type: core.TPhoto}) - } - return medias, nil - } -} - -func (TwitterAPI) makeCaption(author string, tweet *Tweet) string { - re := regexp.MustCompile(`\s?http\S+$`) - text := re.ReplaceAllString(tweet.FullText, "") - return fmt.Sprintf("🐦 %s (by %s)\n%s", tweet.User.ScreenName, tweet.ID, tweet.User.Name, author, text) -} diff --git a/api/twitter_media_factory.go b/api/twitter_media_factory.go new file mode 100644 index 0000000..e1c8160 --- /dev/null +++ b/api/twitter_media_factory.go @@ -0,0 +1,59 @@ +package api + +import ( + "errors" + "fmt" + "regexp" + + "github.com/ailinykh/pullanusbot/v2/core" +) + +func CreateTwitterMediaFactory() *TwitterMediaFactory { + return &TwitterMediaFactory{CreateTwitterAPI()} +} + +type TwitterMediaFactory struct { + api *TwitterAPI +} + +// CreateMedia is a core.IMediaFactory interface implementation +func (tmf *TwitterMediaFactory) CreateMedia(tweetID string, author *core.User) ([]*core.Media, error) { + tweet, err := tmf.api.getTweetByID(tweetID) + if err != nil { + return nil, err + } + + if len(tweet.ExtendedEntities.Media) == 0 && tweet.QuotedStatus != nil && len(tweet.QuotedStatus.ExtendedEntities.Media) > 0 { + tweet = tweet.QuotedStatus + // logger.Warningf("tweet media is empty, using QuotedStatus instead %s", tweet.ID) + } + + media := tweet.ExtendedEntities.Media + + switch len(media) { + case 0: + return []*core.Media{{URL: "", Caption: tmf.makeCaption(author.Username, tweet), Type: core.TText}}, nil + case 1: + if media[0].Type == "video" || media[0].Type == "animated_gif" { + //TODO: Codec ?? + return []*core.Media{{URL: media[0].VideoInfo.best().URL, Caption: tmf.makeCaption(author.Username, tweet), Type: core.TVideo}}, nil + } else if media[0].Type == "photo" { + return []*core.Media{{URL: media[0].MediaURL, Caption: tmf.makeCaption(author.Username, tweet), Type: core.TPhoto}}, nil + } else { + return nil, errors.New("unexpected type: " + media[0].Type) + } + default: + // t.sendAlbum(media, tweet, m) + medias := []*core.Media{} + for _, m := range media { + medias = append(medias, &core.Media{URL: m.MediaURL, Caption: tmf.makeCaption(author.Username, tweet), Type: core.TPhoto}) + } + return medias, nil + } +} + +func (TwitterMediaFactory) makeCaption(author string, tweet *Tweet) string { + re := regexp.MustCompile(`\s?http\S+$`) + text := re.ReplaceAllString(tweet.FullText, "") + return fmt.Sprintf("🐦 %s (by %s)\n%s", tweet.User.ScreenName, tweet.ID, tweet.User.Name, author, text) +} From c7080a3e91fc42677de8ae187951167f5cfe9238 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Mon, 16 Aug 2021 10:48:26 +0300 Subject: [PATCH 154/439] Replace TwitterAPI with TwitterMediaFactory --- pullanusbot.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pullanusbot.go b/pullanusbot.go index 24776b0..db4b223 100644 --- a/pullanusbot.go +++ b/pullanusbot.go @@ -40,8 +40,8 @@ func main() { fileDownloader := infrastructure.CreateFileDownloader() remoteMediaSender := usecases.CreateSendMediaStrategy(logger) localMediaSender := usecases.CreateUploadMediaStrategy(logger, remoteMediaSender, fileDownloader, converter, converter) - twitterAPI := api.CreateTwitterAPI() - twitterFlow := usecases.CreateTwitterFlow(logger, twitterAPI, localMediaSender) + twitterMediaFactory := api.CreateTwitterMediaFactory() + twitterFlow := usecases.CreateTwitterFlow(logger, twitterMediaFactory, localMediaSender) twitterTimeout := usecases.CreateTwitterTimeout(logger, twitterFlow) twitterParser := usecases.CreateTwitterParser(twitterTimeout) telebot.AddHandler(twitterParser) From c06c0f0c77bfd6b8fe67368e01412d30f2269f4c Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Mon, 16 Aug 2021 11:08:44 +0300 Subject: [PATCH 155/439] Add more tokens to twitter API --- api/twitter_api.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/api/twitter_api.go b/api/twitter_api.go index d92c425..91c9b51 100644 --- a/api/twitter_api.go +++ b/api/twitter_api.go @@ -14,6 +14,8 @@ func CreateTwitterAPI() *TwitterAPI { return &TwitterAPI{[]string{ "AAAAAAAAAAAAAAAAAAAAAPYXBAAAAAAACLXUNDekMxqa8h%2F40K4moUkGsoc%3DTYfbDKbT3jJPCEVnMYqilB28NHfOPqkca3qaAxGfsyKCs0wRbw", "AAAAAAAAAAAAAAAAAAAAAPAh2AAAAAAAoInuXrJ%2BcqfgfR5PlJGnQsOniNY%3Dn9galDg4iUr7KyRAU47JGDbQz2q7sdwXRTkonzBX2uLxXRgNv0", + "AAAAAAAAAAAAAAAAAAAAAA4JLwEAAAAAXIyoETwtg%2BiTlR1VTNxGXnphfu4%3D6iSv0IXHo4NWGndWWLC8Bk3XuPkLMyATMxM0h6CfomnfRbGpgK", + "AAAAAAAAAAAAAAAAAAAAAAnuQQEAAAAAkV36hXt9HP5m5Qake9ffdXZMNTI%3DaF9mA4ZreVb938IeW8vfpTpT8HxDYOi0WYi5i4B8Cce9UVpwi6", }} } From eaaa2411ca572e544c1932c217bfe6ba13d0ecec Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Mon, 16 Aug 2021 15:31:26 +0300 Subject: [PATCH 156/439] Add logger to TwitterMediaFactory --- api/twitter_media_factory.go | 7 ++++--- pullanusbot.go | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/api/twitter_media_factory.go b/api/twitter_media_factory.go index e1c8160..1a77b5a 100644 --- a/api/twitter_media_factory.go +++ b/api/twitter_media_factory.go @@ -8,11 +8,12 @@ import ( "github.com/ailinykh/pullanusbot/v2/core" ) -func CreateTwitterMediaFactory() *TwitterMediaFactory { - return &TwitterMediaFactory{CreateTwitterAPI()} +func CreateTwitterMediaFactory(l core.ILogger) *TwitterMediaFactory { + return &TwitterMediaFactory{l, CreateTwitterAPI()} } type TwitterMediaFactory struct { + l core.ILogger api *TwitterAPI } @@ -25,7 +26,7 @@ func (tmf *TwitterMediaFactory) CreateMedia(tweetID string, author *core.User) ( if len(tweet.ExtendedEntities.Media) == 0 && tweet.QuotedStatus != nil && len(tweet.QuotedStatus.ExtendedEntities.Media) > 0 { tweet = tweet.QuotedStatus - // logger.Warningf("tweet media is empty, using QuotedStatus instead %s", tweet.ID) + tmf.l.Warningf("tweet media is empty, using QuotedStatus instead %s", tweet.ID) } media := tweet.ExtendedEntities.Media diff --git a/pullanusbot.go b/pullanusbot.go index db4b223..420f907 100644 --- a/pullanusbot.go +++ b/pullanusbot.go @@ -40,7 +40,7 @@ func main() { fileDownloader := infrastructure.CreateFileDownloader() remoteMediaSender := usecases.CreateSendMediaStrategy(logger) localMediaSender := usecases.CreateUploadMediaStrategy(logger, remoteMediaSender, fileDownloader, converter, converter) - twitterMediaFactory := api.CreateTwitterMediaFactory() + twitterMediaFactory := api.CreateTwitterMediaFactory(logger) twitterFlow := usecases.CreateTwitterFlow(logger, twitterMediaFactory, localMediaSender) twitterTimeout := usecases.CreateTwitterTimeout(logger, twitterFlow) twitterParser := usecases.CreateTwitterParser(twitterTimeout) From 38e8f77d8ec0ff1ca4ccbd4fe1161e37285db5de Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Mon, 16 Aug 2021 17:51:26 +0300 Subject: [PATCH 157/439] Add CreateFakeBot test helper function --- usecases/common_test.go | 16 +++++++++++++--- usecases/twitter_parser_test.go | 2 +- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/usecases/common_test.go b/usecases/common_test.go index baed8d4..ddbbd6d 100644 --- a/usecases/common_test.go +++ b/usecases/common_test.go @@ -2,14 +2,24 @@ package usecases_test import "github.com/ailinykh/pullanusbot/v2/core" +func CreateFakeBot() *FakeBot { + return &FakeBot{[]string{}, []string{}, []string{}} +} + type FakeBot struct { + sentMedias []string sentMessages []string removedMessages []string } -func (FakeBot) SendImage(*core.Image, string) (*core.Message, error) { return nil, nil } -func (FakeBot) SendAlbum([]*core.Image) ([]*core.Message, error) { return nil, nil } -func (FakeBot) SendMedia(*core.Media) (*core.Message, error) { return nil, nil } +func (FakeBot) SendImage(*core.Image, string) (*core.Message, error) { return nil, nil } +func (FakeBot) SendAlbum([]*core.Image) ([]*core.Message, error) { return nil, nil } + +func (b *FakeBot) SendMedia(media *core.Media) (*core.Message, error) { + b.sentMedias = append(b.sentMedias, media.URL) + return nil, nil +} + func (FakeBot) SendPhotoAlbum([]*core.Media) ([]*core.Message, error) { return nil, nil } func (FakeBot) SendVideo(*core.Video, string) (*core.Message, error) { return nil, nil } diff --git a/usecases/twitter_parser_test.go b/usecases/twitter_parser_test.go index e5ca0b4..9f5b247 100644 --- a/usecases/twitter_parser_test.go +++ b/usecases/twitter_parser_test.go @@ -66,7 +66,7 @@ func Test_HandleText_ReturnsErrorOnError(t *testing.T) { func makeTwitterSUT() (*usecases.TwitterParser, *FakeTweetHandler, *FakeBot) { handler := &FakeTweetHandler{[]string{}, nil} parser := usecases.CreateTwitterParser(handler) - bot := &FakeBot{[]string{}, []string{}} + bot := CreateFakeBot() return parser, handler, bot } From 203f92bcebef3ccdd4d6b21e63ece546aa9bddc5 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Mon, 16 Aug 2021 18:18:10 +0300 Subject: [PATCH 158/439] Move FakeBot into test_helpers to be able use it across different packages --- .../fake_bot.go | 14 +++--- usecases/faggot_game_test.go | 43 ++++++++++--------- usecases/twitter_parser_test.go | 9 ++-- 3 files changed, 34 insertions(+), 32 deletions(-) rename usecases/common_test.go => test_helpers/fake_bot.go (75%) diff --git a/usecases/common_test.go b/test_helpers/fake_bot.go similarity index 75% rename from usecases/common_test.go rename to test_helpers/fake_bot.go index ddbbd6d..54fad57 100644 --- a/usecases/common_test.go +++ b/test_helpers/fake_bot.go @@ -1,4 +1,4 @@ -package usecases_test +package test_helpers import "github.com/ailinykh/pullanusbot/v2/core" @@ -7,16 +7,16 @@ func CreateFakeBot() *FakeBot { } type FakeBot struct { - sentMedias []string - sentMessages []string - removedMessages []string + SentMedias []string + SentMessages []string + RemovedMessages []string } func (FakeBot) SendImage(*core.Image, string) (*core.Message, error) { return nil, nil } func (FakeBot) SendAlbum([]*core.Image) ([]*core.Message, error) { return nil, nil } func (b *FakeBot) SendMedia(media *core.Media) (*core.Message, error) { - b.sentMedias = append(b.sentMedias, media.URL) + b.SentMedias = append(b.SentMedias, media.URL) return nil, nil } @@ -24,11 +24,11 @@ func (FakeBot) SendPhotoAlbum([]*core.Media) ([]*core.Message, error) { return n func (FakeBot) SendVideo(*core.Video, string) (*core.Message, error) { return nil, nil } func (b *FakeBot) Delete(message *core.Message) error { - b.removedMessages = append(b.removedMessages, message.Text) + b.RemovedMessages = append(b.RemovedMessages, message.Text) return nil } func (b *FakeBot) SendText(text string, args ...interface{}) (*core.Message, error) { - b.sentMessages = append(b.sentMessages, text) + b.SentMessages = append(b.SentMessages, text) return nil, nil } diff --git a/usecases/faggot_game_test.go b/usecases/faggot_game_test.go index 5721a96..8c59c3e 100644 --- a/usecases/faggot_game_test.go +++ b/usecases/faggot_game_test.go @@ -8,6 +8,7 @@ import ( "time" "github.com/ailinykh/pullanusbot/v2/core" + "github.com/ailinykh/pullanusbot/v2/test_helpers" "github.com/ailinykh/pullanusbot/v2/usecases" "github.com/stretchr/testify/assert" ) @@ -24,7 +25,7 @@ func Test_AllTheCommands_WorksOnlyInGroupChats(t *testing.T) { game.All(message, bot) game.Me(message, bot) - for _, m := range bot.sentMessages { + for _, m := range bot.SentMessages { assert.Equal(t, "group only", m) } } @@ -34,7 +35,7 @@ func Test_RulesCommand_DeliversRules(t *testing.T) { game.Rules(message, bot) - assert.Equal(t, "Game rules:", bot.sentMessages[0]) + assert.Equal(t, "Game rules:", bot.SentMessages[0]) } func Test_Add_AppendsPlayerInGameOnlyOnce(t *testing.T) { @@ -47,12 +48,12 @@ func Test_Add_AppendsPlayerInGameOnlyOnce(t *testing.T) { game.Add(message, bot) assert.Equal(t, storage.players, []*core.User{message.Sender}) - assert.Equal(t, "Player added", bot.sentMessages[0]) + assert.Equal(t, "Player added", bot.SentMessages[0]) game.Add(message, bot) assert.Equal(t, storage.players, []*core.User{message.Sender}) - assert.Equal(t, "Player already in game", bot.sentMessages[1]) + assert.Equal(t, "Player already in game", bot.SentMessages[1]) } func Test_Play_RespondsWithNoPlayers(t *testing.T) { @@ -63,7 +64,7 @@ func Test_Play_RespondsWithNoPlayers(t *testing.T) { game.Play(message, bot) - assert.Equal(t, "Nobody in game. So you win, Faggot!", bot.sentMessages[0]) + assert.Equal(t, "Nobody in game. So you win, Faggot!", bot.SentMessages[0]) } func Test_Play_RespondsNotEnoughPlayers(t *testing.T) { @@ -75,7 +76,7 @@ func Test_Play_RespondsNotEnoughPlayers(t *testing.T) { game.Add(message, bot) game.Play(message, bot) - assert.Equal(t, "Not enough players", bot.sentMessages[1]) + assert.Equal(t, "Not enough players", bot.SentMessages[1]) } func Test_Play_RespondsWithCurrentGameResult(t *testing.T) { @@ -94,10 +95,10 @@ func Test_Play_RespondsWithCurrentGameResult(t *testing.T) { winner := storage.rounds[0].Winner phrase := fmt.Sprintf(`%s %s`, winner.ID, winner.FirstName, winner.LastName) - assert.Equal(t, "0", bot.sentMessages[2]) - assert.Equal(t, "1", bot.sentMessages[3]) - assert.Equal(t, "2", bot.sentMessages[4]) - assert.Equal(t, phrase, bot.sentMessages[5]) + assert.Equal(t, "0", bot.SentMessages[2]) + assert.Equal(t, "1", bot.SentMessages[3]) + assert.Equal(t, "2", bot.SentMessages[4]) + assert.Equal(t, phrase, bot.SentMessages[5]) } func Test_Play_RespondsWinnerAlreadyKnown(t *testing.T) { game, bot, storage := makeSUT(LocalizerDict{ @@ -115,14 +116,14 @@ func Test_Play_RespondsWinnerAlreadyKnown(t *testing.T) { game.Play(m1, bot) winner := storage.rounds[0].Winner.Username - assert.Equal(t, "0", bot.sentMessages[2]) - assert.Equal(t, "1", bot.sentMessages[3]) - assert.Equal(t, "2", bot.sentMessages[4]) - assert.Equal(t, fmt.Sprintf("3 @%s", winner), bot.sentMessages[5]) + assert.Equal(t, "0", bot.SentMessages[2]) + assert.Equal(t, "1", bot.SentMessages[3]) + assert.Equal(t, "2", bot.SentMessages[4]) + assert.Equal(t, fmt.Sprintf("3 @%s", winner), bot.SentMessages[5]) game.Play(m1, bot) - assert.Equal(t, fmt.Sprintf("Winner already known %s", winner), bot.sentMessages[6]) + assert.Equal(t, fmt.Sprintf("Winner already known %s", winner), bot.SentMessages[6]) } func Test_Stats_RespondsWithDescendingResultsForCurrentYear(t *testing.T) { @@ -158,7 +159,7 @@ func Test_Stats_RespondsWithDescendingResultsForCurrentYear(t *testing.T) { } game.Stats(m1, bot) - assert.Equal(t, expected, strings.Split(bot.sentMessages[0], "\n")) + assert.Equal(t, expected, strings.Split(bot.SentMessages[0], "\n")) } func Test_All_RespondsWithDescendingResultsForAllTime(t *testing.T) { @@ -193,7 +194,7 @@ func Test_All_RespondsWithDescendingResultsForAllTime(t *testing.T) { } game.All(m1, bot) - assert.Equal(t, expected, strings.Split(bot.sentMessages[0], "\n")) + assert.Equal(t, expected, strings.Split(bot.SentMessages[0], "\n")) } func Test_Me_RespondsWithPersonalStat(t *testing.T) { @@ -211,10 +212,10 @@ func Test_Me_RespondsWithPersonalStat(t *testing.T) { } game.Me(m1, bot) - assert.Equal(t, fmt.Sprintf("username:%s,scores:%d", m1.Sender.Username, 2), bot.sentMessages[0]) + assert.Equal(t, fmt.Sprintf("username:%s,scores:%d", m1.Sender.Username, 2), bot.SentMessages[0]) game.Me(m2, bot) - assert.Equal(t, fmt.Sprintf("username:%s,scores:%d", m2.Sender.Username, 1), bot.sentMessages[1]) + assert.Equal(t, fmt.Sprintf("username:%s,scores:%d", m2.Sender.Username, 1), bot.SentMessages[1]) } // Helpers @@ -229,10 +230,10 @@ func makeGameMessage(id int, username string) *core.Message { return &core.Message{ID: 0, Sender: player} } -func makeSUT(args ...interface{}) (*usecases.GameFlow, *FakeBot, *GameStorageMock) { +func makeSUT(args ...interface{}) (*usecases.GameFlow, *test_helpers.FakeBot, *GameStorageMock) { dict := LocalizerDict{} storage := &GameStorageMock{players: []*core.User{}} - bot := &FakeBot{} + bot := test_helpers.CreateFakeBot() for _, arg := range args { switch opt := arg.(type) { diff --git a/usecases/twitter_parser_test.go b/usecases/twitter_parser_test.go index 9f5b247..26dca14 100644 --- a/usecases/twitter_parser_test.go +++ b/usecases/twitter_parser_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/ailinykh/pullanusbot/v2/core" + "github.com/ailinykh/pullanusbot/v2/test_helpers" "github.com/ailinykh/pullanusbot/v2/usecases" "github.com/stretchr/testify/assert" ) @@ -41,7 +42,7 @@ func Test_HandleText_RemovesOriginalMessageInCaseOfFullMatch(t *testing.T) { parser.HandleText(m, bot) - assert.Equal(t, []string{"https://twitter.com/username/status/123456"}, bot.removedMessages) + assert.Equal(t, []string{"https://twitter.com/username/status/123456"}, bot.RemovedMessages) } func Test_HandleText_DoesNotRemoveOriginalMessage(t *testing.T) { @@ -50,7 +51,7 @@ func Test_HandleText_DoesNotRemoveOriginalMessage(t *testing.T) { parser.HandleText(m, bot) - assert.Equal(t, []string{}, bot.removedMessages) + assert.Equal(t, []string{}, bot.RemovedMessages) } func Test_HandleText_ReturnsErrorOnError(t *testing.T) { @@ -63,10 +64,10 @@ func Test_HandleText_ReturnsErrorOnError(t *testing.T) { assert.Equal(t, "an error", err.Error()) } -func makeTwitterSUT() (*usecases.TwitterParser, *FakeTweetHandler, *FakeBot) { +func makeTwitterSUT() (*usecases.TwitterParser, *FakeTweetHandler, *test_helpers.FakeBot) { handler := &FakeTweetHandler{[]string{}, nil} parser := usecases.CreateTwitterParser(handler) - bot := CreateFakeBot() + bot := test_helpers.CreateFakeBot() return parser, handler, bot } From b5cc5326fc0fb13712d8dc80b4cacf1d465c63fa Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Mon, 16 Aug 2021 18:18:31 +0300 Subject: [PATCH 159/439] Add FakeLogger to test_helpers --- test_helpers/fake_logger.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 test_helpers/fake_logger.go diff --git a/test_helpers/fake_logger.go b/test_helpers/fake_logger.go new file mode 100644 index 0000000..ff3fce2 --- /dev/null +++ b/test_helpers/fake_logger.go @@ -0,0 +1,14 @@ +package test_helpers + +func CreateFakeLogger() *FakeLogger { + return &FakeLogger{} +} + +type FakeLogger struct{} + +func (FakeLogger) Error(...interface{}) {} +func (FakeLogger) Errorf(string, ...interface{}) {} +func (FakeLogger) Info(...interface{}) {} +func (FakeLogger) Infof(string, ...interface{}) {} +func (FakeLogger) Warning(...interface{}) {} +func (FakeLogger) Warningf(string, ...interface{}) {} From 72e8301040ed4abca04b49d297b9cb96e455008d Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Mon, 16 Aug 2021 18:25:32 +0300 Subject: [PATCH 160/439] Implement SendPhotoAlbum for a FakeBot --- test_helpers/fake_bot.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/test_helpers/fake_bot.go b/test_helpers/fake_bot.go index 54fad57..fa27156 100644 --- a/test_helpers/fake_bot.go +++ b/test_helpers/fake_bot.go @@ -20,8 +20,14 @@ func (b *FakeBot) SendMedia(media *core.Media) (*core.Message, error) { return nil, nil } -func (FakeBot) SendPhotoAlbum([]*core.Media) ([]*core.Message, error) { return nil, nil } -func (FakeBot) SendVideo(*core.Video, string) (*core.Message, error) { return nil, nil } +func (b *FakeBot) SendPhotoAlbum(media []*core.Media) ([]*core.Message, error) { + for _, m := range media { + b.SentMedias = append(b.SentMedias, m.URL) + } + return nil, nil +} + +func (FakeBot) SendVideo(*core.Video, string) (*core.Message, error) { return nil, nil } func (b *FakeBot) Delete(message *core.Message) error { b.RemovedMessages = append(b.RemovedMessages, message.Text) From 6a996c46dbc7187c156a1927f8924f86b780ad1c Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Mon, 16 Aug 2021 18:28:29 +0300 Subject: [PATCH 161/439] Extract all the send media strategies into a separate package --- .../convert_media_strategy.go | 2 +- {usecases => helpers}/send_media_strategy.go | 2 +- helpers/send_media_strategy_test.go | 45 +++++++++++++++++++ .../upload_media_strategy.go | 2 +- pullanusbot.go | 7 +-- 5 files changed, 52 insertions(+), 6 deletions(-) rename {usecases => helpers}/convert_media_strategy.go (99%) rename {usecases => helpers}/send_media_strategy.go (97%) create mode 100644 helpers/send_media_strategy_test.go rename {usecases => helpers}/upload_media_strategy.go (99%) diff --git a/usecases/convert_media_strategy.go b/helpers/convert_media_strategy.go similarity index 99% rename from usecases/convert_media_strategy.go rename to helpers/convert_media_strategy.go index 07e16a9..4dbc89d 100644 --- a/usecases/convert_media_strategy.go +++ b/helpers/convert_media_strategy.go @@ -1,4 +1,4 @@ -package usecases +package helpers import ( "os" diff --git a/usecases/send_media_strategy.go b/helpers/send_media_strategy.go similarity index 97% rename from usecases/send_media_strategy.go rename to helpers/send_media_strategy.go index b0ba26f..4d7e370 100644 --- a/usecases/send_media_strategy.go +++ b/helpers/send_media_strategy.go @@ -1,4 +1,4 @@ -package usecases +package helpers import ( "github.com/ailinykh/pullanusbot/v2/core" diff --git a/helpers/send_media_strategy_test.go b/helpers/send_media_strategy_test.go new file mode 100644 index 0000000..53db0c9 --- /dev/null +++ b/helpers/send_media_strategy_test.go @@ -0,0 +1,45 @@ +package helpers_test + +import ( + "testing" + + "github.com/ailinykh/pullanusbot/v2/core" + "github.com/ailinykh/pullanusbot/v2/helpers" + "github.com/ailinykh/pullanusbot/v2/test_helpers" + "github.com/stretchr/testify/assert" +) + +func Test_SendMedia_DoesNotFailOnEmptyMedia(t *testing.T) { + strategy, bot := makeMediaStrategySUT() + media := []*core.Media{} + + strategy.SendMedia(media, bot) + + assert.Equal(t, []string{}, bot.SentMedias) +} + +func Test_SendMedia_SendsASingleMediaTroughABot(t *testing.T) { + strategy, bot := makeMediaStrategySUT() + media := []*core.Media{{URL: "https://a-url.com"}} + + strategy.SendMedia(media, bot) + + assert.Equal(t, []string{"https://a-url.com"}, bot.SentMedias) +} + +func Test_SendMedia_SendsAGroupMediaTroughABot(t *testing.T) { + strategy, bot := makeMediaStrategySUT() + media := []*core.Media{{URL: "https://a-url.com"}, {URL: "https://another-url.com"}} + + strategy.SendMedia(media, bot) + + assert.Equal(t, []string{"https://a-url.com", "https://another-url.com"}, bot.SentMedias) +} + +// Helpers +func makeMediaStrategySUT() (core.ISendMediaStrategy, *test_helpers.FakeBot) { + logger := test_helpers.CreateFakeLogger() + strategy := helpers.CreateSendMediaStrategy(logger) + bot := test_helpers.CreateFakeBot() + return strategy, bot +} diff --git a/usecases/upload_media_strategy.go b/helpers/upload_media_strategy.go similarity index 99% rename from usecases/upload_media_strategy.go rename to helpers/upload_media_strategy.go index 909da6b..dc24739 100644 --- a/usecases/upload_media_strategy.go +++ b/helpers/upload_media_strategy.go @@ -1,4 +1,4 @@ -package usecases +package helpers import ( "os" diff --git a/pullanusbot.go b/pullanusbot.go index 420f907..a1e2d29 100644 --- a/pullanusbot.go +++ b/pullanusbot.go @@ -8,6 +8,7 @@ import ( "github.com/ailinykh/pullanusbot/v2/api" "github.com/ailinykh/pullanusbot/v2/core" + "github.com/ailinykh/pullanusbot/v2/helpers" "github.com/ailinykh/pullanusbot/v2/infrastructure" "github.com/ailinykh/pullanusbot/v2/usecases" "github.com/google/logger" @@ -38,8 +39,8 @@ func main() { telebot.AddHandler(videoFlow) fileDownloader := infrastructure.CreateFileDownloader() - remoteMediaSender := usecases.CreateSendMediaStrategy(logger) - localMediaSender := usecases.CreateUploadMediaStrategy(logger, remoteMediaSender, fileDownloader, converter, converter) + remoteMediaSender := helpers.CreateSendMediaStrategy(logger) + localMediaSender := helpers.CreateUploadMediaStrategy(logger, remoteMediaSender, fileDownloader, converter, converter) twitterMediaFactory := api.CreateTwitterMediaFactory(logger) twitterFlow := usecases.CreateTwitterFlow(logger, twitterMediaFactory, localMediaSender) twitterTimeout := usecases.CreateTwitterTimeout(logger, twitterFlow) @@ -47,7 +48,7 @@ func main() { telebot.AddHandler(twitterParser) httpClient := api.CreateHttpClient() - convertMediaSender := usecases.CreateConvertMediaStrategy(logger, localMediaSender, fileDownloader, converter, converter) + convertMediaSender := helpers.CreateConvertMediaStrategy(logger, localMediaSender, fileDownloader, converter, converter) linkFlow := usecases.CreateLinkFlow(logger, httpClient, converter, convertMediaSender) telebot.AddHandler(linkFlow) From 6da20bab704db6eb74ce4b5bb05515791f1e49da Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Mon, 16 Aug 2021 18:32:38 +0300 Subject: [PATCH 162/439] Remove unused video file converter from UploadMediaStrategy --- helpers/upload_media_strategy.go | 5 ++--- pullanusbot.go | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/helpers/upload_media_strategy.go b/helpers/upload_media_strategy.go index dc24739..947bd8d 100644 --- a/helpers/upload_media_strategy.go +++ b/helpers/upload_media_strategy.go @@ -8,8 +8,8 @@ import ( "github.com/ailinykh/pullanusbot/v2/core" ) -func CreateUploadMediaStrategy(l core.ILogger, sms core.ISendMediaStrategy, fd core.IFileDownloader, vf core.IVideoFactory, vc core.IVideoConverter) *UploadMediaStrategy { - return &UploadMediaStrategy{l, sms, fd, vf, vc} +func CreateUploadMediaStrategy(l core.ILogger, sms core.ISendMediaStrategy, fd core.IFileDownloader, vf core.IVideoFactory) *UploadMediaStrategy { + return &UploadMediaStrategy{l, sms, fd, vf} } type UploadMediaStrategy struct { @@ -17,7 +17,6 @@ type UploadMediaStrategy struct { sms core.ISendMediaStrategy fd core.IFileDownloader vf core.IVideoFactory - vc core.IVideoConverter } // SendMedia is a core.ISendMediaStrategy interface implementation diff --git a/pullanusbot.go b/pullanusbot.go index a1e2d29..7bcd2d0 100644 --- a/pullanusbot.go +++ b/pullanusbot.go @@ -40,7 +40,7 @@ func main() { fileDownloader := infrastructure.CreateFileDownloader() remoteMediaSender := helpers.CreateSendMediaStrategy(logger) - localMediaSender := helpers.CreateUploadMediaStrategy(logger, remoteMediaSender, fileDownloader, converter, converter) + localMediaSender := helpers.CreateUploadMediaStrategy(logger, remoteMediaSender, fileDownloader, converter) twitterMediaFactory := api.CreateTwitterMediaFactory(logger) twitterFlow := usecases.CreateTwitterFlow(logger, twitterMediaFactory, localMediaSender) twitterTimeout := usecases.CreateTwitterTimeout(logger, twitterFlow) From 01918245ba5892cb49c8d5eb09e8e1b4cb7f0c67 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Mon, 16 Aug 2021 19:26:48 +0300 Subject: [PATCH 163/439] Support multiple codecs before coverting fallback --- helpers/convert_media_strategy.go | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/helpers/convert_media_strategy.go b/helpers/convert_media_strategy.go index 4dbc89d..d5d817e 100644 --- a/helpers/convert_media_strategy.go +++ b/helpers/convert_media_strategy.go @@ -22,13 +22,27 @@ type ConvertMediaStrategy struct { // SendMedia is a core.ISendMediaStrategy interface implementation func (cms *ConvertMediaStrategy) SendMedia(media []*core.Media, bot core.IBot) error { for _, m := range media { - if m.Type == core.TVideo && media[0].Codec != "mp4" { + if cms.needToConvert(m) { + cms.l.Infof("expected mp4/h264 codec, but got %s", m.Codec) return cms.fallbackToConverting(m, bot) } } return cms.sms.SendMedia(media, bot) } +func (cms *ConvertMediaStrategy) needToConvert(media *core.Media) bool { + if media.Type != core.TVideo { + return false + } + + for _, codec := range []string{"mp4", "h264"} { + if media.Codec == codec { + return false + } + } + return true +} + func (cms *ConvertMediaStrategy) fallbackToConverting(media *core.Media, bot core.IBot) error { cms.l.Info("send by converting") file, err := cms.downloadMedia(media) From 617ee871f716f66408d1e748e05c2c3d4dceb2ac Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Mon, 16 Aug 2021 20:52:41 +0300 Subject: [PATCH 164/439] Implement SendVideo method for FakeBot --- test_helpers/fake_bot.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/test_helpers/fake_bot.go b/test_helpers/fake_bot.go index fa27156..45ef81c 100644 --- a/test_helpers/fake_bot.go +++ b/test_helpers/fake_bot.go @@ -2,13 +2,16 @@ package test_helpers import "github.com/ailinykh/pullanusbot/v2/core" +// https://stackoverflow.com/questions/31794141/can-i-create-shared-test-utilities + func CreateFakeBot() *FakeBot { - return &FakeBot{[]string{}, []string{}, []string{}} + return &FakeBot{[]string{}, []string{}, []string{}, []string{}} } type FakeBot struct { SentMedias []string SentMessages []string + SentVideos []string RemovedMessages []string } @@ -27,7 +30,10 @@ func (b *FakeBot) SendPhotoAlbum(media []*core.Media) ([]*core.Message, error) { return nil, nil } -func (FakeBot) SendVideo(*core.Video, string) (*core.Message, error) { return nil, nil } +func (b *FakeBot) SendVideo(video *core.Video, caption string) (*core.Message, error) { + b.SentVideos = append(b.SentVideos, video.Name) + return nil, nil +} func (b *FakeBot) Delete(message *core.Message) error { b.RemovedMessages = append(b.RemovedMessages, message.Text) From 8c19a413270bd997cb30197a8f4af1c0eb1ffe70 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Mon, 16 Aug 2021 20:53:29 +0300 Subject: [PATCH 165/439] Add FakeFileDownloader for testing --- test_helpers/file_downloader.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 test_helpers/file_downloader.go diff --git a/test_helpers/file_downloader.go b/test_helpers/file_downloader.go new file mode 100644 index 0000000..48da3c9 --- /dev/null +++ b/test_helpers/file_downloader.go @@ -0,0 +1,20 @@ +package test_helpers + +import ( + "github.com/ailinykh/pullanusbot/v2/core" +) + +func CreateFileDownloader() *FakeFileDownloader { + return &FakeFileDownloader{make(map[string]string), nil} +} + +type FakeFileDownloader struct { + DownloadedFiles map[string]string + Err error +} + +// Download is a core.IFileDownloader interface implementation +func (ffd *FakeFileDownloader) Download(url core.URL, filepath string) (*core.File, error) { + ffd.DownloadedFiles[url] = filepath + return &core.File{Path: filepath}, ffd.Err +} From 197b825bb6a48b892c464f4a574e393a84b9eec7 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Mon, 16 Aug 2021 20:53:54 +0300 Subject: [PATCH 166/439] Add FakeVideoFactory to test_helpers --- test_helpers/video_factory.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 test_helpers/video_factory.go diff --git a/test_helpers/video_factory.go b/test_helpers/video_factory.go new file mode 100644 index 0000000..2702c7e --- /dev/null +++ b/test_helpers/video_factory.go @@ -0,0 +1,20 @@ +package test_helpers + +import ( + "github.com/ailinykh/pullanusbot/v2/core" +) + +func CreateVideoFactory() *FakeVideoFactory { + return &FakeVideoFactory{[]string{}, nil} +} + +type FakeVideoFactory struct { + CreatedVideos []string + Err error +} + +// CreateVideo is a core.IVideoFactory interface implementation +func (fvf *FakeVideoFactory) CreateVideo(path string) (*core.Video, error) { + fvf.CreatedVideos = append(fvf.CreatedVideos, path) + return &core.Video{File: core.File{Path: path}}, fvf.Err +} From 1545b8f4c5094de187e1674f05485f7d1411ed3a Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Mon, 16 Aug 2021 20:54:09 +0300 Subject: [PATCH 167/439] Add FakeSendMediaStrategy to test_helpers --- test_helpers/send_media_strategy.go | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 test_helpers/send_media_strategy.go diff --git a/test_helpers/send_media_strategy.go b/test_helpers/send_media_strategy.go new file mode 100644 index 0000000..1019856 --- /dev/null +++ b/test_helpers/send_media_strategy.go @@ -0,0 +1,22 @@ +package test_helpers + +import ( + "github.com/ailinykh/pullanusbot/v2/core" +) + +func CreateSendMediaStrategy() *FakeSendMediaStrategy { + return &FakeSendMediaStrategy{[]string{}, nil} +} + +type FakeSendMediaStrategy struct { + SentMedia []string + Err error +} + +// SendMedia is a core.ISendMediaStrategy interface implementation +func (fsms *FakeSendMediaStrategy) SendMedia(media []*core.Media, bot core.IBot) error { + for _, m := range media { + fsms.SentMedia = append(fsms.SentMedia, m.URL) + } + return fsms.Err +} From 1eb5b99240bc669151727ad63b8396b5393dd2dd Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Mon, 16 Aug 2021 20:54:26 +0300 Subject: [PATCH 168/439] Tests for upload media strategy added --- helpers/upload_media_strategy_test.go | 51 +++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 helpers/upload_media_strategy_test.go diff --git a/helpers/upload_media_strategy_test.go b/helpers/upload_media_strategy_test.go new file mode 100644 index 0000000..001af29 --- /dev/null +++ b/helpers/upload_media_strategy_test.go @@ -0,0 +1,51 @@ +package helpers_test + +import ( + "errors" + "testing" + + "github.com/ailinykh/pullanusbot/v2/core" + "github.com/ailinykh/pullanusbot/v2/helpers" + "github.com/ailinykh/pullanusbot/v2/test_helpers" + "github.com/stretchr/testify/assert" +) + +func Test_UploadMedia_DoesNotFailOnEmptyMedia(t *testing.T) { + strategy, _, bot := makeUploadMediaStrategySUT() + media := []*core.Media{} + + strategy.SendMedia(media, bot) + + assert.Equal(t, []string{}, bot.SentMedias) +} + +func Test_UploadMedia_DoesNotFallbackOnGenericError(t *testing.T) { + strategy, proxy, bot := makeUploadMediaStrategySUT() + media := []*core.Media{} + proxy.Err = errors.New("an error") + + err := strategy.SendMedia(media, bot) + + assert.Equal(t, proxy.Err, err) +} + +func Test_UploadMedia_FallbackOnSpecificError(t *testing.T) { + strategy, proxy, bot := makeUploadMediaStrategySUT() + media := []*core.Media{{URL: "https://a-url.com"}} + proxy.Err = errors.New("failed to get HTTP URL content") + + err := strategy.SendMedia(media, bot) + + assert.Equal(t, nil, err) +} + +// Helpers +func makeUploadMediaStrategySUT() (core.ISendMediaStrategy, *test_helpers.FakeSendMediaStrategy, *test_helpers.FakeBot) { + logger := test_helpers.CreateFakeLogger() + send_media_strategy := test_helpers.CreateSendMediaStrategy() + file_downloader := test_helpers.CreateFileDownloader() + video_factory := test_helpers.CreateVideoFactory() + strategy := helpers.CreateUploadMediaStrategy(logger, send_media_strategy, file_downloader, video_factory) + bot := test_helpers.CreateFakeBot() + return strategy, send_media_strategy, bot +} From 6abd4d6de82659da803025e1a54d6e5999e9e5da Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Tue, 17 Aug 2021 10:41:57 +0300 Subject: [PATCH 169/439] Extract temp filename from URL more safe way --- helpers/convert_media_strategy.go | 9 ++++++++- helpers/upload_media_strategy.go | 8 +++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/helpers/convert_media_strategy.go b/helpers/convert_media_strategy.go index d5d817e..03caa3c 100644 --- a/helpers/convert_media_strategy.go +++ b/helpers/convert_media_strategy.go @@ -3,6 +3,7 @@ package helpers import ( "os" "path" + "strings" "github.com/ailinykh/pullanusbot/v2/core" ) @@ -70,7 +71,13 @@ func (cms *ConvertMediaStrategy) fallbackToConverting(media *core.Media, bot cor } func (cms *ConvertMediaStrategy) downloadMedia(media *core.Media) (*core.File, error) { - mediaPath := path.Join(os.TempDir(), path.Base(media.URL)) + //TODO: duplicated code + filename := path.Base(media.URL) + if strings.Contains(filename, "?") { + parts := strings.Split(media.URL, "?") + filename = path.Base(parts[0]) + } + mediaPath := path.Join(os.TempDir(), filename) file, err := cms.fd.Download(media.URL, mediaPath) if err != nil { cms.l.Errorf("video download error: %v", err) diff --git a/helpers/upload_media_strategy.go b/helpers/upload_media_strategy.go index 947bd8d..9e58f40 100644 --- a/helpers/upload_media_strategy.go +++ b/helpers/upload_media_strategy.go @@ -60,7 +60,13 @@ func (ums *UploadMediaStrategy) fallbackToUploading(media *core.Media, bot core. } func (ums *UploadMediaStrategy) downloadMedia(media *core.Media) (*core.File, error) { - mediaPath := path.Join(os.TempDir(), path.Base(media.URL)) + //TODO: duplicated code + filename := path.Base(media.URL) + if strings.Contains(filename, "?") { + parts := strings.Split(media.URL, "?") + filename = path.Base(parts[0]) + } + mediaPath := path.Join(os.TempDir(), filename) file, err := ums.fd.Download(media.URL, mediaPath) if err != nil { ums.l.Errorf("video download error: %v", err) From 93fc02225ce7b6b2ed584805a117877bfe39d20e Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Tue, 17 Aug 2021 10:42:55 +0300 Subject: [PATCH 170/439] Improve file downloader with some additional http headers --- infrastructure/file_downloader.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/infrastructure/file_downloader.go b/infrastructure/file_downloader.go index 9b9b43d..456c5b6 100644 --- a/infrastructure/file_downloader.go +++ b/infrastructure/file_downloader.go @@ -21,11 +21,15 @@ type FileDownloader struct{} func (FileDownloader) Download(url core.URL, filepath string) (*core.File, error) { name := path.Base(filepath) // Get the data - resp, err := http.Get(url) + client := http.DefaultClient + req, _ := http.NewRequest("GET", url, nil) + req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:59.0) Gecko/20100101 Firefox/59.0") + req.Header.Set("Referer", url) + res, err := client.Do(req) if err != nil { return nil, err } - defer resp.Body.Close() + defer res.Body.Close() // Create the file out, err := os.Create(filepath) @@ -35,7 +39,7 @@ func (FileDownloader) Download(url core.URL, filepath string) (*core.File, error defer out.Close() // Write the body to file - _, err = io.Copy(out, resp.Body) + _, err = io.Copy(out, res.Body) if err != nil { return nil, err } From f0e0bcfc7a76457afb2a9cc6e49356a8919321bc Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Tue, 17 Aug 2021 10:43:50 +0300 Subject: [PATCH 171/439] Add GetContent method to IHttpClient protocol --- api/http_client.go | 22 ++++++++++++++++++++++ core/networking.go | 1 + 2 files changed, 23 insertions(+) diff --git a/api/http_client.go b/api/http_client.go index 2ee7fa2..8ba6bbb 100644 --- a/api/http_client.go +++ b/api/http_client.go @@ -1,6 +1,7 @@ package api import ( + "io/ioutil" "net/http" "github.com/ailinykh/pullanusbot/v2/core" @@ -19,6 +20,27 @@ func (HttpClient) GetContentType(url core.URL) (string, error) { if err != nil { return "", err } + defer resp.Body.Close() return resp.Header["Content-Type"][0], nil } + +// GetContent is a core.IHttpClient interface implementation +func (HttpClient) GetContent(url core.URL) (string, error) { + client := http.DefaultClient + req, _ := http.NewRequest("GET", url, nil) + req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:59.0) Gecko/20100101 Firefox/59.0") + + res, err := client.Do(req) + if err != nil { + return "", err + } + defer res.Body.Close() + + body, err := ioutil.ReadAll(res.Body) + if err != nil { + return "", err + } + + return string(body), nil +} diff --git a/core/networking.go b/core/networking.go index cf68d9c..198a307 100644 --- a/core/networking.go +++ b/core/networking.go @@ -18,4 +18,5 @@ type IImageDownloader interface { // IHttpClient retreives remote content info type IHttpClient interface { GetContentType(URL) (string, error) + GetContent(URL) (string, error) } From 22b771cbc487c17cf329f22781990c343833e921 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Tue, 17 Aug 2021 10:44:44 +0300 Subject: [PATCH 172/439] Add OpenGeaph parser as one more media creation factory --- api/opengraph_parser.go | 49 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 api/opengraph_parser.go diff --git a/api/opengraph_parser.go b/api/opengraph_parser.go new file mode 100644 index 0000000..eaf4afa --- /dev/null +++ b/api/opengraph_parser.go @@ -0,0 +1,49 @@ +package api + +import ( + "errors" + "fmt" + "html" + "regexp" + + "github.com/ailinykh/pullanusbot/v2/core" +) + +func CreateOpenGraphParser(l core.ILogger) *OpenGraphParser { + return &OpenGraphParser{l} +} + +type OpenGraphParser struct { + l core.ILogger +} + +// CreateMedia is a core.IMediaFactory interface implementation +func (ogp *OpenGraphParser) CreateMedia(HTMLString string, author *core.User) ([]*core.Media, error) { + video := ogp.parseMeta(HTMLString, "og:video") + if len(video) == 0 { + return nil, errors.New("video not found") + } + + video = html.UnescapeString(video) + title := ogp.parseMeta(HTMLString, "og:title") + description := ogp.parseMeta(HTMLString, "og:description") + url := ogp.parseMeta(HTMLString, "og:url") + + media := &core.Media{ + URL: video, + Type: core.TVideo, + Caption: fmt.Sprintf("🎵 %s (by %s)\n%s", url, title, author.Username, description), + } + return []*core.Media{media}, nil +} + +func (ogp *OpenGraphParser) parseMeta(html string, property string) string { + r := regexp.MustCompile(fmt.Sprintf(``, property)) + match := r.FindStringSubmatch(html) + if len(match) == 0 { + ogp.l.Errorf("can't find %s", property) + return "" + } + + return match[1] +} From 176798672f590e90587be7035fb41a7cea2889f4 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Tue, 17 Aug 2021 10:59:59 +0300 Subject: [PATCH 173/439] Add tiktok flow --- pullanusbot.go | 4 ++++ usecases/tiktok_flow.go | 50 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 usecases/tiktok_flow.go diff --git a/pullanusbot.go b/pullanusbot.go index 7bcd2d0..6201dea 100644 --- a/pullanusbot.go +++ b/pullanusbot.go @@ -52,6 +52,10 @@ func main() { linkFlow := usecases.CreateLinkFlow(logger, httpClient, converter, convertMediaSender) telebot.AddHandler(linkFlow) + openGraphParser := api.CreateOpenGraphParser(logger) + tiktokFlow := usecases.CreateTikTokFlow(logger, httpClient, openGraphParser, localMediaSender) + telebot.AddHandler(tiktokFlow) + fileUploader := api.CreateTelegraphAPI() //TODO: image_downloader := api.CreateTelebotImageDownloader() imageFlow := usecases.CreateImageFlow(logger, fileUploader, telebot) diff --git a/usecases/tiktok_flow.go b/usecases/tiktok_flow.go new file mode 100644 index 0000000..244f387 --- /dev/null +++ b/usecases/tiktok_flow.go @@ -0,0 +1,50 @@ +package usecases + +import ( + "regexp" + + "github.com/ailinykh/pullanusbot/v2/core" +) + +func CreateTikTokFlow(l core.ILogger, hc core.IHttpClient, mf core.IMediaFactory, sms core.ISendMediaStrategy) *TikTokFlow { + return &TikTokFlow{l, hc, mf, sms} +} + +type TikTokFlow struct { + l core.ILogger + hc core.IHttpClient + mf core.IMediaFactory + sms core.ISendMediaStrategy +} + +// HandleText is a core.ITextHandler protocol implementation +func (ttf *TikTokFlow) HandleText(message *core.Message, bot core.IBot) error { + r := regexp.MustCompile(`https?://\w+\.tiktok.com/\S+`) + links := r.FindAllString(message.Text, -1) + for _, l := range links { + err := ttf.handleURL(l, message, bot) + if err != nil { + return err + } + } + + if len(links) > 0 { + return bot.Delete(message) + } + return nil +} + +func (ttf *TikTokFlow) handleURL(url string, message *core.Message, bot core.IBot) error { + ttf.l.Info("processing ", url) + HTMLString, err := ttf.hc.GetContent(url) + if err != nil { + return err + } + + media, err := ttf.mf.CreateMedia(HTMLString, message.Sender) + if err != nil { + return err + } + + return ttf.sms.SendMedia(media, bot) +} From 6b1be1d86994f9a810e31ab1d20809641daadc70 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Tue, 17 Aug 2021 11:06:48 +0300 Subject: [PATCH 174/439] Delete original message if it completely matches tweet link --- usecases/twitter_flow.go | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/usecases/twitter_flow.go b/usecases/twitter_flow.go index 4f63421..d1081c5 100644 --- a/usecases/twitter_flow.go +++ b/usecases/twitter_flow.go @@ -22,12 +22,22 @@ type TwitterFlow struct { // HandleTweet is a ITweetHandler protocol implementation func (tf *TwitterFlow) HandleTweet(tweetID string, message *core.Message, bot core.IBot, deleteOriginal bool) error { - tf.l.Infof("processing tweet %s", tweetID) + tf.l.Infof("processing tweet %s, delete original: %t", tweetID, deleteOriginal) media, err := tf.mf.CreateMedia(tweetID, message.Sender) if err != nil { tf.l.Error(err) return err } - return tf.sms.SendMedia(media, bot) + err = tf.sms.SendMedia(media, bot) + if err != nil { + tf.l.Error(err) + return err + } + + if deleteOriginal { + return bot.Delete(message) + } + + return nil } From b7b83347722179d746b652247a38673b29595f0d Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Wed, 18 Aug 2021 13:10:01 +0300 Subject: [PATCH 175/439] Extend Media struct with resource url, title and description fields --- core/media.go | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/core/media.go b/core/media.go index a1f7f4e..88e1f0c 100644 --- a/core/media.go +++ b/core/media.go @@ -14,11 +14,14 @@ const ( // Media ... type Media struct { - URL string - Caption string - Duration int - Codec string // only video - Type MediaType + ResourceURL URL + URL URL + Title string + Description string + Caption string + Duration int // video only + Codec string // video only + Type MediaType } type ISendMediaStrategy interface { From cc740a8e505b70854c67dd4c6a6781d57fd86f9d Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Wed, 18 Aug 2021 13:11:19 +0300 Subject: [PATCH 176/439] Replace media caption with title for youtube API --- api/youtube_api.go | 11 ++++++----- usecases/youtube_flow.go | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/api/youtube_api.go b/api/youtube_api.go index a632c07..29eb380 100644 --- a/api/youtube_api.go +++ b/api/youtube_api.go @@ -35,11 +35,12 @@ func (y *YoutubeAPI) CreateMedia(url string, author *core.User) ([]*core.Media, return []*core.Media{ { - URL: video.ID, - Caption: video.Title, - Duration: video.Duration, - Codec: vf.VCodec, - Type: core.TVideo, + URL: video.ID, + Title: video.Title, + Description: video.Description, + Duration: video.Duration, + Codec: vf.VCodec, + Type: core.TVideo, }, }, nil } diff --git a/usecases/youtube_flow.go b/usecases/youtube_flow.go index 018266e..97c9db5 100644 --- a/usecases/youtube_flow.go +++ b/usecases/youtube_flow.go @@ -53,7 +53,7 @@ func (f *YoutubeFlow) process(id string, message *core.Message, bot core.IBot) e return nil } - title := media[0].Caption + title := media[0].Title f.l.Infof("downloading %s", id) file, err := f.vff.CreateVideo(id) if err != nil { From 721e11a47e92a8758e91fca2dfbb9360de6c1e42 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Wed, 18 Aug 2021 13:55:57 +0300 Subject: [PATCH 177/439] Get rid of media.Caption filling in opengraph parser --- api/opengraph_parser.go | 10 ++++++---- usecases/tiktok_flow.go | 4 ++++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/api/opengraph_parser.go b/api/opengraph_parser.go index eaf4afa..8c20477 100644 --- a/api/opengraph_parser.go +++ b/api/opengraph_parser.go @@ -18,7 +18,7 @@ type OpenGraphParser struct { } // CreateMedia is a core.IMediaFactory interface implementation -func (ogp *OpenGraphParser) CreateMedia(HTMLString string, author *core.User) ([]*core.Media, error) { +func (ogp *OpenGraphParser) CreateMedia(HTMLString string, _ *core.User) ([]*core.Media, error) { video := ogp.parseMeta(HTMLString, "og:video") if len(video) == 0 { return nil, errors.New("video not found") @@ -30,9 +30,11 @@ func (ogp *OpenGraphParser) CreateMedia(HTMLString string, author *core.User) ([ url := ogp.parseMeta(HTMLString, "og:url") media := &core.Media{ - URL: video, - Type: core.TVideo, - Caption: fmt.Sprintf("🎵 %s (by %s)\n%s", url, title, author.Username, description), + ResourceURL: video, + URL: url, + Title: title, + Description: description, + Type: core.TVideo, } return []*core.Media{media}, nil } diff --git a/usecases/tiktok_flow.go b/usecases/tiktok_flow.go index 244f387..b32930c 100644 --- a/usecases/tiktok_flow.go +++ b/usecases/tiktok_flow.go @@ -1,6 +1,7 @@ package usecases import ( + "fmt" "regexp" "github.com/ailinykh/pullanusbot/v2/core" @@ -46,5 +47,8 @@ func (ttf *TikTokFlow) handleURL(url string, message *core.Message, bot core.IBo return err } + for _, m := range media { + m.Caption = fmt.Sprintf("🎵 %s (by %s)\n%s", m.URL, m.Title, message.Sender.Username, m.Description) + } return ttf.sms.SendMedia(media, bot) } From 7a14040a3c0960613debcedc3b8f85e19009280e Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Wed, 18 Aug 2021 13:57:54 +0300 Subject: [PATCH 178/439] Move media.Caption creation logic from twitter media factory to twitter flow --- api/twitter_media_factory.go | 19 ++++++------------- usecases/twitter_flow.go | 9 +++++++++ 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/api/twitter_media_factory.go b/api/twitter_media_factory.go index 1a77b5a..12cfd4b 100644 --- a/api/twitter_media_factory.go +++ b/api/twitter_media_factory.go @@ -2,8 +2,6 @@ package api import ( "errors" - "fmt" - "regexp" "github.com/ailinykh/pullanusbot/v2/core" ) @@ -18,7 +16,7 @@ type TwitterMediaFactory struct { } // CreateMedia is a core.IMediaFactory interface implementation -func (tmf *TwitterMediaFactory) CreateMedia(tweetID string, author *core.User) ([]*core.Media, error) { +func (tmf *TwitterMediaFactory) CreateMedia(tweetID string, _ *core.User) ([]*core.Media, error) { tweet, err := tmf.api.getTweetByID(tweetID) if err != nil { return nil, err @@ -30,16 +28,17 @@ func (tmf *TwitterMediaFactory) CreateMedia(tweetID string, author *core.User) ( } media := tweet.ExtendedEntities.Media + url := "https://twitter.com/" + tweet.User.ScreenName + "/status/" + tweet.ID switch len(media) { case 0: - return []*core.Media{{URL: "", Caption: tmf.makeCaption(author.Username, tweet), Type: core.TText}}, nil + return []*core.Media{{URL: url, Title: tweet.User.Name, Description: tweet.FullText, Type: core.TText}}, nil case 1: if media[0].Type == "video" || media[0].Type == "animated_gif" { //TODO: Codec ?? - return []*core.Media{{URL: media[0].VideoInfo.best().URL, Caption: tmf.makeCaption(author.Username, tweet), Type: core.TVideo}}, nil + return []*core.Media{{ResourceURL: media[0].VideoInfo.best().URL, URL: url, Title: tweet.User.Name, Description: tweet.FullText, Type: core.TVideo}}, nil } else if media[0].Type == "photo" { - return []*core.Media{{URL: media[0].MediaURL, Caption: tmf.makeCaption(author.Username, tweet), Type: core.TPhoto}}, nil + return []*core.Media{{ResourceURL: media[0].MediaURL, URL: url, Title: tweet.User.Name, Description: tweet.FullText, Type: core.TPhoto}}, nil } else { return nil, errors.New("unexpected type: " + media[0].Type) } @@ -47,14 +46,8 @@ func (tmf *TwitterMediaFactory) CreateMedia(tweetID string, author *core.User) ( // t.sendAlbum(media, tweet, m) medias := []*core.Media{} for _, m := range media { - medias = append(medias, &core.Media{URL: m.MediaURL, Caption: tmf.makeCaption(author.Username, tweet), Type: core.TPhoto}) + medias = append(medias, &core.Media{ResourceURL: m.MediaURL, URL: url, Title: tweet.User.Name, Description: tweet.FullText, Type: core.TPhoto}) } return medias, nil } } - -func (TwitterMediaFactory) makeCaption(author string, tweet *Tweet) string { - re := regexp.MustCompile(`\s?http\S+$`) - text := re.ReplaceAllString(tweet.FullText, "") - return fmt.Sprintf("🐦 %s (by %s)\n%s", tweet.User.ScreenName, tweet.ID, tweet.User.Name, author, text) -} diff --git a/usecases/twitter_flow.go b/usecases/twitter_flow.go index d1081c5..d4ea8fa 100644 --- a/usecases/twitter_flow.go +++ b/usecases/twitter_flow.go @@ -1,6 +1,9 @@ package usecases import ( + "fmt" + "regexp" + "github.com/ailinykh/pullanusbot/v2/core" ) @@ -29,6 +32,12 @@ func (tf *TwitterFlow) HandleTweet(tweetID string, message *core.Message, bot co return err } + for _, m := range media { + re := regexp.MustCompile(`\s?http\S+$`) + text := re.ReplaceAllString(m.Description, "") + m.Caption = fmt.Sprintf("🐦 %s (by %s)\n%s", m.URL, m.Title, message.Sender.Username, text) + } + err = tf.sms.SendMedia(media, bot) if err != nil { tf.l.Error(err) From 0531b60c03f318685d8542ec6b6894af21d60372 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Wed, 18 Aug 2021 13:59:45 +0300 Subject: [PATCH 179/439] Prefer ResourceURL in prior to URL --- infrastructure/ffmpeg_converter.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/infrastructure/ffmpeg_converter.go b/infrastructure/ffmpeg_converter.go index 62953e2..98a0ffe 100644 --- a/infrastructure/ffmpeg_converter.go +++ b/infrastructure/ffmpeg_converter.go @@ -59,7 +59,7 @@ func (c *FfmpegConverter) GetCodec(path string) string { } // CreateMedia is a core.IMediaFactory interface implementation -func (c *FfmpegConverter) CreateMedia(url string, author *core.User) ([]*core.Media, error) { +func (c *FfmpegConverter) CreateMedia(url string, _ *core.User) ([]*core.Media, error) { ffprobe, err := c.getFFProbe(url) if err != nil { c.l.Error(err) @@ -73,10 +73,10 @@ func (c *FfmpegConverter) CreateMedia(url string, author *core.User) ([]*core.Me } if ffprobe.Format.FormatName == "image2" { - return []*core.Media{{URL: url, Codec: stream.CodecName, Type: core.TPhoto}}, nil + return []*core.Media{{ResourceURL: url, URL: url, Codec: stream.CodecName, Type: core.TPhoto}}, nil } - return []*core.Media{{URL: url, Codec: stream.CodecName, Type: core.TVideo}}, nil + return []*core.Media{{ResourceURL: url, URL: url, Codec: stream.CodecName, Type: core.TVideo}}, nil } // CreateVideo is a core.IVideoSplitter interface implementation From cf5f5bf02f13ea2e75a78ebcddef1101ab8f190a Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Wed, 18 Aug 2021 14:00:37 +0300 Subject: [PATCH 180/439] Change media.URL to media.ResourceURL within telebot adapter --- api/telebot_adapter.go | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/api/telebot_adapter.go b/api/telebot_adapter.go index 3e5ea40..f3356b4 100644 --- a/api/telebot_adapter.go +++ b/api/telebot_adapter.go @@ -63,22 +63,23 @@ func (a *TelebotAdapter) SendAlbum(images []*core.Image) ([]*core.Message, error func (a *TelebotAdapter) SendMedia(media *core.Media) (*core.Message, error) { var sent *tb.Message var err error + opts := &tb.SendOptions{ParseMode: tb.ModeHTML, DisableWebPagePreview: true} switch media.Type { case core.TPhoto: a.t.logger.Infof("sending media as photo: %v", media) - file := &tb.Photo{File: tb.FromURL(media.URL)} + file := &tb.Photo{File: tb.FromURL(media.ResourceURL)} file.Caption = media.Caption a.t.bot.Notify(a.m.Chat, tb.UploadingPhoto) - sent, err = a.t.bot.Send(a.m.Chat, file, &tb.SendOptions{ParseMode: tb.ModeHTML}) + sent, err = a.t.bot.Send(a.m.Chat, file, opts) case core.TVideo: a.t.logger.Infof("sending media as video: %v", media) - file := &tb.Video{File: tb.FromURL(media.URL)} + file := &tb.Video{File: tb.FromURL(media.ResourceURL)} file.Caption = media.Caption a.t.bot.Notify(a.m.Chat, tb.UploadingVideo) - sent, err = a.t.bot.Send(a.m.Chat, file, &tb.SendOptions{ParseMode: tb.ModeHTML}) + sent, err = a.t.bot.Send(a.m.Chat, file, opts) case core.TText: a.t.logger.Infof("sending media as text: %v", media) - sent, err = a.t.bot.Send(a.m.Chat, media.Caption, &tb.SendOptions{ParseMode: tb.ModeHTML}) + sent, err = a.t.bot.Send(a.m.Chat, media.Caption, opts) } if err != nil { @@ -93,7 +94,7 @@ func (a *TelebotAdapter) SendPhotoAlbum(medias []*core.Media) ([]*core.Message, var album = tb.Album{} for i, m := range medias { - photo = &tb.Photo{File: tb.FromURL(m.URL)} + photo = &tb.Photo{File: tb.FromURL(m.ResourceURL)} if i == len(medias)-1 { photo.Caption = m.Caption photo.ParseMode = tb.ModeHTML From 9cb7922bba6c49e0a4871a38ef06b51871017d18 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Wed, 18 Aug 2021 14:01:20 +0300 Subject: [PATCH 181/439] Change FakeBot media sending behaviour --- test_helpers/fake_bot.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test_helpers/fake_bot.go b/test_helpers/fake_bot.go index 45ef81c..d2cdd4b 100644 --- a/test_helpers/fake_bot.go +++ b/test_helpers/fake_bot.go @@ -19,13 +19,13 @@ func (FakeBot) SendImage(*core.Image, string) (*core.Message, error) { return ni func (FakeBot) SendAlbum([]*core.Image) ([]*core.Message, error) { return nil, nil } func (b *FakeBot) SendMedia(media *core.Media) (*core.Message, error) { - b.SentMedias = append(b.SentMedias, media.URL) + b.SentMedias = append(b.SentMedias, media.ResourceURL) return nil, nil } func (b *FakeBot) SendPhotoAlbum(media []*core.Media) ([]*core.Message, error) { for _, m := range media { - b.SentMedias = append(b.SentMedias, m.URL) + b.SentMedias = append(b.SentMedias, m.ResourceURL) } return nil, nil } From f6d94ec72732ed884fb7dc8b33fcd796f6a272e6 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Wed, 18 Aug 2021 14:01:59 +0300 Subject: [PATCH 182/439] Refactor send media strategies, replacce URL wirh ResourceURL --- helpers/convert_media_strategy.go | 6 +++--- helpers/send_media_strategy_test.go | 4 ++-- helpers/upload_media_strategy.go | 6 +++--- helpers/upload_media_strategy_test.go | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/helpers/convert_media_strategy.go b/helpers/convert_media_strategy.go index 03caa3c..37d0533 100644 --- a/helpers/convert_media_strategy.go +++ b/helpers/convert_media_strategy.go @@ -72,13 +72,13 @@ func (cms *ConvertMediaStrategy) fallbackToConverting(media *core.Media, bot cor func (cms *ConvertMediaStrategy) downloadMedia(media *core.Media) (*core.File, error) { //TODO: duplicated code - filename := path.Base(media.URL) + filename := path.Base(media.ResourceURL) if strings.Contains(filename, "?") { - parts := strings.Split(media.URL, "?") + parts := strings.Split(media.ResourceURL, "?") filename = path.Base(parts[0]) } mediaPath := path.Join(os.TempDir(), filename) - file, err := cms.fd.Download(media.URL, mediaPath) + file, err := cms.fd.Download(media.ResourceURL, mediaPath) if err != nil { cms.l.Errorf("video download error: %v", err) return nil, err diff --git a/helpers/send_media_strategy_test.go b/helpers/send_media_strategy_test.go index 53db0c9..5708164 100644 --- a/helpers/send_media_strategy_test.go +++ b/helpers/send_media_strategy_test.go @@ -20,7 +20,7 @@ func Test_SendMedia_DoesNotFailOnEmptyMedia(t *testing.T) { func Test_SendMedia_SendsASingleMediaTroughABot(t *testing.T) { strategy, bot := makeMediaStrategySUT() - media := []*core.Media{{URL: "https://a-url.com"}} + media := []*core.Media{{ResourceURL: "https://a-url.com"}} strategy.SendMedia(media, bot) @@ -29,7 +29,7 @@ func Test_SendMedia_SendsASingleMediaTroughABot(t *testing.T) { func Test_SendMedia_SendsAGroupMediaTroughABot(t *testing.T) { strategy, bot := makeMediaStrategySUT() - media := []*core.Media{{URL: "https://a-url.com"}, {URL: "https://another-url.com"}} + media := []*core.Media{{ResourceURL: "https://a-url.com"}, {ResourceURL: "https://another-url.com"}} strategy.SendMedia(media, bot) diff --git a/helpers/upload_media_strategy.go b/helpers/upload_media_strategy.go index 9e58f40..ddab9d3 100644 --- a/helpers/upload_media_strategy.go +++ b/helpers/upload_media_strategy.go @@ -61,13 +61,13 @@ func (ums *UploadMediaStrategy) fallbackToUploading(media *core.Media, bot core. func (ums *UploadMediaStrategy) downloadMedia(media *core.Media) (*core.File, error) { //TODO: duplicated code - filename := path.Base(media.URL) + filename := path.Base(media.ResourceURL) if strings.Contains(filename, "?") { - parts := strings.Split(media.URL, "?") + parts := strings.Split(media.ResourceURL, "?") filename = path.Base(parts[0]) } mediaPath := path.Join(os.TempDir(), filename) - file, err := ums.fd.Download(media.URL, mediaPath) + file, err := ums.fd.Download(media.ResourceURL, mediaPath) if err != nil { ums.l.Errorf("video download error: %v", err) return nil, err diff --git a/helpers/upload_media_strategy_test.go b/helpers/upload_media_strategy_test.go index 001af29..7e0ffe6 100644 --- a/helpers/upload_media_strategy_test.go +++ b/helpers/upload_media_strategy_test.go @@ -31,7 +31,7 @@ func Test_UploadMedia_DoesNotFallbackOnGenericError(t *testing.T) { func Test_UploadMedia_FallbackOnSpecificError(t *testing.T) { strategy, proxy, bot := makeUploadMediaStrategySUT() - media := []*core.Media{{URL: "https://a-url.com"}} + media := []*core.Media{{ResourceURL: "https://a-url.com"}} proxy.Err = errors.New("failed to get HTTP URL content") err := strategy.SendMedia(media, bot) From 3e3f8efb42763f5cf0289788224b7104dfa4631a Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Wed, 18 Aug 2021 14:05:32 +0300 Subject: [PATCH 183/439] Remove unnesessary User from IMediaFactory --- api/opengraph_parser.go | 2 +- api/twitter_media_factory.go | 2 +- api/youtube_api.go | 2 +- core/media_loader.go | 2 +- infrastructure/ffmpeg_converter.go | 2 +- usecases/link_flow.go | 2 +- usecases/tiktok_flow.go | 2 +- usecases/twitter_flow.go | 2 +- usecases/youtube_flow.go | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/api/opengraph_parser.go b/api/opengraph_parser.go index 8c20477..34e94d2 100644 --- a/api/opengraph_parser.go +++ b/api/opengraph_parser.go @@ -18,7 +18,7 @@ type OpenGraphParser struct { } // CreateMedia is a core.IMediaFactory interface implementation -func (ogp *OpenGraphParser) CreateMedia(HTMLString string, _ *core.User) ([]*core.Media, error) { +func (ogp *OpenGraphParser) CreateMedia(HTMLString string) ([]*core.Media, error) { video := ogp.parseMeta(HTMLString, "og:video") if len(video) == 0 { return nil, errors.New("video not found") diff --git a/api/twitter_media_factory.go b/api/twitter_media_factory.go index 12cfd4b..ee50353 100644 --- a/api/twitter_media_factory.go +++ b/api/twitter_media_factory.go @@ -16,7 +16,7 @@ type TwitterMediaFactory struct { } // CreateMedia is a core.IMediaFactory interface implementation -func (tmf *TwitterMediaFactory) CreateMedia(tweetID string, _ *core.User) ([]*core.Media, error) { +func (tmf *TwitterMediaFactory) CreateMedia(tweetID string) ([]*core.Media, error) { tweet, err := tmf.api.getTweetByID(tweetID) if err != nil { return nil, err diff --git a/api/youtube_api.go b/api/youtube_api.go index 29eb380..6ed9741 100644 --- a/api/youtube_api.go +++ b/api/youtube_api.go @@ -22,7 +22,7 @@ type YoutubeAPI struct { } // CreateMedia is a core.IMediaFactory interface implementation -func (y *YoutubeAPI) CreateMedia(url string, author *core.User) ([]*core.Media, error) { +func (y *YoutubeAPI) CreateMedia(url string) ([]*core.Media, error) { video, err := y.getInfo(url) if err != nil { return nil, err diff --git a/core/media_loader.go b/core/media_loader.go index adfa072..e807fec 100644 --- a/core/media_loader.go +++ b/core/media_loader.go @@ -5,5 +5,5 @@ type URL = string // IMediaFactory creates Media from URL type IMediaFactory interface { - CreateMedia(URL, *User) ([]*Media, error) + CreateMedia(URL) ([]*Media, error) } diff --git a/infrastructure/ffmpeg_converter.go b/infrastructure/ffmpeg_converter.go index 98a0ffe..52387be 100644 --- a/infrastructure/ffmpeg_converter.go +++ b/infrastructure/ffmpeg_converter.go @@ -59,7 +59,7 @@ func (c *FfmpegConverter) GetCodec(path string) string { } // CreateMedia is a core.IMediaFactory interface implementation -func (c *FfmpegConverter) CreateMedia(url string, _ *core.User) ([]*core.Media, error) { +func (c *FfmpegConverter) CreateMedia(url string) ([]*core.Media, error) { ffprobe, err := c.getFFProbe(url) if err != nil { c.l.Error(err) diff --git a/usecases/link_flow.go b/usecases/link_flow.go index 38682e8..b6dc4a5 100644 --- a/usecases/link_flow.go +++ b/usecases/link_flow.go @@ -42,7 +42,7 @@ func (lf *LinkFlow) handleURL(message *core.Message, bot core.IBot) error { return nil } - media, err := lf.mf.CreateMedia(message.Text, message.Sender) + media, err := lf.mf.CreateMedia(message.Text) if err != nil { lf.l.Error(err) return err diff --git a/usecases/tiktok_flow.go b/usecases/tiktok_flow.go index b32930c..380b8c2 100644 --- a/usecases/tiktok_flow.go +++ b/usecases/tiktok_flow.go @@ -42,7 +42,7 @@ func (ttf *TikTokFlow) handleURL(url string, message *core.Message, bot core.IBo return err } - media, err := ttf.mf.CreateMedia(HTMLString, message.Sender) + media, err := ttf.mf.CreateMedia(HTMLString) if err != nil { return err } diff --git a/usecases/twitter_flow.go b/usecases/twitter_flow.go index d4ea8fa..6b7e176 100644 --- a/usecases/twitter_flow.go +++ b/usecases/twitter_flow.go @@ -26,7 +26,7 @@ type TwitterFlow struct { // HandleTweet is a ITweetHandler protocol implementation func (tf *TwitterFlow) HandleTweet(tweetID string, message *core.Message, bot core.IBot, deleteOriginal bool) error { tf.l.Infof("processing tweet %s, delete original: %t", tweetID, deleteOriginal) - media, err := tf.mf.CreateMedia(tweetID, message.Sender) + media, err := tf.mf.CreateMedia(tweetID) if err != nil { tf.l.Error(err) return err diff --git a/usecases/youtube_flow.go b/usecases/youtube_flow.go index 97c9db5..a13ecb1 100644 --- a/usecases/youtube_flow.go +++ b/usecases/youtube_flow.go @@ -43,7 +43,7 @@ func (f *YoutubeFlow) process(id string, message *core.Message, bot core.IBot) e defer f.m.Unlock() f.l.Infof("processing %s", id) - media, err := f.mf.CreateMedia(id, message.Sender) + media, err := f.mf.CreateMedia(id) if err != nil { return err } From 1d4ae9d623203b93008e69034b12c7c4a954d790 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Wed, 18 Aug 2021 14:08:12 +0300 Subject: [PATCH 184/439] Move IMediaFactory protocol into media.go file --- core/media.go | 8 ++++++++ core/media_loader.go | 9 --------- 2 files changed, 8 insertions(+), 9 deletions(-) delete mode 100644 core/media_loader.go diff --git a/core/media.go b/core/media.go index 88e1f0c..6da23a3 100644 --- a/core/media.go +++ b/core/media.go @@ -1,5 +1,8 @@ package core +// URL ... +type URL = string + // MediaType ... type MediaType int @@ -24,6 +27,11 @@ type Media struct { Type MediaType } +// IMediaFactory creates Media from URL +type IMediaFactory interface { + CreateMedia(URL) ([]*Media, error) +} + type ISendMediaStrategy interface { SendMedia([]*Media, IBot) error } diff --git a/core/media_loader.go b/core/media_loader.go deleted file mode 100644 index e807fec..0000000 --- a/core/media_loader.go +++ /dev/null @@ -1,9 +0,0 @@ -package core - -// URL ... -type URL = string - -// IMediaFactory creates Media from URL -type IMediaFactory interface { - CreateMedia(URL) ([]*Media, error) -} From 7b7287a4414d191d66fdca2ead38c10466659ccf Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Thu, 19 Aug 2021 14:21:32 +0300 Subject: [PATCH 185/439] Pass url separetely from message on link_flow --- usecases/link_flow.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/usecases/link_flow.go b/usecases/link_flow.go index b6dc4a5..6d231cb 100644 --- a/usecases/link_flow.go +++ b/usecases/link_flow.go @@ -26,13 +26,13 @@ type LinkFlow struct { func (lf *LinkFlow) HandleText(message *core.Message, bot core.IBot) error { r := regexp.MustCompile(`^http(\S+)$`) if r.MatchString(message.Text) { - return lf.handleURL(message, bot) + return lf.handleURL(message.Text, message, bot) } return nil } -func (lf *LinkFlow) handleURL(message *core.Message, bot core.IBot) error { - contentType, err := lf.hc.GetContentType(message.Text) +func (lf *LinkFlow) handleURL(url core.URL, message *core.Message, bot core.IBot) error { + contentType, err := lf.hc.GetContentType(url) if err != nil { lf.l.Error(err) return err @@ -42,7 +42,7 @@ func (lf *LinkFlow) handleURL(message *core.Message, bot core.IBot) error { return nil } - media, err := lf.mf.CreateMedia(message.Text) + media, err := lf.mf.CreateMedia(url) if err != nil { lf.l.Error(err) return err From a5e721218c6485ba8a498c122f5c03071eb18b6c Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Thu, 19 Aug 2021 16:30:53 +0300 Subject: [PATCH 186/439] Add ability to send videos by ID --- api/telebot_factory.go | 29 +++++++++++++++++------------ core/video.go | 1 + 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/api/telebot_factory.go b/api/telebot_factory.go index 47c05f9..7cbffe6 100644 --- a/api/telebot_factory.go +++ b/api/telebot_factory.go @@ -6,19 +6,24 @@ import ( ) func makeTbVideo(vf *core.Video, caption string) *tb.Video { - video := tb.Video{File: tb.FromDisk(vf.Path)} - video.FileName = vf.File.Name - video.Width = vf.Width - video.Height = vf.Height - video.Caption = caption - video.Duration = vf.Duration - video.SupportsStreaming = true - video.Thumbnail = &tb.Photo{ - File: tb.FromDisk(vf.Thumb.Path), - Width: vf.Thumb.Width, - Height: vf.Thumb.Height, + var video *tb.Video + if len(vf.ID) > 0 { + video = &tb.Video{File: tb.File{FileID: vf.ID}} + } else { + video = &tb.Video{File: tb.FromDisk(vf.Path)} + video.FileName = vf.File.Name + video.Width = vf.Width + video.Height = vf.Height + video.Caption = caption + video.Duration = vf.Duration + video.SupportsStreaming = true + video.Thumbnail = &tb.Photo{ + File: tb.FromDisk(vf.Thumb.Path), + Width: vf.Thumb.Width, + Height: vf.Thumb.Height, + } } - return &video + return video } func makeTbPhoto(image *core.Image, caption string) *tb.Photo { diff --git a/core/video.go b/core/video.go index 7e0f9bb..9550956 100644 --- a/core/video.go +++ b/core/video.go @@ -5,6 +5,7 @@ import "os" // Video ... type Video struct { File + ID string Width int Height int Bitrate int From 9949b90e39daaae65f368505d6c5da89493d9eda Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Thu, 19 Aug 2021 16:39:57 +0300 Subject: [PATCH 187/439] Add IVideoHandler for future video processing --- api/telebot.go | 20 +++++++++++++++++++- core/handlers.go | 5 +++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/api/telebot.go b/api/telebot.go index bf2f492..9b6b69e 100644 --- a/api/telebot.go +++ b/api/telebot.go @@ -21,6 +21,7 @@ type Telebot struct { textHandlers []core.ITextHandler documentHandlers []core.IDocumentHandler imageHandlers []core.IImageHandler + videoHandlers []core.IVideoHandler } // CreateTelebot is a default Telebot factory @@ -39,7 +40,7 @@ func CreateTelebot(token string, logger core.ILogger) *Telebot { panic(err) } - telebot := &Telebot{bot, logger, []string{}, []core.ITextHandler{}, []core.IDocumentHandler{}, []core.IImageHandler{}} + telebot := &Telebot{bot, logger, []string{}, []core.ITextHandler{}, []core.IDocumentHandler{}, []core.IImageHandler{}, []core.IVideoHandler{}} bot.Handle(tb.OnText, func(m *tb.Message) { for _, h := range telebot.textHandlers { @@ -102,6 +103,23 @@ func CreateTelebot(token string, logger core.ILogger) *Telebot { } }) + bot.Handle(tb.OnVideo, func(m *tb.Message) { + + video := &core.Video{ + ID: m.Video.FileID, + Width: m.Video.Width, + Height: m.Video.Height, + } + + for _, h := range telebot.videoHandlers { + err := h.HandleImage(video, makeMessage(m), &TelebotAdapter{m, telebot}) + if err != nil { + logger.Errorf("%T: %s", h, err) + telebot.reportError(m, err) + } + } + }) + return telebot } diff --git a/core/handlers.go b/core/handlers.go index e0c99c5..5a208a3 100644 --- a/core/handlers.go +++ b/core/handlers.go @@ -14,3 +14,8 @@ type ITextHandler interface { type IImageHandler interface { HandleImage(*Image, *Message, IBot) error } + +// IVideoHandler responds to videos +type IVideoHandler interface { + HandleImage(*Video, *Message, IBot) error +} From 1e2e2197b87c04c8bc8b7d63607c26f483e3d399 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Thu, 19 Aug 2021 16:40:36 +0300 Subject: [PATCH 188/439] Hello, Andrew --- usecases/i_do_not_care.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/usecases/i_do_not_care.go b/usecases/i_do_not_care.go index 32002ba..00d2725 100644 --- a/usecases/i_do_not_care.go +++ b/usecases/i_do_not_care.go @@ -18,5 +18,9 @@ func (IDoNotCare) HandleText(message *core.Message, bot core.IBot) error { _, err := bot.SendText("https://coub.com/view/1ov5oi", false) return err } + if strings.Contains(strings.ToLower(message.Text), "привет, андрей") { + _, err := bot.SendVideo(&core.Video{ID: "BAACAgIAAxkBAAIziWEeZBqlM1_1n2AVaxedGFn3vS-sAAKgDwACSl7xSImLuE-s8DMbIAQ"}, "") + return err + } return nil } From bb15428c98184100a03d2458607b8cf8ec9f2b44 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Mon, 23 Aug 2021 15:48:19 +0300 Subject: [PATCH 189/439] Use HEAD http method to retreive a content-type --- api/http_client.go | 12 +++++++++--- usecases/link_flow.go | 2 +- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/api/http_client.go b/api/http_client.go index 8ba6bbb..438dffe 100644 --- a/api/http_client.go +++ b/api/http_client.go @@ -1,6 +1,7 @@ package api import ( + "errors" "io/ioutil" "net/http" @@ -15,14 +16,19 @@ type HttpClient struct{} // GetContentType is a core.IHttpClient interface implementation func (HttpClient) GetContentType(url core.URL) (string, error) { - resp, err := http.Get(url) + client := http.DefaultClient + req, _ := http.NewRequest("HEAD", url, nil) + req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:59.0) Gecko/20100101 Firefox/59.0") + res, err := client.Do(req) if err != nil { return "", err } - defer resp.Body.Close() - return resp.Header["Content-Type"][0], nil + if header, ok := res.Header["Content-Type"]; ok { + return header[0], nil + } + return "", errors.New("content-type not found") } // GetContent is a core.IHttpClient interface implementation diff --git a/usecases/link_flow.go b/usecases/link_flow.go index 6d231cb..ab3848a 100644 --- a/usecases/link_flow.go +++ b/usecases/link_flow.go @@ -34,7 +34,7 @@ func (lf *LinkFlow) HandleText(message *core.Message, bot core.IBot) error { func (lf *LinkFlow) handleURL(url core.URL, message *core.Message, bot core.IBot) error { contentType, err := lf.hc.GetContentType(url) if err != nil { - lf.l.Error(err) + lf.l.Error(err, url) return err } From 1245340d08ff21ae3796aaba576ac18ad4c2052a Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Wed, 25 Aug 2021 11:24:26 +0300 Subject: [PATCH 190/439] Add GetRedirectLocation method to IHttpClient protocol to obtain final location --- api/http_client.go | 13 +++++++++++++ core/networking.go | 1 + 2 files changed, 14 insertions(+) diff --git a/api/http_client.go b/api/http_client.go index 438dffe..65ea0d5 100644 --- a/api/http_client.go +++ b/api/http_client.go @@ -14,6 +14,19 @@ func CreateHttpClient() *HttpClient { type HttpClient struct{} +func (HttpClient) GetRedirectLocation(url core.URL) (core.URL, error) { + client := http.DefaultClient + req, _ := http.NewRequest("HEAD", url, nil) + req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:59.0) Gecko/20100101 Firefox/59.0") + + res, err := client.Do(req) + if err != nil { + return "", err + } + + return res.Request.URL.String(), nil +} + // GetContentType is a core.IHttpClient interface implementation func (HttpClient) GetContentType(url core.URL) (string, error) { client := http.DefaultClient diff --git a/core/networking.go b/core/networking.go index 198a307..7619aba 100644 --- a/core/networking.go +++ b/core/networking.go @@ -19,4 +19,5 @@ type IImageDownloader interface { type IHttpClient interface { GetContentType(URL) (string, error) GetContent(URL) (string, error) + GetRedirectLocation(url URL) (URL, error) } From cfdae9eae03d3dcd933144f248d4287b98e30e4d Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Wed, 25 Aug 2021 11:27:41 +0300 Subject: [PATCH 191/439] Migrate to TikTok node/share/video api --- usecases/tiktok.go | 42 +++++++++++++++++++++++++++++++++++++ usecases/tiktok_flow.go | 46 ++++++++++++++++++++++++++++++++++++----- 2 files changed, 83 insertions(+), 5 deletions(-) create mode 100644 usecases/tiktok.go diff --git a/usecases/tiktok.go b/usecases/tiktok.go new file mode 100644 index 0000000..2567ff3 --- /dev/null +++ b/usecases/tiktok.go @@ -0,0 +1,42 @@ +package usecases + +type TikTokResponse struct { + StatusCode int + ItemInfo TikTokItemInfo +} + +type TikTokItemInfo struct { + ItemStruct TikTokItemStruct +} + +type TikTokItemStruct struct { + Desc string + Author TikTokAuthor + Music TikTokMusic + Video TikTokVideo + StickersOnItem []TikTokSticker +} + +type TikTokAuthor struct { + UniqueId string + Nickname string +} + +type TikTokMusic struct { + Id string + Title string + AuthorName string +} + +type TikTokVideo struct { + Id string + DownloadAddr string + ShareCover []string + Bitrate int + CodecType string +} + +type TikTokSticker struct { + StickerText []string + StickerType int +} diff --git a/usecases/tiktok_flow.go b/usecases/tiktok_flow.go index 380b8c2..b58f0f1 100644 --- a/usecases/tiktok_flow.go +++ b/usecases/tiktok_flow.go @@ -1,6 +1,7 @@ package usecases import ( + "encoding/json" "fmt" "regexp" @@ -37,18 +38,53 @@ func (ttf *TikTokFlow) HandleText(message *core.Message, bot core.IBot) error { func (ttf *TikTokFlow) handleURL(url string, message *core.Message, bot core.IBot) error { ttf.l.Info("processing ", url) - HTMLString, err := ttf.hc.GetContent(url) + fullURL, err := ttf.hc.GetRedirectLocation(url) if err != nil { return err } - media, err := ttf.mf.CreateMedia(HTMLString) + r := regexp.MustCompile(`tiktok\.com/(@\S+)/video/(\d+)`) + match := r.FindStringSubmatch(fullURL) + if len(match) != 3 { + ttf.l.Error(match) + return fmt.Errorf("unexpected redirect location %s", fullURL) + } + + apiURL := "https://www.tiktok.com/node/share/video/" + match[1] + "/" + match[2] + jsonString, err := ttf.hc.GetContent(apiURL) if err != nil { return err } - for _, m := range media { - m.Caption = fmt.Sprintf("🎵 %s (by %s)\n%s", m.URL, m.Title, message.Sender.Username, m.Description) + var resp TikTokResponse + err = json.Unmarshal([]byte(jsonString), &resp) + if err != nil { + return err + } + + if resp.StatusCode != 0 { + ttf.l.Error(jsonString) + return fmt.Errorf("%d not equal to zero", resp.StatusCode) + } + + title := resp.ItemInfo.ItemStruct.Desc + if len(title) == 0 { + title = fmt.Sprintf("%s (@%s)", resp.ItemInfo.ItemStruct.Author.Nickname, resp.ItemInfo.ItemStruct.Author.UniqueId) + } + + description := fmt.Sprintf("%s (@%s) has created a short video on TikTok with music %s.", resp.ItemInfo.ItemStruct.Author.Nickname, resp.ItemInfo.ItemStruct.Author.UniqueId, resp.ItemInfo.ItemStruct.Music.Title) + for _, s := range resp.ItemInfo.ItemStruct.StickersOnItem { + for _, t := range s.StickerText { + description = description + " | " + t + } + } + + media := &core.Media{ + URL: url, + ResourceURL: resp.ItemInfo.ItemStruct.Video.DownloadAddr, + Title: title, + Description: description, } - return ttf.sms.SendMedia(media, bot) + media.Caption = fmt.Sprintf("🎵 %s (by %s)\n%s", url, title, message.Sender.Username, description) + return ttf.sms.SendMedia([]*core.Media{media}, bot) } From d985d51af8999ba0dcce77e8fa77d62d2ffc36aa Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Wed, 25 Aug 2021 11:32:11 +0300 Subject: [PATCH 192/439] Get rid of opengraph parser --- pullanusbot.go | 3 +-- usecases/tiktok_flow.go | 5 ++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/pullanusbot.go b/pullanusbot.go index 6201dea..ede557c 100644 --- a/pullanusbot.go +++ b/pullanusbot.go @@ -52,8 +52,7 @@ func main() { linkFlow := usecases.CreateLinkFlow(logger, httpClient, converter, convertMediaSender) telebot.AddHandler(linkFlow) - openGraphParser := api.CreateOpenGraphParser(logger) - tiktokFlow := usecases.CreateTikTokFlow(logger, httpClient, openGraphParser, localMediaSender) + tiktokFlow := usecases.CreateTikTokFlow(logger, httpClient, localMediaSender) telebot.AddHandler(tiktokFlow) fileUploader := api.CreateTelegraphAPI() diff --git a/usecases/tiktok_flow.go b/usecases/tiktok_flow.go index b58f0f1..f21a4f7 100644 --- a/usecases/tiktok_flow.go +++ b/usecases/tiktok_flow.go @@ -8,14 +8,13 @@ import ( "github.com/ailinykh/pullanusbot/v2/core" ) -func CreateTikTokFlow(l core.ILogger, hc core.IHttpClient, mf core.IMediaFactory, sms core.ISendMediaStrategy) *TikTokFlow { - return &TikTokFlow{l, hc, mf, sms} +func CreateTikTokFlow(l core.ILogger, hc core.IHttpClient, sms core.ISendMediaStrategy) *TikTokFlow { + return &TikTokFlow{l, hc, sms} } type TikTokFlow struct { l core.ILogger hc core.IHttpClient - mf core.IMediaFactory sms core.ISendMediaStrategy } From e7f7ae1be7ab71bf4e71220ddf4f5aacff8ee7cd Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Wed, 25 Aug 2021 13:40:00 +0300 Subject: [PATCH 193/439] log error right in place --- usecases/youtube_flow.go | 1 + 1 file changed, 1 insertion(+) diff --git a/usecases/youtube_flow.go b/usecases/youtube_flow.go index a13ecb1..cca121f 100644 --- a/usecases/youtube_flow.go +++ b/usecases/youtube_flow.go @@ -45,6 +45,7 @@ func (f *YoutubeFlow) process(id string, message *core.Message, bot core.IBot) e f.l.Infof("processing %s", id) media, err := f.mf.CreateMedia(id) if err != nil { + f.l.Error(err) return err } From 548af7aa0ce99ffd9296fd3932dfb58986344376 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Wed, 25 Aug 2021 13:40:57 +0300 Subject: [PATCH 194/439] Retreive youtube thumbnails from ffmpeg converting --- api/youtube_api.go | 60 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 51 insertions(+), 9 deletions(-) diff --git a/api/youtube_api.go b/api/youtube_api.go index 6ed9741..18c4a2e 100644 --- a/api/youtube_api.go +++ b/api/youtube_api.go @@ -68,10 +68,7 @@ func (y *YoutubeAPI) CreateVideo(id string) (*core.Video, error) { return nil, errors.New(string(out)) } - thumb := video.thumb() - thumbPath := path.Join(os.TempDir(), name+".jpg") - y.l.Infof("downloading thumb %s", thumb.URL) - file, err := y.fd.Download(thumb.URL, thumbPath) // will be disposed with Video + thumb, err := y.getThumbV2(video, vf) if err != nil { return nil, err } @@ -83,11 +80,7 @@ func (y *YoutubeAPI) CreateVideo(id string) (*core.Video, error) { Bitrate: 0, Duration: video.Duration, Codec: vf.VCodec, - Thumb: &core.Image{ - File: *file, - Width: thumb.Width, - Height: thumb.Height, - }, + Thumb: thumb, }, nil } @@ -127,3 +120,52 @@ func (y *YoutubeAPI) getFormats(video *Video) (*Format, *Format, error) { return vf, af, nil } + +func (y *YoutubeAPI) getThumb(video *Video, vf *Format) (*core.Image, error) { + thumb := video.thumb() + filename := fmt.Sprintf("youtube-%s-%s.jpg", video.ID, vf.FormatID) + thumbPath := path.Join(os.TempDir(), filename) + y.l.Infof("downloading thumb %s", thumb.URL) + file, err := y.fd.Download(thumb.URL, thumbPath) + if err != nil { + return nil, err + } + return &core.Image{ + File: *file, + Width: thumb.Width, + Height: thumb.Height, + }, nil +} + +func (y *YoutubeAPI) getThumbV2(video *Video, vf *Format) (*core.Image, error) { + filename := fmt.Sprintf("youtube-%s-%s-maxres.jpg", video.ID, vf.FormatID) + originalThumbPath := path.Join(os.TempDir(), filename+"-original") + thumbPath := path.Join(os.TempDir(), filename) + y.l.Infof("downloading thumb %s", video.Thumbnail) + file, err := y.fd.Download(video.Thumbnail, originalThumbPath) + if err != nil { + y.l.Error(err) + return y.getThumb(video, vf) + } + defer file.Dispose() + + cmd := fmt.Sprintf(`ffmpeg -v error -y -i "%s" -vf scale=%d:%d "%s"`, originalThumbPath, vf.Width, vf.Height, thumbPath) + out, err := exec.Command("/bin/sh", "-c", cmd).CombinedOutput() + if err != nil { + y.l.Error(err) + y.l.Error(string(out)) + return y.getThumb(video, vf) + } + + stat, err := os.Stat(thumbPath) + if err != nil { + y.l.Error(err) + return y.getThumb(video, vf) + } + + return &core.Image{ + File: core.File{Name: filename, Path: thumbPath, Size: stat.Size()}, + Width: vf.Width, + Height: vf.Height, + }, nil +} From 3618de3473c3493c6b214ddce98f251d848cd210 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Wed, 25 Aug 2021 23:41:48 +0300 Subject: [PATCH 195/439] Add ability to check that user is member of the chat --- api/telebot_adapter.go | 12 ++++++++++++ api/telebot_factory.go | 9 +++++++++ core/bot.go | 1 + test_helpers/fake_bot.go | 12 +++++++++++- 4 files changed, 33 insertions(+), 1 deletion(-) diff --git a/api/telebot_adapter.go b/api/telebot_adapter.go index f3356b4..64f4910 100644 --- a/api/telebot_adapter.go +++ b/api/telebot_adapter.go @@ -121,3 +121,15 @@ func (a *TelebotAdapter) SendVideo(vf *core.Video, caption string) (*core.Messag a.t.logger.Infof("%s successfully sent", vf.Name) return makeMessage(sent), err } + +// IsUserMemberOfChat is a core.IBot interface implementation +func (a *TelebotAdapter) IsUserMemberOfChat(user *core.User, chatID int64) bool { + chat := &tb.Chat{ID: chatID} + member, err := a.t.bot.ChatMemberOf(chat, makeTbUser(user)) + if err != nil { + a.t.logger.Error(err, member) + } + return member == nil || + member.Role == tb.Left || + member.Role == tb.Kicked +} diff --git a/api/telebot_factory.go b/api/telebot_factory.go index 7cbffe6..0f14769 100644 --- a/api/telebot_factory.go +++ b/api/telebot_factory.go @@ -34,3 +34,12 @@ func makeTbPhoto(image *core.Image, caption string) *tb.Photo { photo.Caption = caption return photo } + +func makeTbUser(user *core.User) *tb.User { + return &tb.User{ + ID: user.ID, + FirstName: user.FirstName, + LastName: user.LastName, + Username: user.Username, + } +} diff --git a/core/bot.go b/core/bot.go index f9bfd26..d2d6275 100644 --- a/core/bot.go +++ b/core/bot.go @@ -9,4 +9,5 @@ type IBot interface { SendMedia(*Media) (*Message, error) SendPhotoAlbum([]*Media) ([]*Message, error) SendVideo(*Video, string) (*Message, error) + IsUserMemberOfChat(*User, int64) bool } diff --git a/test_helpers/fake_bot.go b/test_helpers/fake_bot.go index d2cdd4b..98c5da2 100644 --- a/test_helpers/fake_bot.go +++ b/test_helpers/fake_bot.go @@ -5,7 +5,7 @@ import "github.com/ailinykh/pullanusbot/v2/core" // https://stackoverflow.com/questions/31794141/can-i-create-shared-test-utilities func CreateFakeBot() *FakeBot { - return &FakeBot{[]string{}, []string{}, []string{}, []string{}} + return &FakeBot{[]string{}, []string{}, []string{}, []string{}, map[int64][]string{}} } type FakeBot struct { @@ -13,6 +13,7 @@ type FakeBot struct { SentMessages []string SentVideos []string RemovedMessages []string + ChatMembers map[int64][]string } func (FakeBot) SendImage(*core.Image, string) (*core.Message, error) { return nil, nil } @@ -44,3 +45,12 @@ func (b *FakeBot) SendText(text string, args ...interface{}) (*core.Message, err b.SentMessages = append(b.SentMessages, text) return nil, nil } + +func (b *FakeBot) IsUserMemberOfChat(user *core.User, chatID int64) bool { + for _, username := range b.ChatMembers[chatID] { + if username == user.Username { + return true + } + } + return false +} From 167fedcd10e755f9a5d2a2ba0fc297b791015081 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Thu, 26 Aug 2021 08:49:03 +0300 Subject: [PATCH 196/439] Add test for checking winner left the chat --- usecases/faggot_game.go | 6 ++++++ usecases/faggot_game_test.go | 18 +++++++++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/usecases/faggot_game.go b/usecases/faggot_game.go index 6517055..823aa50 100644 --- a/usecases/faggot_game.go +++ b/usecases/faggot_game.go @@ -90,6 +90,12 @@ func (flow *GameFlow) Play(message *core.Message, bot core.IBot) error { } winner := players[rand.Intn(len(players))] + + if !bot.IsUserMemberOfChat(winner, message.ChatID) { + _, err := bot.SendText(flow.l.I18n("faggot_winner_left")) + return err + } + round := &core.Round{Day: day, Winner: winner} flow.s.AddRound(message.ChatID, round) diff --git a/usecases/faggot_game_test.go b/usecases/faggot_game_test.go index 8c59c3e..d40fed9 100644 --- a/usecases/faggot_game_test.go +++ b/usecases/faggot_game_test.go @@ -86,6 +86,7 @@ func Test_Play_RespondsWithCurrentGameResult(t *testing.T) { "faggot_game_2_0": "2", "faggot_game_3_0": "%s", }) + bot.ChatMembers[0] = []string{""} m1 := makeGameMessage(1, "") m2 := makeGameMessage(2, "") @@ -108,6 +109,7 @@ func Test_Play_RespondsWinnerAlreadyKnown(t *testing.T) { "faggot_game_3_0": "3 %s", "faggot_winner_known": "Winner already known %s", }) + bot.ChatMembers[0] = []string{"Faggot1", "Faggot2"} m1 := makeGameMessage(1, "Faggot1") m2 := makeGameMessage(2, "Faggot2") @@ -126,6 +128,20 @@ func Test_Play_RespondsWinnerAlreadyKnown(t *testing.T) { assert.Equal(t, fmt.Sprintf("Winner already known %s", winner), bot.SentMessages[6]) } +func Test_Play_RespondsWinnerLeftTheChat(t *testing.T) { + game, bot, storage := makeSUT(LocalizerDict{ + "faggot_winner_left": "winner left", + }) + m1 := makeGameMessage(1, "Faggot1") + m2 := makeGameMessage(2, "Faggot2") + storage.players = []*core.User{m1.Sender, m2.Sender} + + game.Play(m1, bot) + + assert.Equal(t, []*core.Round{}, storage.rounds) + assert.Equal(t, []string{"winner left"}, bot.SentMessages) +} + func Test_Stats_RespondsWithDescendingResultsForCurrentYear(t *testing.T) { year := strconv.Itoa(time.Now().Year()) game, bot, storage := makeSUT(LocalizerDict{ @@ -232,7 +248,7 @@ func makeGameMessage(id int, username string) *core.Message { func makeSUT(args ...interface{}) (*usecases.GameFlow, *test_helpers.FakeBot, *GameStorageMock) { dict := LocalizerDict{} - storage := &GameStorageMock{players: []*core.User{}} + storage := &GameStorageMock{players: []*core.User{}, rounds: []*core.Round{}} bot := test_helpers.CreateFakeBot() for _, arg := range args { From 4498432594d7c8d6167455842e845212782351b5 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Thu, 26 Aug 2021 09:24:00 +0300 Subject: [PATCH 197/439] Move Player and Round infrastructure models into one file --- infrastructure/{player.go => game.go} | 13 +++++++++++++ infrastructure/round.go | 14 -------------- 2 files changed, 13 insertions(+), 14 deletions(-) rename infrastructure/{player.go => game.go} (59%) delete mode 100644 infrastructure/round.go diff --git a/infrastructure/player.go b/infrastructure/game.go similarity index 59% rename from infrastructure/player.go rename to infrastructure/game.go index 87f4b4b..c2e122a 100644 --- a/infrastructure/player.go +++ b/infrastructure/game.go @@ -14,3 +14,16 @@ type Player struct { func (Player) TableName() string { return "faggot_players" } + +// Round that can be persistent on disk +type Round struct { + GameID int64 + UserID int + Day string `gorm:"primaryKey"` + Username string +} + +// TableName gorm API +func (Round) TableName() string { + return "faggot_rounds" +} diff --git a/infrastructure/round.go b/infrastructure/round.go deleted file mode 100644 index 8e484a4..0000000 --- a/infrastructure/round.go +++ /dev/null @@ -1,14 +0,0 @@ -package infrastructure - -// Round that can be persistent on disk -type Round struct { - GameID int64 - UserID int - Day string `gorm:"primaryKey"` - Username string -} - -// TableName gorm API -func (Round) TableName() string { - return "faggot_rounds" -} From ce988fe881e29839e6d57e0a5cb7022ba2cf1933 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Fri, 27 Aug 2021 19:07:53 +0300 Subject: [PATCH 198/439] Add UpdatePlayer method to IGameStorage protocol --- core/game_storage.go | 1 + infrastructure/game_storage.go | 11 ++++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/core/game_storage.go b/core/game_storage.go index e2fa271..8386978 100644 --- a/core/game_storage.go +++ b/core/game_storage.go @@ -5,5 +5,6 @@ type IGameStorage interface { GetPlayers(int64) ([]*User, error) GetRounds(int64) ([]*Round, error) AddPlayer(int64, *User) error + UpdatePlayer(int64, *User) error AddRound(int64, *Round) error } diff --git a/infrastructure/game_storage.go b/infrastructure/game_storage.go index 27a481a..2ff0951 100644 --- a/infrastructure/game_storage.go +++ b/infrastructure/game_storage.go @@ -73,11 +73,20 @@ func (s *GameStorage) AddPlayer(gameID int64, user *core.User) error { Username: user.Username, LanguageCode: user.LanguageCode, } - player.GameID = gameID s.conn.Create(&player) return nil } +// AddPlayer is a core.IGameStorage interface implementation +func (s *GameStorage) UpdatePlayer(gameID int64, user *core.User) error { + player := Player{ + GameID: gameID, + UserID: user.ID, + } + s.conn.Model(&player).Updates(Player{FirstName: user.FirstName, LastName: user.LastName, Username: user.Username}) + return nil +} + // AddRound is a core.IGameStorage interface implementation func (s *GameStorage) AddRound(gameID int64, round *core.Round) error { dbRound := Round{ From b10d1d7e12f79efde941fccb47260f1164154ebf Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Fri, 27 Aug 2021 19:08:42 +0300 Subject: [PATCH 199/439] Add ability to update player info using /pidoreg command --- infrastructure/game_localizer.go | 1 + usecases/faggot_game.go | 5 +++++ usecases/faggot_game_test.go | 24 ++++++++++++++++++++++++ 3 files changed, 30 insertions(+) diff --git a/infrastructure/game_localizer.go b/infrastructure/game_localizer.go index 9774023..bcf3629 100644 --- a/infrastructure/game_localizer.go +++ b/infrastructure/game_localizer.go @@ -24,6 +24,7 @@ var ru = map[string]string{ "faggot_not_available_for_private": "Извините, данная команда недоступна в личных чатах.", "faggot_added_to_game": "Ты в игре!", + "faggot_info_updated": "Профиль игрока обновлён!", "faggot_already_in_game": "Эй! Ты уже в игре!", "faggot_no_players": "Зарегистрированных в игру еще нет, а значит пидор ты - %s", "faggot_not_enough_players": "Нужно как минимум два игрока, чтобы начать игру! Зарегистрируйся используя /pidoreg", diff --git a/usecases/faggot_game.go b/usecases/faggot_game.go index 823aa50..18aab4f 100644 --- a/usecases/faggot_game.go +++ b/usecases/faggot_game.go @@ -43,6 +43,11 @@ func (flow *GameFlow) Add(message *core.Message, bot core.IBot) error { players, _ := flow.s.GetPlayers(message.ChatID) for _, p := range players { if p.ID == message.Sender.ID { + if p.FirstName != message.Sender.FirstName || p.LastName != message.Sender.LastName || p.Username != message.Sender.Username { + _ = flow.s.UpdatePlayer(message.ChatID, message.Sender) + _, err := bot.SendText(flow.l.I18n("faggot_info_updated")) + return err + } _, err := bot.SendText(flow.l.I18n("faggot_already_in_game")) return err } diff --git a/usecases/faggot_game_test.go b/usecases/faggot_game_test.go index d40fed9..b2859d0 100644 --- a/usecases/faggot_game_test.go +++ b/usecases/faggot_game_test.go @@ -38,6 +38,19 @@ func Test_RulesCommand_DeliversRules(t *testing.T) { assert.Equal(t, "Game rules:", bot.SentMessages[0]) } +func Test_Add_ChecksAndReplacesPlayerInfoIfNeeded(t *testing.T) { + game, bot, storage := makeSUT() + message := makeGameMessage(1, "Faggot") + player := *message.Sender + player.Username = "old_username" + storage.players = []*core.User{&player} + + game.Add(message, bot) + + assert.Equal(t, []*core.User{message.Sender}, storage.players) + assert.Equal(t, []string{"faggot_info_updated"}, bot.SentMessages) +} + func Test_Add_AppendsPlayerInGameOnlyOnce(t *testing.T) { game, bot, storage := makeSUT(LocalizerDict{ "faggot_added_to_game": "Player added", @@ -298,6 +311,17 @@ func (s *GameStorageMock) AddPlayer(gameID int64, player *core.User) error { return nil } +func (s *GameStorageMock) UpdatePlayer(gameID int64, user *core.User) error { + for _, p := range s.players { + if p.ID == user.ID { + p.FirstName = user.FirstName + p.LastName = user.LastName + p.Username = user.Username + } + } + return nil +} + func (s *GameStorageMock) GetPlayers(gameID int64) ([]*core.User, error) { return s.players, nil } From 5f61341dd1c2a44ba07d702ff69062ebd066386b Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Fri, 27 Aug 2021 20:34:05 +0300 Subject: [PATCH 200/439] Do not use Username as primary key --- infrastructure/game_storage.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infrastructure/game_storage.go b/infrastructure/game_storage.go index 2ff0951..aa8834e 100644 --- a/infrastructure/game_storage.go +++ b/infrastructure/game_storage.go @@ -54,7 +54,7 @@ func (s *GameStorage) GetRounds(gameID int64) ([]*core.Round, error) { s.conn.Where("game_id = ?", gameID).Find(&dbRounds) for _, r := range dbRounds { for _, p := range players { - if p.Username == r.Username { + if p.ID == r.UserID { coreRounds = append(coreRounds, &core.Round{Day: r.Day, Winner: p}) break } From df6aa880fd3c983a04f8f300b81150aea255253a Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Sat, 28 Aug 2021 00:28:16 +0300 Subject: [PATCH 201/439] go get -u; go mod tidy --- go.mod | 10 +++++----- go.sum | 2 -- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index 0dd28a7..d2ff831 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,9 @@ module github.com/ailinykh/pullanusbot/v2 go 1.16 require ( - github.com/google/logger v1.1.0 - github.com/stretchr/testify v1.7.0 - gopkg.in/tucnak/telebot.v2 v2.3.4 - gorm.io/driver/sqlite v1.1.3 + github.com/google/logger v1.1.0 + github.com/stretchr/testify v1.7.0 + gopkg.in/tucnak/telebot.v2 v2.3.4 + gorm.io/driver/sqlite v1.1.3 gorm.io/gorm v1.20.2 -) \ No newline at end of file +) diff --git a/go.sum b/go.sum index 87bff63..9c7956f 100644 --- a/go.sum +++ b/go.sum @@ -12,7 +12,6 @@ github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= @@ -23,7 +22,6 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/tucnak/telebot.v2 v2.3.4 h1:LtZ1hahdWDYFX723PlkLDMo56p99uMzrvBL9BRhyNy4= gopkg.in/tucnak/telebot.v2 v2.3.4/go.mod h1:t+KVAiqFsG9ZDF0hz1ZPFTyENtlrDrDS3qmRRqhICBg= -gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From 0c98977a90eccf0dd8b44cade200bb5f2ff83074 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Sat, 28 Aug 2021 00:28:47 +0300 Subject: [PATCH 202/439] Do not call makeMessage before error check --- api/telebot_adapter.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/api/telebot_adapter.go b/api/telebot_adapter.go index 64f4910..1907794 100644 --- a/api/telebot_adapter.go +++ b/api/telebot_adapter.go @@ -25,6 +25,9 @@ func (a *TelebotAdapter) SendText(text string, params ...interface{}) (*core.Mes } } sent, err := a.t.bot.Send(a.m.Chat, text, &opts) + if err != nil { + return nil, err + } return makeMessage(sent), err } @@ -52,6 +55,10 @@ func (a *TelebotAdapter) SendAlbum(images []*core.Image) ([]*core.Message, error } sent, err := a.t.bot.SendAlbum(a.m.Chat, album) + if err != nil { + return nil, err + } + var messages []*core.Message for _, m := range sent { messages = append(messages, makeMessage(&m)) @@ -103,6 +110,10 @@ func (a *TelebotAdapter) SendPhotoAlbum(medias []*core.Media) ([]*core.Message, } sent, err := a.t.bot.SendAlbum(a.m.Chat, album) + if err != nil { + return nil, err + } + var messages []*core.Message for _, m := range sent { messages = append(messages, makeMessage(&m)) From 140bdb902e64cd126dd1818e7fd446f250215910 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Sat, 28 Aug 2021 08:07:12 +0300 Subject: [PATCH 203/439] Fix IsUserMemberOfChat telebot implementation --- api/telebot_adapter.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/api/telebot_adapter.go b/api/telebot_adapter.go index 1907794..6e3dcfa 100644 --- a/api/telebot_adapter.go +++ b/api/telebot_adapter.go @@ -140,7 +140,7 @@ func (a *TelebotAdapter) IsUserMemberOfChat(user *core.User, chatID int64) bool if err != nil { a.t.logger.Error(err, member) } - return member == nil || - member.Role == tb.Left || - member.Role == tb.Kicked + return member != nil && + member.Role != tb.Left && + member.Role != tb.Kicked } From f808d97b431070bfd9a39174d7ff56a123fbfcf2 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Wed, 1 Sep 2021 09:52:04 +0300 Subject: [PATCH 204/439] Add ability to set header for any http reuest --- api/http_client.go | 32 ++++++++++++++++++++++++-------- core/networking.go | 1 + 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/api/http_client.go b/api/http_client.go index 65ea0d5..8fedc11 100644 --- a/api/http_client.go +++ b/api/http_client.go @@ -9,15 +9,20 @@ import ( ) func CreateHttpClient() *HttpClient { - return &HttpClient{} + return &HttpClient{map[string]string{"User-Agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:59.0) Gecko/20100101 Firefox/59.0"}} } -type HttpClient struct{} +type HttpClient struct { + headers map[string]string +} -func (HttpClient) GetRedirectLocation(url core.URL) (core.URL, error) { +func (c *HttpClient) GetRedirectLocation(url core.URL) (core.URL, error) { client := http.DefaultClient req, _ := http.NewRequest("HEAD", url, nil) - req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:59.0) Gecko/20100101 Firefox/59.0") + + for k, v := range c.headers { + req.Header.Set(k, v) + } res, err := client.Do(req) if err != nil { @@ -28,10 +33,13 @@ func (HttpClient) GetRedirectLocation(url core.URL) (core.URL, error) { } // GetContentType is a core.IHttpClient interface implementation -func (HttpClient) GetContentType(url core.URL) (string, error) { +func (c *HttpClient) GetContentType(url core.URL) (string, error) { client := http.DefaultClient req, _ := http.NewRequest("HEAD", url, nil) - req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:59.0) Gecko/20100101 Firefox/59.0") + + for k, v := range c.headers { + req.Header.Set(k, v) + } res, err := client.Do(req) if err != nil { @@ -45,10 +53,13 @@ func (HttpClient) GetContentType(url core.URL) (string, error) { } // GetContent is a core.IHttpClient interface implementation -func (HttpClient) GetContent(url core.URL) (string, error) { +func (c *HttpClient) GetContent(url core.URL) (string, error) { client := http.DefaultClient req, _ := http.NewRequest("GET", url, nil) - req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:59.0) Gecko/20100101 Firefox/59.0") + + for k, v := range c.headers { + req.Header.Set(k, v) + } res, err := client.Do(req) if err != nil { @@ -63,3 +74,8 @@ func (HttpClient) GetContent(url core.URL) (string, error) { return string(body), nil } + +// SetHeader remembers all passed values and applies it to every request +func (c *HttpClient) SetHeader(key string, value string) { + c.headers[key] = value +} diff --git a/core/networking.go b/core/networking.go index 7619aba..83d6648 100644 --- a/core/networking.go +++ b/core/networking.go @@ -20,4 +20,5 @@ type IHttpClient interface { GetContentType(URL) (string, error) GetContent(URL) (string, error) GetRedirectLocation(url URL) (URL, error) + SetHeader(string, string) } From 6745f8472bbf98d3693a99cb0f7a2a02cd20b25d Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Wed, 1 Sep 2021 10:06:56 +0300 Subject: [PATCH 205/439] Add .mp4 suffix to upload media strategy file if not present --- helpers/upload_media_strategy.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/helpers/upload_media_strategy.go b/helpers/upload_media_strategy.go index ddab9d3..0b63d26 100644 --- a/helpers/upload_media_strategy.go +++ b/helpers/upload_media_strategy.go @@ -66,6 +66,11 @@ func (ums *UploadMediaStrategy) downloadMedia(media *core.Media) (*core.File, er parts := strings.Split(media.ResourceURL, "?") filename = path.Base(parts[0]) } + + if !strings.HasSuffix(filename, ".mp4") { + filename = filename + ".mp4" + } + mediaPath := path.Join(os.TempDir(), filename) file, err := ums.fd.Download(media.ResourceURL, mediaPath) if err != nil { From fc197db6a3504978eb92e77abf8fcb94ea1a2816 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Wed, 1 Sep 2021 10:07:41 +0300 Subject: [PATCH 206/439] TikTok flow updated for HTML parsing --- usecases/tiktok.go | 8 ++++++++ usecases/tiktok_flow.go | 45 ++++++++++++++++++++++++++++++----------- 2 files changed, 41 insertions(+), 12 deletions(-) diff --git a/usecases/tiktok.go b/usecases/tiktok.go index 2567ff3..29f4c2d 100644 --- a/usecases/tiktok.go +++ b/usecases/tiktok.go @@ -1,5 +1,13 @@ package usecases +type TikTokHTMLResponse struct { + Props TikTokHTMLProps +} + +type TikTokHTMLProps struct { + PageProps TikTokResponse +} + type TikTokResponse struct { StatusCode int ItemInfo TikTokItemInfo diff --git a/usecases/tiktok_flow.go b/usecases/tiktok_flow.go index f21a4f7..b221ba3 100644 --- a/usecases/tiktok_flow.go +++ b/usecases/tiktok_flow.go @@ -3,12 +3,15 @@ package usecases import ( "encoding/json" "fmt" + "math/rand" "regexp" + "strconv" "github.com/ailinykh/pullanusbot/v2/core" ) func CreateTikTokFlow(l core.ILogger, hc core.IHttpClient, sms core.ISendMediaStrategy) *TikTokFlow { + hc.SetHeader("Referrer", "https://www.tiktok.com/") return &TikTokFlow{l, hc, sms} } @@ -49,30 +52,48 @@ func (ttf *TikTokFlow) handleURL(url string, message *core.Message, bot core.IBo return fmt.Errorf("unexpected redirect location %s", fullURL) } - apiURL := "https://www.tiktok.com/node/share/video/" + match[1] + "/" + match[2] - jsonString, err := ttf.hc.GetContent(apiURL) + // apiURL := "https://www.tiktok.com/node/share/video/" + match[1] + "/" + match[2] + apiURL := "https://www.tiktok.com/" + match[1] + "/video/" + match[2] + ttf.l.Info(apiURL) + + getRand := func(count int) string { + rv := "" + for i := 1; i < count; i++ { + rv = rv + strconv.Itoa(rand.Intn(10)) + } + return rv + } + ttf.hc.SetHeader("Cookie", "tt_webid_v2=69"+getRand(17)+"; Domain=tiktok.com; Path=/; Secure; hostOnly=false; hostOnly=false; aAge=4ms; cAge=4ms") + htmlString, err := ttf.hc.GetContent(apiURL) if err != nil { return err } - var resp TikTokResponse - err = json.Unmarshal([]byte(jsonString), &resp) + r = regexp.MustCompile(``) + match := r.FindSubmatch(body) + if len(match) < 1 { + api.l.Error(match) + return nil, fmt.Errorf("unexpected html") + } + + var reel IgReel + err = json.Unmarshal([]byte(match[1]), &reel) + if err != nil { + api.l.Error(err) + return nil, err + } + + return &reel, nil +} + +type IgReel struct { + Items []IgReelItem +} + +type IgReelUser struct { + Username string + FullName string `json:"full_name"` +} + +type IgReelItem struct { + Code string + User IgReelUser + Caption IgReelCaption + VideoVersions []IgReelVideo `json:"video_versions"` +} + +type IgReelVideo struct { + Width int + Height int + URL string +} + +type IgReelCaption struct { + Text string +} diff --git a/api/instagram_media_factory.go b/api/instagram_media_factory.go new file mode 100644 index 0000000..91cd18d --- /dev/null +++ b/api/instagram_media_factory.go @@ -0,0 +1,32 @@ +package api + +import ( + "fmt" + + "github.com/ailinykh/pullanusbot/v2/core" +) + +func CreateInstagramMediaFactory(l core.ILogger, cookiesFile string) *InstagramMediaFactory { + return &InstagramMediaFactory{l, CreateInstagramAPI(l, cookiesFile)} +} + +type InstagramMediaFactory struct { + l core.ILogger + api *InstagramAPI +} + +// CreateMedia is a core.IMediaFactory interface implementation +func (factory *InstagramMediaFactory) CreateMedia(url string) ([]*core.Media, error) { + reel, err := factory.api.GetReel(url) + if err != nil { + factory.l.Error(err) + return nil, err + } + + if len(reel.Items) < 1 { + return nil, fmt.Errorf("insufficient reel items") + } + + item := reel.Items[0] + return []*core.Media{{ResourceURL: item.VideoVersions[0].URL, URL: "https://www.instagram.com/reel/" + item.Code + "/", Title: item.User.FullName, Caption: item.Caption.Text}}, nil +} diff --git a/pullanusbot.go b/pullanusbot.go index 3fc9df4..11cd936 100644 --- a/pullanusbot.go +++ b/pullanusbot.go @@ -88,6 +88,11 @@ func main() { iDoNotCare := usecases.CreateIDoNotCare() telebot.AddHandler(iDoNotCare) + + reelsAPI := api.CreateInstagramMediaFactory(logger, path.Join(getWorkingDir(), "cookies.json")) + reelsFlow := usecases.CreateReelsFlow(logger, reelsAPI, localMediaSender) + telebot.AddHandler(reelsFlow) + // Start endless loop telebot.Run() } diff --git a/usecases/reels_flow.go b/usecases/reels_flow.go new file mode 100644 index 0000000..3df8e95 --- /dev/null +++ b/usecases/reels_flow.go @@ -0,0 +1,46 @@ +package usecases + +import ( + "fmt" + "regexp" + + "github.com/ailinykh/pullanusbot/v2/core" +) + +func CreateReelsFlow(l core.ILogger, mediaFactory core.IMediaFactory, sendMediaStrategy core.ISendMediaStrategy) core.ITextHandler { + return &ReelsFlow{l, mediaFactory, sendMediaStrategy} +} + +type ReelsFlow struct { + l core.ILogger + mediaFactory core.IMediaFactory + sendMediaStrategy core.ISendMediaStrategy +} + +// HandleText is a core.ITextHandler protocol implementation +func (flow *ReelsFlow) HandleText(message *core.Message, bot core.IBot) error { + flow.l.Infof("%+v\n", message) + r := regexp.MustCompile(`https://www.instagram.com/reel/\S+`) + match := r.FindAllStringSubmatch(message.Text, -1) + + if len(match) < 1 { + return fmt.Errorf("not implemented") + } + + media, err := flow.mediaFactory.CreateMedia(match[0][0]) + if err != nil { + flow.l.Error(err) + return err + } + + if len(media) < 1 { + return fmt.Errorf("unexpected count of media") + } + + m := &core.Media{ + ResourceURL: media[0].ResourceURL, + Caption: fmt.Sprintf("📷 %s (by %s)\n%s", match[0][0], media[0].Title, message.Sender.DisplayName(), media[0].Caption), + } + + return flow.sendMediaStrategy.SendMedia([]*core.Media{m}, bot) +} From 7bea9c3124fe7a76570f2b075c41e2d3df2fb997 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Tue, 15 Mar 2022 14:21:09 +0300 Subject: [PATCH 300/439] Add remove source decorator for instagram reels flow --- pullanusbot.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pullanusbot.go b/pullanusbot.go index 11cd936..fd4cb38 100644 --- a/pullanusbot.go +++ b/pullanusbot.go @@ -91,7 +91,8 @@ func main() { reelsAPI := api.CreateInstagramMediaFactory(logger, path.Join(getWorkingDir(), "cookies.json")) reelsFlow := usecases.CreateReelsFlow(logger, reelsAPI, localMediaSender) - telebot.AddHandler(reelsFlow) + removeReelsSourceDecorator := usecases.CreateRemoveSourceDecorator(logger, reelsFlow, settingsStorage) + telebot.AddHandler(removeReelsSourceDecorator) // Start endless loop telebot.Run() From 517992606758a6d14febcbb713572682165ce1a4 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Tue, 15 Mar 2022 14:21:23 +0300 Subject: [PATCH 301/439] Remove redundant log message --- usecases/reels_flow.go | 1 - 1 file changed, 1 deletion(-) diff --git a/usecases/reels_flow.go b/usecases/reels_flow.go index 3df8e95..3081021 100644 --- a/usecases/reels_flow.go +++ b/usecases/reels_flow.go @@ -19,7 +19,6 @@ type ReelsFlow struct { // HandleText is a core.ITextHandler protocol implementation func (flow *ReelsFlow) HandleText(message *core.Message, bot core.IBot) error { - flow.l.Infof("%+v\n", message) r := regexp.MustCompile(`https://www.instagram.com/reel/\S+`) match := r.FindAllStringSubmatch(message.Text, -1) From f37286a9d24095d6f9f97a09ed6bce5e592cf56c Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Wed, 16 Mar 2022 11:17:00 +0300 Subject: [PATCH 302/439] Fix incorrect settings storage data saved --- infrastructure/settings_storage.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/infrastructure/settings_storage.go b/infrastructure/settings_storage.go index 2a318d6..613bfe9 100644 --- a/infrastructure/settings_storage.go +++ b/infrastructure/settings_storage.go @@ -60,15 +60,16 @@ func (s *SettingsStorage) SetSettings(chatID int64, settings *core.Settings) err return err } - sett := &Settings{ChatID: chatID, Data: data} + sett := &Settings{ChatID: chatID} res := s.conn.First(&sett, chatID) + sett.Data = data if errors.Is(res.Error, gorm.ErrRecordNotFound) { s.l.Infof("creating settings %d %s", chatID, string(data)) - res = s.conn.Create(&settings) + res = s.conn.Create(&sett) } else { s.l.Infof("updating settings %d %s", chatID, string(data)) - res = s.conn.Save(&settings) + res = s.conn.Save(&sett) } return res.Error From 92ae359ba4a2febbaa231307cc1ba4a4120ae96d Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Wed, 16 Mar 2022 11:17:33 +0300 Subject: [PATCH 303/439] Extend default settings with payload array of strings --- core/settings.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/core/settings.go b/core/settings.go index c8f9fe4..93b0c09 100644 --- a/core/settings.go +++ b/core/settings.go @@ -1,12 +1,13 @@ package core type Settings struct { - FaggotGameCommandsEnabled bool `json:"faggot_game_enabled"` - RemoveSourceOnSucccess bool `json:"remove_source_on_success"` + FaggotGameCommandsEnabled bool `json:"faggot_game_enabled"` + Payload []string `json:"payload"` + RemoveSourceOnSucccess bool `json:"remove_source_on_success"` } func DefaultSettings() Settings { - return Settings{false, true} + return Settings{false, []string{}, true} } type ISettingsStorage interface { From df112ae956e89875ba8d3f3ac0fd13475551cdaf Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Wed, 16 Mar 2022 22:53:02 +0300 Subject: [PATCH 304/439] Move LocalizerMock into a separate file --- test_helpers/localizer.go | 26 ++++++++++++++++++ usecases/faggot_game_test.go | 53 ++++++++++-------------------------- 2 files changed, 41 insertions(+), 38 deletions(-) create mode 100644 test_helpers/localizer.go diff --git a/test_helpers/localizer.go b/test_helpers/localizer.go new file mode 100644 index 0000000..e950904 --- /dev/null +++ b/test_helpers/localizer.go @@ -0,0 +1,26 @@ +package test_helpers + +import "fmt" + +func CreateLocalizer(data map[string]string) *FakeLocalizer { + return &FakeLocalizer{data} +} + +type FakeLocalizer struct { + data map[string]string +} + +func (l *FakeLocalizer) I18n(key string, args ...interface{}) string { + if val, ok := l.data[key]; ok { + return fmt.Sprintf(val, args...) + } + return key +} + +func (l *FakeLocalizer) AllKeys() []string { + keys := make([]string, 0, len(l.data)) + for k := range l.data { + keys = append(keys, k) + } + return keys +} diff --git a/usecases/faggot_game_test.go b/usecases/faggot_game_test.go index a4d01c7..c5044a5 100644 --- a/usecases/faggot_game_test.go +++ b/usecases/faggot_game_test.go @@ -14,7 +14,7 @@ import ( ) func Test_AllTheCommands_WorksOnlyInGroupChats(t *testing.T) { - game, bot, _ := makeSUT(LocalizerDict{"faggot_not_available_for_private": "group only"}) + game, bot, _ := makeSUT(map[string]string{"faggot_not_available_for_private": "group only"}) message := makeGameMessage(1, "Faggot") message.IsPrivate = true @@ -30,7 +30,7 @@ func Test_AllTheCommands_WorksOnlyInGroupChats(t *testing.T) { } } func Test_RulesCommand_DeliversRules(t *testing.T) { - game, bot, _ := makeSUT(LocalizerDict{"faggot_rules": "Game rules:"}) + game, bot, _ := makeSUT(map[string]string{"faggot_rules": "Game rules:"}) message := makeGameMessage(1, "Faggot") game.Rules(message, bot) @@ -52,7 +52,7 @@ func Test_Add_ChecksAndReplacesPlayerInfoIfNeeded(t *testing.T) { } func Test_Add_AppendsPlayerInGameOnlyOnce(t *testing.T) { - game, bot, storage := makeSUT(LocalizerDict{ + game, bot, storage := makeSUT(map[string]string{ "faggot_added_to_game": "Player added", "faggot_already_in_game": "Player already in game", }) @@ -70,7 +70,7 @@ func Test_Add_AppendsPlayerInGameOnlyOnce(t *testing.T) { } func Test_Play_RespondsWithNoPlayers(t *testing.T) { - game, bot, _ := makeSUT(LocalizerDict{ + game, bot, _ := makeSUT(map[string]string{ "faggot_no_players": "Nobody in game. So you win, %s!", }) message := makeGameMessage(1, "Faggot") @@ -81,7 +81,7 @@ func Test_Play_RespondsWithNoPlayers(t *testing.T) { } func Test_Play_RespondsNotEnoughPlayers(t *testing.T) { - game, bot, _ := makeSUT(LocalizerDict{ + game, bot, _ := makeSUT(map[string]string{ "faggot_not_enough_players": "Not enough players", }) message := makeGameMessage(1, "Faggot") @@ -93,7 +93,7 @@ func Test_Play_RespondsNotEnoughPlayers(t *testing.T) { } func Test_Play_RespondsWithCurrentGameResult(t *testing.T) { - game, bot, storage := makeSUT(LocalizerDict{ + game, bot, storage := makeSUT(map[string]string{ "faggot_game_0_0": "0", "faggot_game_1_0": "1", "faggot_game_2_0": "2", @@ -115,7 +115,7 @@ func Test_Play_RespondsWithCurrentGameResult(t *testing.T) { assert.Equal(t, phrase, bot.SentMessages[5]) } func Test_Play_RespondsWinnerAlreadyKnown(t *testing.T) { - game, bot, storage := makeSUT(LocalizerDict{ + game, bot, storage := makeSUT(map[string]string{ "faggot_game_0_0": "0", "faggot_game_1_0": "1", "faggot_game_2_0": "2", @@ -142,7 +142,7 @@ func Test_Play_RespondsWinnerAlreadyKnown(t *testing.T) { } func Test_Play_RespondsWinnerLeftTheChat(t *testing.T) { - game, bot, storage := makeSUT(LocalizerDict{ + game, bot, storage := makeSUT(map[string]string{ "faggot_winner_left": "winner left", }) m1 := makeGameMessage(1, "Faggot1") @@ -157,7 +157,7 @@ func Test_Play_RespondsWinnerLeftTheChat(t *testing.T) { func Test_Stats_RespondsWithDescendingResultsForCurrentYear(t *testing.T) { year := strconv.Itoa(time.Now().Year()) - game, bot, storage := makeSUT(LocalizerDict{ + game, bot, storage := makeSUT(map[string]string{ "faggot_stats_top": "top", "faggot_stats_entry": "index:%d,player:%s,scores:%d", "faggot_stats_bottom": "total_players:%d", @@ -194,7 +194,7 @@ func Test_Stats_RespondsWithDescendingResultsForCurrentYear(t *testing.T) { } func Test_Stats_RespondsOnlyForTop10Players(t *testing.T) { - game, bot, storage := makeSUT(LocalizerDict{ + game, bot, storage := makeSUT(map[string]string{ "faggot_stats_top": "top", "faggot_stats_entry": "index:%d,player:%s,scores:%d", "faggot_stats_bottom": "total_players:%d", @@ -232,7 +232,7 @@ func Test_Stats_RespondsOnlyForTop10Players(t *testing.T) { } func Test_All_RespondsWithDescendingResultsForAllTime(t *testing.T) { - game, bot, storage := makeSUT(LocalizerDict{ + game, bot, storage := makeSUT(map[string]string{ "faggot_all_top": "top", "faggot_all_entry": "index:%d,player:%s,scores:%d", "faggot_all_bottom": "total_players:%d", @@ -267,7 +267,7 @@ func Test_All_RespondsWithDescendingResultsForAllTime(t *testing.T) { } func Test_Me_RespondsWithPersonalStat(t *testing.T) { - game, bot, storage := makeSUT(LocalizerDict{ + game, bot, storage := makeSUT(map[string]string{ "faggot_me": "username:%s,scores:%d", }) @@ -300,47 +300,24 @@ func makeGameMessage(id int, username string) *core.Message { } func makeSUT(args ...interface{}) (*usecases.GameFlow, *test_helpers.FakeBot, *GameStorageMock) { - dict := LocalizerDict{} + dict := map[string]string{} storage := &GameStorageMock{players: []*core.User{}, rounds: []*core.Round{}} bot := test_helpers.CreateBot() for _, arg := range args { switch opt := arg.(type) { - case LocalizerDict: + case map[string]string: dict = opt } } l := &test_helpers.FakeLogger{} - t := &LocalizerMock{dict: dict} + t := test_helpers.CreateLocalizer(dict) r := &RandMock{} game := usecases.CreateGameFlow(l, t, storage, r) return game, bot, storage } -// LocalizerMock - -type LocalizerMock struct { - dict LocalizerDict -} - -type LocalizerDict = map[string]string - -func (l *LocalizerMock) I18n(key string, args ...interface{}) string { - if val, ok := l.dict[key]; ok { - return fmt.Sprintf(val, args...) - } - return key -} - -func (l *LocalizerMock) AllKeys() []string { - keys := make([]string, 0, len(l.dict)) - for k := range l.dict { - keys = append(keys, k) - } - return keys -} - // GameStorageMock type GameStorageMock struct { From 0252247e12d3c50ec110f40c32ea7ac4f3ef0d4b Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Wed, 16 Mar 2022 22:53:28 +0300 Subject: [PATCH 305/439] Make ChatID primary key --- infrastructure/settings.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infrastructure/settings.go b/infrastructure/settings.go index 4ed8554..c0eef89 100644 --- a/infrastructure/settings.go +++ b/infrastructure/settings.go @@ -3,7 +3,7 @@ package infrastructure import "time" type Settings struct { - ChatID int64 + ChatID int64 `gorm:"primaryKey"` Data []byte CreatedAt time.Time UpdatedAt time.Time `gorm:"autoCreateTime"` From 35a42719931e77bd98b5366a19cf01c676b9bee6 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Wed, 16 Mar 2022 23:09:13 +0300 Subject: [PATCH 306/439] Add fake settings storage for test cases --- test_helpers/settings_storage.go | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 test_helpers/settings_storage.go diff --git a/test_helpers/settings_storage.go b/test_helpers/settings_storage.go new file mode 100644 index 0000000..956d1a2 --- /dev/null +++ b/test_helpers/settings_storage.go @@ -0,0 +1,29 @@ +package test_helpers + +import "github.com/ailinykh/pullanusbot/v2/core" + +func CreateSettingsStorage() *FakeSettingsStorage { + return &FakeSettingsStorage{make(map[int64]*core.Settings), nil} +} + +type FakeSettingsStorage struct { + Data map[int64]*core.Settings + Err error +} + +// GetSettings is a core.ISettingsStorage interface implementation +func (s *FakeSettingsStorage) GetSettings(chatID int64) (*core.Settings, error) { + if settings, ok := s.Data[chatID]; ok { + return settings, nil + } + + settings := core.DefaultSettings() + s.Data[chatID] = &settings + return &settings, nil +} + +// SetSettings is a core.ISettingsStorage interface implementation +func (s *FakeSettingsStorage) SetSettings(chatID int64, settings *core.Settings) error { + s.Data[chatID] = settings + return nil +} From 24acef9eba6c90df90448e4d003038c502c4c074 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Thu, 17 Mar 2022 09:03:21 +0300 Subject: [PATCH 307/439] Welcome message and `/start` command added with ability to save payload --- infrastructure/common_localizer.go | 49 ++++++++++++++++++++++++++++++ pullanusbot.go | 3 ++ usecases/start_flow.go | 43 ++++++++++++++++++++++++++ usecases/start_flow_test.go | 47 ++++++++++++++++++++++++++++ 4 files changed, 142 insertions(+) create mode 100644 infrastructure/common_localizer.go create mode 100644 usecases/start_flow.go create mode 100644 usecases/start_flow_test.go diff --git a/infrastructure/common_localizer.go b/infrastructure/common_localizer.go new file mode 100644 index 0000000..451a819 --- /dev/null +++ b/infrastructure/common_localizer.go @@ -0,0 +1,49 @@ +package infrastructure + +import ( + "fmt" + "runtime" +) + +func CreateCommonLocalizer() *CommonLocalizer { + return &CommonLocalizer{ + map[string]map[string]string{"ru": { + "start_welcome": `Привет! Вот что я могу: + +- видео, загруженное как файл, я сконвертирую в mp4 и отправлю обратно (до 20MB) +- если прислать мне сылку на видео, я скачаю его и загружу в этот же чат как видео +- ссылки на видео в tiktok, twitter и instagram reels так же поддерживаются +- у меня можно получить доступ к proxy для telegram (на случай, если его опять заблокируют) +- в групповых чатах ролики на youtube длиною до 10 минут я так же скачиваю и присылаю как видео +- если дать мне права на удаление сообщений, я буду удалять исходное сообщение с ссылкой +- в личном чате я могу скачать и прислать частями по 50MB любой ролик на youtube, достаточно просто прислать мне ссылку +- если прислать мне картинку, я загружу её на telegra.ph и отправлю ссылку в ответ +- функционал постоянно добавляется`, + }}, + } +} + +// CommonLocalizer for faggot game +type CommonLocalizer struct { + langs map[string]map[string]string +} + +// I18n is a core.ILocalizer implementation +func (l *CommonLocalizer) I18n(key string, args ...interface{}) string { + + if val, ok := l.langs["ru"][key]; ok { + return fmt.Sprintf(val, args...) + } + + _, file, line, _ := runtime.Caller(0) + return fmt.Sprintf("%s:%d KEY_MISSED:\"%s\"", file, line, key) +} + +// AllKeys is a core.ILocalizer implementation +func (l *CommonLocalizer) AllKeys() []string { + keys := make([]string, 0, len(ru)) + for k := range l.langs["ru"] { + keys = append(keys, k) + } + return keys +} diff --git a/pullanusbot.go b/pullanusbot.go index fd4cb38..2615bf2 100644 --- a/pullanusbot.go +++ b/pullanusbot.go @@ -94,6 +94,9 @@ func main() { removeReelsSourceDecorator := usecases.CreateRemoveSourceDecorator(logger, reelsFlow, settingsStorage) telebot.AddHandler(removeReelsSourceDecorator) + commonLocalizer := infrastructure.CreateCommonLocalizer() + startFlow := usecases.CreateStartFlow(logger, commonLocalizer, settingsStorage) + telebot.AddHandler("/start", startFlow.HandleText) // Start endless loop telebot.Run() } diff --git a/usecases/start_flow.go b/usecases/start_flow.go new file mode 100644 index 0000000..0981aea --- /dev/null +++ b/usecases/start_flow.go @@ -0,0 +1,43 @@ +package usecases + +import "github.com/ailinykh/pullanusbot/v2/core" + +func CreateStartFlow(l core.ILogger, loc core.ILocalizer, settingsStorage core.ISettingsStorage) core.ITextHandler { + return &StartFlow{l, loc, settingsStorage} +} + +type StartFlow struct { + l core.ILogger + loc core.ILocalizer + settingsStorage core.ISettingsStorage +} + +// HandleText is a core.ITextHandler protocol implementation +func (flow *StartFlow) HandleText(message *core.Message, bot core.IBot) error { + settings, err := flow.settingsStorage.GetSettings(message.ChatID) + if err != nil { + return err + } + + if len(message.Text) > 7 { + payload := message.Text[7:] + if !flow.contains(payload, settings.Payload) { + settings.Payload = append(settings.Payload, payload) + err = flow.settingsStorage.SetSettings(message.ChatID, settings) + if err != nil { + return err + } + } + } + _, err = bot.SendText(flow.loc.I18n("start_welcome")) + return err +} + +func (flow *StartFlow) contains(payload string, current []string) bool { + for _, p := range current { + if p == payload { + return true + } + } + return false +} diff --git a/usecases/start_flow_test.go b/usecases/start_flow_test.go new file mode 100644 index 0000000..a69b3e7 --- /dev/null +++ b/usecases/start_flow_test.go @@ -0,0 +1,47 @@ +package usecases_test + +import ( + "testing" + + "github.com/ailinykh/pullanusbot/v2/core" + "github.com/ailinykh/pullanusbot/v2/test_helpers" + "github.com/ailinykh/pullanusbot/v2/usecases" + "github.com/stretchr/testify/assert" +) + +func Test_HandleText_CreatesSettingsWithPayload(t *testing.T) { + logger := test_helpers.CreateLogger() + loc := test_helpers.CreateLocalizer(map[string]string{}) + settingsStorage := test_helpers.CreateSettingsStorage() + startFlow := usecases.CreateStartFlow(logger, loc, settingsStorage) + + message := &core.Message{Text: "/start payload", ChatID: 1488} + bot := test_helpers.CreateBot() + + startFlow.HandleText(message, bot) + + expectedSettings := core.DefaultSettings() + expectedSettings.Payload = []string{"payload"} + assert.Equal(t, map[int64]*core.Settings{1488: &expectedSettings}, settingsStorage.Data) +} + +func Test_HandleText_MergePayloadInSettings(t *testing.T) { + logger := test_helpers.CreateLogger() + loc := test_helpers.CreateLocalizer(map[string]string{}) + settingsStorage := test_helpers.CreateSettingsStorage() + startFlow := usecases.CreateStartFlow(logger, loc, settingsStorage) + + initialSettings := core.DefaultSettings() + initialSettings.Payload = []string{"payload"} + settingsStorage.SetSettings(1488, &initialSettings) + bot := test_helpers.CreateBot() + + startFlow.HandleText(&core.Message{Text: "/start another_payload", ChatID: 1488}, bot) + + expectedSettings := core.DefaultSettings() + expectedSettings.Payload = []string{"payload", "another_payload"} + assert.Equal(t, map[int64]*core.Settings{1488: &expectedSettings}, settingsStorage.Data) + + startFlow.HandleText(&core.Message{Text: "/start payload", ChatID: 1488}, bot) + assert.Equal(t, map[int64]*core.Settings{1488: &expectedSettings}, settingsStorage.Data) +} From c21d452bf1c6753546f4cba66af2932b0f77c775 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Fri, 18 Mar 2022 20:19:19 +0100 Subject: [PATCH 308/439] Extend command with settings upadate --- usecases/start_flow.go | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/usecases/start_flow.go b/usecases/start_flow.go index 0981aea..5ee4fec 100644 --- a/usecases/start_flow.go +++ b/usecases/start_flow.go @@ -1,19 +1,31 @@ package usecases -import "github.com/ailinykh/pullanusbot/v2/core" +import ( + "strings" + + "github.com/ailinykh/pullanusbot/v2/core" +) func CreateStartFlow(l core.ILogger, loc core.ILocalizer, settingsStorage core.ISettingsStorage) core.ITextHandler { - return &StartFlow{l, loc, settingsStorage} + return &StartFlow{l, loc, settingsStorage, make(map[int64]bool)} } type StartFlow struct { l core.ILogger loc core.ILocalizer settingsStorage core.ISettingsStorage + cache map[int64]bool } // HandleText is a core.ITextHandler protocol implementation func (flow *StartFlow) HandleText(message *core.Message, bot core.IBot) error { + if strings.HasPrefix(message.Text, "/start") { + return flow.handleStart(message, bot) + } + return flow.handleSettingsChack(message, bot) +} + +func (flow *StartFlow) handleStart(message *core.Message, bot core.IBot) error { settings, err := flow.settingsStorage.GetSettings(message.ChatID) if err != nil { return err @@ -33,6 +45,18 @@ func (flow *StartFlow) HandleText(message *core.Message, bot core.IBot) error { return err } +func (flow *StartFlow) handleSettingsChack(message *core.Message, bot core.IBot) error { + if _, ok := flow.cache[message.ChatID]; !ok { + flow.cache[message.ChatID] = true + _, err := flow.settingsStorage.GetSettings(message.ChatID) // create settings if needed + if err != nil { + flow.l.Error(err) + return err + } + } + return nil +} + func (flow *StartFlow) contains(payload string, current []string) bool { for _, p := range current { if p == payload { From 6a362cc86cfaba7f9f3416758c40c2098fd943da Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Fri, 18 Mar 2022 20:22:14 +0100 Subject: [PATCH 309/439] Ugrade handler from command to text handler --- pullanusbot.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pullanusbot.go b/pullanusbot.go index 2615bf2..a561977 100644 --- a/pullanusbot.go +++ b/pullanusbot.go @@ -96,7 +96,7 @@ func main() { commonLocalizer := infrastructure.CreateCommonLocalizer() startFlow := usecases.CreateStartFlow(logger, commonLocalizer, settingsStorage) - telebot.AddHandler("/start", startFlow.HandleText) + telebot.AddHandler(startFlow) // Start endless loop telebot.Run() } From 00829760995845013d595d9c1e47161ae0afee14 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Sat, 19 Mar 2022 16:25:47 +0300 Subject: [PATCH 310/439] Avoid multiple settings records on bot launch --- usecases/start_flow.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/usecases/start_flow.go b/usecases/start_flow.go index 5ee4fec..7a8790f 100644 --- a/usecases/start_flow.go +++ b/usecases/start_flow.go @@ -2,12 +2,13 @@ package usecases import ( "strings" + "sync" "github.com/ailinykh/pullanusbot/v2/core" ) func CreateStartFlow(l core.ILogger, loc core.ILocalizer, settingsStorage core.ISettingsStorage) core.ITextHandler { - return &StartFlow{l, loc, settingsStorage, make(map[int64]bool)} + return &StartFlow{l, loc, settingsStorage, make(map[int64]bool), sync.Mutex{}} } type StartFlow struct { @@ -15,10 +16,14 @@ type StartFlow struct { loc core.ILocalizer settingsStorage core.ISettingsStorage cache map[int64]bool + lock sync.Mutex } // HandleText is a core.ITextHandler protocol implementation func (flow *StartFlow) HandleText(message *core.Message, bot core.IBot) error { + flow.lock.Lock() + defer flow.lock.Unlock() + if strings.HasPrefix(message.Text, "/start") { return flow.handleStart(message, bot) } From e1fae2dc8b1bb55a13f40ae2a9cc328ee48ed75f Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Sat, 19 Mar 2022 23:37:44 +0300 Subject: [PATCH 311/439] More logs in `/start` command handler --- usecases/start_flow.go | 1 + 1 file changed, 1 insertion(+) diff --git a/usecases/start_flow.go b/usecases/start_flow.go index 7a8790f..e66fe1c 100644 --- a/usecases/start_flow.go +++ b/usecases/start_flow.go @@ -52,6 +52,7 @@ func (flow *StartFlow) handleStart(message *core.Message, bot core.IBot) error { func (flow *StartFlow) handleSettingsChack(message *core.Message, bot core.IBot) error { if _, ok := flow.cache[message.ChatID]; !ok { + flow.l.Infof("%+v %+v", message, message.Sender) flow.cache[message.ChatID] = true _, err := flow.settingsStorage.GetSettings(message.ChatID) // create settings if needed if err != nil { From 84fb5e02f33cc3d40c087a034a503133b59e48be Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Sun, 20 Mar 2022 14:27:09 +0300 Subject: [PATCH 312/439] Add `IUserStorage` interface to persist user info --- core/user_storage.go | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 core/user_storage.go diff --git a/core/user_storage.go b/core/user_storage.go new file mode 100644 index 0000000..2b7671a --- /dev/null +++ b/core/user_storage.go @@ -0,0 +1,6 @@ +package core + +type IUserStorage interface { + GetUserById(UserID) (*User, error) + CreateUser(*User) error +} From ce0c84c64ed4cd08b1cf7c9035cc650ea57dd801 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Sun, 20 Mar 2022 14:27:44 +0300 Subject: [PATCH 313/439] Add `UserStorage` gorm implemetration --- infrastructure/user_storage.go | 63 ++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 infrastructure/user_storage.go diff --git a/infrastructure/user_storage.go b/infrastructure/user_storage.go new file mode 100644 index 0000000..50a2e98 --- /dev/null +++ b/infrastructure/user_storage.go @@ -0,0 +1,63 @@ +package infrastructure + +import ( + "github.com/ailinykh/pullanusbot/v2/core" + "gorm.io/driver/sqlite" + "gorm.io/gorm" + "gorm.io/gorm/logger" +) + +func CreateUserStorage(dbFile string, l core.ILogger) core.IUserStorage { + conn, err := gorm.Open(sqlite.Open(dbFile+"?cache=shared"), &gorm.Config{ + Logger: logger.Default.LogMode(logger.Error), + }) + if err != nil { + panic(err) + } + + conn.AutoMigrate(&User{}) + return &UserStorage{conn, l} +} + +type UserStorage struct { + conn *gorm.DB + l core.ILogger +} + +// User +type User struct { + UserID int `gorm:"primaryKey"` + FirstName string + LastName string + Username string + LanguageCode string +} + +// GetUserById is a core.IUserStorage interface implementation +func (storage *UserStorage) GetUserById(userID core.UserID) (*core.User, error) { + var user User + res := storage.conn.First(&user, userID) + + if res.Error != nil { + storage.l.Error(res.Error) + return nil, res.Error + } + return &core.User{ + ID: user.UserID, + FirstName: user.FirstName, + LastName: user.LastName, + Username: user.Username, + LanguageCode: user.LanguageCode}, nil +} + +// CreateUser is a core.IUserStorage interface implementation +func (storage *UserStorage) CreateUser(user *core.User) error { + res := storage.conn.Create(User{ + UserID: user.ID, + FirstName: user.FirstName, + LastName: user.LastName, + Username: user.Username, + LanguageCode: user.LanguageCode, + }) + return res.Error +} From 59f2c895c7d984149ce6dffa54c3517b34d61e4a Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Sun, 20 Mar 2022 14:28:27 +0300 Subject: [PATCH 314/439] Add `FakeUserStorage` for test cases --- test_helpers/user_storage.go | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 test_helpers/user_storage.go diff --git a/test_helpers/user_storage.go b/test_helpers/user_storage.go new file mode 100644 index 0000000..328047d --- /dev/null +++ b/test_helpers/user_storage.go @@ -0,0 +1,30 @@ +package test_helpers + +import ( + "fmt" + + "github.com/ailinykh/pullanusbot/v2/core" +) + +func CreateUserStorage() *FakeUserStorage { + return &FakeUserStorage{make(map[int]*core.User), nil} +} + +type FakeUserStorage struct { + users map[core.UserID]*core.User + Err error +} + +// GetUserById is a core.IUserStorage interface implementation +func (storage *FakeUserStorage) GetUserById(userID core.UserID) (*core.User, error) { + if user, ok := storage.users[userID]; ok { + return user, nil + } + return nil, fmt.Errorf("user with id %d not found", userID) +} + +// CreateUser is a core.IUserStorage interface implementation +func (storage *FakeUserStorage) CreateUser(user *core.User) error { + storage.users[user.ID] = user + return nil +} From 357836692f2e6745e216e6d5a40e70a53f3f1e4f Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Sun, 20 Mar 2022 14:31:04 +0300 Subject: [PATCH 315/439] Add user cache to `/start` command --- pullanusbot.go | 3 ++- usecases/start_flow.go | 29 ++++++++++++++++++++++------- usecases/start_flow_test.go | 6 ++++-- 3 files changed, 28 insertions(+), 10 deletions(-) diff --git a/pullanusbot.go b/pullanusbot.go index a561977..07a0d60 100644 --- a/pullanusbot.go +++ b/pullanusbot.go @@ -95,7 +95,8 @@ func main() { telebot.AddHandler(removeReelsSourceDecorator) commonLocalizer := infrastructure.CreateCommonLocalizer() - startFlow := usecases.CreateStartFlow(logger, commonLocalizer, settingsStorage) + usersStorraage := infrastructure.CreateUserStorage(dbFile, logger) + startFlow := usecases.CreateStartFlow(logger, commonLocalizer, settingsStorage, usersStorraage) telebot.AddHandler(startFlow) // Start endless loop telebot.Run() diff --git a/usecases/start_flow.go b/usecases/start_flow.go index e66fe1c..7fbf02e 100644 --- a/usecases/start_flow.go +++ b/usecases/start_flow.go @@ -7,15 +7,17 @@ import ( "github.com/ailinykh/pullanusbot/v2/core" ) -func CreateStartFlow(l core.ILogger, loc core.ILocalizer, settingsStorage core.ISettingsStorage) core.ITextHandler { - return &StartFlow{l, loc, settingsStorage, make(map[int64]bool), sync.Mutex{}} +func CreateStartFlow(l core.ILogger, loc core.ILocalizer, settingsStorage core.ISettingsStorage, userStorage core.IUserStorage) core.ITextHandler { + return &StartFlow{l, loc, settingsStorage, userStorage, make(map[int64]bool), make(map[int]bool), sync.Mutex{}} } type StartFlow struct { l core.ILogger loc core.ILocalizer settingsStorage core.ISettingsStorage - cache map[int64]bool + userStorage core.IUserStorage + settingsCache map[int64]bool + usersCache map[core.UserID]bool lock sync.Mutex } @@ -27,7 +29,7 @@ func (flow *StartFlow) HandleText(message *core.Message, bot core.IBot) error { if strings.HasPrefix(message.Text, "/start") { return flow.handleStart(message, bot) } - return flow.handleSettingsChack(message, bot) + return flow.checkSettingsAndUserPresence(message, bot) } func (flow *StartFlow) handleStart(message *core.Message, bot core.IBot) error { @@ -50,16 +52,29 @@ func (flow *StartFlow) handleStart(message *core.Message, bot core.IBot) error { return err } -func (flow *StartFlow) handleSettingsChack(message *core.Message, bot core.IBot) error { - if _, ok := flow.cache[message.ChatID]; !ok { +func (flow *StartFlow) checkSettingsAndUserPresence(message *core.Message, bot core.IBot) error { + if _, ok := flow.settingsCache[message.ChatID]; !ok { flow.l.Infof("%+v %+v", message, message.Sender) - flow.cache[message.ChatID] = true + flow.settingsCache[message.ChatID] = true _, err := flow.settingsStorage.GetSettings(message.ChatID) // create settings if needed if err != nil { flow.l.Error(err) return err } } + + if _, ok := flow.usersCache[message.Sender.ID]; !ok { + flow.l.Infof("%+v %+v", message, message.Sender) + flow.usersCache[message.Sender.ID] = true + _, err := flow.userStorage.GetUserById(message.Sender.ID) + if err != nil { + if err.Error() == "record not found" { + return flow.userStorage.CreateUser(message.Sender) + } + flow.l.Error(err) + return err + } + } return nil } diff --git a/usecases/start_flow_test.go b/usecases/start_flow_test.go index a69b3e7..126c271 100644 --- a/usecases/start_flow_test.go +++ b/usecases/start_flow_test.go @@ -13,7 +13,8 @@ func Test_HandleText_CreatesSettingsWithPayload(t *testing.T) { logger := test_helpers.CreateLogger() loc := test_helpers.CreateLocalizer(map[string]string{}) settingsStorage := test_helpers.CreateSettingsStorage() - startFlow := usecases.CreateStartFlow(logger, loc, settingsStorage) + userStorage := test_helpers.CreateUserStorage() + startFlow := usecases.CreateStartFlow(logger, loc, settingsStorage, userStorage) message := &core.Message{Text: "/start payload", ChatID: 1488} bot := test_helpers.CreateBot() @@ -29,7 +30,8 @@ func Test_HandleText_MergePayloadInSettings(t *testing.T) { logger := test_helpers.CreateLogger() loc := test_helpers.CreateLocalizer(map[string]string{}) settingsStorage := test_helpers.CreateSettingsStorage() - startFlow := usecases.CreateStartFlow(logger, loc, settingsStorage) + userStorage := test_helpers.CreateUserStorage() + startFlow := usecases.CreateStartFlow(logger, loc, settingsStorage, userStorage) initialSettings := core.DefaultSettings() initialSettings.Payload = []string{"payload"} From d618e0066dbf00832a25f362d40b2a40862a4bcc Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Sun, 20 Mar 2022 14:31:54 +0300 Subject: [PATCH 316/439] Create custom `UserID` type as alias to `int` --- core/user.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/user.go b/core/user.go index 6aae83e..86be263 100644 --- a/core/user.go +++ b/core/user.go @@ -1,8 +1,10 @@ package core +type UserID = int + // User ... type User struct { - ID int + ID UserID FirstName string LastName string Username string From cefb3959c780477cfe505e49bbcb98c3d18c0fb3 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Sun, 20 Mar 2022 15:54:39 +0300 Subject: [PATCH 317/439] Add `gorm:"autoUpdateTime"` to settings struct --- infrastructure/settings.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infrastructure/settings.go b/infrastructure/settings.go index c0eef89..acab79c 100644 --- a/infrastructure/settings.go +++ b/infrastructure/settings.go @@ -5,6 +5,6 @@ import "time" type Settings struct { ChatID int64 `gorm:"primaryKey"` Data []byte - CreatedAt time.Time + CreatedAt time.Time `gorm:"autoUpdateTime"` UpdatedAt time.Time `gorm:"autoCreateTime"` } From 0e69261735a79f41d0f049e4685f14f1140aeb2b Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Sun, 20 Mar 2022 17:23:29 +0300 Subject: [PATCH 318/439] typo fixed --- pullanusbot.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pullanusbot.go b/pullanusbot.go index 07a0d60..027dbcc 100644 --- a/pullanusbot.go +++ b/pullanusbot.go @@ -95,8 +95,8 @@ func main() { telebot.AddHandler(removeReelsSourceDecorator) commonLocalizer := infrastructure.CreateCommonLocalizer() - usersStorraage := infrastructure.CreateUserStorage(dbFile, logger) - startFlow := usecases.CreateStartFlow(logger, commonLocalizer, settingsStorage, usersStorraage) + userStorage := infrastructure.CreateUserStorage(dbFile, logger) + startFlow := usecases.CreateStartFlow(logger, commonLocalizer, settingsStorage, userStorage) telebot.AddHandler(startFlow) // Start endless loop telebot.Run() From 01daa14947820c6636a2cd931e5f14c486418079 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Mon, 21 Mar 2022 09:23:42 +0300 Subject: [PATCH 319/439] Add `OutlineAPI` struct for outline server communication --- api/outline_api.go | 127 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 api/outline_api.go diff --git a/api/outline_api.go b/api/outline_api.go new file mode 100644 index 0000000..caddcb7 --- /dev/null +++ b/api/outline_api.go @@ -0,0 +1,127 @@ +package api + +import ( + "bytes" + "crypto/tls" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + + "github.com/ailinykh/pullanusbot/v2/core" +) + +// CreateOutlineAPI is a default OutlineAPI factory +func CreateOutlineAPI(l core.ILogger, url string) *OutlineAPI { + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + client := &http.Client{Transport: tr} + return &OutlineAPI{l, url, client} +} + +type OutlineAPI struct { + l core.ILogger + url string + client *http.Client +} + +type OutlineAPIKeys struct { + AccessKeys []*VpnKey +} + +type VpnKey struct { + ID string + Name string + Password string + Port int + Method string + AccessURL string +} + +func (api *OutlineAPI) GetKeys(chatID int64) ([]*VpnKey, error) { + res, err := api.client.Get(api.url + "/access-keys") + if err != nil { + api.l.Error(err) + return nil, err + } + defer res.Body.Close() + + var keys OutlineAPIKeys + body, _ := ioutil.ReadAll(res.Body) + + err = json.Unmarshal(body, &keys) + if err != nil { + return nil, err + } + + return keys.AccessKeys, nil +} + +func (api *OutlineAPI) CreateKey(chatID int64, name string) (*VpnKey, error) { + res, err := api.client.Post(api.url+"/access-keys", "application/json", bytes.NewBuffer([]byte{})) + + if err != nil { + api.l.Error(err) + return nil, err + } + defer res.Body.Close() + + var key VpnKey + body, _ := ioutil.ReadAll(res.Body) + + err = json.Unmarshal(body, &key) + if err != nil { + api.l.Error(err) + return nil, err + } + + values := map[string]string{"name": name} + data, err := json.Marshal(values) + + if err != nil { + api.l.Error(err) + return nil, err + } + + req, err := http.NewRequest(http.MethodPut, api.url+"/access-keys/"+key.ID+"/name", bytes.NewBuffer(data)) + if err != nil { + api.l.Error(err) + return nil, err + } + + req.Header.Set("Content-Type", "application/json; charset=utf-8") + res, err = api.client.Do(req) + if err != nil { + api.l.Error(err) + return nil, err + } + + if res.StatusCode != 204 { + api.l.Warningf("unexpected response: %+v", res) + return nil, fmt.Errorf("can't rename created key") + } + + return &key, nil +} + +func (api *OutlineAPI) DeleteKey(key *core.VpnKey) error { + req, err := http.NewRequest(http.MethodDelete, api.url+"/access-keys/"+key.ID, bytes.NewBuffer([]byte{})) + if err != nil { + api.l.Error(err) + return err + } + + res, err := api.client.Do(req) + if err != nil { + api.l.Error(err) + return err + } + + if res.StatusCode != 204 { + api.l.Warningf("unexpected response: %+v", res) + return fmt.Errorf("can't remove key") + } + + return nil +} From 42f467305870b28771dc1368b08c0767d73430f5 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Mon, 21 Mar 2022 09:24:22 +0300 Subject: [PATCH 320/439] Add `OutlineStorage` for storing keys in the database --- infrastructure/outline_storage.go | 74 +++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 infrastructure/outline_storage.go diff --git a/infrastructure/outline_storage.go b/infrastructure/outline_storage.go new file mode 100644 index 0000000..a0730d2 --- /dev/null +++ b/infrastructure/outline_storage.go @@ -0,0 +1,74 @@ +package infrastructure + +import ( + "time" + + "github.com/ailinykh/pullanusbot/v2/core" + "gorm.io/driver/sqlite" + "gorm.io/gorm" + "gorm.io/gorm/logger" +) + +// CreateOutlineStorage is a default OutlineStorage factory +func CreateOutlineStorage(dbFile string, l core.ILogger) *OutlineStorage { + conn, err := gorm.Open(sqlite.Open(dbFile+"?cache=shared"), &gorm.Config{ + Logger: logger.Default.LogMode(logger.Error), + }) + if err != nil { + panic(err) + } + + conn.AutoMigrate(&VpnKey{}) + return &OutlineStorage{conn, l} +} + +type OutlineStorage struct { + conn *gorm.DB + l core.ILogger +} + +type VpnKey struct { + ID string `gorm:"primaryKey"` + ChatID int64 `gorm:"primaryKey"` + Host string `gorm:"primaryKey"` + Title string + Key string + CreatedAt time.Time `gorm:"autoUpdateTime"` + UpdatedAt time.Time `gorm:"autoCreateTime"` +} + +func (storage *OutlineStorage) GetKeys(chatID int64) ([]*VpnKey, error) { + var keys []*VpnKey + res := storage.conn.Where("chat_id = ?", chatID).Find(&keys) + + if res.Error != nil { + storage.l.Error(res.Error) + return nil, res.Error + } + + return keys, nil +} + +func (storage *OutlineStorage) CreateKey(id string, chatID int64, host string, title string, key string) error { + storage.l.Infof("creating key with id: %s, chat_id: %d, host: %s, title: %s, key: %s", id, chatID, host, title, key) + k := VpnKey{ + ID: id, + ChatID: chatID, + Host: host, + Title: title, + Key: key, + } + res := storage.conn.Create(&k) + return res.Error +} + +func (storage *OutlineStorage) DeleteKey(key *core.VpnKey, host string) error { + res := storage.conn.Delete(VpnKey{ + ID: key.ID, + ChatID: key.ChatID, + Host: host, + Title: key.Title, + Key: key.Key, + }) + return res.Error +} From 5242dc68551829fce918b0e327e72a6422bdeb9d Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Mon, 21 Mar 2022 09:25:44 +0300 Subject: [PATCH 321/439] Add `OutlineVpnFacade` for composing outline storage and outline api --- usecases/ouline_vpn_facade.go | 94 +++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 usecases/ouline_vpn_facade.go diff --git a/usecases/ouline_vpn_facade.go b/usecases/ouline_vpn_facade.go new file mode 100644 index 0000000..0ccc0c3 --- /dev/null +++ b/usecases/ouline_vpn_facade.go @@ -0,0 +1,94 @@ +package usecases + +import ( + "fmt" + "net/url" + + "github.com/ailinykh/pullanusbot/v2/api" + "github.com/ailinykh/pullanusbot/v2/core" + "github.com/ailinykh/pullanusbot/v2/infrastructure" +) + +func CreateOutlineVpnFacade(apiUrl string, dbFile string, l core.ILogger, userStorage core.IUserStorage) core.IVpnAPI { + u, err := url.Parse(apiUrl) + if err != nil { + panic(err) + } + + api := api.CreateOutlineAPI(l, apiUrl) + outlineStorage := infrastructure.CreateOutlineStorage(dbFile, l) + return &OutlineVpnFacade{l, api, u.Host, outlineStorage, userStorage} +} + +type OutlineVpnFacade struct { + l core.ILogger + api *api.OutlineAPI + host string + outlineStorage *infrastructure.OutlineStorage + userStorage core.IUserStorage +} + +// GetKeys is a core.IVpnAPI interface implementation +func (facade *OutlineVpnFacade) GetKeys(chatID int64) ([]*core.VpnKey, error) { + keys, err := facade.outlineStorage.GetKeys(chatID) + if err != nil { + facade.l.Error(err) + return nil, err + } + + keys2 := []*core.VpnKey{} + for _, k := range keys { + keys2 = append(keys2, &core.VpnKey{ + ID: k.ID, + ChatID: k.ChatID, + Title: k.Title, + Key: k.Key, + }) + } + return keys2, nil +} + +// CreateKey is a core.IVpnAPI interface implementation +func (facade *OutlineVpnFacade) CreateKey(chatID int64, title string) (*core.VpnKey, error) { + keys, err := facade.outlineStorage.GetKeys(chatID) + if err != nil { + facade.l.Error(err) + return nil, err + } + + user, err := facade.userStorage.GetUserById(int(chatID)) // should exist + if err != nil { + facade.l.Error(err) + return nil, err + } + + key, err := facade.api.CreateKey(chatID, fmt.Sprintf("%s %d", user.DisplayName(), len(keys))) + if err != nil { + facade.l.Error(err) + return nil, err + } + + err = facade.outlineStorage.CreateKey(key.ID, chatID, facade.host, title, key.AccessURL) + if err != nil { + facade.l.Error(err) + return nil, err + } + + return &core.VpnKey{ + ID: key.ID, + ChatID: chatID, + Title: title, + Key: key.AccessURL, + }, nil +} + +// DeleteKey is a core.IVpnAPI interface implementation +func (facade *OutlineVpnFacade) DeleteKey(key *core.VpnKey) error { + err := facade.api.DeleteKey(key) + if err != nil { + facade.l.Error(err) + return err + } + + return facade.outlineStorage.DeleteKey(key, facade.host) +} From abc9a33441a920cbdaacfcc5fdda85d492025af8 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Mon, 21 Mar 2022 09:26:21 +0300 Subject: [PATCH 322/439] Add `VpnKey` struct and `IVpnAPI` for key management --- core/vpn.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 core/vpn.go diff --git a/core/vpn.go b/core/vpn.go new file mode 100644 index 0000000..1538f6f --- /dev/null +++ b/core/vpn.go @@ -0,0 +1,14 @@ +package core + +type VpnKey struct { + ID string + ChatID int64 + Title string + Key string +} + +type IVpnAPI interface { + GetKeys(int64) ([]*VpnKey, error) + CreateKey(int64, string) (*VpnKey, error) + DeleteKey(*VpnKey) error +} From cac98f072c72869029712b1db44625df979b375f Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Mon, 21 Mar 2022 09:27:01 +0300 Subject: [PATCH 323/439] Add `VpnLocalizer` for the whole vpn key management process localization --- infrastructure/vpn_localizer.go | 54 +++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 infrastructure/vpn_localizer.go diff --git a/infrastructure/vpn_localizer.go b/infrastructure/vpn_localizer.go new file mode 100644 index 0000000..30e11c3 --- /dev/null +++ b/infrastructure/vpn_localizer.go @@ -0,0 +1,54 @@ +package infrastructure + +import ( + "fmt" + "runtime" +) + +func CreateVpnLocalizer() *VpnLocalizer { + return &VpnLocalizer{ + map[string]map[string]string{"ru": { + "vpn_button_create_key": "🔑 Создать новый ключ", + "vpn_button_manage_key": "🔐 Управление ключами", + "vpn_button_remove_key": "❌ Удалить ключ", + "vpn_button_back": "⏪ Назад", + "vpn_button_cancel": "❌ Отмена", + "vpn_enter_create_key_name": "Придумайте имя для ключа.\nЭто может быть любой набор слов, который поможет вам понять, для чего вы используете тот или иной ключ.\n\nНапример:\n- Мой ключ\n- Ключ для друзей\n- Родители\n\nнапишите имя в следующем сообщении", + "vpn_enter_create_key_name_too_long": "Давайте придумаем что-то более лаконичное", + "vpn_enter_delete_key_name_top": "Введите имя ключа, который хотите удалить\n", + "vpn_enter_delete_key_name_item": "%s", + "vpn_key_created": "✅ Вы успешно создали новый ключ\n\n%s\n\nтеперь скопируйте ключ в буффер обмена (простым нажатием на него) и вставьте его в приложение", + "vpn_key_deleted": "✅ Ключ \"%s\" удалён!\n\n", + "vpn_key_not_found": "❌ Ключ не найден\n\n", + "vpn_key_list_top": "🔑 Активные ключи:\n", + "vpn_key_list_item": "%d. %s\n%s\n", + "vpn_key_list_bottom": "\nВсего ключей: %d", + "vpn_welcome": "🌏 VPN всего за 3 простых шага\n\n1️⃣ Установите клиент outline на ваше устройство:\n\n📱 iOS / iPhone / iPad\n📱 Android\n\n🖥 macOS\n🪟 Windows\n🐧 Linux\n\n2️⃣ Нажмите на кнопку \"Создать новый ключ\"\n\n3️⃣ Скопируйте полученный ключ в клиент", + }}, + } +} + +// VpnLocalizer for faggot game +type VpnLocalizer struct { + langs map[string]map[string]string +} + +// I18n is a core.ILocalizer implementation +func (l *VpnLocalizer) I18n(key string, args ...interface{}) string { + + if val, ok := l.langs["ru"][key]; ok { + return fmt.Sprintf(val, args...) + } + + _, file, line, _ := runtime.Caller(0) + return fmt.Sprintf("%s:%d KEY_MISSED:\"%s\"", file, line, key) +} + +// AllKeys is a core.ILocalizer implementation +func (l *VpnLocalizer) AllKeys() []string { + keys := make([]string, 0, len(ru)) + for k := range l.langs["ru"] { + keys = append(keys, k) + } + return keys +} From cef3a5e5e92c389cdbf5fe102634d4920550d208 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Mon, 21 Mar 2022 09:29:46 +0300 Subject: [PATCH 324/439] Add `OutlineVpnFlow` as interface between bot and vpn key manager --- usecases/outline_vpn_flow.go | 218 +++++++++++++++++++++++++++++++++++ 1 file changed, 218 insertions(+) create mode 100644 usecases/outline_vpn_flow.go diff --git a/usecases/outline_vpn_flow.go b/usecases/outline_vpn_flow.go new file mode 100644 index 0000000..c6fc8e0 --- /dev/null +++ b/usecases/outline_vpn_flow.go @@ -0,0 +1,218 @@ +package usecases + +import ( + "fmt" + "strings" + + "github.com/ailinykh/pullanusbot/v2/core" +) + +func CreateVpnFlow(l core.ILogger, loc core.ILocalizer, api core.IVpnAPI) core.ITextHandler { + flow := OutlineVpnFlow{l, loc, api, make(map[string]func(*core.Message, core.IBot) error), make(map[int64]OutlineVpnState)} + flow.callbacks["vpn_create_key"] = flow.create + flow.callbacks["vpn_manage_key"] = flow.manage + flow.callbacks["vpn_delete_key"] = flow.delete + flow.callbacks["vpn_back"] = flow.back + flow.callbacks["vpn_cancel"] = flow.cancel + return &flow +} + +type OutlineVpnFlow struct { + l core.ILogger + loc core.ILocalizer + api core.IVpnAPI + + callbacks map[string]func(*core.Message, core.IBot) error + state map[int64]OutlineVpnState +} + +type OutlineVpnState struct { + action string + source int +} + +// GetButtonIds is a core.IButtonHandler protocol implementation +func (flow *OutlineVpnFlow) GetButtonIds() []string { + keys := make([]string, len(flow.callbacks)) + + i := 0 + for k := range flow.callbacks { + keys[i] = k + i++ + } + + return keys +} + +// ButtonPressed is a core.IButtonHandler protocol implementation +func (flow *OutlineVpnFlow) ButtonPressed(id string, message *core.Message, bot core.IBot) error { + if callback, ok := flow.callbacks[id]; ok { + return callback(message, bot) + } + return fmt.Errorf("not implemented") +} + +// HandleText is a core.ITextHandler protocol implementation +func (flow *OutlineVpnFlow) HandleText(message *core.Message, bot core.IBot) error { + if !message.IsPrivate { + return fmt.Errorf("not implemented") + } + + if state, ok := flow.state[message.ChatID]; ok { + return flow.handleAction(state, message, bot) + } + + if message.Text != "/vpnhelp" { + return fmt.Errorf("not implemented") + } + + return flow.help(message, bot) +} + +func (flow *OutlineVpnFlow) help(message *core.Message, bot core.IBot) error { + keys, err := flow.api.GetKeys(message.ChatID) + if err != nil { + flow.l.Error(err) + return err + } + + _, err = bot.SendText(flow.loc.I18n("vpn_welcome"), flow.getKeyboard(keys)) + return err +} + +func (flow *OutlineVpnFlow) create(message *core.Message, bot core.IBot) error { + flow.state[message.ChatID] = OutlineVpnState{"create", message.ID} + keyboard := core.Keyboard{[]*core.Button{{ID: "vpn_back", Text: flow.loc.I18n("vpn_button_back")}}} + + _, err := bot.Edit(message, flow.loc.I18n("vpn_enter_create_key_name"), keyboard) + return err +} + +func (flow *OutlineVpnFlow) manage(message *core.Message, bot core.IBot) error { + keys, err := flow.api.GetKeys(message.ChatID) + if err != nil { + flow.l.Error(err) + return err + } + + text := []string{flow.loc.I18n("vpn_key_list_top")} + + for idx, key := range keys { + text = append(text, flow.loc.I18n("vpn_key_list_item", idx+1, key.Title, key.Key)) + } + + text = append(text, flow.loc.I18n("vpn_key_list_bottom", len(keys))) + + keyboard := core.Keyboard{ + []*core.Button{{ID: "vpn_delete_key", Text: flow.loc.I18n("vpn_button_remove_key")}}, + []*core.Button{{ID: "vpn_back", Text: flow.loc.I18n("vpn_button_back")}}, + } + _, err = bot.Edit(message, strings.Join(text, "\n"), keyboard) + return err +} + +func (flow *OutlineVpnFlow) back(message *core.Message, bot core.IBot) error { + keys, err := flow.api.GetKeys(message.ChatID) + if err != nil { + flow.l.Error(err) + return err + } + + delete(flow.state, message.ChatID) + + _, err = bot.Edit(message, flow.loc.I18n("vpn_welcome"), flow.getKeyboard(keys)) + return err +} + +func (flow *OutlineVpnFlow) delete(message *core.Message, bot core.IBot) error { + keys, err := flow.api.GetKeys(message.ChatID) + if err != nil { + flow.l.Error(err) + return err + } + + flow.state[message.ChatID] = OutlineVpnState{"delete", message.ID} + + text := []string{flow.loc.I18n("vpn_enter_delete_key_name_top")} + + for _, key := range keys { + text = append(text, flow.loc.I18n("vpn_enter_delete_key_name_item", key.Title)) + } + + keyboard := core.Keyboard{[]*core.Button{ + {ID: "vpn_cancel", Text: flow.loc.I18n("vpn_button_cancel")}, + }} + _, err = bot.Edit(message, strings.Join(text, "\n"), keyboard) + return err +} + +func (flow *OutlineVpnFlow) cancel(message *core.Message, bot core.IBot) error { + return flow.back(message, bot) +} + +func (flow *OutlineVpnFlow) getKeyboard(keys []*core.VpnKey) core.Keyboard { + keyboard := core.Keyboard{} + + if len(keys) < 10 { + keyboard = append(keyboard, []*core.Button{{ID: "vpn_create_key", Text: flow.loc.I18n("vpn_button_create_key")}}) + } + + if len(keys) > 0 { + keyboard = append(keyboard, []*core.Button{{ID: "vpn_manage_key", Text: flow.loc.I18n("vpn_button_manage_key")}}) + } + + return keyboard +} + +func (flow *OutlineVpnFlow) handleAction(state OutlineVpnState, message *core.Message, bot core.IBot) error { + if state.action == "create" { + if len(message.Text) > 64 { + _, err := bot.SendText(flow.loc.I18n("vpn_enter_create_key_name_too_long")) + return err + } + key, err := flow.api.CreateKey(message.ChatID, message.Text) + if err != nil { + flow.l.Error(err) + return err + } + + delete(flow.state, message.ChatID) + + _ = bot.Delete(&core.Message{ID: state.source, ChatID: message.ChatID}) + + keyboard := core.Keyboard{[]*core.Button{{ID: "vpn_manage_key", Text: flow.loc.I18n("vpn_button_manage_key")}}} + _, err = bot.SendText(flow.loc.I18n("vpn_key_created", key.Key), keyboard) + return err + } + + if state.action == "delete" { + keys, err := flow.api.GetKeys(message.ChatID) + if err != nil { + flow.l.Error(err) + return err + } + + delete(flow.state, message.ChatID) + + _ = bot.Delete(&core.Message{ID: state.source, ChatID: message.ChatID}) + + keyboard := core.Keyboard{ + []*core.Button{{ID: "vpn_back", Text: flow.loc.I18n("vpn_button_back")}}, + } + + for _, k := range keys { + if k.Title == message.Text { + err = flow.api.DeleteKey(k) + if err != nil { + return err + } + _, err = bot.SendText(flow.loc.I18n("vpn_key_deleted", k.Title), keyboard) + return err + } + } + _, err = bot.SendText(flow.loc.I18n("vpn_key_not_found"), keyboard) + return err + } + + return fmt.Errorf("unexpected action: %s", state.action) +} From 531a212ef8d15295f7b72443d30bb1ecea6c92e3 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Mon, 21 Mar 2022 09:30:26 +0300 Subject: [PATCH 325/439] Ignore "not implemented" error reports --- api/telebot.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/telebot.go b/api/telebot.go index 63edc07..e249b78 100644 --- a/api/telebot.go +++ b/api/telebot.go @@ -46,7 +46,7 @@ func CreateTelebot(token string, logger core.ILogger) *Telebot { bot.Handle(tb.OnText, func(m *tb.Message) { for _, h := range telebot.textHandlers { err := h.HandleText(makeMessage(m), makeIBot(m, telebot)) - if err != nil { + if err != nil && err.Error() != "not implemented" { logger.Errorf("%T: %s", h, err) telebot.reportError(m, err) } From 47d76857d1fe7340714d327f4394fd0d874286b2 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Tue, 22 Mar 2022 09:17:48 +0300 Subject: [PATCH 326/439] Add `Chat` struct for chat info representation --- core/chat.go | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 core/chat.go diff --git a/core/chat.go b/core/chat.go new file mode 100644 index 0000000..de3e484 --- /dev/null +++ b/core/chat.go @@ -0,0 +1,8 @@ +package core + +type Chat struct { + ID int64 + Title string + Type string + Settings *Settings +} From b4dc4954c5b1b5551b8fc230eb1a5b2a0051c951 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Tue, 22 Mar 2022 09:23:02 +0300 Subject: [PATCH 327/439] Replace `Message.ChatID` field with `Chat` struct --- api/telebot.go | 15 ++++++++++++++- api/telebot_adapter.go | 2 +- api/telebot_factory.go | 2 +- core/message.go | 2 +- usecases/faggot_game.go | 22 +++++++++++----------- usecases/faggot_game_test.go | 2 +- usecases/outline_vpn_flow.go | 28 ++++++++++++++-------------- usecases/publisher_flow.go | 6 +++--- usecases/remove_source_decorator.go | 4 ++-- usecases/start_flow.go | 10 +++++----- 10 files changed, 53 insertions(+), 40 deletions(-) diff --git a/api/telebot.go b/api/telebot.go index e249b78..78aca84 100644 --- a/api/telebot.go +++ b/api/telebot.go @@ -209,7 +209,7 @@ func makeMessage(m *tb.Message) *core.Message { } message := &core.Message{ ID: m.ID, - ChatID: m.Chat.ID, + Chat: makeChat(m.Chat), IsPrivate: m.Private(), Sender: makeUser(m.Sender), Text: text, @@ -226,6 +226,19 @@ func makeMessage(m *tb.Message) *core.Message { return message } +func makeChat(c *tb.Chat) *core.Chat { + title := c.Title + if c.Type == tb.ChatPrivate { + title = c.FirstName + " " + c.LastName + } + return &core.Chat{ + ID: c.ID, + Title: title, + Type: string(c.Type), + Settings: nil, //TODO: obtain settings from database + } +} + func makeUser(u *tb.User) *core.User { return &core.User{ ID: u.ID, diff --git a/api/telebot_adapter.go b/api/telebot_adapter.go index f577d44..a51dc68 100644 --- a/api/telebot_adapter.go +++ b/api/telebot_adapter.go @@ -37,7 +37,7 @@ func (a *TelebotAdapter) SendText(text string, params ...interface{}) (*core.Mes // Delete is a core.IBot interface implementation func (a *TelebotAdapter) Delete(message *core.Message) error { - return a.t.bot.Delete(&tb.Message{ID: message.ID, Chat: &tb.Chat{ID: message.ChatID}}) + return a.t.bot.Delete(&tb.Message{ID: message.ID, Chat: &tb.Chat{ID: message.Chat.ID}}) } // Edit is a core.IBot interface implementation diff --git a/api/telebot_factory.go b/api/telebot_factory.go index 36c18bd..30cb86b 100644 --- a/api/telebot_factory.go +++ b/api/telebot_factory.go @@ -8,7 +8,7 @@ import ( func makeTbMessage(m *core.Message) *tb.Message { message := &tb.Message{ ID: m.ID, - Chat: &tb.Chat{ID: m.ChatID}, + Chat: &tb.Chat{ID: m.Chat.ID}, Sender: makeTbUser(m.Sender), } if m.ReplyTo != nil { diff --git a/core/message.go b/core/message.go index 6de372e..c1860d9 100644 --- a/core/message.go +++ b/core/message.go @@ -3,7 +3,7 @@ package core // Message from chat type Message struct { ID int - ChatID int64 + Chat *Chat IsPrivate bool Sender *User Text string diff --git a/usecases/faggot_game.go b/usecases/faggot_game.go index 093ec24..2e6a934 100644 --- a/usecases/faggot_game.go +++ b/usecases/faggot_game.go @@ -42,11 +42,11 @@ func (flow *GameFlow) Add(message *core.Message, bot core.IBot) error { _, err := bot.SendText(flow.t.I18n("faggot_not_available_for_private")) return err } - players, _ := flow.s.GetPlayers(message.ChatID) + players, _ := flow.s.GetPlayers(message.Chat.ID) for _, p := range players { if p.ID == message.Sender.ID { if p.FirstName != message.Sender.FirstName || p.LastName != message.Sender.LastName || p.Username != message.Sender.Username { - _ = flow.s.UpdatePlayer(message.ChatID, message.Sender) + _ = flow.s.UpdatePlayer(message.Chat.ID, message.Sender) _, err := bot.SendText(flow.t.I18n("faggot_info_updated")) return err } @@ -55,7 +55,7 @@ func (flow *GameFlow) Add(message *core.Message, bot core.IBot) error { } } - err := flow.s.AddPlayer(message.ChatID, message.Sender) + err := flow.s.AddPlayer(message.Chat.ID, message.Sender) if err != nil { return err } @@ -75,9 +75,9 @@ func (flow *GameFlow) Play(message *core.Message, bot core.IBot) error { mutex.Lock() defer mutex.Unlock() - flow.l.Infof("chat_id: %d, game started by %v", message.ChatID, message.Sender) + flow.l.Infof("chat_id: %d, game started by %v", message.Chat.ID, message.Sender) - players, _ := flow.s.GetPlayers(message.ChatID) + players, _ := flow.s.GetPlayers(message.Chat.ID) switch len(players) { case 0: _, err := bot.SendText(flow.t.I18n("faggot_no_players", message.Sender.DisplayName())) @@ -87,7 +87,7 @@ func (flow *GameFlow) Play(message *core.Message, bot core.IBot) error { return err } - games, _ := flow.s.GetRounds(message.ChatID) + games, _ := flow.s.GetRounds(message.Chat.ID) loc, _ := time.LoadLocation("Europe/Zurich") day := time.Now().In(loc).Format("2006-01-02") @@ -100,7 +100,7 @@ func (flow *GameFlow) Play(message *core.Message, bot core.IBot) error { winner := players[rand.Intn(len(players))] - if !bot.IsUserMemberOfChat(winner, message.ChatID) { + if !bot.IsUserMemberOfChat(winner, message.Chat.ID) { _, err := bot.SendText(flow.t.I18n("faggot_winner_left")) return err } @@ -109,7 +109,7 @@ func (flow *GameFlow) Play(message *core.Message, bot core.IBot) error { if winner.ID == message.Sender.ID { if winner.FirstName != message.Sender.FirstName || winner.LastName != message.Sender.LastName || winner.Username != message.Sender.Username { - err := flow.s.UpdatePlayer(message.ChatID, message.Sender) + err := flow.s.UpdatePlayer(message.Chat.ID, message.Sender) if err != nil { flow.l.Error(err) } else { @@ -119,7 +119,7 @@ func (flow *GameFlow) Play(message *core.Message, bot core.IBot) error { } round := &core.Round{Day: day, Winner: winner} - flow.s.AddRound(message.ChatID, round) + flow.s.AddRound(message.Chat.ID, round) for i := 0; i <= 3; i++ { templates := []string{} @@ -180,7 +180,7 @@ func (flow *GameFlow) Stats(message *core.Message, bot core.IBot) error { } year := strconv.Itoa(time.Now().Year()) - rounds, _ := flow.s.GetRounds(message.ChatID) + rounds, _ := flow.s.GetRounds(message.Chat.ID) entries := []Stat{} players := map[int]bool{} @@ -237,7 +237,7 @@ func (flow *GameFlow) Me(message *core.Message, bot core.IBot) error { func (flow *GameFlow) getStat(message *core.Message) ([]Stat, error) { entries := []Stat{} - rounds, err := flow.s.GetRounds(message.ChatID) + rounds, err := flow.s.GetRounds(message.Chat.ID) if err != nil { return nil, err diff --git a/usecases/faggot_game_test.go b/usecases/faggot_game_test.go index c5044a5..9372cef 100644 --- a/usecases/faggot_game_test.go +++ b/usecases/faggot_game_test.go @@ -296,7 +296,7 @@ func makeGameMessage(id int, username string) *core.Message { LastName: "LastName" + fmt.Sprint(id), Username: username, } - return &core.Message{ID: 0, Sender: player} + return &core.Message{ID: 0, Chat: &core.Chat{ID: 0}, Sender: player} } func makeSUT(args ...interface{}) (*usecases.GameFlow, *test_helpers.FakeBot, *GameStorageMock) { diff --git a/usecases/outline_vpn_flow.go b/usecases/outline_vpn_flow.go index c6fc8e0..da7bdc6 100644 --- a/usecases/outline_vpn_flow.go +++ b/usecases/outline_vpn_flow.go @@ -58,7 +58,7 @@ func (flow *OutlineVpnFlow) HandleText(message *core.Message, bot core.IBot) err return fmt.Errorf("not implemented") } - if state, ok := flow.state[message.ChatID]; ok { + if state, ok := flow.state[message.Chat.ID]; ok { return flow.handleAction(state, message, bot) } @@ -70,7 +70,7 @@ func (flow *OutlineVpnFlow) HandleText(message *core.Message, bot core.IBot) err } func (flow *OutlineVpnFlow) help(message *core.Message, bot core.IBot) error { - keys, err := flow.api.GetKeys(message.ChatID) + keys, err := flow.api.GetKeys(message.Chat.ID) if err != nil { flow.l.Error(err) return err @@ -81,7 +81,7 @@ func (flow *OutlineVpnFlow) help(message *core.Message, bot core.IBot) error { } func (flow *OutlineVpnFlow) create(message *core.Message, bot core.IBot) error { - flow.state[message.ChatID] = OutlineVpnState{"create", message.ID} + flow.state[message.Chat.ID] = OutlineVpnState{"create", message.ID} keyboard := core.Keyboard{[]*core.Button{{ID: "vpn_back", Text: flow.loc.I18n("vpn_button_back")}}} _, err := bot.Edit(message, flow.loc.I18n("vpn_enter_create_key_name"), keyboard) @@ -89,7 +89,7 @@ func (flow *OutlineVpnFlow) create(message *core.Message, bot core.IBot) error { } func (flow *OutlineVpnFlow) manage(message *core.Message, bot core.IBot) error { - keys, err := flow.api.GetKeys(message.ChatID) + keys, err := flow.api.GetKeys(message.Chat.ID) if err != nil { flow.l.Error(err) return err @@ -112,26 +112,26 @@ func (flow *OutlineVpnFlow) manage(message *core.Message, bot core.IBot) error { } func (flow *OutlineVpnFlow) back(message *core.Message, bot core.IBot) error { - keys, err := flow.api.GetKeys(message.ChatID) + keys, err := flow.api.GetKeys(message.Chat.ID) if err != nil { flow.l.Error(err) return err } - delete(flow.state, message.ChatID) + delete(flow.state, message.Chat.ID) _, err = bot.Edit(message, flow.loc.I18n("vpn_welcome"), flow.getKeyboard(keys)) return err } func (flow *OutlineVpnFlow) delete(message *core.Message, bot core.IBot) error { - keys, err := flow.api.GetKeys(message.ChatID) + keys, err := flow.api.GetKeys(message.Chat.ID) if err != nil { flow.l.Error(err) return err } - flow.state[message.ChatID] = OutlineVpnState{"delete", message.ID} + flow.state[message.Chat.ID] = OutlineVpnState{"delete", message.ID} text := []string{flow.loc.I18n("vpn_enter_delete_key_name_top")} @@ -170,15 +170,15 @@ func (flow *OutlineVpnFlow) handleAction(state OutlineVpnState, message *core.Me _, err := bot.SendText(flow.loc.I18n("vpn_enter_create_key_name_too_long")) return err } - key, err := flow.api.CreateKey(message.ChatID, message.Text) + key, err := flow.api.CreateKey(message.Chat.ID, message.Text) if err != nil { flow.l.Error(err) return err } - delete(flow.state, message.ChatID) + delete(flow.state, message.Chat.ID) - _ = bot.Delete(&core.Message{ID: state.source, ChatID: message.ChatID}) + _ = bot.Delete(&core.Message{ID: state.source, Chat: message.Chat}) keyboard := core.Keyboard{[]*core.Button{{ID: "vpn_manage_key", Text: flow.loc.I18n("vpn_button_manage_key")}}} _, err = bot.SendText(flow.loc.I18n("vpn_key_created", key.Key), keyboard) @@ -186,15 +186,15 @@ func (flow *OutlineVpnFlow) handleAction(state OutlineVpnState, message *core.Me } if state.action == "delete" { - keys, err := flow.api.GetKeys(message.ChatID) + keys, err := flow.api.GetKeys(message.Chat.ID) if err != nil { flow.l.Error(err) return err } - delete(flow.state, message.ChatID) + delete(flow.state, message.Chat.ID) - _ = bot.Delete(&core.Message{ID: state.source, ChatID: message.ChatID}) + _ = bot.Delete(&core.Message{ID: state.source, Chat: message.Chat}) keyboard := core.Keyboard{ []*core.Button{{ID: "vpn_back", Text: flow.loc.I18n("vpn_button_back")}}, diff --git a/usecases/publisher_flow.go b/usecases/publisher_flow.go index 868507b..a7d22a5 100644 --- a/usecases/publisher_flow.go +++ b/usecases/publisher_flow.go @@ -51,7 +51,7 @@ type msgSource struct { // HandleImage is a core.IImageHandler protocol implementation func (p *PublisherFlow) HandleImage(image *core.Image, message *core.Message, bot core.IBot) error { - if message.ChatID == p.chatID && message.Sender.Username == p.username { + if message.Chat.ID == p.chatID && message.Sender.Username == p.username { p.imageChan <- imgSource{image.ID, bot} } @@ -59,7 +59,7 @@ func (p *PublisherFlow) HandleImage(image *core.Image, message *core.Message, bo } func (p *PublisherFlow) HandleRequest(message *core.Message, bot core.IBot) error { - if message.ChatID == p.chatID { + if message.Chat.ID == p.chatID { p.requestChan <- msgSource{*message, bot} } @@ -72,7 +72,7 @@ func (p *PublisherFlow) runLoop() { disposal := func(m core.Message, bot core.IBot, timeout int) { time.Sleep(time.Duration(timeout) * time.Second) - p.l.Infof("disposing message %d from chat %d", m.ID, m.ChatID) + p.l.Infof("disposing message %d from chat %d", m.ID, m.Chat.ID) err := bot.Delete(&m) if err != nil { p.l.Error(err) diff --git a/usecases/remove_source_decorator.go b/usecases/remove_source_decorator.go index bcbfddb..23f1ede 100644 --- a/usecases/remove_source_decorator.go +++ b/usecases/remove_source_decorator.go @@ -27,14 +27,14 @@ func (decorator *RemoveSourceDecorator) HandleText(message *core.Message, bot co return err } - settings, err := decorator.settingsStorage.GetSettings(message.ChatID) + settings, err := decorator.settingsStorage.GetSettings(message.Chat.ID) if err != nil { decorator.l.Error(err) return err } if settings.RemoveSourceOnSucccess { - decorator.l.Infof("removing chat %d message %d", message.ChatID, message.ID) + decorator.l.Infof("removing chat %d message %d", message.Chat.ID, message.ID) return bot.Delete(message) } diff --git a/usecases/start_flow.go b/usecases/start_flow.go index 7fbf02e..1b050a8 100644 --- a/usecases/start_flow.go +++ b/usecases/start_flow.go @@ -33,7 +33,7 @@ func (flow *StartFlow) HandleText(message *core.Message, bot core.IBot) error { } func (flow *StartFlow) handleStart(message *core.Message, bot core.IBot) error { - settings, err := flow.settingsStorage.GetSettings(message.ChatID) + settings, err := flow.settingsStorage.GetSettings(message.Chat.ID) if err != nil { return err } @@ -42,7 +42,7 @@ func (flow *StartFlow) handleStart(message *core.Message, bot core.IBot) error { payload := message.Text[7:] if !flow.contains(payload, settings.Payload) { settings.Payload = append(settings.Payload, payload) - err = flow.settingsStorage.SetSettings(message.ChatID, settings) + err = flow.settingsStorage.SetSettings(message.Chat.ID, settings) if err != nil { return err } @@ -53,10 +53,10 @@ func (flow *StartFlow) handleStart(message *core.Message, bot core.IBot) error { } func (flow *StartFlow) checkSettingsAndUserPresence(message *core.Message, bot core.IBot) error { - if _, ok := flow.settingsCache[message.ChatID]; !ok { + if _, ok := flow.settingsCache[message.Chat.ID]; !ok { flow.l.Infof("%+v %+v", message, message.Sender) - flow.settingsCache[message.ChatID] = true - _, err := flow.settingsStorage.GetSettings(message.ChatID) // create settings if needed + flow.settingsCache[message.Chat.ID] = true + _, err := flow.settingsStorage.GetSettings(message.Chat.ID) // create settings if needed if err != nil { flow.l.Error(err) return err From e5b165ac3a5ebc33c104452f1f1a9a2a0624faac Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Tue, 22 Mar 2022 13:37:54 +0300 Subject: [PATCH 328/439] Add `IChatStorage` interface for chat data persisting --- core/chat_storage.go | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 core/chat_storage.go diff --git a/core/chat_storage.go b/core/chat_storage.go new file mode 100644 index 0000000..2f88bef --- /dev/null +++ b/core/chat_storage.go @@ -0,0 +1,6 @@ +package core + +type IChatStorage interface { + GetChatByID(int64) (*Chat, error) + CreateChat(int64, string, string, *Settings) error +} From 6dd9c29850d41da0e20ef194bc1b37c0e272e0e4 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Tue, 22 Mar 2022 13:38:23 +0300 Subject: [PATCH 329/439] Add `ChatStorage` sqlite implementation of `IChatStorage` --- infrastructure/chat_storage.go | 81 ++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 infrastructure/chat_storage.go diff --git a/infrastructure/chat_storage.go b/infrastructure/chat_storage.go new file mode 100644 index 0000000..04e8491 --- /dev/null +++ b/infrastructure/chat_storage.go @@ -0,0 +1,81 @@ +package infrastructure + +import ( + "encoding/json" + "time" + + "github.com/ailinykh/pullanusbot/v2/core" + "gorm.io/driver/sqlite" + "gorm.io/gorm" + "gorm.io/gorm/logger" +) + +// CreateChatStorage is a default ChatStorage factory +func CreateChatStorage(dbFile string, l core.ILogger) *ChatStorage { + conn, err := gorm.Open(sqlite.Open(dbFile+"?cache=shared"), &gorm.Config{ + Logger: logger.Default.LogMode(logger.Error), + }) + if err != nil { + panic(err) + } + + conn.AutoMigrate(&Chat{}) + return &ChatStorage{conn, l} +} + +// ChatStorage implements core.IChatStorage interface +type ChatStorage struct { + conn *gorm.DB + l core.ILogger +} + +type Chat struct { + ID int64 `gorm:"primaryKey"` + Title string + Type string + Settings []byte + CreatedAt time.Time `gorm:"autoUpdateTime"` + UpdatedAt time.Time `gorm:"autoCreateTime"` +} + +// GetChatByID is a core.IChatStorage interface implementation +func (s *ChatStorage) GetChatByID(chatID int64) (*core.Chat, error) { + var chat Chat + err := s.conn.First(&chat, chatID).Error + + if err != nil { + s.l.Error(err) + return nil, err + } + + var settings core.Settings + err = json.Unmarshal(chat.Settings, &settings) + + if err != nil { + s.l.Error(err) + return nil, err + } + + return &core.Chat{ID: chat.ID, Title: chat.Title, Type: chat.Type, Settings: &settings}, nil +} + +// CreateChat is a core.IChatStorage interface implementation +func (s *ChatStorage) CreateChat(chatID int64, title string, type_ string, settings *core.Settings) error { + data, err := json.Marshal(&settings) + + if err != nil { + s.l.Error(err) + return err + } + + s.l.Infof("creating chat id: %d, title: %s, type: %s, data: %s", chatID, title, type_, data) + chat := Chat{ID: chatID, Title: title, Type: type_, Settings: data} + err = s.conn.Create(&chat).Error + if err != nil { + s.l.Error(err) + return err + } + + s.l.Info("chat created: %+v", chat) + return nil +} From c307f4c1b950c1f1df0f4526d478b8ff59a19ffd Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Tue, 22 Mar 2022 13:38:57 +0300 Subject: [PATCH 330/439] Add `FakeChatStorage` implementation of `IChatStorage` for testing --- test_helpers/chat_storage.go | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 test_helpers/chat_storage.go diff --git a/test_helpers/chat_storage.go b/test_helpers/chat_storage.go new file mode 100644 index 0000000..8f87fe3 --- /dev/null +++ b/test_helpers/chat_storage.go @@ -0,0 +1,30 @@ +package test_helpers + +import ( + "fmt" + + "github.com/ailinykh/pullanusbot/v2/core" +) + +func CreateChatStorage() *FakeChatStorage { + return &FakeChatStorage{make(map[int64]*core.Chat), nil} +} + +type FakeChatStorage struct { + chats map[int64]*core.Chat + Err error +} + +// GetChatByID is a core.IChatStorage interface implementation +func (storage *FakeChatStorage) GetChatByID(chatID int64) (*core.Chat, error) { + if user, ok := storage.chats[chatID]; ok { + return user, nil + } + return nil, fmt.Errorf("chat with id %d not found", chatID) +} + +// CreateChat is a core.IChatStorage interface implementation +func (s *FakeChatStorage) CreateChat(chatID int64, title string, type_ string, settings *core.Settings) error { + s.chats[chatID] = &core.Chat{ID: chatID, Title: title, Type: type_, Settings: settings} + return nil +} From 5886e5f318ed7095eda11ffdebaecdae1ea8fc64 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Tue, 22 Mar 2022 15:06:01 +0300 Subject: [PATCH 331/439] Return "record not found" error in test_helpers --- test_helpers/chat_storage.go | 2 +- test_helpers/user_storage.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test_helpers/chat_storage.go b/test_helpers/chat_storage.go index 8f87fe3..bc52382 100644 --- a/test_helpers/chat_storage.go +++ b/test_helpers/chat_storage.go @@ -20,7 +20,7 @@ func (storage *FakeChatStorage) GetChatByID(chatID int64) (*core.Chat, error) { if user, ok := storage.chats[chatID]; ok { return user, nil } - return nil, fmt.Errorf("chat with id %d not found", chatID) + return nil, fmt.Errorf("record not found") } // CreateChat is a core.IChatStorage interface implementation diff --git a/test_helpers/user_storage.go b/test_helpers/user_storage.go index 328047d..b364d35 100644 --- a/test_helpers/user_storage.go +++ b/test_helpers/user_storage.go @@ -20,7 +20,7 @@ func (storage *FakeUserStorage) GetUserById(userID core.UserID) (*core.User, err if user, ok := storage.users[userID]; ok { return user, nil } - return nil, fmt.Errorf("user with id %d not found", userID) + return nil, fmt.Errorf("record not found") } // CreateUser is a core.IUserStorage interface implementation From 5209241de5569e51c1a2f987747a9efb8db32d5b Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Tue, 22 Mar 2022 15:06:36 +0300 Subject: [PATCH 332/439] Make `Users` accessible in `FakeUserStorage` --- test_helpers/user_storage.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test_helpers/user_storage.go b/test_helpers/user_storage.go index b364d35..ef77b4f 100644 --- a/test_helpers/user_storage.go +++ b/test_helpers/user_storage.go @@ -11,13 +11,13 @@ func CreateUserStorage() *FakeUserStorage { } type FakeUserStorage struct { - users map[core.UserID]*core.User + Users map[core.UserID]*core.User Err error } // GetUserById is a core.IUserStorage interface implementation func (storage *FakeUserStorage) GetUserById(userID core.UserID) (*core.User, error) { - if user, ok := storage.users[userID]; ok { + if user, ok := storage.Users[userID]; ok { return user, nil } return nil, fmt.Errorf("record not found") @@ -25,6 +25,6 @@ func (storage *FakeUserStorage) GetUserById(userID core.UserID) (*core.User, err // CreateUser is a core.IUserStorage interface implementation func (storage *FakeUserStorage) CreateUser(user *core.User) error { - storage.users[user.ID] = user + storage.Users[user.ID] = user return nil } From 8f8f3022a697248ffe1f88f53c8db70f14f6dcf0 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Tue, 22 Mar 2022 15:08:35 +0300 Subject: [PATCH 333/439] Extend `IChatStorage` interface with `UpdateSettings` method --- core/chat_storage.go | 1 + infrastructure/chat_storage.go | 10 ++++++++++ test_helpers/chat_storage.go | 11 ++++++++++- 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/core/chat_storage.go b/core/chat_storage.go index 2f88bef..1d66f45 100644 --- a/core/chat_storage.go +++ b/core/chat_storage.go @@ -3,4 +3,5 @@ package core type IChatStorage interface { GetChatByID(int64) (*Chat, error) CreateChat(int64, string, string, *Settings) error + UpdateSettings(int64, *Settings) error } diff --git a/infrastructure/chat_storage.go b/infrastructure/chat_storage.go index 04e8491..15380b2 100644 --- a/infrastructure/chat_storage.go +++ b/infrastructure/chat_storage.go @@ -79,3 +79,13 @@ func (s *ChatStorage) CreateChat(chatID int64, title string, type_ string, setti s.l.Info("chat created: %+v", chat) return nil } + +// UpdateSettings is a core.IChatStorage interface implementation +func (s *ChatStorage) UpdateSettings(chatID int64, settings *core.Settings) error { + chat, err := s.GetChatByID(chatID) + if err != nil { + return err + } + chat.Settings = settings + return s.conn.Save(&chat).Error +} diff --git a/test_helpers/chat_storage.go b/test_helpers/chat_storage.go index bc52382..28d2aa7 100644 --- a/test_helpers/chat_storage.go +++ b/test_helpers/chat_storage.go @@ -25,6 +25,15 @@ func (storage *FakeChatStorage) GetChatByID(chatID int64) (*core.Chat, error) { // CreateChat is a core.IChatStorage interface implementation func (s *FakeChatStorage) CreateChat(chatID int64, title string, type_ string, settings *core.Settings) error { - s.chats[chatID] = &core.Chat{ID: chatID, Title: title, Type: type_, Settings: settings} + return nil +} + +// UpdateSettings is a core.IChatStorage interface implementation +func (s *FakeChatStorage) UpdateSettings(chatID int64, settings *core.Settings) error { + chat, err := s.GetChatByID(chatID) + if err != nil { + return err + } + chat.Settings = settings return nil } From 8ee41507d3e8eb9085037dea890ca907fcabbf42 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Tue, 22 Mar 2022 15:09:35 +0300 Subject: [PATCH 334/439] Make `Chats` variable accessible from outside in test_helpers --- test_helpers/chat_storage.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test_helpers/chat_storage.go b/test_helpers/chat_storage.go index 28d2aa7..bc986db 100644 --- a/test_helpers/chat_storage.go +++ b/test_helpers/chat_storage.go @@ -11,13 +11,13 @@ func CreateChatStorage() *FakeChatStorage { } type FakeChatStorage struct { - chats map[int64]*core.Chat + Chats map[int64]*core.Chat Err error } // GetChatByID is a core.IChatStorage interface implementation func (storage *FakeChatStorage) GetChatByID(chatID int64) (*core.Chat, error) { - if user, ok := storage.chats[chatID]; ok { + if user, ok := storage.Chats[chatID]; ok { return user, nil } return nil, fmt.Errorf("record not found") @@ -25,6 +25,7 @@ func (storage *FakeChatStorage) GetChatByID(chatID int64) (*core.Chat, error) { // CreateChat is a core.IChatStorage interface implementation func (s *FakeChatStorage) CreateChat(chatID int64, title string, type_ string, settings *core.Settings) error { + s.Chats[chatID] = &core.Chat{ID: chatID, Title: title, Type: type_, Settings: settings} return nil } From 4d1f43d8c71aa8d07959ac0b70df745f3f5601a6 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Tue, 22 Mar 2022 15:11:35 +0300 Subject: [PATCH 335/439] Test `StartFlow` async creating user and chat data --- usecases/start_flow.go | 69 ++++++++++++++++++++++-------- usecases/start_flow_test.go | 83 +++++++++++++++++++++++++++++-------- 2 files changed, 117 insertions(+), 35 deletions(-) diff --git a/usecases/start_flow.go b/usecases/start_flow.go index 1b050a8..c711331 100644 --- a/usecases/start_flow.go +++ b/usecases/start_flow.go @@ -7,17 +7,19 @@ import ( "github.com/ailinykh/pullanusbot/v2/core" ) -func CreateStartFlow(l core.ILogger, loc core.ILocalizer, settingsStorage core.ISettingsStorage, userStorage core.IUserStorage) core.ITextHandler { - return &StartFlow{l, loc, settingsStorage, userStorage, make(map[int64]bool), make(map[int]bool), sync.Mutex{}} +func CreateStartFlow(l core.ILogger, loc core.ILocalizer, settingsStorage core.ISettingsStorage, chatStorage core.IChatStorage, userStorage core.IUserStorage) core.ITextHandler { + return &StartFlow{l, loc, settingsStorage, chatStorage, userStorage, make(map[int64]bool), make(map[int]bool), make(map[int64]bool), sync.Mutex{}} } type StartFlow struct { l core.ILogger loc core.ILocalizer settingsStorage core.ISettingsStorage + chatStorage core.IChatStorage userStorage core.IUserStorage settingsCache map[int64]bool usersCache map[core.UserID]bool + chatCache map[int64]bool lock sync.Mutex } @@ -26,33 +28,66 @@ func (flow *StartFlow) HandleText(message *core.Message, bot core.IBot) error { flow.lock.Lock() defer flow.lock.Unlock() + err := flow.ensureUserExists(message, bot) + if err != nil { + flow.l.Error(err) + //Do not return? + } + + err = flow.ensureChatExists(message, bot) + if err != nil { + flow.l.Error(err) + //Do not return? + } + if strings.HasPrefix(message.Text, "/start") { - return flow.handleStart(message, bot) + if len(message.Text) > 7 { + payload := message.Text[7:] + flow.handlePayload(payload, message.Chat.ID) + if err != nil { + flow.l.Error(err) + //Do not return? + } + } + _, err = bot.SendText(flow.loc.I18n("start_welcome")) + return err } - return flow.checkSettingsAndUserPresence(message, bot) + + return err } -func (flow *StartFlow) handleStart(message *core.Message, bot core.IBot) error { - settings, err := flow.settingsStorage.GetSettings(message.Chat.ID) +func (flow *StartFlow) handlePayload(payload string, chatID int64) error { + chat, err := flow.chatStorage.GetChatByID(chatID) if err != nil { + flow.l.Error(err) return err } - if len(message.Text) > 7 { - payload := message.Text[7:] - if !flow.contains(payload, settings.Payload) { - settings.Payload = append(settings.Payload, payload) - err = flow.settingsStorage.SetSettings(message.Chat.ID, settings) - if err != nil { - return err + if flow.contains(payload, chat.Settings.Payload) { + return nil + } + + chat.Settings.Payload = append(chat.Settings.Payload, payload) + return flow.chatStorage.UpdateSettings(chat.ID, chat.Settings) +} + +func (flow *StartFlow) ensureChatExists(message *core.Message, bot core.IBot) error { + if _, ok := flow.chatCache[message.Chat.ID]; !ok { + flow.chatCache[message.Chat.ID] = true + _, err := flow.chatStorage.GetChatByID(message.Chat.ID) + if err != nil { + if err.Error() == "record not found" { + settings := core.DefaultSettings() + return flow.chatStorage.CreateChat(message.Chat.ID, message.Chat.Title, message.Chat.Type, &settings) } + flow.l.Error(err) + return err } } - _, err = bot.SendText(flow.loc.I18n("start_welcome")) - return err + return nil } -func (flow *StartFlow) checkSettingsAndUserPresence(message *core.Message, bot core.IBot) error { +func (flow *StartFlow) ensureUserExists(message *core.Message, bot core.IBot) error { if _, ok := flow.settingsCache[message.Chat.ID]; !ok { flow.l.Infof("%+v %+v", message, message.Sender) flow.settingsCache[message.Chat.ID] = true @@ -64,7 +99,6 @@ func (flow *StartFlow) checkSettingsAndUserPresence(message *core.Message, bot c } if _, ok := flow.usersCache[message.Sender.ID]; !ok { - flow.l.Infof("%+v %+v", message, message.Sender) flow.usersCache[message.Sender.ID] = true _, err := flow.userStorage.GetUserById(message.Sender.ID) if err != nil { @@ -75,6 +109,7 @@ func (flow *StartFlow) checkSettingsAndUserPresence(message *core.Message, bot c return err } } + return nil } diff --git a/usecases/start_flow_test.go b/usecases/start_flow_test.go index 126c271..c7179e2 100644 --- a/usecases/start_flow_test.go +++ b/usecases/start_flow_test.go @@ -1,6 +1,7 @@ package usecases_test import ( + "sync" "testing" "github.com/ailinykh/pullanusbot/v2/core" @@ -9,41 +10,87 @@ import ( "github.com/stretchr/testify/assert" ) -func Test_HandleText_CreatesSettingsWithPayload(t *testing.T) { +func Test_HandleText_CreateUserData(t *testing.T) { logger := test_helpers.CreateLogger() loc := test_helpers.CreateLocalizer(map[string]string{}) settingsStorage := test_helpers.CreateSettingsStorage() + chatStorage := test_helpers.CreateChatStorage() userStorage := test_helpers.CreateUserStorage() - startFlow := usecases.CreateStartFlow(logger, loc, settingsStorage, userStorage) + startFlow := usecases.CreateStartFlow(logger, loc, settingsStorage, chatStorage, userStorage) - message := &core.Message{Text: "/start payload", ChatID: 1488} bot := test_helpers.CreateBot() - startFlow.HandleText(message, bot) + messages := []string{ + "/start", + "/start payload", + "/start another_payload", + } + wg := sync.WaitGroup{} - expectedSettings := core.DefaultSettings() - expectedSettings.Payload = []string{"payload"} - assert.Equal(t, map[int64]*core.Settings{1488: &expectedSettings}, settingsStorage.Data) + for _, message := range messages { + wg.Add(1) + go func(text string) { + startFlow.HandleText(makeMessage(text), bot) + wg.Done() + }(message) + } + + wg.Wait() + + assert.Equal(t, 1, len(userStorage.Users)) + + message := makeMessage("/start") + user, _ := userStorage.GetUserById(message.Sender.ID) + assert.Equal(t, message.Sender, user) } -func Test_HandleText_MergePayloadInSettings(t *testing.T) { +func Test_HandleText_CreateChatData(t *testing.T) { logger := test_helpers.CreateLogger() loc := test_helpers.CreateLocalizer(map[string]string{}) settingsStorage := test_helpers.CreateSettingsStorage() + chatStorage := test_helpers.CreateChatStorage() userStorage := test_helpers.CreateUserStorage() - startFlow := usecases.CreateStartFlow(logger, loc, settingsStorage, userStorage) + startFlow := usecases.CreateStartFlow(logger, loc, settingsStorage, chatStorage, userStorage) - initialSettings := core.DefaultSettings() - initialSettings.Payload = []string{"payload"} - settingsStorage.SetSettings(1488, &initialSettings) bot := test_helpers.CreateBot() - startFlow.HandleText(&core.Message{Text: "/start another_payload", ChatID: 1488}, bot) + messages := []string{ + "/start", + "/start payload", + "/start another_payload", + } + wg := sync.WaitGroup{} + + for _, message := range messages { + wg.Add(1) + go func(text string) { + startFlow.HandleText(makeMessage(text), bot) + wg.Done() + }(message) + } + + wg.Wait() - expectedSettings := core.DefaultSettings() - expectedSettings.Payload = []string{"payload", "another_payload"} - assert.Equal(t, map[int64]*core.Settings{1488: &expectedSettings}, settingsStorage.Data) + assert.Equal(t, 1, len(chatStorage.Chats)) + + message := makeMessage("/start") + chat, _ := chatStorage.GetChatByID(message.Chat.ID) + assert.Equal(t, true, contains("payload", chat.Settings.Payload)) + assert.Equal(t, true, contains("another_payload", chat.Settings.Payload)) +} + +func makeMessage(text string) *core.Message { + settings := core.DefaultSettings() + chat := core.Chat{ID: 1488, Title: "Paul Durov", Type: "private", Settings: &settings} + sender := core.User{ID: 1, FirstName: "Paul", LastName: "Durov"} + return &core.Message{Text: text, Chat: &chat, Sender: &sender} +} - startFlow.HandleText(&core.Message{Text: "/start payload", ChatID: 1488}, bot) - assert.Equal(t, map[int64]*core.Settings{1488: &expectedSettings}, settingsStorage.Data) +func contains(message string, messages []string) bool { + for _, m := range messages { + if m == message { + return true + } + } + return false } From ebdb47d7a494947a9785eab633b3e0be38e1f5b0 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Tue, 22 Mar 2022 15:14:08 +0300 Subject: [PATCH 336/439] Remove `ISettingsStorage` from `StartFlow` --- pullanusbot.go | 3 ++- usecases/start_flow.go | 31 ++++++++++--------------------- usecases/start_flow_test.go | 6 ++---- 3 files changed, 14 insertions(+), 26 deletions(-) diff --git a/pullanusbot.go b/pullanusbot.go index 027dbcc..e640e8d 100644 --- a/pullanusbot.go +++ b/pullanusbot.go @@ -95,8 +95,9 @@ func main() { telebot.AddHandler(removeReelsSourceDecorator) commonLocalizer := infrastructure.CreateCommonLocalizer() + chatStorage := infrastructure.CreateChatStorage(dbFile, logger) userStorage := infrastructure.CreateUserStorage(dbFile, logger) - startFlow := usecases.CreateStartFlow(logger, commonLocalizer, settingsStorage, userStorage) + startFlow := usecases.CreateStartFlow(logger, commonLocalizer, chatStorage, userStorage) telebot.AddHandler(startFlow) // Start endless loop telebot.Run() diff --git a/usecases/start_flow.go b/usecases/start_flow.go index c711331..b0882b1 100644 --- a/usecases/start_flow.go +++ b/usecases/start_flow.go @@ -7,20 +7,19 @@ import ( "github.com/ailinykh/pullanusbot/v2/core" ) -func CreateStartFlow(l core.ILogger, loc core.ILocalizer, settingsStorage core.ISettingsStorage, chatStorage core.IChatStorage, userStorage core.IUserStorage) core.ITextHandler { - return &StartFlow{l, loc, settingsStorage, chatStorage, userStorage, make(map[int64]bool), make(map[int]bool), make(map[int64]bool), sync.Mutex{}} +func CreateStartFlow(l core.ILogger, loc core.ILocalizer, chatStorage core.IChatStorage, userStorage core.IUserStorage) core.ITextHandler { + return &StartFlow{l, loc, chatStorage, userStorage, make(map[int64]bool), make(map[int]bool), make(map[int64]bool), sync.Mutex{}} } type StartFlow struct { - l core.ILogger - loc core.ILocalizer - settingsStorage core.ISettingsStorage - chatStorage core.IChatStorage - userStorage core.IUserStorage - settingsCache map[int64]bool - usersCache map[core.UserID]bool - chatCache map[int64]bool - lock sync.Mutex + l core.ILogger + loc core.ILocalizer + chatStorage core.IChatStorage + userStorage core.IUserStorage + settingsCache map[int64]bool + usersCache map[core.UserID]bool + chatCache map[int64]bool + lock sync.Mutex } // HandleText is a core.ITextHandler protocol implementation @@ -88,16 +87,6 @@ func (flow *StartFlow) ensureChatExists(message *core.Message, bot core.IBot) er } func (flow *StartFlow) ensureUserExists(message *core.Message, bot core.IBot) error { - if _, ok := flow.settingsCache[message.Chat.ID]; !ok { - flow.l.Infof("%+v %+v", message, message.Sender) - flow.settingsCache[message.Chat.ID] = true - _, err := flow.settingsStorage.GetSettings(message.Chat.ID) // create settings if needed - if err != nil { - flow.l.Error(err) - return err - } - } - if _, ok := flow.usersCache[message.Sender.ID]; !ok { flow.usersCache[message.Sender.ID] = true _, err := flow.userStorage.GetUserById(message.Sender.ID) diff --git a/usecases/start_flow_test.go b/usecases/start_flow_test.go index c7179e2..b65b169 100644 --- a/usecases/start_flow_test.go +++ b/usecases/start_flow_test.go @@ -13,10 +13,9 @@ import ( func Test_HandleText_CreateUserData(t *testing.T) { logger := test_helpers.CreateLogger() loc := test_helpers.CreateLocalizer(map[string]string{}) - settingsStorage := test_helpers.CreateSettingsStorage() chatStorage := test_helpers.CreateChatStorage() userStorage := test_helpers.CreateUserStorage() - startFlow := usecases.CreateStartFlow(logger, loc, settingsStorage, chatStorage, userStorage) + startFlow := usecases.CreateStartFlow(logger, loc, chatStorage, userStorage) bot := test_helpers.CreateBot() @@ -47,10 +46,9 @@ func Test_HandleText_CreateUserData(t *testing.T) { func Test_HandleText_CreateChatData(t *testing.T) { logger := test_helpers.CreateLogger() loc := test_helpers.CreateLocalizer(map[string]string{}) - settingsStorage := test_helpers.CreateSettingsStorage() chatStorage := test_helpers.CreateChatStorage() userStorage := test_helpers.CreateUserStorage() - startFlow := usecases.CreateStartFlow(logger, loc, settingsStorage, chatStorage, userStorage) + startFlow := usecases.CreateStartFlow(logger, loc, chatStorage, userStorage) bot := test_helpers.CreateBot() From baa29429b528ed6cb765df7edab386bd689763af Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Tue, 22 Mar 2022 15:29:56 +0300 Subject: [PATCH 337/439] More traces in `UserStorage` create user method --- infrastructure/user_storage.go | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/infrastructure/user_storage.go b/infrastructure/user_storage.go index 50a2e98..aac0f95 100644 --- a/infrastructure/user_storage.go +++ b/infrastructure/user_storage.go @@ -52,12 +52,19 @@ func (storage *UserStorage) GetUserById(userID core.UserID) (*core.User, error) // CreateUser is a core.IUserStorage interface implementation func (storage *UserStorage) CreateUser(user *core.User) error { - res := storage.conn.Create(User{ + u := User{ UserID: user.ID, FirstName: user.FirstName, LastName: user.LastName, Username: user.Username, LanguageCode: user.LanguageCode, - }) - return res.Error + } + err := storage.conn.Create(&u).Error + if err != nil { + storage.l.Error(err) + return err + } + + storage.l.Infof("user created: %v", user) + return nil } From efff742a317e969ec05df0e4c77f81abd54f3d80 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Tue, 22 Mar 2022 15:30:41 +0300 Subject: [PATCH 338/439] Fix update settings method logic in `ChatStorage` --- infrastructure/chat_storage.go | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/infrastructure/chat_storage.go b/infrastructure/chat_storage.go index 15380b2..c14b9d5 100644 --- a/infrastructure/chat_storage.go +++ b/infrastructure/chat_storage.go @@ -62,13 +62,11 @@ func (s *ChatStorage) GetChatByID(chatID int64) (*core.Chat, error) { // CreateChat is a core.IChatStorage interface implementation func (s *ChatStorage) CreateChat(chatID int64, title string, type_ string, settings *core.Settings) error { data, err := json.Marshal(&settings) - if err != nil { s.l.Error(err) return err } - s.l.Infof("creating chat id: %d, title: %s, type: %s, data: %s", chatID, title, type_, data) chat := Chat{ID: chatID, Title: title, Type: type_, Settings: data} err = s.conn.Create(&chat).Error if err != nil { @@ -76,7 +74,7 @@ func (s *ChatStorage) CreateChat(chatID int64, title string, type_ string, setti return err } - s.l.Info("chat created: %+v", chat) + s.l.Infof("chat created: {%d %s %s}", chat.ID, chat.Title, chat.Type) return nil } @@ -86,6 +84,18 @@ func (s *ChatStorage) UpdateSettings(chatID int64, settings *core.Settings) erro if err != nil { return err } - chat.Settings = settings - return s.conn.Save(&chat).Error + + data, err := json.Marshal(&settings) + if err != nil { + s.l.Error(err) + return err + } + + c := Chat{ + ID: chat.ID, + Title: chat.Title, + Type: chat.Type, + Settings: data, + } + return s.conn.Save(&c).Error } From 9c6b78a9bdc4915180cfa0ac92e331ae78605982 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Tue, 22 Mar 2022 18:10:07 +0300 Subject: [PATCH 339/439] Remove settingsCache from `StartFlow` since it deprecated --- usecases/start_flow.go | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/usecases/start_flow.go b/usecases/start_flow.go index b0882b1..4ea5cb2 100644 --- a/usecases/start_flow.go +++ b/usecases/start_flow.go @@ -8,18 +8,17 @@ import ( ) func CreateStartFlow(l core.ILogger, loc core.ILocalizer, chatStorage core.IChatStorage, userStorage core.IUserStorage) core.ITextHandler { - return &StartFlow{l, loc, chatStorage, userStorage, make(map[int64]bool), make(map[int]bool), make(map[int64]bool), sync.Mutex{}} + return &StartFlow{l, loc, chatStorage, userStorage, make(map[int]bool), make(map[int64]bool), sync.Mutex{}} } type StartFlow struct { - l core.ILogger - loc core.ILocalizer - chatStorage core.IChatStorage - userStorage core.IUserStorage - settingsCache map[int64]bool - usersCache map[core.UserID]bool - chatCache map[int64]bool - lock sync.Mutex + l core.ILogger + loc core.ILocalizer + chatStorage core.IChatStorage + userStorage core.IUserStorage + usersCache map[core.UserID]bool + chatCache map[int64]bool + lock sync.Mutex } // HandleText is a core.ITextHandler protocol implementation From ae796c9a2c541930ec4bd5403fbbace5a0baaaad Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Thu, 24 Mar 2022 19:47:34 +0300 Subject: [PATCH 340/439] Refactor `start_flow` methods to more plain signatures --- usecases/start_flow.go | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/usecases/start_flow.go b/usecases/start_flow.go index 4ea5cb2..d49e4df 100644 --- a/usecases/start_flow.go +++ b/usecases/start_flow.go @@ -26,13 +26,13 @@ func (flow *StartFlow) HandleText(message *core.Message, bot core.IBot) error { flow.lock.Lock() defer flow.lock.Unlock() - err := flow.ensureUserExists(message, bot) + err := flow.ensureUserExists(message.Sender) if err != nil { flow.l.Error(err) //Do not return? } - err = flow.ensureChatExists(message, bot) + err = flow.ensureChatExists(message.Chat) if err != nil { flow.l.Error(err) //Do not return? @@ -69,14 +69,14 @@ func (flow *StartFlow) handlePayload(payload string, chatID int64) error { return flow.chatStorage.UpdateSettings(chat.ID, chat.Settings) } -func (flow *StartFlow) ensureChatExists(message *core.Message, bot core.IBot) error { - if _, ok := flow.chatCache[message.Chat.ID]; !ok { - flow.chatCache[message.Chat.ID] = true - _, err := flow.chatStorage.GetChatByID(message.Chat.ID) +func (flow *StartFlow) ensureChatExists(chat *core.Chat) error { + if _, ok := flow.chatCache[chat.ID]; !ok { + flow.chatCache[chat.ID] = true + _, err := flow.chatStorage.GetChatByID(chat.ID) if err != nil { if err.Error() == "record not found" { settings := core.DefaultSettings() - return flow.chatStorage.CreateChat(message.Chat.ID, message.Chat.Title, message.Chat.Type, &settings) + return flow.chatStorage.CreateChat(chat.ID, chat.Title, chat.Type, &settings) } flow.l.Error(err) return err @@ -85,13 +85,13 @@ func (flow *StartFlow) ensureChatExists(message *core.Message, bot core.IBot) er return nil } -func (flow *StartFlow) ensureUserExists(message *core.Message, bot core.IBot) error { - if _, ok := flow.usersCache[message.Sender.ID]; !ok { - flow.usersCache[message.Sender.ID] = true - _, err := flow.userStorage.GetUserById(message.Sender.ID) +func (flow *StartFlow) ensureUserExists(user *core.User) error { + if _, ok := flow.usersCache[user.ID]; !ok { + flow.usersCache[user.ID] = true + _, err := flow.userStorage.GetUserById(user.ID) if err != nil { if err.Error() == "record not found" { - return flow.userStorage.CreateUser(message.Sender) + return flow.userStorage.CreateUser(user) } flow.l.Error(err) return err From 05e993c786810dad7ddfa707fdb1503411c8611d Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Thu, 24 Mar 2022 19:51:06 +0300 Subject: [PATCH 341/439] Refactor `Makefile` and move executable file to `bin` directory --- .dockerignore | 3 ++- .gitignore | 1 + Dockerfile | 2 +- Makefile | 32 +++++++++++++++++++++++++------- 4 files changed, 29 insertions(+), 9 deletions(-) diff --git a/.dockerignore b/.dockerignore index 6320cd2..f677037 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1 +1,2 @@ -data \ No newline at end of file +data +bin \ No newline at end of file diff --git a/.gitignore b/.gitignore index 850426b..9ce2384 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ *.db coverage.txt .env +bin # Test binary, built with `go test -c` *.test diff --git a/Dockerfile b/Dockerfile index ddcfb84..2692f01 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,7 +12,7 @@ RUN apk update && apk add tzdata python3 --no-cache && \ wget https://yt-dl.org/downloads/latest/youtube-dl -O /usr/local/bin/youtube-dl && \ chmod a+rx /usr/local/bin/youtube-dl && \ ln -s /usr/bin/python3 /usr/bin/python -COPY --from=builder /go/src/github.com/ailinykh/pullanusbot/pullanusbot /usr/local/bin/pullanusbot +COPY --from=builder /go/src/github.com/ailinykh/pullanusbot/bin/pullanusbot /usr/local/bin/pullanusbot WORKDIR /usr/local/share VOLUME [ "/usr/local/share/pullanusbot-data" ] ENTRYPOINT pullanusbot \ No newline at end of file diff --git a/Makefile b/Makefile index caa454c..f574421 100644 --- a/Makefile +++ b/Makefile @@ -1,17 +1,35 @@ -include .env -.PHONY: test run build clean +.PHONY: all serve kill run test build clean restart -all: build run +APP ?= bin/pullanusbot +PID = $(APP).pid +GO_FILES = $(wildcard *.go) -run: - ./pullanusbot +all: serve + +before: + @echo "🛠 rebuilding an app..." + +serve: run + @fswatch -x -o --event Created --event Updated --event Renamed -r -e '.*' -i '\.go$$' . | xargs -n1 -I{} make restart || make kill + +$(APP): $(GO_FILES) + @go build $? -o $@ + +kill: + @kill `cat $(PID)` || true + +run: build + @$(APP) & echo $$! > $(PID) test: GO_ENV=testing go test ./... -v -coverprofile=coverage.txt -race -covermode=atomic -build: clean *.go - go build . +build: $(GO_FILES) + @go build -o $(APP) . clean: - rm -f pullanusbot \ No newline at end of file + rm -f $(APP) + +restart: kill before build run \ No newline at end of file From b4b9ad3af11687e9350fb8f477df81167a5529ac Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Thu, 24 Mar 2022 22:07:23 +0300 Subject: [PATCH 342/439] Upgrade `go` version up to 1.17 --- .github/workflows/build.yml | 8 ++++---- go.mod | 13 ++++++++++++- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 24eb6e5..392a296 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,11 +12,11 @@ jobs: ACTIONS_ALLOW_UNSECURE_COMMANDS: true steps: - - name: Set up Go 1.15 - uses: actions/setup-go@v1 + - name: Set up Go 1.17 + uses: actions/setup-go@v2 with: - go-version: 1.15 - id: go + go-version: 1.17 + run: go version - name: Check out code into the Go module directory uses: actions/checkout@v1 diff --git a/go.mod b/go.mod index 0e92d38..3d66234 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/ailinykh/pullanusbot/v2 -go 1.16 +go 1.17 require ( github.com/google/logger v1.1.0 @@ -11,3 +11,14 @@ require ( gorm.io/driver/sqlite v1.1.3 gorm.io/gorm v1.20.2 ) + +require ( + github.com/davecgh/go-spew v1.1.0 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.1 // indirect + github.com/mattn/go-sqlite3 v1.14.3 // indirect + github.com/pkg/errors v0.8.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a // indirect + gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect +) From 96a5bfaad7e12e52e0b0669632ea103998c868a3 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Thu, 24 Mar 2022 22:12:20 +0300 Subject: [PATCH 343/439] Rollback Dockerfile `/bin/` path --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 2692f01..ddcfb84 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,7 +12,7 @@ RUN apk update && apk add tzdata python3 --no-cache && \ wget https://yt-dl.org/downloads/latest/youtube-dl -O /usr/local/bin/youtube-dl && \ chmod a+rx /usr/local/bin/youtube-dl && \ ln -s /usr/bin/python3 /usr/bin/python -COPY --from=builder /go/src/github.com/ailinykh/pullanusbot/bin/pullanusbot /usr/local/bin/pullanusbot +COPY --from=builder /go/src/github.com/ailinykh/pullanusbot/pullanusbot /usr/local/bin/pullanusbot WORKDIR /usr/local/share VOLUME [ "/usr/local/share/pullanusbot-data" ] ENTRYPOINT pullanusbot \ No newline at end of file From 95371dd8adabd8a8c7fa35582bd990935fa1f3ed Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Thu, 24 Mar 2022 22:14:24 +0300 Subject: [PATCH 344/439] Update build.yml --- .github/workflows/build.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 24eb6e5..724b69c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,11 +12,13 @@ jobs: ACTIONS_ALLOW_UNSECURE_COMMANDS: true steps: - - name: Set up Go 1.15 - uses: actions/setup-go@v1 + - name: Set up Go 1.17 + uses: actions/setup-go@v2 with: - go-version: 1.15 - id: go + go-version: 1.17 + + - name: Print go version + run: go version - name: Check out code into the Go module directory uses: actions/checkout@v1 From 23e51775f72e7d118cf6afbf9ab128a7a41f0297 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Thu, 24 Mar 2022 22:17:14 +0300 Subject: [PATCH 345/439] revert build.yaml --- .github/workflows/build.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 392a296..24eb6e5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,11 +12,11 @@ jobs: ACTIONS_ALLOW_UNSECURE_COMMANDS: true steps: - - name: Set up Go 1.17 - uses: actions/setup-go@v2 + - name: Set up Go 1.15 + uses: actions/setup-go@v1 with: - go-version: 1.17 - run: go version + go-version: 1.15 + id: go - name: Check out code into the Go module directory uses: actions/checkout@v1 From 528a8489cfedd1bea65c35a33e378104f1f78945 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Thu, 24 Mar 2022 22:56:36 +0300 Subject: [PATCH 346/439] Upgrade all `go.mod` depeendencies --- go.mod | 23 ++++++++++++----------- go.sum | 27 +++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 11 deletions(-) diff --git a/go.mod b/go.mod index 3d66234..0ff0052 100644 --- a/go.mod +++ b/go.mod @@ -3,22 +3,23 @@ module github.com/ailinykh/pullanusbot/v2 go 1.17 require ( - github.com/google/logger v1.1.0 + github.com/google/logger v1.1.1 github.com/google/uuid v1.3.0 github.com/streadway/amqp v1.0.0 - github.com/stretchr/testify v1.7.0 - gopkg.in/tucnak/telebot.v2 v2.3.4 - gorm.io/driver/sqlite v1.1.3 - gorm.io/gorm v1.20.2 + github.com/stretchr/testify v1.7.1 + gopkg.in/tucnak/telebot.v2 v2.5.0 + gorm.io/driver/sqlite v1.3.1 + gorm.io/gorm v1.23.3 ) require ( - github.com/davecgh/go-spew v1.1.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/jinzhu/inflection v1.0.0 // indirect - github.com/jinzhu/now v1.1.1 // indirect - github.com/mattn/go-sqlite3 v1.14.3 // indirect - github.com/pkg/errors v0.8.1 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/mattn/go-sqlite3 v1.14.12 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a // indirect - gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect + github.com/stretchr/objx v0.1.0 // indirect + golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8 // indirect + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect ) diff --git a/go.sum b/go.sum index 93f86e3..6a1b9a5 100644 --- a/go.sum +++ b/go.sum @@ -1,36 +1,63 @@ github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/google/logger v1.1.0 h1:saB74Etb4EAJNH3z74CVbCKk75hld/8T0CsXKetWCwM= github.com/google/logger v1.1.0/go.mod h1:w7O8nrRr0xufejBlQMI83MXqRusvREoJdaAxV+CoAB4= +github.com/google/logger v1.1.1 h1:+6Z2geNxc9G+4D4oDO9njjjn2d0wN5d7uOo0vOIW1NQ= +github.com/google/logger v1.1.1/go.mod h1:BkeJZ+1FhQ+/d087r4dzojEg1u2ZX+ZqG1jTUrLM+zQ= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.1 h1:g39TucaRWyV3dwDO++eEc6qf8TVIQ/Da48WmqjZ3i7E= github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/mattn/go-sqlite3 v1.14.3 h1:j7a/xn1U6TKA/PHHxqZuzh64CdtRc7rU9M+AvkOl5bA= github.com/mattn/go-sqlite3 v1.14.3/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI= +github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mattn/go-sqlite3 v1.14.12 h1:TJ1bhYJPV44phC+IMu1u2K/i5RriLTPe+yc68XDJ1Z0= +github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/streadway/amqp v1.0.0 h1:kuuDrUJFZL1QYL9hUNuCxNObNzB0bV/ZG5jV3RWAQgo= github.com/streadway/amqp v1.0.0/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8 h1:OH54vjqzRWmbJ62fjuhxy7AxFFgoHN0/DPc/UrL8cAs= +golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/tucnak/telebot.v2 v2.3.4 h1:LtZ1hahdWDYFX723PlkLDMo56p99uMzrvBL9BRhyNy4= gopkg.in/tucnak/telebot.v2 v2.3.4/go.mod h1:t+KVAiqFsG9ZDF0hz1ZPFTyENtlrDrDS3qmRRqhICBg= +gopkg.in/tucnak/telebot.v2 v2.5.0 h1:i+NynLo443Vp+Zn3Gv9JBjh3Z/PaiKAQwcnhNI7y6Po= +gopkg.in/tucnak/telebot.v2 v2.5.0/go.mod h1:BgaIIx50PSRS9pG59JH+geT82cfvoJU/IaI5TJdN3v8= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/driver/sqlite v1.1.3 h1:BYfdVuZB5He/u9dt4qDpZqiqDJ6KhPqs5QUqsr/Eeuc= gorm.io/driver/sqlite v1.1.3/go.mod h1:AKDgRWk8lcSQSw+9kxCJnX/yySj8G3rdwYlU57cB45c= +gorm.io/driver/sqlite v1.3.1 h1:bwfE+zTEWklBYoEodIOIBwuWHpnx52Z9zJFW5F33WLk= +gorm.io/driver/sqlite v1.3.1/go.mod h1:wJx0hJspfycZ6myN38x1O/AqLtNS6c5o9TndewFbELg= gorm.io/gorm v1.20.1/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= gorm.io/gorm v1.20.2 h1:bZzSEnq7NDGsrd+n3evOOedDrY5oLM5QPlCjZJUK2ro= gorm.io/gorm v1.20.2/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= +gorm.io/gorm v1.23.1/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= +gorm.io/gorm v1.23.3 h1:jYh3nm7uLZkrMVfA8WVNjDZryKfr7W+HTlInVgKFJAg= +gorm.io/gorm v1.23.3/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= From 2d1073d8062ce421042c3c43b69b6d60125e7ae9 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Thu, 24 Mar 2022 22:58:32 +0300 Subject: [PATCH 347/439] Change User ID from `int` to `int64` --- api/telebot_adapter.go | 4 ++-- core/user.go | 4 +--- core/user_storage.go | 2 +- infrastructure/game.go | 4 ++-- infrastructure/user_storage.go | 4 ++-- test_helpers/user_storage.go | 6 +++--- usecases/faggot_game.go | 2 +- usecases/faggot_game_test.go | 5 +++-- usecases/faggot_stat.go | 2 +- usecases/ouline_vpn_facade.go | 2 +- usecases/start_flow.go | 4 ++-- 11 files changed, 19 insertions(+), 20 deletions(-) diff --git a/api/telebot_adapter.go b/api/telebot_adapter.go index a51dc68..589f320 100644 --- a/api/telebot_adapter.go +++ b/api/telebot_adapter.go @@ -133,17 +133,17 @@ func (a *TelebotAdapter) SendMedia(media *core.Media) (*core.Message, error) { func (a *TelebotAdapter) SendPhotoAlbum(medias []*core.Media) ([]*core.Message, error) { var photo *tb.Photo var album = tb.Album{} + opts := &tb.SendOptions{ParseMode: tb.ModeHTML, DisableWebPagePreview: true} for i, m := range medias { photo = &tb.Photo{File: tb.FromURL(m.ResourceURL)} if i == len(medias)-1 { photo.Caption = m.Caption - photo.ParseMode = tb.ModeHTML } album = append(album, photo) } - sent, err := a.t.bot.SendAlbum(a.m.Chat, album) + sent, err := a.t.bot.SendAlbum(a.m.Chat, album, opts) if err != nil { return nil, err } diff --git a/core/user.go b/core/user.go index 86be263..723cccb 100644 --- a/core/user.go +++ b/core/user.go @@ -1,10 +1,8 @@ package core -type UserID = int - // User ... type User struct { - ID UserID + ID int64 FirstName string LastName string Username string diff --git a/core/user_storage.go b/core/user_storage.go index 2b7671a..22fa54b 100644 --- a/core/user_storage.go +++ b/core/user_storage.go @@ -1,6 +1,6 @@ package core type IUserStorage interface { - GetUserById(UserID) (*User, error) + GetUserById(int64) (*User, error) CreateUser(*User) error } diff --git a/infrastructure/game.go b/infrastructure/game.go index c2e122a..d16c01e 100644 --- a/infrastructure/game.go +++ b/infrastructure/game.go @@ -3,7 +3,7 @@ package infrastructure // Player that can be persistent on disk type Player struct { GameID int64 `gorm:"primaryKey"` - UserID int `gorm:"primaryKey"` + UserID int64 `gorm:"primaryKey"` FirstName string LastName string Username string @@ -18,7 +18,7 @@ func (Player) TableName() string { // Round that can be persistent on disk type Round struct { GameID int64 - UserID int + UserID int64 Day string `gorm:"primaryKey"` Username string } diff --git a/infrastructure/user_storage.go b/infrastructure/user_storage.go index aac0f95..e1bfa43 100644 --- a/infrastructure/user_storage.go +++ b/infrastructure/user_storage.go @@ -26,7 +26,7 @@ type UserStorage struct { // User type User struct { - UserID int `gorm:"primaryKey"` + UserID int64 `gorm:"primaryKey"` FirstName string LastName string Username string @@ -34,7 +34,7 @@ type User struct { } // GetUserById is a core.IUserStorage interface implementation -func (storage *UserStorage) GetUserById(userID core.UserID) (*core.User, error) { +func (storage *UserStorage) GetUserById(userID int64) (*core.User, error) { var user User res := storage.conn.First(&user, userID) diff --git a/test_helpers/user_storage.go b/test_helpers/user_storage.go index ef77b4f..7fd1788 100644 --- a/test_helpers/user_storage.go +++ b/test_helpers/user_storage.go @@ -7,16 +7,16 @@ import ( ) func CreateUserStorage() *FakeUserStorage { - return &FakeUserStorage{make(map[int]*core.User), nil} + return &FakeUserStorage{make(map[int64]*core.User), nil} } type FakeUserStorage struct { - Users map[core.UserID]*core.User + Users map[int64]*core.User Err error } // GetUserById is a core.IUserStorage interface implementation -func (storage *FakeUserStorage) GetUserById(userID core.UserID) (*core.User, error) { +func (storage *FakeUserStorage) GetUserById(userID int64) (*core.User, error) { if user, ok := storage.Users[userID]; ok { return user, nil } diff --git a/usecases/faggot_game.go b/usecases/faggot_game.go index 2e6a934..76ece0f 100644 --- a/usecases/faggot_game.go +++ b/usecases/faggot_game.go @@ -182,7 +182,7 @@ func (flow *GameFlow) Stats(message *core.Message, bot core.IBot) error { year := strconv.Itoa(time.Now().Year()) rounds, _ := flow.s.GetRounds(message.Chat.ID) entries := []Stat{} - players := map[int]bool{} + players := map[int64]bool{} for _, r := range rounds { players[r.Winner.ID] = true diff --git a/usecases/faggot_game_test.go b/usecases/faggot_game_test.go index 9372cef..3d9ef81 100644 --- a/usecases/faggot_game_test.go +++ b/usecases/faggot_game_test.go @@ -218,7 +218,8 @@ func Test_Stats_RespondsOnlyForTop10Players(t *testing.T) { } var messages []*core.Message - for i := 1; i < 100; i++ { + var i int64 + for i = 1; i < 100; i++ { messages = append(messages, makeGameMessage(i, fmt.Sprintf("Faggot%02d", i))) } @@ -289,7 +290,7 @@ func Test_Me_RespondsWithPersonalStat(t *testing.T) { // Helpers -func makeGameMessage(id int, username string) *core.Message { +func makeGameMessage(id int64, username string) *core.Message { player := &core.User{ ID: id, FirstName: "FirstName" + fmt.Sprint(id), diff --git a/usecases/faggot_stat.go b/usecases/faggot_stat.go index 13322f9..1983efe 100644 --- a/usecases/faggot_stat.go +++ b/usecases/faggot_stat.go @@ -11,7 +11,7 @@ type Stat struct { } // Find player by username in current stat -func Find(a []Stat, id int) int { +func Find(a []Stat, id int64) int { for i, n := range a { if id == n.Player.ID { return i diff --git a/usecases/ouline_vpn_facade.go b/usecases/ouline_vpn_facade.go index 0ccc0c3..9abf14d 100644 --- a/usecases/ouline_vpn_facade.go +++ b/usecases/ouline_vpn_facade.go @@ -56,7 +56,7 @@ func (facade *OutlineVpnFacade) CreateKey(chatID int64, title string) (*core.Vpn return nil, err } - user, err := facade.userStorage.GetUserById(int(chatID)) // should exist + user, err := facade.userStorage.GetUserById(chatID) // should exist if err != nil { facade.l.Error(err) return nil, err diff --git a/usecases/start_flow.go b/usecases/start_flow.go index d49e4df..c2751c7 100644 --- a/usecases/start_flow.go +++ b/usecases/start_flow.go @@ -8,7 +8,7 @@ import ( ) func CreateStartFlow(l core.ILogger, loc core.ILocalizer, chatStorage core.IChatStorage, userStorage core.IUserStorage) core.ITextHandler { - return &StartFlow{l, loc, chatStorage, userStorage, make(map[int]bool), make(map[int64]bool), sync.Mutex{}} + return &StartFlow{l, loc, chatStorage, userStorage, make(map[int64]bool), make(map[int64]bool), sync.Mutex{}} } type StartFlow struct { @@ -16,7 +16,7 @@ type StartFlow struct { loc core.ILocalizer chatStorage core.IChatStorage userStorage core.IUserStorage - usersCache map[core.UserID]bool + usersCache map[int64]bool chatCache map[int64]bool lock sync.Mutex } From d5c2f2543c6402cd8aff75e9e3b76a44cb13560a Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Fri, 25 Mar 2022 17:39:47 +0300 Subject: [PATCH 348/439] Upgrade `telebot` to version 3 --- api/telebot.go | 59 +++++++++++++++++++++++++++--------------- api/telebot_adapter.go | 2 +- api/telebot_factory.go | 4 +-- api/telebot_info.go | 8 +++--- go.mod | 4 +-- go.sum | 34 ++++++++++++++++++++++++ 6 files changed, 82 insertions(+), 29 deletions(-) diff --git a/api/telebot.go b/api/telebot.go index 78aca84..257c48f 100644 --- a/api/telebot.go +++ b/api/telebot.go @@ -11,7 +11,7 @@ import ( "github.com/ailinykh/pullanusbot/v2/core" "github.com/ailinykh/pullanusbot/v2/helpers" - tb "gopkg.in/tucnak/telebot.v2" + tb "gopkg.in/telebot.v3" ) // Telebot is a telegram API @@ -43,19 +43,28 @@ func CreateTelebot(token string, logger core.ILogger) *Telebot { telebot := &Telebot{bot, logger, []string{}, []core.ITextHandler{}, []core.IDocumentHandler{}, []core.IImageHandler{}, []core.IVideoHandler{}} - bot.Handle(tb.OnText, func(m *tb.Message) { + bot.Handle(tb.OnText, func(c tb.Context) error { + var err error + var m = c.Message() for _, h := range telebot.textHandlers { - err := h.HandleText(makeMessage(m), makeIBot(m, telebot)) - if err != nil && err.Error() != "not implemented" { - logger.Errorf("%T: %s", h, err) - telebot.reportError(m, err) + err = h.HandleText(makeMessage(m), makeIBot(m, telebot)) + if err != nil { + if err.Error() == "not implemented" { + err = nil // skip "not implemented" error + } else { + logger.Errorf("%T: %s", h, err) + telebot.reportError(m, err) + } } } + return err }) var mutex sync.Mutex - bot.Handle(tb.OnDocument, func(m *tb.Message) { + bot.Handle(tb.OnDocument, func(c tb.Context) error { + var err error + var m = c.Message() // TODO: inject `download` to get rid of MIME cheking if m.Document.MIME[:5] == "video" || m.Document.MIME == "image/gif" { mutex.Lock() @@ -67,14 +76,14 @@ func CreateTelebot(token string, logger core.ILogger) *Telebot { err := bot.Download(&m.Document.File, path) if err != nil { logger.Error(err) - return + return err } logger.Infof("Downloaded to %s", strings.ReplaceAll(path, os.TempDir(), "$TMPDIR/")) defer os.Remove(path) for _, h := range telebot.documentHandlers { - err := h.HandleDocument(&core.Document{ + err = h.HandleDocument(&core.Document{ File: core.File{Name: m.Document.FileName, Path: path}, MIME: m.Document.MIME, }, makeMessage(m), makeIBot(m, telebot)) @@ -84,10 +93,12 @@ func CreateTelebot(token string, logger core.ILogger) *Telebot { } } } + return err }) - bot.Handle(tb.OnPhoto, func(m *tb.Message) { - + bot.Handle(tb.OnPhoto, func(c tb.Context) error { + var err error + var m = c.Message() image := &core.Image{ ID: m.Photo.FileID, FileURL: m.Photo.FileURL, @@ -96,16 +107,18 @@ func CreateTelebot(token string, logger core.ILogger) *Telebot { } for _, h := range telebot.imageHandlers { - err := h.HandleImage(image, makeMessage(m), makeIBot(m, telebot)) + err = h.HandleImage(image, makeMessage(m), makeIBot(m, telebot)) if err != nil { logger.Errorf("%T: %s", h, err) telebot.reportError(m, err) } } + return err }) - bot.Handle(tb.OnVideo, func(m *tb.Message) { - + bot.Handle(tb.OnVideo, func(c tb.Context) error { + var err error + var m = c.Message() video := &core.Video{ ID: m.Video.FileID, Width: m.Video.Width, @@ -113,12 +126,13 @@ func CreateTelebot(token string, logger core.ILogger) *Telebot { } for _, h := range telebot.videoHandlers { - err := h.HandleImage(video, makeMessage(m), makeIBot(m, telebot)) + err = h.HandleImage(video, makeMessage(m), makeIBot(m, telebot)) if err != nil { logger.Errorf("%T: %s", h, err) telebot.reportError(m, err) } } + return err }) return telebot @@ -153,8 +167,9 @@ func (t *Telebot) AddHandler(handler ...interface{}) { case string: t.registerCommand(h) if f, ok := handler[1].(func(*core.Message, core.IBot) error); ok { - t.bot.Handle(h, func(m *tb.Message) { - f(makeMessage(m), &TelebotAdapter{m, t}) + t.bot.Handle(h, func(c tb.Context) error { + m := c.Message() + return f(makeMessage(m), &TelebotAdapter{m, t}) }) } else { panic("interface must implement func(*core.Message, core.IBot) error") @@ -165,13 +180,15 @@ func (t *Telebot) AddHandler(handler ...interface{}) { if h, ok := handler[0].(core.IButtonHandler); ok { for _, id := range h.GetButtonIds() { - t.bot.Handle("\f"+id, func(c *tb.Callback) { - err := h.ButtonPressed(c.Data, makeMessage(c.Message), makeIBot(c.Message, t)) + t.bot.Handle("\f"+id, func(c tb.Context) error { + m := c.Message() + cb := c.Callback() + err := h.ButtonPressed(cb.Data, makeMessage(m), makeIBot(m, t)) if err != nil { t.logger.Error(err) - t.reportError(c.Message, err) + t.reportError(m, err) } - t.bot.Respond(c, &tb.CallbackResponse{CallbackID: c.ID}) + return t.bot.Respond(cb, &tb.CallbackResponse{CallbackID: cb.ID}) }) } } diff --git a/api/telebot_adapter.go b/api/telebot_adapter.go index 589f320..9a2800a 100644 --- a/api/telebot_adapter.go +++ b/api/telebot_adapter.go @@ -4,7 +4,7 @@ import ( "fmt" "github.com/ailinykh/pullanusbot/v2/core" - tb "gopkg.in/tucnak/telebot.v2" + tb "gopkg.in/telebot.v3" ) // TelebotAdapter combines Telebot and core.IBot diff --git a/api/telebot_factory.go b/api/telebot_factory.go index 30cb86b..7d97346 100644 --- a/api/telebot_factory.go +++ b/api/telebot_factory.go @@ -2,7 +2,7 @@ package api import ( "github.com/ailinykh/pullanusbot/v2/core" - tb "gopkg.in/tucnak/telebot.v2" + tb "gopkg.in/telebot.v3" ) func makeTbMessage(m *core.Message) *tb.Message { @@ -32,7 +32,7 @@ func makeTbVideo(vf *core.Video, caption string) *tb.Video { video.Height = vf.Height video.Caption = caption video.Duration = vf.Duration - video.SupportsStreaming = true + video.Streaming = true video.Thumbnail = &tb.Photo{ File: tb.FromDisk(vf.Thumb.Path), Width: vf.Thumb.Width, diff --git a/api/telebot_info.go b/api/telebot_info.go index d98e880..de21666 100644 --- a/api/telebot_info.go +++ b/api/telebot_info.go @@ -4,12 +4,13 @@ import ( "fmt" "strings" - tb "gopkg.in/tucnak/telebot.v2" + tb "gopkg.in/telebot.v3" ) // SetupInfo ... func (t *Telebot) SetupInfo() { - t.bot.Handle("/info", func(m *tb.Message) { + t.bot.Handle("/info", func(c tb.Context) error { + m := c.Message() info := []string{ "💬 Chat", fmt.Sprintf("ID: *%d*", m.Chat.ID), @@ -47,6 +48,7 @@ func (t *Telebot) SetupInfo() { } } - t.bot.Send(m.Chat, strings.Join(info, "\n"), &tb.SendOptions{ParseMode: tb.ModeMarkdown}) + _, err := t.bot.Send(m.Chat, strings.Join(info, "\n"), &tb.SendOptions{ParseMode: tb.ModeMarkdown}) + return err }) } diff --git a/go.mod b/go.mod index 0ff0052..5c3547b 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/google/uuid v1.3.0 github.com/streadway/amqp v1.0.0 github.com/stretchr/testify v1.7.1 - gopkg.in/tucnak/telebot.v2 v2.5.0 + gopkg.in/telebot.v3 v3.0.0 gorm.io/driver/sqlite v1.3.1 gorm.io/gorm v1.23.3 ) @@ -19,7 +19,7 @@ require ( github.com/mattn/go-sqlite3 v1.14.12 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/stretchr/objx v0.1.0 // indirect + github.com/stretchr/objx v0.3.0 // indirect golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8 // indirect gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect ) diff --git a/go.sum b/go.sum index 6a1b9a5..b62939f 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,13 @@ github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= +github.com/goccy/go-yaml v1.9.5/go.mod h1:U/jl18uSupI5rdI2jmuCswEA2htH9eXfferR3KfscvA= github.com/google/logger v1.1.0 h1:saB74Etb4EAJNH3z74CVbCKk75hld/8T0CsXKetWCwM= github.com/google/logger v1.1.0/go.mod h1:w7O8nrRr0xufejBlQMI83MXqRusvREoJdaAxV+CoAB4= github.com/google/logger v1.1.1 h1:+6Z2geNxc9G+4D4oDO9njjjn2d0wN5d7uOo0vOIW1NQ= @@ -15,6 +22,12 @@ github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/ github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-sqlite3 v1.14.3 h1:j7a/xn1U6TKA/PHHxqZuzh64CdtRc7rU9M+AvkOl5bA= github.com/mattn/go-sqlite3 v1.14.3/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI= github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= @@ -26,22 +39,43 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/streadway/amqp v1.0.0 h1:kuuDrUJFZL1QYL9hUNuCxNObNzB0bV/ZG5jV3RWAQgo= github.com/streadway/amqp v1.0.0/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.3.0 h1:NGXK3lHquSN08v5vWalVI/L8XU9hdzE/G6xsrze47As= +github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8 h1:OH54vjqzRWmbJ62fjuhxy7AxFFgoHN0/DPc/UrL8cAs= golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/telebot.v3 v3.0.0 h1:UgHIiE/RdjoDi6nf4xACM7PU3TqiPVV9vvTydCEnrTo= +gopkg.in/telebot.v3 v3.0.0/go.mod h1:7rExV8/0mDDNu9epSrDm/8j22KLaActH1Tbee6YjzWg= gopkg.in/tucnak/telebot.v2 v2.3.4 h1:LtZ1hahdWDYFX723PlkLDMo56p99uMzrvBL9BRhyNy4= gopkg.in/tucnak/telebot.v2 v2.3.4/go.mod h1:t+KVAiqFsG9ZDF0hz1ZPFTyENtlrDrDS3qmRRqhICBg= gopkg.in/tucnak/telebot.v2 v2.5.0 h1:i+NynLo443Vp+Zn3Gv9JBjh3Z/PaiKAQwcnhNI7y6Po= From c2e864cefb8d949ac5531f563107a6537b46dc52 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Fri, 25 Mar 2022 22:26:52 +0300 Subject: [PATCH 349/439] Remove deprecacted `ISettingsStorage` protocol --- api/telebot.go | 54 +++++++++++++++++------------ api/telebot_adapter.go | 16 ++++----- core/settings.go | 5 --- pullanusbot.go | 16 ++++----- usecases/remove_source_decorator.go | 17 +++------ 5 files changed, 53 insertions(+), 55 deletions(-) diff --git a/api/telebot.go b/api/telebot.go index 257c48f..fec4b65 100644 --- a/api/telebot.go +++ b/api/telebot.go @@ -18,6 +18,7 @@ import ( type Telebot struct { bot *tb.Bot logger core.ILogger + coreFactory *CoreFactory commandHandlers []string textHandlers []core.ITextHandler documentHandlers []core.IDocumentHandler @@ -26,7 +27,7 @@ type Telebot struct { } // CreateTelebot is a default Telebot factory -func CreateTelebot(token string, logger core.ILogger) *Telebot { +func CreateTelebot(token string, logger core.ILogger, chatStorage core.IChatStorage) *Telebot { poller := tb.NewMiddlewarePoller(&tb.LongPoller{Timeout: 10 * time.Second}, func(upd *tb.Update) bool { return true }) @@ -41,13 +42,13 @@ func CreateTelebot(token string, logger core.ILogger) *Telebot { panic(err) } - telebot := &Telebot{bot, logger, []string{}, []core.ITextHandler{}, []core.IDocumentHandler{}, []core.IImageHandler{}, []core.IVideoHandler{}} + telebot := &Telebot{bot, logger, &CoreFactory{chatStorage: chatStorage}, []string{}, []core.ITextHandler{}, []core.IDocumentHandler{}, []core.IImageHandler{}, []core.IVideoHandler{}} bot.Handle(tb.OnText, func(c tb.Context) error { var err error var m = c.Message() for _, h := range telebot.textHandlers { - err = h.HandleText(makeMessage(m), makeIBot(m, telebot)) + err = h.HandleText(telebot.coreFactory.makeMessage(m), telebot.coreFactory.makeIBot(m, telebot)) if err != nil { if err.Error() == "not implemented" { err = nil // skip "not implemented" error @@ -86,7 +87,7 @@ func CreateTelebot(token string, logger core.ILogger) *Telebot { err = h.HandleDocument(&core.Document{ File: core.File{Name: m.Document.FileName, Path: path}, MIME: m.Document.MIME, - }, makeMessage(m), makeIBot(m, telebot)) + }, telebot.coreFactory.makeMessage(m), telebot.coreFactory.makeIBot(m, telebot)) if err != nil { logger.Errorf("%T: %s", h, err) telebot.reportError(m, err) @@ -107,7 +108,7 @@ func CreateTelebot(token string, logger core.ILogger) *Telebot { } for _, h := range telebot.imageHandlers { - err = h.HandleImage(image, makeMessage(m), makeIBot(m, telebot)) + err = h.HandleImage(image, telebot.coreFactory.makeMessage(m), telebot.coreFactory.makeIBot(m, telebot)) if err != nil { logger.Errorf("%T: %s", h, err) telebot.reportError(m, err) @@ -126,7 +127,7 @@ func CreateTelebot(token string, logger core.ILogger) *Telebot { } for _, h := range telebot.videoHandlers { - err = h.HandleImage(video, makeMessage(m), makeIBot(m, telebot)) + err = h.HandleImage(video, telebot.coreFactory.makeMessage(m), telebot.coreFactory.makeIBot(m, telebot)) if err != nil { logger.Errorf("%T: %s", h, err) telebot.reportError(m, err) @@ -152,7 +153,7 @@ func (t *Telebot) Download(image *core.Image) (*core.File, error) { } t.logger.Infof("image %s downloaded to %s", file.UniqueID, path) - return makeFile(name, path), nil + return t.coreFactory.makeFile(name, path), nil } // AddHandler register object as one of core.Handler's @@ -169,7 +170,7 @@ func (t *Telebot) AddHandler(handler ...interface{}) { if f, ok := handler[1].(func(*core.Message, core.IBot) error); ok { t.bot.Handle(h, func(c tb.Context) error { m := c.Message() - return f(makeMessage(m), &TelebotAdapter{m, t}) + return f(t.coreFactory.makeMessage(m), &TelebotAdapter{m, t}) }) } else { panic("interface must implement func(*core.Message, core.IBot) error") @@ -183,7 +184,7 @@ func (t *Telebot) AddHandler(handler ...interface{}) { t.bot.Handle("\f"+id, func(c tb.Context) error { m := c.Message() cb := c.Callback() - err := h.ButtonPressed(cb.Data, makeMessage(m), makeIBot(m, t)) + err := h.ButtonPressed(cb.Data, t.coreFactory.makeMessage(m), t.coreFactory.makeIBot(m, t)) if err != nil { t.logger.Error(err) t.reportError(m, err) @@ -219,31 +220,40 @@ func (t *Telebot) reportError(m *tb.Message, e error) { t.bot.Send(chat, e.Error(), opts) } -func makeMessage(m *tb.Message) *core.Message { +type CoreFactory struct { + chatStorage core.IChatStorage +} + +func (factory *CoreFactory) makeMessage(m *tb.Message) *core.Message { text := m.Text if m.Document != nil { text = m.Caption } message := &core.Message{ ID: m.ID, - Chat: makeChat(m.Chat), + Chat: factory.makeChat(m.Chat), IsPrivate: m.Private(), - Sender: makeUser(m.Sender), + Sender: factory.makeUser(m.Sender), Text: text, } if m.ReplyTo != nil { - message.ReplyTo = makeMessage(m.ReplyTo) + message.ReplyTo = factory.makeMessage(m.ReplyTo) } if m.Video != nil { - message.Video = makeVideo(m.Video) + message.Video = factory.makeVideo(m.Video) } return message } -func makeChat(c *tb.Chat) *core.Chat { +func (factory *CoreFactory) makeChat(c *tb.Chat) *core.Chat { + settings := core.DefaultSettings() + chat, err := factory.chatStorage.GetChatByID(c.ID) + if err == nil { + settings = *chat.Settings + } title := c.Title if c.Type == tb.ChatPrivate { title = c.FirstName + " " + c.LastName @@ -252,11 +262,11 @@ func makeChat(c *tb.Chat) *core.Chat { ID: c.ID, Title: title, Type: string(c.Type), - Settings: nil, //TODO: obtain settings from database + Settings: &settings, } } -func makeUser(u *tb.User) *core.User { +func (CoreFactory) makeUser(u *tb.User) *core.User { return &core.User{ ID: u.ID, FirstName: u.FirstName, @@ -266,7 +276,7 @@ func makeUser(u *tb.User) *core.User { } } -func makeVideo(v *tb.Video) *core.Video { +func (factory *CoreFactory) makeVideo(v *tb.Video) *core.Video { return &core.Video{ File: core.File{ Name: v.FileName, @@ -278,11 +288,11 @@ func makeVideo(v *tb.Video) *core.Video { Bitrate: 0, Duration: v.Duration, Codec: "", - Thumb: makePhoto(v.Thumbnail), + Thumb: factory.makePhoto(v.Thumbnail), } } -func makePhoto(p *tb.Photo) *core.Image { +func (CoreFactory) makePhoto(p *tb.Photo) *core.Image { return &core.Image{ File: core.File{ Name: p.FileLocal, @@ -295,13 +305,13 @@ func makePhoto(p *tb.Photo) *core.Image { } } -func makeFile(name string, path string) *core.File { +func (CoreFactory) makeFile(name string, path string) *core.File { return &core.File{ Name: name, Path: path, } } -func makeIBot(m *tb.Message, t *Telebot) core.IBot { +func (CoreFactory) makeIBot(m *tb.Message, t *Telebot) core.IBot { return &TelebotAdapter{m, t} } diff --git a/api/telebot_adapter.go b/api/telebot_adapter.go index 9a2800a..827b695 100644 --- a/api/telebot_adapter.go +++ b/api/telebot_adapter.go @@ -32,7 +32,7 @@ func (a *TelebotAdapter) SendText(text string, params ...interface{}) (*core.Mes if err != nil { return nil, err } - return makeMessage(sent), err + return a.t.coreFactory.makeMessage(sent), err } // Delete is a core.IBot interface implementation @@ -49,7 +49,7 @@ func (a *TelebotAdapter) Edit(message *core.Message, what interface{}, options . if err != nil { return nil, err } - return makeMessage(m), nil + return a.t.coreFactory.makeMessage(m), nil case string: opts := &tb.SendOptions{ParseMode: tb.ModeHTML, DisableWebPagePreview: true} for _, opt := range options { @@ -64,7 +64,7 @@ func (a *TelebotAdapter) Edit(message *core.Message, what interface{}, options . if err != nil { return nil, err } - return makeMessage(m), nil + return a.t.coreFactory.makeMessage(m), nil default: } return nil, fmt.Errorf("not implemented") @@ -77,7 +77,7 @@ func (a *TelebotAdapter) SendImage(image *core.Image, caption string) (*core.Mes if err != nil { return nil, err } - return makeMessage(sent), err + return a.t.coreFactory.makeMessage(sent), err } // SendAlbum is a core.IBot interface implementation @@ -95,7 +95,7 @@ func (a *TelebotAdapter) SendAlbum(images []*core.Image) ([]*core.Message, error var messages []*core.Message for _, m := range sent { - messages = append(messages, makeMessage(&m)) + messages = append(messages, a.t.coreFactory.makeMessage(&m)) } return messages, err } @@ -126,7 +126,7 @@ func (a *TelebotAdapter) SendMedia(media *core.Media) (*core.Message, error) { if err != nil { return nil, err } - return makeMessage(sent), err + return a.t.coreFactory.makeMessage(sent), err } // SendPhotoAlbum is a core.IBot interface implementation @@ -150,7 +150,7 @@ func (a *TelebotAdapter) SendPhotoAlbum(medias []*core.Media) ([]*core.Message, var messages []*core.Message for _, m := range sent { - messages = append(messages, makeMessage(&m)) + messages = append(messages, a.t.coreFactory.makeMessage(&m)) } return messages, err } @@ -164,7 +164,7 @@ func (a *TelebotAdapter) SendVideo(vf *core.Video, caption string) (*core.Messag return nil, err } a.t.logger.Infof("%s successfully sent", vf.Name) - return makeMessage(sent), err + return a.t.coreFactory.makeMessage(sent), err } // IsUserMemberOfChat is a core.IBot interface implementation diff --git a/core/settings.go b/core/settings.go index 93b0c09..3673708 100644 --- a/core/settings.go +++ b/core/settings.go @@ -9,8 +9,3 @@ type Settings struct { func DefaultSettings() Settings { return Settings{false, []string{}, true} } - -type ISettingsStorage interface { - GetSettings(int64) (*Settings, error) - SetSettings(int64, *Settings) error -} diff --git a/pullanusbot.go b/pullanusbot.go index e640e8d..85be585 100644 --- a/pullanusbot.go +++ b/pullanusbot.go @@ -16,12 +16,13 @@ func main() { logger, close := createLogger() defer close() - telebot := api.CreateTelebot(os.Getenv("BOT_TOKEN"), logger) + dbFile := path.Join(getWorkingDir(), "pullanusbot.db") + + chatStorage := infrastructure.CreateChatStorage(dbFile, logger) + telebot := api.CreateTelebot(os.Getenv("BOT_TOKEN"), logger, chatStorage) telebot.SetupInfo() localizer := infrastructure.GameLocalizer{} - dbFile := path.Join(getWorkingDir(), "pullanusbot.db") - settingsStorage := infrastructure.CreateSettingsStorage(dbFile, logger) gameStorage := infrastructure.CreateGameStorage(dbFile) rand := infrastructure.CreateMathRand() gameFlow := usecases.CreateGameFlow(logger, localizer, gameStorage, rand) @@ -48,13 +49,13 @@ func main() { twitterFlow := usecases.CreateTwitterFlow(logger, twitterMediaFactory, localMediaSender) twitterTimeout := usecases.CreateTwitterTimeout(logger, twitterFlow) twitterParser := usecases.CreateTwitterParser(logger, twitterTimeout) - twitterRemoveSourceDecorator := usecases.CreateRemoveSourceDecorator(logger, twitterParser, settingsStorage) + twitterRemoveSourceDecorator := usecases.CreateRemoveSourceDecorator(logger, twitterParser) telebot.AddHandler(twitterRemoveSourceDecorator) httpClient := api.CreateHttpClient() convertMediaSender := helpers.CreateConvertMediaStrategy(logger, localMediaSender, fileDownloader, converter, converter) linkFlow := usecases.CreateLinkFlow(logger, httpClient, converter, convertMediaSender) - removeLinkSourceDecorator := usecases.CreateRemoveSourceDecorator(logger, linkFlow, settingsStorage) + removeLinkSourceDecorator := usecases.CreateRemoveSourceDecorator(logger, linkFlow) telebot.AddHandler(removeLinkSourceDecorator) tiktokHttpClient := api.CreateHttpClient() // domain specific headers and cookies @@ -78,7 +79,7 @@ func main() { sendVideoStrategy := helpers.CreateSendVideoStrategy(logger) sendVideoStrategySplitDecorator := helpers.CreateSendVideoStrategySplitDecorator(logger, sendVideoStrategy, converter) youtubeFlow := usecases.CreateYoutubeFlow(logger, youtubeAPI, youtubeAPI, sendVideoStrategySplitDecorator) - removeYoutubeSourceDecorator := usecases.CreateRemoveSourceDecorator(logger, youtubeFlow, settingsStorage) + removeYoutubeSourceDecorator := usecases.CreateRemoveSourceDecorator(logger, youtubeFlow) telebot.AddHandler(removeYoutubeSourceDecorator) telebot.AddHandler("/proxy", func(m *core.Message, bot core.IBot) error { @@ -91,11 +92,10 @@ func main() { reelsAPI := api.CreateInstagramMediaFactory(logger, path.Join(getWorkingDir(), "cookies.json")) reelsFlow := usecases.CreateReelsFlow(logger, reelsAPI, localMediaSender) - removeReelsSourceDecorator := usecases.CreateRemoveSourceDecorator(logger, reelsFlow, settingsStorage) + removeReelsSourceDecorator := usecases.CreateRemoveSourceDecorator(logger, reelsFlow) telebot.AddHandler(removeReelsSourceDecorator) commonLocalizer := infrastructure.CreateCommonLocalizer() - chatStorage := infrastructure.CreateChatStorage(dbFile, logger) userStorage := infrastructure.CreateUserStorage(dbFile, logger) startFlow := usecases.CreateStartFlow(logger, commonLocalizer, chatStorage, userStorage) telebot.AddHandler(startFlow) diff --git a/usecases/remove_source_decorator.go b/usecases/remove_source_decorator.go index 23f1ede..572ddb0 100644 --- a/usecases/remove_source_decorator.go +++ b/usecases/remove_source_decorator.go @@ -4,14 +4,13 @@ import ( "github.com/ailinykh/pullanusbot/v2/core" ) -func CreateRemoveSourceDecorator(l core.ILogger, decoratee core.ITextHandler, settingsStorage core.ISettingsStorage) *RemoveSourceDecorator { - return &RemoveSourceDecorator{l, decoratee, settingsStorage} +func CreateRemoveSourceDecorator(l core.ILogger, decoratee core.ITextHandler) *RemoveSourceDecorator { + return &RemoveSourceDecorator{l, decoratee} } type RemoveSourceDecorator struct { - l core.ILogger - decoratee core.ITextHandler - settingsStorage core.ISettingsStorage + l core.ILogger + decoratee core.ITextHandler } // HandleText is a core.ITextHandler protocol implementation @@ -27,13 +26,7 @@ func (decorator *RemoveSourceDecorator) HandleText(message *core.Message, bot co return err } - settings, err := decorator.settingsStorage.GetSettings(message.Chat.ID) - if err != nil { - decorator.l.Error(err) - return err - } - - if settings.RemoveSourceOnSucccess { + if message.Chat.Settings.RemoveSourceOnSucccess { decorator.l.Infof("removing chat %d message %d", message.Chat.ID, message.ID) return bot.Delete(message) } From c2ffe48166d5e8229fe342d5d70e6b4e78d58dc7 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Sat, 26 Mar 2022 15:13:52 +0300 Subject: [PATCH 350/439] Add `InMemoryUserStorage` for chace user data --- infrastructure/in_memory_user_storage.go | 29 ++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 infrastructure/in_memory_user_storage.go diff --git a/infrastructure/in_memory_user_storage.go b/infrastructure/in_memory_user_storage.go new file mode 100644 index 0000000..44d610d --- /dev/null +++ b/infrastructure/in_memory_user_storage.go @@ -0,0 +1,29 @@ +package infrastructure + +import ( + "fmt" + + "github.com/ailinykh/pullanusbot/v2/core" +) + +func CreateInMemoryUserStorage() core.IUserStorage { + return &InMemoryUserStorage{make(map[int64]*core.User)} +} + +type InMemoryUserStorage struct { + cache map[int64]*core.User +} + +// GetUserById is a core.IUserStorage interface implementation +func (storage *InMemoryUserStorage) GetUserById(userID int64) (*core.User, error) { + if user, ok := storage.cache[userID]; ok { + return user, nil + } + return nil, fmt.Errorf("record not found") +} + +// CreateUser is a core.IUserStorage interface implementation +func (storage *InMemoryUserStorage) CreateUser(user *core.User) error { + storage.cache[user.ID] = user + return fmt.Errorf("method not implemented") +} From 9c0edf707b0eae2361f60e7958e2a7eae5558e28 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Sat, 26 Mar 2022 15:14:42 +0300 Subject: [PATCH 351/439] Add `UserStorageDecorator` to fallback between different user storages --- usecases/user_storage_decorator.go | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 usecases/user_storage_decorator.go diff --git a/usecases/user_storage_decorator.go b/usecases/user_storage_decorator.go new file mode 100644 index 0000000..10f0101 --- /dev/null +++ b/usecases/user_storage_decorator.go @@ -0,0 +1,30 @@ +package usecases + +import "github.com/ailinykh/pullanusbot/v2/core" + +func CreateUserStorageDecorator(primary core.IUserStorage, secondary core.IUserStorage) core.IUserStorage { + return &UserStorageDecorator{primary, secondary} +} + +type UserStorageDecorator struct { + primary core.IUserStorage + secondary core.IUserStorage +} + +// GetUserById is a core.IUserStorage interface implementation +func (decorator *UserStorageDecorator) GetUserById(userID int64) (*core.User, error) { + user, err := decorator.primary.GetUserById(userID) + if err != nil { + return decorator.secondary.GetUserById(userID) + } + return user, err +} + +// CreateUser is a core.IUserStorage interface implementation +func (decorator *UserStorageDecorator) CreateUser(user *core.User) error { + err := decorator.primary.CreateUser(user) + if err != nil { + return decorator.secondary.CreateUser(user) + } + return err +} From 395d4ecc80b4b9644487a037ced8aef289ce147a Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Sat, 26 Mar 2022 15:15:32 +0300 Subject: [PATCH 352/439] Combine inmemory and database user storage in composition root --- pullanusbot.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pullanusbot.go b/pullanusbot.go index 85be585..0ec93d9 100644 --- a/pullanusbot.go +++ b/pullanusbot.go @@ -96,8 +96,10 @@ func main() { telebot.AddHandler(removeReelsSourceDecorator) commonLocalizer := infrastructure.CreateCommonLocalizer() - userStorage := infrastructure.CreateUserStorage(dbFile, logger) - startFlow := usecases.CreateStartFlow(logger, commonLocalizer, chatStorage, userStorage) + databaseUserStorage := infrastructure.CreateUserStorage(dbFile, logger) + inMemoryUserStorage := infrastructure.CreateInMemoryUserStorage() + userStorageDecorator := usecases.CreateUserStorageDecorator(inMemoryUserStorage, databaseUserStorage) + startFlow := usecases.CreateStartFlow(logger, commonLocalizer, chatStorage, userStorageDecorator) telebot.AddHandler(startFlow) // Start endless loop telebot.Run() From 84c604e456a9ce184e9a0585734c774d87fcd9a6 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Sat, 26 Mar 2022 15:16:08 +0300 Subject: [PATCH 353/439] Remove redundant users cache from `StartFlow` --- usecases/start_flow.go | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/usecases/start_flow.go b/usecases/start_flow.go index c2751c7..09838b3 100644 --- a/usecases/start_flow.go +++ b/usecases/start_flow.go @@ -8,7 +8,7 @@ import ( ) func CreateStartFlow(l core.ILogger, loc core.ILocalizer, chatStorage core.IChatStorage, userStorage core.IUserStorage) core.ITextHandler { - return &StartFlow{l, loc, chatStorage, userStorage, make(map[int64]bool), make(map[int64]bool), sync.Mutex{}} + return &StartFlow{l, loc, chatStorage, userStorage, make(map[int64]bool), sync.Mutex{}} } type StartFlow struct { @@ -16,7 +16,6 @@ type StartFlow struct { loc core.ILocalizer chatStorage core.IChatStorage userStorage core.IUserStorage - usersCache map[int64]bool chatCache map[int64]bool lock sync.Mutex } @@ -86,19 +85,15 @@ func (flow *StartFlow) ensureChatExists(chat *core.Chat) error { } func (flow *StartFlow) ensureUserExists(user *core.User) error { - if _, ok := flow.usersCache[user.ID]; !ok { - flow.usersCache[user.ID] = true - _, err := flow.userStorage.GetUserById(user.ID) - if err != nil { - if err.Error() == "record not found" { - return flow.userStorage.CreateUser(user) - } - flow.l.Error(err) - return err + _, err := flow.userStorage.GetUserById(user.ID) + if err != nil { + if err.Error() == "record not found" { + return flow.userStorage.CreateUser(user) } + flow.l.Error(err) } - return nil + return err } func (flow *StartFlow) contains(payload string, current []string) bool { From 094d22000239b90d1bcf629767ede48e7c84a967 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Sat, 26 Mar 2022 16:53:29 +0300 Subject: [PATCH 354/439] Add database user `CreatedAt` and `UpdatedAt` fields --- infrastructure/user_storage.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/infrastructure/user_storage.go b/infrastructure/user_storage.go index e1bfa43..06cdc33 100644 --- a/infrastructure/user_storage.go +++ b/infrastructure/user_storage.go @@ -1,6 +1,8 @@ package infrastructure import ( + "time" + "github.com/ailinykh/pullanusbot/v2/core" "gorm.io/driver/sqlite" "gorm.io/gorm" @@ -31,6 +33,8 @@ type User struct { LastName string Username string LanguageCode string + CreatedAt time.Time `gorm:"autoUpdateTime"` + UpdatedAt time.Time `gorm:"autoCreateTime"` } // GetUserById is a core.IUserStorage interface implementation From 4976ad070f064a36c991b55aaf098d1282469f32 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Sat, 26 Mar 2022 16:54:52 +0300 Subject: [PATCH 355/439] Increase performance in `telebot` core message and bot factory --- api/telebot.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/api/telebot.go b/api/telebot.go index fec4b65..9641f30 100644 --- a/api/telebot.go +++ b/api/telebot.go @@ -46,15 +46,16 @@ func CreateTelebot(token string, logger core.ILogger, chatStorage core.IChatStor bot.Handle(tb.OnText, func(c tb.Context) error { var err error - var m = c.Message() + var message = telebot.coreFactory.makeMessage(c.Message()) + var bot = telebot.coreFactory.makeIBot(c.Message(), telebot) for _, h := range telebot.textHandlers { - err = h.HandleText(telebot.coreFactory.makeMessage(m), telebot.coreFactory.makeIBot(m, telebot)) + err = h.HandleText(message, bot) if err != nil { if err.Error() == "not implemented" { err = nil // skip "not implemented" error } else { logger.Errorf("%T: %s", h, err) - telebot.reportError(m, err) + telebot.reportError(c.Message(), err) } } } From 4865639ffdb9b0d3a9397900b21a300f95555ab6 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Sat, 26 Mar 2022 16:55:26 +0300 Subject: [PATCH 356/439] Add `InMemoryChatStorage` for chat info cache --- infrastructure/in_memory_chat_storage.go | 44 ++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 infrastructure/in_memory_chat_storage.go diff --git a/infrastructure/in_memory_chat_storage.go b/infrastructure/in_memory_chat_storage.go new file mode 100644 index 0000000..e91bade --- /dev/null +++ b/infrastructure/in_memory_chat_storage.go @@ -0,0 +1,44 @@ +package infrastructure + +import ( + "fmt" + + "github.com/ailinykh/pullanusbot/v2/core" +) + +func CreateInMemoryChatStorage() core.IChatStorage { + return &InMemoryChatStorage{make(map[int64]*core.Chat)} +} + +type InMemoryChatStorage struct { + cache map[int64]*core.Chat +} + +// GetChatByID is a core.IChatStorage interface implementation +func (storage *InMemoryChatStorage) GetChatByID(chatID int64) (*core.Chat, error) { + if chat, ok := storage.cache[chatID]; ok { + return chat, nil + } + return nil, fmt.Errorf("record not found") +} + +// CreateChat is a core.IChatStorage interface implementation +func (storage *InMemoryChatStorage) CreateChat(chatID int64, title string, type_ string, settings *core.Settings) error { + storage.cache[chatID] = &core.Chat{ + ID: chatID, + Title: title, + Type: type_, + Settings: settings, + } + return nil +} + +// UpdateSettings is a core.IChatStorage interface implementation +func (storage *InMemoryChatStorage) UpdateSettings(chatID int64, settings *core.Settings) error { + chat, err := storage.GetChatByID(chatID) + if err != nil { + return err + } + chat.Settings = settings + return nil +} From 17c54baabb1adae38b667a05164974c52d1e5f61 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Sat, 26 Mar 2022 16:56:12 +0300 Subject: [PATCH 357/439] Fix `InMemoryUserStorage` create user method return error --- infrastructure/in_memory_user_storage.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infrastructure/in_memory_user_storage.go b/infrastructure/in_memory_user_storage.go index 44d610d..c8de98e 100644 --- a/infrastructure/in_memory_user_storage.go +++ b/infrastructure/in_memory_user_storage.go @@ -25,5 +25,5 @@ func (storage *InMemoryUserStorage) GetUserById(userID int64) (*core.User, error // CreateUser is a core.IUserStorage interface implementation func (storage *InMemoryUserStorage) CreateUser(user *core.User) error { storage.cache[user.ID] = user - return fmt.Errorf("method not implemented") + return nil } From 4de32eb9ba40e7c26f3e9427785461181b2c048c Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Sat, 26 Mar 2022 16:57:01 +0300 Subject: [PATCH 358/439] Update `UserStorageDecorator` to work with cache and database storage --- usecases/user_storage_decorator.go | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/usecases/user_storage_decorator.go b/usecases/user_storage_decorator.go index 10f0101..185748c 100644 --- a/usecases/user_storage_decorator.go +++ b/usecases/user_storage_decorator.go @@ -7,24 +7,26 @@ func CreateUserStorageDecorator(primary core.IUserStorage, secondary core.IUserS } type UserStorageDecorator struct { - primary core.IUserStorage - secondary core.IUserStorage + cache core.IUserStorage + db core.IUserStorage } // GetUserById is a core.IUserStorage interface implementation func (decorator *UserStorageDecorator) GetUserById(userID int64) (*core.User, error) { - user, err := decorator.primary.GetUserById(userID) + user, err := decorator.cache.GetUserById(userID) if err != nil { - return decorator.secondary.GetUserById(userID) + user, err := decorator.db.GetUserById(userID) + if err != nil { + return nil, err + } + _ = decorator.cache.CreateUser(user) + return user, err } return user, err } // CreateUser is a core.IUserStorage interface implementation func (decorator *UserStorageDecorator) CreateUser(user *core.User) error { - err := decorator.primary.CreateUser(user) - if err != nil { - return decorator.secondary.CreateUser(user) - } - return err + _ = decorator.cache.CreateUser(user) + return decorator.db.CreateUser(user) } From 1ef8ae1f4573dc618c2356a7abf3035a64f8b3ca Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Sat, 26 Mar 2022 16:57:30 +0300 Subject: [PATCH 359/439] Add `ChatStorageDecorator` to combine cache and database chat logic --- usecases/chat_storage_decorator.go | 38 ++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 usecases/chat_storage_decorator.go diff --git a/usecases/chat_storage_decorator.go b/usecases/chat_storage_decorator.go new file mode 100644 index 0000000..b6a61aa --- /dev/null +++ b/usecases/chat_storage_decorator.go @@ -0,0 +1,38 @@ +package usecases + +import "github.com/ailinykh/pullanusbot/v2/core" + +func CreateChatStorageDecorator(cache core.IChatStorage, db core.IChatStorage) core.IChatStorage { + return &ChatStorageDecorator{cache, db} +} + +type ChatStorageDecorator struct { + cache core.IChatStorage + db core.IChatStorage +} + +// GetChatByID is a core.IChatStorage interface implementation +func (decorator *ChatStorageDecorator) GetChatByID(chatID int64) (*core.Chat, error) { + chat, err := decorator.cache.GetChatByID(chatID) + if err != nil { + chat, err := decorator.db.GetChatByID(chatID) + if err != nil { + return nil, err + } + _ = decorator.cache.CreateChat(chat.ID, chat.Title, chat.Type, chat.Settings) + return chat, nil + } + return chat, nil +} + +// CreateChat is a core.IChatStorage interface implementation +func (decorator *ChatStorageDecorator) CreateChat(chatID int64, title string, type_ string, settings *core.Settings) error { + _ = decorator.cache.CreateChat(chatID, title, type_, settings) + return decorator.db.CreateChat(chatID, title, type_, settings) +} + +// UpdateSettings is a core.IChatStorage interface implementation +func (decorator *ChatStorageDecorator) UpdateSettings(chatID int64, settings *core.Settings) error { + _ = decorator.cache.UpdateSettings(chatID, settings) + return decorator.db.UpdateSettings(chatID, settings) +} From 79ddeb51bcddec365432f508cc129b4628ea8084 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Sat, 26 Mar 2022 16:57:52 +0300 Subject: [PATCH 360/439] Remove redundant code from `StartFlow` --- usecases/start_flow.go | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/usecases/start_flow.go b/usecases/start_flow.go index 09838b3..ced8afc 100644 --- a/usecases/start_flow.go +++ b/usecases/start_flow.go @@ -8,7 +8,7 @@ import ( ) func CreateStartFlow(l core.ILogger, loc core.ILocalizer, chatStorage core.IChatStorage, userStorage core.IUserStorage) core.ITextHandler { - return &StartFlow{l, loc, chatStorage, userStorage, make(map[int64]bool), sync.Mutex{}} + return &StartFlow{l, loc, chatStorage, userStorage, sync.Mutex{}} } type StartFlow struct { @@ -16,7 +16,6 @@ type StartFlow struct { loc core.ILocalizer chatStorage core.IChatStorage userStorage core.IUserStorage - chatCache map[int64]bool lock sync.Mutex } @@ -69,19 +68,15 @@ func (flow *StartFlow) handlePayload(payload string, chatID int64) error { } func (flow *StartFlow) ensureChatExists(chat *core.Chat) error { - if _, ok := flow.chatCache[chat.ID]; !ok { - flow.chatCache[chat.ID] = true - _, err := flow.chatStorage.GetChatByID(chat.ID) - if err != nil { - if err.Error() == "record not found" { - settings := core.DefaultSettings() - return flow.chatStorage.CreateChat(chat.ID, chat.Title, chat.Type, &settings) - } - flow.l.Error(err) - return err + _, err := flow.chatStorage.GetChatByID(chat.ID) + if err != nil { + if err.Error() == "record not found" { + settings := core.DefaultSettings() + return flow.chatStorage.CreateChat(chat.ID, chat.Title, chat.Type, &settings) } + flow.l.Error(err) } - return nil + return err } func (flow *StartFlow) ensureUserExists(user *core.User) error { @@ -92,7 +87,6 @@ func (flow *StartFlow) ensureUserExists(user *core.User) error { } flow.l.Error(err) } - return err } From acbe6e5240628a3fbbdb5947ebfd86bcea77e0b7 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Sat, 26 Mar 2022 16:58:53 +0300 Subject: [PATCH 361/439] Increase performance by using in memory chat cache in telebot --- pullanusbot.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pullanusbot.go b/pullanusbot.go index 0ec93d9..78cfd44 100644 --- a/pullanusbot.go +++ b/pullanusbot.go @@ -18,8 +18,10 @@ func main() { dbFile := path.Join(getWorkingDir(), "pullanusbot.db") - chatStorage := infrastructure.CreateChatStorage(dbFile, logger) - telebot := api.CreateTelebot(os.Getenv("BOT_TOKEN"), logger, chatStorage) + databaseChatStorage := infrastructure.CreateChatStorage(dbFile, logger) + inMemoryChatStorage := infrastructure.CreateInMemoryChatStorage() + chatStorageDecorator := usecases.CreateChatStorageDecorator(inMemoryChatStorage, databaseChatStorage) + telebot := api.CreateTelebot(os.Getenv("BOT_TOKEN"), logger, chatStorageDecorator) telebot.SetupInfo() localizer := infrastructure.GameLocalizer{} @@ -99,7 +101,7 @@ func main() { databaseUserStorage := infrastructure.CreateUserStorage(dbFile, logger) inMemoryUserStorage := infrastructure.CreateInMemoryUserStorage() userStorageDecorator := usecases.CreateUserStorageDecorator(inMemoryUserStorage, databaseUserStorage) - startFlow := usecases.CreateStartFlow(logger, commonLocalizer, chatStorage, userStorageDecorator) + startFlow := usecases.CreateStartFlow(logger, commonLocalizer, chatStorageDecorator, userStorageDecorator) telebot.AddHandler(startFlow) // Start endless loop telebot.Run() From 53af8927c613adfc08b502d16878ba9edcafac1e Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Sun, 27 Mar 2022 16:31:50 +0300 Subject: [PATCH 362/439] Add initial chat and user creation logic into `bootstrap_flow` and make it sync --- usecases/bootstrap_flow.go | 61 ++++++++++++++++++++++++++++ usecases/bootstrap_flow_test.go | 70 +++++++++++++++++++++++++++++++++ 2 files changed, 131 insertions(+) create mode 100644 usecases/bootstrap_flow.go create mode 100644 usecases/bootstrap_flow_test.go diff --git a/usecases/bootstrap_flow.go b/usecases/bootstrap_flow.go new file mode 100644 index 0000000..5189aa0 --- /dev/null +++ b/usecases/bootstrap_flow.go @@ -0,0 +1,61 @@ +package usecases + +import ( + "sync" + + "github.com/ailinykh/pullanusbot/v2/core" +) + +func CreateBootstrapFlow(l core.ILogger, chatStorage core.IChatStorage, userStorage core.IUserStorage) core.ITextHandler { + return &BootstrapFlow{l, chatStorage, userStorage, sync.Mutex{}} +} + +type BootstrapFlow struct { + l core.ILogger + chatStorage core.IChatStorage + userStorage core.IUserStorage + lock sync.Mutex +} + +// HandleText is a core.ITextHandler protocol implementation +func (flow *BootstrapFlow) HandleText(message *core.Message, bot core.IBot) error { + flow.lock.Lock() + defer flow.lock.Unlock() + + err := flow.ensureUserExists(message.Sender) + if err != nil { + flow.l.Error(err) + //Do not return? + } + + err = flow.ensureChatExists(message.Chat) + if err != nil { + flow.l.Error(err) + //Do not return? + } + + return err +} + +func (flow *BootstrapFlow) ensureChatExists(chat *core.Chat) error { + _, err := flow.chatStorage.GetChatByID(chat.ID) + if err != nil { + if err.Error() == "record not found" { + settings := core.DefaultSettings() + return flow.chatStorage.CreateChat(chat.ID, chat.Title, chat.Type, &settings) + } + flow.l.Error(err) + } + return err +} + +func (flow *BootstrapFlow) ensureUserExists(user *core.User) error { + _, err := flow.userStorage.GetUserById(user.ID) + if err != nil { + if err.Error() == "record not found" { + return flow.userStorage.CreateUser(user) + } + flow.l.Error(err) + } + return err +} diff --git a/usecases/bootstrap_flow_test.go b/usecases/bootstrap_flow_test.go new file mode 100644 index 0000000..4e3934b --- /dev/null +++ b/usecases/bootstrap_flow_test.go @@ -0,0 +1,70 @@ +package usecases_test + +import ( + "sync" + "testing" + + "github.com/ailinykh/pullanusbot/v2/test_helpers" + "github.com/ailinykh/pullanusbot/v2/usecases" + "github.com/stretchr/testify/assert" +) + +func Test_HandleText_CreateUserData(t *testing.T) { + logger := test_helpers.CreateLogger() + chatStorage := test_helpers.CreateChatStorage() + userStorage := test_helpers.CreateUserStorage() + bootstrapFlow := usecases.CreateBootstrapFlow(logger, chatStorage, userStorage) + + bot := test_helpers.CreateBot() + + messages := []string{ + "/start", + "/start payload", + "/start another_payload", + } + wg := sync.WaitGroup{} + + for _, message := range messages { + wg.Add(1) + go func(text string) { + bootstrapFlow.HandleText(makeMessage(text), bot) + wg.Done() + }(message) + } + + wg.Wait() + + assert.Equal(t, 1, len(userStorage.Users)) + + message := makeMessage("/start") + user, _ := userStorage.GetUserById(message.Sender.ID) + assert.Equal(t, message.Sender, user) +} + +func Test_HandleText_CreateChatData(t *testing.T) { + logger := test_helpers.CreateLogger() + chatStorage := test_helpers.CreateChatStorage() + userStorage := test_helpers.CreateUserStorage() + bootstrapFlow := usecases.CreateBootstrapFlow(logger, chatStorage, userStorage) + + bot := test_helpers.CreateBot() + + messages := []string{ + "/start", + "/some_command", + "some text message", + } + wg := sync.WaitGroup{} + + for _, message := range messages { + wg.Add(1) + go func(text string) { + bootstrapFlow.HandleText(makeMessage(text), bot) + wg.Done() + }(message) + } + + wg.Wait() + + assert.Equal(t, 1, len(chatStorage.Chats)) +} From 39ec5d16569340b0db828e59140061823db756cf Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Sun, 27 Mar 2022 16:43:31 +0300 Subject: [PATCH 363/439] Remove redundant logic from `start_flow` --- pullanusbot.go | 11 +++++---- usecases/start_flow.go | 46 ++++--------------------------------- usecases/start_flow_test.go | 40 ++++---------------------------- 3 files changed, 16 insertions(+), 81 deletions(-) diff --git a/pullanusbot.go b/pullanusbot.go index 78cfd44..e2e5ad5 100644 --- a/pullanusbot.go +++ b/pullanusbot.go @@ -24,6 +24,12 @@ func main() { telebot := api.CreateTelebot(os.Getenv("BOT_TOKEN"), logger, chatStorageDecorator) telebot.SetupInfo() + databaseUserStorage := infrastructure.CreateUserStorage(dbFile, logger) + inMemoryUserStorage := infrastructure.CreateInMemoryUserStorage() + userStorageDecorator := usecases.CreateUserStorageDecorator(inMemoryUserStorage, databaseUserStorage) + bootstrapFlow := usecases.CreateBootstrapFlow(logger, chatStorageDecorator, userStorageDecorator) + telebot.AddHandler(bootstrapFlow) + localizer := infrastructure.GameLocalizer{} gameStorage := infrastructure.CreateGameStorage(dbFile) rand := infrastructure.CreateMathRand() @@ -98,10 +104,7 @@ func main() { telebot.AddHandler(removeReelsSourceDecorator) commonLocalizer := infrastructure.CreateCommonLocalizer() - databaseUserStorage := infrastructure.CreateUserStorage(dbFile, logger) - inMemoryUserStorage := infrastructure.CreateInMemoryUserStorage() - userStorageDecorator := usecases.CreateUserStorageDecorator(inMemoryUserStorage, databaseUserStorage) - startFlow := usecases.CreateStartFlow(logger, commonLocalizer, chatStorageDecorator, userStorageDecorator) + startFlow := usecases.CreateStartFlow(logger, commonLocalizer, chatStorageDecorator) telebot.AddHandler(startFlow) // Start endless loop telebot.Run() diff --git a/usecases/start_flow.go b/usecases/start_flow.go index ced8afc..1da7bcf 100644 --- a/usecases/start_flow.go +++ b/usecases/start_flow.go @@ -7,15 +7,14 @@ import ( "github.com/ailinykh/pullanusbot/v2/core" ) -func CreateStartFlow(l core.ILogger, loc core.ILocalizer, chatStorage core.IChatStorage, userStorage core.IUserStorage) core.ITextHandler { - return &StartFlow{l, loc, chatStorage, userStorage, sync.Mutex{}} +func CreateStartFlow(l core.ILogger, loc core.ILocalizer, chatStorage core.IChatStorage) core.ITextHandler { + return &StartFlow{l, loc, chatStorage, sync.Mutex{}} } type StartFlow struct { l core.ILogger loc core.ILocalizer chatStorage core.IChatStorage - userStorage core.IUserStorage lock sync.Mutex } @@ -24,32 +23,20 @@ func (flow *StartFlow) HandleText(message *core.Message, bot core.IBot) error { flow.lock.Lock() defer flow.lock.Unlock() - err := flow.ensureUserExists(message.Sender) - if err != nil { - flow.l.Error(err) - //Do not return? - } - - err = flow.ensureChatExists(message.Chat) - if err != nil { - flow.l.Error(err) - //Do not return? - } - if strings.HasPrefix(message.Text, "/start") { if len(message.Text) > 7 { payload := message.Text[7:] - flow.handlePayload(payload, message.Chat.ID) + err := flow.handlePayload(payload, message.Chat.ID) if err != nil { flow.l.Error(err) //Do not return? } } - _, err = bot.SendText(flow.loc.I18n("start_welcome")) + _, err := bot.SendText(flow.loc.I18n("start_welcome")) return err } - return err + return nil } func (flow *StartFlow) handlePayload(payload string, chatID int64) error { @@ -67,29 +54,6 @@ func (flow *StartFlow) handlePayload(payload string, chatID int64) error { return flow.chatStorage.UpdateSettings(chat.ID, chat.Settings) } -func (flow *StartFlow) ensureChatExists(chat *core.Chat) error { - _, err := flow.chatStorage.GetChatByID(chat.ID) - if err != nil { - if err.Error() == "record not found" { - settings := core.DefaultSettings() - return flow.chatStorage.CreateChat(chat.ID, chat.Title, chat.Type, &settings) - } - flow.l.Error(err) - } - return err -} - -func (flow *StartFlow) ensureUserExists(user *core.User) error { - _, err := flow.userStorage.GetUserById(user.ID) - if err != nil { - if err.Error() == "record not found" { - return flow.userStorage.CreateUser(user) - } - flow.l.Error(err) - } - return err -} - func (flow *StartFlow) contains(payload string, current []string) bool { for _, p := range current { if p == payload { diff --git a/usecases/start_flow_test.go b/usecases/start_flow_test.go index b65b169..739fa53 100644 --- a/usecases/start_flow_test.go +++ b/usecases/start_flow_test.go @@ -10,46 +10,14 @@ import ( "github.com/stretchr/testify/assert" ) -func Test_HandleText_CreateUserData(t *testing.T) { +func Test_HandleText_CreateChatPayload(t *testing.T) { logger := test_helpers.CreateLogger() loc := test_helpers.CreateLocalizer(map[string]string{}) chatStorage := test_helpers.CreateChatStorage() - userStorage := test_helpers.CreateUserStorage() - startFlow := usecases.CreateStartFlow(logger, loc, chatStorage, userStorage) - - bot := test_helpers.CreateBot() - - messages := []string{ - "/start", - "/start payload", - "/start another_payload", - } - wg := sync.WaitGroup{} - - for _, message := range messages { - wg.Add(1) - go func(text string) { - startFlow.HandleText(makeMessage(text), bot) - wg.Done() - }(message) - } - - wg.Wait() - - assert.Equal(t, 1, len(userStorage.Users)) - - message := makeMessage("/start") - user, _ := userStorage.GetUserById(message.Sender.ID) - assert.Equal(t, message.Sender, user) -} - -func Test_HandleText_CreateChatData(t *testing.T) { - logger := test_helpers.CreateLogger() - loc := test_helpers.CreateLocalizer(map[string]string{}) - chatStorage := test_helpers.CreateChatStorage() - userStorage := test_helpers.CreateUserStorage() - startFlow := usecases.CreateStartFlow(logger, loc, chatStorage, userStorage) + startFlow := usecases.CreateStartFlow(logger, loc, chatStorage) + settings := core.DefaultSettings() + chatStorage.CreateChat(1488, "Paul Durov", "private", &settings) bot := test_helpers.CreateBot() messages := []string{ From dfacf5b8a5d250d85338cac0d5605302ce60c5d1 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Sun, 27 Mar 2022 17:07:51 +0300 Subject: [PATCH 364/439] Extend `chat_storage_decorator` with logger --- pullanusbot.go | 2 +- usecases/chat_storage_decorator.go | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/pullanusbot.go b/pullanusbot.go index e2e5ad5..58abf7a 100644 --- a/pullanusbot.go +++ b/pullanusbot.go @@ -20,7 +20,7 @@ func main() { databaseChatStorage := infrastructure.CreateChatStorage(dbFile, logger) inMemoryChatStorage := infrastructure.CreateInMemoryChatStorage() - chatStorageDecorator := usecases.CreateChatStorageDecorator(inMemoryChatStorage, databaseChatStorage) + chatStorageDecorator := usecases.CreateChatStorageDecorator(logger, inMemoryChatStorage, databaseChatStorage) telebot := api.CreateTelebot(os.Getenv("BOT_TOKEN"), logger, chatStorageDecorator) telebot.SetupInfo() diff --git a/usecases/chat_storage_decorator.go b/usecases/chat_storage_decorator.go index b6a61aa..045b46e 100644 --- a/usecases/chat_storage_decorator.go +++ b/usecases/chat_storage_decorator.go @@ -2,11 +2,12 @@ package usecases import "github.com/ailinykh/pullanusbot/v2/core" -func CreateChatStorageDecorator(cache core.IChatStorage, db core.IChatStorage) core.IChatStorage { - return &ChatStorageDecorator{cache, db} +func CreateChatStorageDecorator(l core.ILogger, cache core.IChatStorage, db core.IChatStorage) core.IChatStorage { + return &ChatStorageDecorator{l, cache, db} } type ChatStorageDecorator struct { + l core.ILogger cache core.IChatStorage db core.IChatStorage } From e4fc901cdb75f3605af4e4c1a63cbc7ad9cf9986 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Mon, 28 Mar 2022 22:52:27 +0300 Subject: [PATCH 365/439] Remove obsolete settings storage --- infrastructure/settings.go | 10 ---- infrastructure/settings_storage.go | 87 ------------------------------ 2 files changed, 97 deletions(-) delete mode 100644 infrastructure/settings.go delete mode 100644 infrastructure/settings_storage.go diff --git a/infrastructure/settings.go b/infrastructure/settings.go deleted file mode 100644 index acab79c..0000000 --- a/infrastructure/settings.go +++ /dev/null @@ -1,10 +0,0 @@ -package infrastructure - -import "time" - -type Settings struct { - ChatID int64 `gorm:"primaryKey"` - Data []byte - CreatedAt time.Time `gorm:"autoUpdateTime"` - UpdatedAt time.Time `gorm:"autoCreateTime"` -} diff --git a/infrastructure/settings_storage.go b/infrastructure/settings_storage.go deleted file mode 100644 index 613bfe9..0000000 --- a/infrastructure/settings_storage.go +++ /dev/null @@ -1,87 +0,0 @@ -package infrastructure - -import ( - "encoding/json" - "errors" - - "github.com/ailinykh/pullanusbot/v2/core" - "gorm.io/driver/sqlite" - "gorm.io/gorm" - "gorm.io/gorm/logger" -) - -// CreateSettingsStorage is a default SettingsStorage factory -func CreateSettingsStorage(dbFile string, l core.ILogger) *SettingsStorage { - conn, err := gorm.Open(sqlite.Open(dbFile+"?cache=shared"), &gorm.Config{ - Logger: logger.Default.LogMode(logger.Error), - }) - if err != nil { - panic(err) - } - - conn.AutoMigrate(&Settings{}) - return &SettingsStorage{conn, l} -} - -// SettingsStorage implements core.ISettingsStorage interface -type SettingsStorage struct { - conn *gorm.DB - l core.ILogger -} - -// GetSettings is a core.ISettingsStorage interface implementation -func (s *SettingsStorage) GetSettings(chatID int64) (*core.Settings, error) { - defalt := core.DefaultSettings() - data, err := json.Marshal(&defalt) - - if err != nil { - s.l.Error(err) - return nil, err - } - - settings := &Settings{ChatID: chatID, Data: data} - res := s.conn.Where("chat_id = ?", chatID).FirstOrCreate(settings) - - if res.Error != nil { - s.l.Error(res.Error) - return nil, res.Error - } - - s.l.Infof("get settings %d %s", chatID, string(settings.Data)) - return makeSettings(settings.Data) -} - -// SetSettings is a core.ISettingsStorage interface implementation -func (s *SettingsStorage) SetSettings(chatID int64, settings *core.Settings) error { - data, err := json.Marshal(settings) - - if err != nil { - s.l.Error(err) - return err - } - - sett := &Settings{ChatID: chatID} - res := s.conn.First(&sett, chatID) - sett.Data = data - - if errors.Is(res.Error, gorm.ErrRecordNotFound) { - s.l.Infof("creating settings %d %s", chatID, string(data)) - res = s.conn.Create(&sett) - } else { - s.l.Infof("updating settings %d %s", chatID, string(data)) - res = s.conn.Save(&sett) - } - - return res.Error -} - -func makeSettings(data []byte) (*core.Settings, error) { - var settings *core.Settings - err := json.Unmarshal(data, &settings) - - if err != nil { - return nil, err - } - - return settings, nil -} From 5e97fc54821009f99c59958df9874f29c72b772e Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Tue, 29 Mar 2022 23:03:25 +0300 Subject: [PATCH 366/439] Add core `command` entity --- core/command.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 core/command.go diff --git a/core/command.go b/core/command.go new file mode 100644 index 0000000..6dd1d9b --- /dev/null +++ b/core/command.go @@ -0,0 +1,15 @@ +package core + +type Command struct { + Text string + Description string +} + +func DefaultCommands() []Command { + return []Command{{Text: "help", Description: ""}} +} + +type ICommandService interface { + EnableCommands(int64, []Command) error + DisableCommands(int64, []Command) error +} From ff3d9853b4d0b30f7dcf7f17d9df1adb2842f8c3 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Wed, 30 Mar 2022 09:48:38 +0300 Subject: [PATCH 367/439] Extend core `IBot` with `GetCommands` and `SetCommands` methods --- api/telebot.go | 12 ++++++++++++ api/telebot_adapter.go | 22 ++++++++++++++++++++++ api/telebot_factory.go | 12 ++++++++++++ core/bot.go | 2 ++ test_helpers/bot.go | 18 +++++++++++++++++- 5 files changed, 65 insertions(+), 1 deletion(-) diff --git a/api/telebot.go b/api/telebot.go index 9641f30..121d7f1 100644 --- a/api/telebot.go +++ b/api/telebot.go @@ -306,6 +306,18 @@ func (CoreFactory) makePhoto(p *tb.Photo) *core.Image { } } +func (CoreFactory) makeCommands(commands []tb.Command) []core.Command { + comands := []core.Command{} + for _, command := range commands { + c := core.Command{ + Text: command.Text, + Description: command.Description, + } + comands = append(comands, c) + } + return comands +} + func (CoreFactory) makeFile(name string, path string) *core.File { return &core.File{ Name: name, diff --git a/api/telebot_adapter.go b/api/telebot_adapter.go index 827b695..d0c2bb0 100644 --- a/api/telebot_adapter.go +++ b/api/telebot_adapter.go @@ -178,3 +178,25 @@ func (a *TelebotAdapter) IsUserMemberOfChat(user *core.User, chatID int64) bool member.Role != tb.Left && member.Role != tb.Kicked } + +// GetCommands is a core.IBot interface implementation +func (a *TelebotAdapter) GetCommands(chatID int64) ([]core.Command, error) { + scope := tb.CommandScope{ + Type: tb.CommandScopeChat, + ChatID: chatID, + } + commands, err := a.t.bot.Commands(scope) + if err != nil { + return nil, err + } + return a.t.coreFactory.makeCommands(commands), nil +} + +// SetCommands is a core.IBot interface implementation +func (a *TelebotAdapter) SetCommands(chatID int64, commands []core.Command) error { + scope := tb.CommandScope{ + Type: tb.CommandScopeChat, + ChatID: chatID, + } + return a.t.bot.SetCommands(makeTbCommands(commands), scope) +} diff --git a/api/telebot_factory.go b/api/telebot_factory.go index 7d97346..2e29532 100644 --- a/api/telebot_factory.go +++ b/api/telebot_factory.go @@ -72,3 +72,15 @@ func makeInlineKeyboard(k core.Keyboard) [][]tb.InlineButton { } return keyboard } + +func makeTbCommands(commands []core.Command) []tb.Command { + comands := []tb.Command{} + for _, command := range commands { + c := tb.Command{ + Text: command.Text, + Description: command.Description, + } + comands = append(comands, c) + } + return comands +} diff --git a/core/bot.go b/core/bot.go index d33a641..5bc5e07 100644 --- a/core/bot.go +++ b/core/bot.go @@ -11,4 +11,6 @@ type IBot interface { SendPhotoAlbum([]*Media) ([]*Message, error) SendVideo(*Video, string) (*Message, error) IsUserMemberOfChat(*User, int64) bool + GetCommands(int64) ([]Command, error) + SetCommands(int64, []Command) error } diff --git a/test_helpers/bot.go b/test_helpers/bot.go index 3b6d6b4..b0fcfc6 100644 --- a/test_helpers/bot.go +++ b/test_helpers/bot.go @@ -9,7 +9,7 @@ import ( // https://stackoverflow.com/questions/31794141/can-i-create-shared-test-utilities func CreateBot() *FakeBot { - return &FakeBot{[]string{}, []string{}, []string{}, []string{}, map[int64][]string{}} + return &FakeBot{[]string{}, []string{}, []string{}, []string{}, make(map[int64][]core.Command), []string{}, map[int64][]string{}} } type FakeBot struct { @@ -17,6 +17,8 @@ type FakeBot struct { SentMessages []string SentVideos []string RemovedMessages []string + Commands map[int64][]core.Command + ActionLog []string ChatMembers map[int64][]string } @@ -62,3 +64,17 @@ func (b *FakeBot) IsUserMemberOfChat(user *core.User, chatID int64) bool { } return false } + +func (bot *FakeBot) GetCommands(chatID int64) ([]core.Command, error) { + bot.ActionLog = append(bot.ActionLog, fmt.Sprint("get commands ", chatID)) + if commands, ok := bot.Commands[chatID]; ok { + return commands, nil + } + return []core.Command{}, nil +} + +func (bot *FakeBot) SetCommands(chatID int64, commands []core.Command) error { + bot.ActionLog = append(bot.ActionLog, fmt.Sprint("set commands ", chatID, commands)) + bot.Commands[chatID] = commands + return nil +} From 430027007cd439c534e64c25afb2ce3ffcf42c2c Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Wed, 30 Mar 2022 17:12:56 +0300 Subject: [PATCH 368/439] Extend `ICommandService` methods woth `IBot` interface --- core/command.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/command.go b/core/command.go index 6dd1d9b..a3d50ec 100644 --- a/core/command.go +++ b/core/command.go @@ -10,6 +10,6 @@ func DefaultCommands() []Command { } type ICommandService interface { - EnableCommands(int64, []Command) error - DisableCommands(int64, []Command) error + EnableCommands(int64, []Command, IBot) error + DisableCommands(int64, []Command, IBot) error } From 887fa24d1f856dbbde1cff406b38eb3908a1c686 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Wed, 30 Mar 2022 17:13:32 +0300 Subject: [PATCH 369/439] Add `CommandService` into usecases --- test_helpers/command_service.go | 28 ++++++++++ usecases/command_service.go | 88 ++++++++++++++++++++++++++++++++ usecases/command_service_test.go | 40 +++++++++++++++ 3 files changed, 156 insertions(+) create mode 100644 test_helpers/command_service.go create mode 100644 usecases/command_service.go create mode 100644 usecases/command_service_test.go diff --git a/test_helpers/command_service.go b/test_helpers/command_service.go new file mode 100644 index 0000000..5ec1b90 --- /dev/null +++ b/test_helpers/command_service.go @@ -0,0 +1,28 @@ +package test_helpers + +import ( + "fmt" + + "github.com/ailinykh/pullanusbot/v2/core" +) + +func CreateCommandService(l core.ILogger) *CommandServiceMock { + return &CommandServiceMock{l, []string{}} +} + +type CommandServiceMock struct { + l core.ILogger + ActionLog []string +} + +// EnableCommands is a core.ICommandService interface implementation +func (service *CommandServiceMock) EnableCommands(chatID int64, commands []core.Command, bot core.IBot) error { + service.ActionLog = append(service.ActionLog, fmt.Sprint("enable commands ", chatID, commands)) + return nil +} + +// DisableCommands is a core.ICommandService interface implementation +func (service *CommandServiceMock) DisableCommands(chatID int64, commands []core.Command, bot core.IBot) error { + service.ActionLog = append(service.ActionLog, fmt.Sprint("disable commands ", chatID, commands)) + return nil +} diff --git a/usecases/command_service.go b/usecases/command_service.go new file mode 100644 index 0000000..75e41f8 --- /dev/null +++ b/usecases/command_service.go @@ -0,0 +1,88 @@ +package usecases + +import ( + "sort" + + "github.com/ailinykh/pullanusbot/v2/core" +) + +func CreateCommandService(l core.ILogger) core.ICommandService { + return &CommandService{l, make(map[int64][]core.Command)} +} + +type CommandService struct { + l core.ILogger + cache map[int64][]core.Command +} + +type ByText []core.Command + +func (t ByText) Len() int { return len(t) } +func (t ByText) Less(i, j int) bool { return t[i].Text < t[j].Text } +func (t ByText) Swap(i, j int) { t[i], t[j] = t[j], t[i] } + +// EnableCommands is a core.ICommandService interface implementation +func (service *CommandService) EnableCommands(chatID int64, commands []core.Command, bot core.IBot) error { + var existing []core.Command + var err error + if found, ok := service.cache[chatID]; ok { + existing = found + } else { + existing, err = bot.GetCommands(chatID) + if err != nil { + return nil + } + } + + new := []core.Command{} + for _, c := range commands { + if service.contains(c, existing) { + continue + } + new = append(new, c) + } + + if len(new) == 0 { + // service.l.Warning("all the commands already enabled") + return nil + } + + new = append(new, existing...) + service.cache[chatID] = new + sort.Sort(ByText(new)) + return bot.SetCommands(chatID, new) +} + +// DisableCommands is a core.ICommandService interface implementation +func (service *CommandService) DisableCommands(chatID int64, commands []core.Command, bot core.IBot) error { + var existing []core.Command + var err error + if found, ok := service.cache[chatID]; ok { + existing = found + } else { + existing, err = bot.GetCommands(chatID) + if err != nil { + return nil + } + } + + actual := []core.Command{} + for _, c := range existing { + if service.contains(c, commands) { + continue + } + actual = append(actual, c) + } + + service.cache[chatID] = actual + return bot.SetCommands(chatID, actual) +} + +func (CommandService) contains(command core.Command, commands []core.Command) bool { + for _, c := range commands { + if c.Text == command.Text { + return true + } + } + return false +} diff --git a/usecases/command_service_test.go b/usecases/command_service_test.go new file mode 100644 index 0000000..1d8b6f2 --- /dev/null +++ b/usecases/command_service_test.go @@ -0,0 +1,40 @@ +package usecases_test + +import ( + "testing" + + "github.com/ailinykh/pullanusbot/v2/core" + "github.com/ailinykh/pullanusbot/v2/test_helpers" + "github.com/ailinykh/pullanusbot/v2/usecases" + "github.com/stretchr/testify/assert" +) + +func Test_EnableCommands_DoNotCallsSetCommandsMoreThanOneTime(t *testing.T) { + bot := test_helpers.CreateBot() + logger := test_helpers.CreateLogger() + service := usecases.CreateCommandService(logger) + + service.EnableCommands(1, []core.Command{{Text: "c1", Description: "d1"}}, bot) + assert.Equal(t, []string{"get commands 1", "set commands 1 [{c1 d1}]"}, bot.ActionLog) + + service.EnableCommands(1, []core.Command{{Text: "c1", Description: "d1"}}, bot) + assert.Equal(t, []string{"get commands 1", "set commands 1 [{c1 d1}]"}, bot.ActionLog) + + service.EnableCommands(1, []core.Command{{Text: "c2", Description: "d2"}}, bot) + assert.Equal(t, []string{"get commands 1", "set commands 1 [{c1 d1}]", "set commands 1 [{c1 d1} {c2 d2}]"}, bot.ActionLog) +} + +func Test_DisableCommands_DoNotCallsSetCommandsMoreThanOneTime(t *testing.T) { + bot := test_helpers.CreateBot() + logger := test_helpers.CreateLogger() + service := usecases.CreateCommandService(logger) + + service.EnableCommands(14, []core.Command{ + {Text: "one", Description: "1"}, + {Text: "two", Description: "2"}, + }, bot) + assert.Equal(t, []string{"get commands 14", "set commands 14 [{one 1} {two 2}]"}, bot.ActionLog) + + service.DisableCommands(14, []core.Command{{Text: "two", Description: "2"}}, bot) + assert.Equal(t, []string{"get commands 14", "set commands 14 [{one 1} {two 2}]", "set commands 14 [{one 1}]"}, bot.ActionLog) +} From f420d8a692a6f03cd3bf582376e44cc8268f4578 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Wed, 30 Mar 2022 17:14:22 +0300 Subject: [PATCH 370/439] Enable `/proxy` command depending on `/proxy` invocation --- pullanusbot.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pullanusbot.go b/pullanusbot.go index 58abf7a..e21d457 100644 --- a/pullanusbot.go +++ b/pullanusbot.go @@ -90,7 +90,10 @@ func main() { removeYoutubeSourceDecorator := usecases.CreateRemoveSourceDecorator(logger, youtubeFlow) telebot.AddHandler(removeYoutubeSourceDecorator) + commandService := usecases.CreateCommandService(logger) + telebot.AddHandler("/proxy", func(m *core.Message, bot core.IBot) error { + _ = commandService.EnableCommands(m.Chat.ID, []core.Command{{Text: "proxy", Description: "proxy server for telegram"}}, bot) _, err := bot.SendText("tg://proxy?server=proxy.ailinykh.com&port=443&secret=dd71ce3b5bf1b7015dc62a76dc244c5aec") return err }) From d4a1ed7e0d55c48257c07bc019fe16c314cd3dec Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Wed, 30 Mar 2022 17:15:37 +0300 Subject: [PATCH 371/439] Extend `start_flow` with `/help` command and commandService --- infrastructure/common_localizer.go | 5 +++-- pullanusbot.go | 5 +++-- usecases/start_flow.go | 31 ++++++++++++++++++++---------- usecases/start_flow_test.go | 12 ++++++++++-- 4 files changed, 37 insertions(+), 16 deletions(-) diff --git a/infrastructure/common_localizer.go b/infrastructure/common_localizer.go index 451a819..5528ff9 100644 --- a/infrastructure/common_localizer.go +++ b/infrastructure/common_localizer.go @@ -8,12 +8,13 @@ import ( func CreateCommonLocalizer() *CommonLocalizer { return &CommonLocalizer{ map[string]map[string]string{"ru": { - "start_welcome": `Привет! Вот что я могу: + "start_welcome": "Привет!", + "help": `Вот что я могу: - видео, загруженное как файл, я сконвертирую в mp4 и отправлю обратно (до 20MB) - если прислать мне сылку на видео, я скачаю его и загружу в этот же чат как видео - ссылки на видео в tiktok, twitter и instagram reels так же поддерживаются -- у меня можно получить доступ к proxy для telegram (на случай, если его опять заблокируют) +- у меня можно получить доступ к /proxy для telegram (на случай, если его опять заблокируют) - в групповых чатах ролики на youtube длиною до 10 минут я так же скачиваю и присылаю как видео - если дать мне права на удаление сообщений, я буду удалять исходное сообщение с ссылкой - в личном чате я могу скачать и прислать частями по 50MB любой ролик на youtube, достаточно просто прислать мне ссылку diff --git a/pullanusbot.go b/pullanusbot.go index e21d457..68ed5d9 100644 --- a/pullanusbot.go +++ b/pullanusbot.go @@ -107,8 +107,9 @@ func main() { telebot.AddHandler(removeReelsSourceDecorator) commonLocalizer := infrastructure.CreateCommonLocalizer() - startFlow := usecases.CreateStartFlow(logger, commonLocalizer, chatStorageDecorator) - telebot.AddHandler(startFlow) + startFlow := usecases.CreateStartFlow(logger, commonLocalizer, chatStorageDecorator, commandService) + telebot.AddHandler("/start", startFlow.Start) + telebot.AddHandler("/help", startFlow.Help) // Start endless loop telebot.Run() } diff --git a/usecases/start_flow.go b/usecases/start_flow.go index 1da7bcf..26cf776 100644 --- a/usecases/start_flow.go +++ b/usecases/start_flow.go @@ -7,19 +7,19 @@ import ( "github.com/ailinykh/pullanusbot/v2/core" ) -func CreateStartFlow(l core.ILogger, loc core.ILocalizer, chatStorage core.IChatStorage) core.ITextHandler { - return &StartFlow{l, loc, chatStorage, sync.Mutex{}} +func CreateStartFlow(l core.ILogger, loc core.ILocalizer, chatStorage core.IChatStorage, commandService core.ICommandService) *StartFlow { + return &StartFlow{l, loc, chatStorage, commandService, sync.Mutex{}} } type StartFlow struct { - l core.ILogger - loc core.ILocalizer - chatStorage core.IChatStorage - lock sync.Mutex + l core.ILogger + loc core.ILocalizer + chatStorage core.IChatStorage + commandService core.ICommandService + lock sync.Mutex } -// HandleText is a core.ITextHandler protocol implementation -func (flow *StartFlow) HandleText(message *core.Message, bot core.IBot) error { +func (flow *StartFlow) Start(message *core.Message, bot core.IBot) error { flow.lock.Lock() defer flow.lock.Unlock() @@ -29,16 +29,27 @@ func (flow *StartFlow) HandleText(message *core.Message, bot core.IBot) error { err := flow.handlePayload(payload, message.Chat.ID) if err != nil { flow.l.Error(err) - //Do not return? + //return err ? } } - _, err := bot.SendText(flow.loc.I18n("start_welcome")) + + err := flow.commandService.EnableCommands(message.Chat.ID, []core.Command{{Text: "help", Description: "show help message"}}, bot) + if err != nil { + flow.l.Error(err) + // return err ? + } + _, err = bot.SendText(flow.loc.I18n("start_welcome") + " " + flow.loc.I18n("help")) return err } return nil } +func (flow *StartFlow) Help(message *core.Message, bot core.IBot) error { + _, err := bot.SendText(flow.loc.I18n("help")) + return err +} + func (flow *StartFlow) handlePayload(payload string, chatID int64) error { chat, err := flow.chatStorage.GetChatByID(chatID) if err != nil { diff --git a/usecases/start_flow_test.go b/usecases/start_flow_test.go index 739fa53..04da375 100644 --- a/usecases/start_flow_test.go +++ b/usecases/start_flow_test.go @@ -14,7 +14,8 @@ func Test_HandleText_CreateChatPayload(t *testing.T) { logger := test_helpers.CreateLogger() loc := test_helpers.CreateLocalizer(map[string]string{}) chatStorage := test_helpers.CreateChatStorage() - startFlow := usecases.CreateStartFlow(logger, loc, chatStorage) + commandService := test_helpers.CreateCommandService(logger) + startFlow := usecases.CreateStartFlow(logger, loc, chatStorage, commandService) settings := core.DefaultSettings() chatStorage.CreateChat(1488, "Paul Durov", "private", &settings) @@ -30,7 +31,7 @@ func Test_HandleText_CreateChatPayload(t *testing.T) { for _, message := range messages { wg.Add(1) go func(text string) { - startFlow.HandleText(makeMessage(text), bot) + startFlow.Start(makeMessage(text), bot) wg.Done() }(message) } @@ -43,6 +44,13 @@ func Test_HandleText_CreateChatPayload(t *testing.T) { chat, _ := chatStorage.GetChatByID(message.Chat.ID) assert.Equal(t, true, contains("payload", chat.Settings.Payload)) assert.Equal(t, true, contains("another_payload", chat.Settings.Payload)) + + expected := []string{ + "enable commands 1488 [{help show help message}]", + "enable commands 1488 [{help show help message}]", + "enable commands 1488 [{help show help message}]", + } + assert.Equal(t, expected, commandService.ActionLog) } func makeMessage(text string) *core.Message { From efad7a5d48dae1e69549941ea7031d69a3c32711 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Wed, 30 Mar 2022 17:20:11 +0300 Subject: [PATCH 372/439] Remove mutex out of global scope --- usecases/faggot_game.go | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/usecases/faggot_game.go b/usecases/faggot_game.go index 76ece0f..d8d1d4a 100644 --- a/usecases/faggot_game.go +++ b/usecases/faggot_game.go @@ -15,15 +15,16 @@ import ( // CreateGameFlow is a simple GameFlow factory func CreateGameFlow(l core.ILogger, t core.ILocalizer, s core.IGameStorage, r core.IRand) *GameFlow { - return &GameFlow{l, t, s, r} + return &GameFlow{l, t, s, r, sync.Mutex{}} } // GameFlow represents faggot game logic type GameFlow struct { - l core.ILogger - t core.ILocalizer - s core.IGameStorage - r core.IRand + l core.ILogger + t core.ILocalizer + s core.IGameStorage + r core.IRand + mutex sync.Mutex } // Rules of the game @@ -64,16 +65,14 @@ func (flow *GameFlow) Add(message *core.Message, bot core.IBot) error { return err } -var mutex sync.Mutex - // Play game func (flow *GameFlow) Play(message *core.Message, bot core.IBot) error { if message.IsPrivate { _, err := bot.SendText(flow.t.I18n("faggot_not_available_for_private")) return err } - mutex.Lock() - defer mutex.Unlock() + flow.mutex.Lock() + defer flow.mutex.Unlock() flow.l.Infof("chat_id: %d, game started by %v", message.Chat.ID, message.Sender) From d4ef90f3da78ad3ec1e2ea7f685c339787deb978 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Wed, 30 Mar 2022 21:34:48 +0300 Subject: [PATCH 373/439] Add `chatStorage` and `commandService` to the `GameFlow` --- pullanusbot.go | 5 ++--- usecases/faggot_game.go | 16 +++++++++------- usecases/faggot_game_test.go | 4 +++- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/pullanusbot.go b/pullanusbot.go index 68ed5d9..2524abe 100644 --- a/pullanusbot.go +++ b/pullanusbot.go @@ -33,7 +33,8 @@ func main() { localizer := infrastructure.GameLocalizer{} gameStorage := infrastructure.CreateGameStorage(dbFile) rand := infrastructure.CreateMathRand() - gameFlow := usecases.CreateGameFlow(logger, localizer, gameStorage, rand) + commandService := usecases.CreateCommandService(logger) + gameFlow := usecases.CreateGameFlow(logger, localizer, gameStorage, rand, chatStorageDecorator, commandService) telebot.AddHandler("/pidorules", gameFlow.Rules) telebot.AddHandler("/pidoreg", gameFlow.Add) telebot.AddHandler("/pidor", gameFlow.Play) @@ -90,8 +91,6 @@ func main() { removeYoutubeSourceDecorator := usecases.CreateRemoveSourceDecorator(logger, youtubeFlow) telebot.AddHandler(removeYoutubeSourceDecorator) - commandService := usecases.CreateCommandService(logger) - telebot.AddHandler("/proxy", func(m *core.Message, bot core.IBot) error { _ = commandService.EnableCommands(m.Chat.ID, []core.Command{{Text: "proxy", Description: "proxy server for telegram"}}, bot) _, err := bot.SendText("tg://proxy?server=proxy.ailinykh.com&port=443&secret=dd71ce3b5bf1b7015dc62a76dc244c5aec") diff --git a/usecases/faggot_game.go b/usecases/faggot_game.go index d8d1d4a..60b50c8 100644 --- a/usecases/faggot_game.go +++ b/usecases/faggot_game.go @@ -14,17 +14,19 @@ import ( ) // CreateGameFlow is a simple GameFlow factory -func CreateGameFlow(l core.ILogger, t core.ILocalizer, s core.IGameStorage, r core.IRand) *GameFlow { - return &GameFlow{l, t, s, r, sync.Mutex{}} +func CreateGameFlow(l core.ILogger, t core.ILocalizer, s core.IGameStorage, r core.IRand, chatStorage core.IChatStorage, commandService core.ICommandService) *GameFlow { + return &GameFlow{l, t, s, r, chatStorage, commandService, sync.Mutex{}} } // GameFlow represents faggot game logic type GameFlow struct { - l core.ILogger - t core.ILocalizer - s core.IGameStorage - r core.IRand - mutex sync.Mutex + l core.ILogger + t core.ILocalizer + s core.IGameStorage + r core.IRand + chatStorage core.IChatStorage + commandService core.ICommandService + mutex sync.Mutex } // Rules of the game diff --git a/usecases/faggot_game_test.go b/usecases/faggot_game_test.go index 3d9ef81..13bcc99 100644 --- a/usecases/faggot_game_test.go +++ b/usecases/faggot_game_test.go @@ -315,7 +315,9 @@ func makeSUT(args ...interface{}) (*usecases.GameFlow, *test_helpers.FakeBot, *G l := &test_helpers.FakeLogger{} t := test_helpers.CreateLocalizer(dict) r := &RandMock{} - game := usecases.CreateGameFlow(l, t, storage, r) + s := test_helpers.CreateChatStorage() + c := test_helpers.CreateCommandService(l) + game := usecases.CreateGameFlow(l, t, storage, r, s, c) return game, bot, storage } From f6db8e0c756e49607812e801c858c7538b9a17b0 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Wed, 30 Mar 2022 21:36:37 +0300 Subject: [PATCH 374/439] Enable `GameFlow` commands depending on chat settings --- usecases/faggot_game.go | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/usecases/faggot_game.go b/usecases/faggot_game.go index 60b50c8..e020b44 100644 --- a/usecases/faggot_game.go +++ b/usecases/faggot_game.go @@ -76,6 +76,28 @@ func (flow *GameFlow) Play(message *core.Message, bot core.IBot) error { flow.mutex.Lock() defer flow.mutex.Unlock() + if !message.Chat.Settings.FaggotGameCommandsEnabled { + settings := message.Chat.Settings + settings.FaggotGameCommandsEnabled = true + err := flow.chatStorage.UpdateSettings(message.Chat.ID, settings) + if err != nil { + return err + } + + commands := []core.Command{ + {Text: "pidor", Description: "play the game, see /pidorules first"}, + {Text: "pidorules", Description: "POTD game rules"}, + {Text: "pidoreg", Description: "register for POTD game"}, + {Text: "pidorstats", Description: "POTD game stats for this year"}, + {Text: "pidorall", Description: "POTD game stats for all time"}, + {Text: "pidorme", Description: "POTD personal stats"}, + } + err = flow.commandService.EnableCommands(message.Chat.ID, commands, bot) + if err != nil { + return err + } + } + flow.l.Infof("chat_id: %d, game started by %v", message.Chat.ID, message.Sender) players, _ := flow.s.GetPlayers(message.Chat.ID) From 02cb8f549a53748c848180cab7840fcf73da1aaa Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Wed, 30 Mar 2022 21:59:57 +0300 Subject: [PATCH 375/439] Respect chat settings in game tests --- usecases/faggot_game_test.go | 41 ++++++++++++++++++++---------------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/usecases/faggot_game_test.go b/usecases/faggot_game_test.go index 13bcc99..1cd195b 100644 --- a/usecases/faggot_game_test.go +++ b/usecases/faggot_game_test.go @@ -70,21 +70,22 @@ func Test_Add_AppendsPlayerInGameOnlyOnce(t *testing.T) { } func Test_Play_RespondsWithNoPlayers(t *testing.T) { + message := makeGameMessage(1, "Faggot") game, bot, _ := makeSUT(map[string]string{ "faggot_no_players": "Nobody in game. So you win, %s!", - }) - message := makeGameMessage(1, "Faggot") + }, message) - game.Play(message, bot) + err := game.Play(message, bot) + assert.Nil(t, err) assert.Equal(t, "Nobody in game. So you win, Faggot!", bot.SentMessages[0]) } func Test_Play_RespondsNotEnoughPlayers(t *testing.T) { + message := makeGameMessage(1, "Faggot") game, bot, _ := makeSUT(map[string]string{ "faggot_not_enough_players": "Not enough players", - }) - message := makeGameMessage(1, "Faggot") + }, message) game.Add(message, bot) game.Play(message, bot) @@ -93,15 +94,15 @@ func Test_Play_RespondsNotEnoughPlayers(t *testing.T) { } func Test_Play_RespondsWithCurrentGameResult(t *testing.T) { + m1 := makeGameMessage(1, "") + m2 := makeGameMessage(2, "") game, bot, storage := makeSUT(map[string]string{ "faggot_game_0_0": "0", "faggot_game_1_0": "1", "faggot_game_2_0": "2", "faggot_game_3_0": "%s", - }) + }, m1, m2) bot.ChatMembers[0] = []string{""} - m1 := makeGameMessage(1, "") - m2 := makeGameMessage(2, "") game.Add(m1, bot) game.Add(m2, bot) @@ -115,16 +116,16 @@ func Test_Play_RespondsWithCurrentGameResult(t *testing.T) { assert.Equal(t, phrase, bot.SentMessages[5]) } func Test_Play_RespondsWinnerAlreadyKnown(t *testing.T) { + m1 := makeGameMessage(1, "Faggot1") + m2 := makeGameMessage(2, "Faggot2") game, bot, storage := makeSUT(map[string]string{ "faggot_game_0_0": "0", "faggot_game_1_0": "1", "faggot_game_2_0": "2", "faggot_game_3_0": "3 %s", "faggot_winner_known": "Winner already known %s", - }) + }, m1) bot.ChatMembers[0] = []string{"Faggot1", "Faggot2"} - m1 := makeGameMessage(1, "Faggot1") - m2 := makeGameMessage(2, "Faggot2") game.Add(m1, bot) game.Add(m2, bot) @@ -142,11 +143,12 @@ func Test_Play_RespondsWinnerAlreadyKnown(t *testing.T) { } func Test_Play_RespondsWinnerLeftTheChat(t *testing.T) { - game, bot, storage := makeSUT(map[string]string{ - "faggot_winner_left": "winner left", - }) m1 := makeGameMessage(1, "Faggot1") m2 := makeGameMessage(2, "Faggot2") + game, bot, storage := makeSUT(map[string]string{ + "faggot_winner_left": "winner left", + }, m1) + storage.players = []*core.User{m1.Sender, m2.Sender} game.Play(m1, bot) @@ -297,26 +299,29 @@ func makeGameMessage(id int64, username string) *core.Message { LastName: "LastName" + fmt.Sprint(id), Username: username, } - return &core.Message{ID: 0, Chat: &core.Chat{ID: 0}, Sender: player} + settings := core.DefaultSettings() + return &core.Message{ID: 0, Chat: &core.Chat{ID: 0, Settings: &settings}, Sender: player} } func makeSUT(args ...interface{}) (*usecases.GameFlow, *test_helpers.FakeBot, *GameStorageMock) { dict := map[string]string{} storage := &GameStorageMock{players: []*core.User{}, rounds: []*core.Round{}} bot := test_helpers.CreateBot() + l := &test_helpers.FakeLogger{} + s := test_helpers.CreateChatStorage() for _, arg := range args { switch opt := arg.(type) { case map[string]string: dict = opt + case *core.Message: + s.Chats[opt.Chat.ID] = opt.Chat } } - l := &test_helpers.FakeLogger{} t := test_helpers.CreateLocalizer(dict) - r := &RandMock{} - s := test_helpers.CreateChatStorage() c := test_helpers.CreateCommandService(l) + r := &RandMock{} game := usecases.CreateGameFlow(l, t, storage, r, s, c) return game, bot, storage } From 39c1c6734f68eea85382284cb67aa5620c1a893e Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Sat, 9 Apr 2022 11:16:54 +0300 Subject: [PATCH 376/439] Fix instagram reel regexp --- api/instagram_api.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/api/instagram_api.go b/api/instagram_api.go index 2dc0ab7..46b0927 100644 --- a/api/instagram_api.go +++ b/api/instagram_api.go @@ -46,7 +46,9 @@ func (api *InstagramAPI) GetReel(url string) (*IgReel, error) { return nil, err } - r := regexp.MustCompile(`window.__additionalDataLoaded\('[\w\/]+',(.*?)\);`) + // os.WriteFile("instagram.html", body, 0644) + + r := regexp.MustCompile(`window.__additionalDataLoaded\('[\w\/-]+',(.*?)\);`) match := r.FindSubmatch(body) if len(match) < 1 { api.l.Error(match) From 3ff5c01a3a5bf01dfda382949a6a78f639541596 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Sat, 9 Apr 2022 11:28:30 +0300 Subject: [PATCH 377/439] Fix for `MEDIA_CAPTION_TOO_LONG` telegram error --- api/telebot_adapter.go | 6 +++--- api/telebot_factory.go | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/api/telebot_adapter.go b/api/telebot_adapter.go index d0c2bb0..12618a1 100644 --- a/api/telebot_adapter.go +++ b/api/telebot_adapter.go @@ -109,18 +109,18 @@ func (a *TelebotAdapter) SendMedia(media *core.Media) (*core.Message, error) { case core.TPhoto: a.t.logger.Infof("sending media as photo: %v", media) file := &tb.Photo{File: tb.FromURL(media.ResourceURL)} - file.Caption = media.Caption + file.Caption = media.Caption[:1024] a.t.bot.Notify(a.m.Chat, tb.UploadingPhoto) sent, err = a.t.bot.Send(a.m.Chat, file, opts) case core.TVideo: a.t.logger.Infof("sending media as video: %v", media) file := &tb.Video{File: tb.FromURL(media.ResourceURL)} - file.Caption = media.Caption + file.Caption = media.Caption[:1024] a.t.bot.Notify(a.m.Chat, tb.UploadingVideo) sent, err = a.t.bot.Send(a.m.Chat, file, opts) case core.TText: a.t.logger.Infof("sending media as text: %v", media) - sent, err = a.t.bot.Send(a.m.Chat, media.Caption, opts) + sent, err = a.t.bot.Send(a.m.Chat, media.Caption[:1024], opts) } if err != nil { diff --git a/api/telebot_factory.go b/api/telebot_factory.go index 2e29532..fb20e33 100644 --- a/api/telebot_factory.go +++ b/api/telebot_factory.go @@ -24,13 +24,13 @@ func makeTbVideo(vf *core.Video, caption string) *tb.Video { var video *tb.Video if len(vf.ID) > 0 { video = &tb.Video{File: tb.File{FileID: vf.ID}} - video.Caption = caption + video.Caption = caption[:1024] } else { video = &tb.Video{File: tb.FromDisk(vf.Path)} video.FileName = vf.File.Name video.Width = vf.Width video.Height = vf.Height - video.Caption = caption + video.Caption = caption[:1024] video.Duration = vf.Duration video.Streaming = true video.Thumbnail = &tb.Photo{ @@ -47,7 +47,7 @@ func makeTbPhoto(image *core.Image, caption string) *tb.Photo { if len(image.ID) > 0 { photo = &tb.Photo{File: tb.File{FileID: image.ID}} } - photo.Caption = caption + photo.Caption = caption[:1024] return photo } From ae3bb99622a26cf238b9e2e4774f5133ae365629 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Sat, 9 Apr 2022 11:38:28 +0300 Subject: [PATCH 378/439] add makeCaption func to telebot factory --- api/telebot_adapter.go | 6 +++--- api/telebot_factory.go | 13 ++++++++++--- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/api/telebot_adapter.go b/api/telebot_adapter.go index 12618a1..412ca55 100644 --- a/api/telebot_adapter.go +++ b/api/telebot_adapter.go @@ -109,18 +109,18 @@ func (a *TelebotAdapter) SendMedia(media *core.Media) (*core.Message, error) { case core.TPhoto: a.t.logger.Infof("sending media as photo: %v", media) file := &tb.Photo{File: tb.FromURL(media.ResourceURL)} - file.Caption = media.Caption[:1024] + file.Caption = makeCaption(media.Caption) a.t.bot.Notify(a.m.Chat, tb.UploadingPhoto) sent, err = a.t.bot.Send(a.m.Chat, file, opts) case core.TVideo: a.t.logger.Infof("sending media as video: %v", media) file := &tb.Video{File: tb.FromURL(media.ResourceURL)} - file.Caption = media.Caption[:1024] + file.Caption = makeCaption(media.Caption) a.t.bot.Notify(a.m.Chat, tb.UploadingVideo) sent, err = a.t.bot.Send(a.m.Chat, file, opts) case core.TText: a.t.logger.Infof("sending media as text: %v", media) - sent, err = a.t.bot.Send(a.m.Chat, media.Caption[:1024], opts) + sent, err = a.t.bot.Send(a.m.Chat, makeCaption(media.Caption), opts) } if err != nil { diff --git a/api/telebot_factory.go b/api/telebot_factory.go index fb20e33..6dc90d7 100644 --- a/api/telebot_factory.go +++ b/api/telebot_factory.go @@ -24,13 +24,13 @@ func makeTbVideo(vf *core.Video, caption string) *tb.Video { var video *tb.Video if len(vf.ID) > 0 { video = &tb.Video{File: tb.File{FileID: vf.ID}} - video.Caption = caption[:1024] + video.Caption = makeCaption(caption) } else { video = &tb.Video{File: tb.FromDisk(vf.Path)} video.FileName = vf.File.Name video.Width = vf.Width video.Height = vf.Height - video.Caption = caption[:1024] + video.Caption = makeCaption(caption) video.Duration = vf.Duration video.Streaming = true video.Thumbnail = &tb.Photo{ @@ -47,7 +47,7 @@ func makeTbPhoto(image *core.Image, caption string) *tb.Photo { if len(image.ID) > 0 { photo = &tb.Photo{File: tb.File{FileID: image.ID}} } - photo.Caption = caption[:1024] + photo.Caption = makeCaption(caption) return photo } @@ -84,3 +84,10 @@ func makeTbCommands(commands []core.Command) []tb.Command { } return comands } + +func makeCaption(caption string) string { + if len(caption) > 1024 { + return caption[:1024] + } + return caption +} From 4b053cc255edab5112e4deb2142847427534186b Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Sat, 9 Apr 2022 12:58:53 +0300 Subject: [PATCH 379/439] Extract instagram reel id using regexp --- api/instagram_api.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/api/instagram_api.go b/api/instagram_api.go index 46b0927..fa43ead 100644 --- a/api/instagram_api.go +++ b/api/instagram_api.go @@ -48,15 +48,16 @@ func (api *InstagramAPI) GetReel(url string) (*IgReel, error) { // os.WriteFile("instagram.html", body, 0644) - r := regexp.MustCompile(`window.__additionalDataLoaded\('[\w\/-]+',(.*?)\);`) + r := regexp.MustCompile(`window.__additionalDataLoaded\('\/reel\/([\w-]+)\/',(.*?)\);`) match := r.FindSubmatch(body) - if len(match) < 1 { - api.l.Error(match) + if len(match) < 2 { return nil, fmt.Errorf("unexpected html") } + // os.WriteFile("instagram"+string(match[1])+".json", match[2], 0644) + var reel IgReel - err = json.Unmarshal([]byte(match[1]), &reel) + err = json.Unmarshal([]byte(match[2]), &reel) if err != nil { api.l.Error(err) return nil, err From baa66163557ed67bf37be72915df21fc9e611f73 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Sat, 9 Apr 2022 12:59:37 +0300 Subject: [PATCH 380/439] Add reel clips metadata to `IgReelItem` --- api/instagram_api.go | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/api/instagram_api.go b/api/instagram_api.go index fa43ead..e4e92be 100644 --- a/api/instagram_api.go +++ b/api/instagram_api.go @@ -79,7 +79,8 @@ type IgReelItem struct { Code string User IgReelUser Caption IgReelCaption - VideoVersions []IgReelVideo `json:"video_versions"` + VideoVersions []IgReelVideo `json:"video_versions"` + ClipsMetadata IgReelClipsMetadata `json:"clips_metadata"` } type IgReelVideo struct { @@ -91,3 +92,22 @@ type IgReelVideo struct { type IgReelCaption struct { Text string } + +type IgReelClipsMetadata struct { + MusicInfo *IgReelMusicInfo `json:"music_info"` + OriginalSoundInfo *IgReelOriginalSoundInfo `json:"original_sound_info"` +} + +type IgReelMusicInfo struct { + MusicAssetInfo IgReelMusicAssetInfo `json:"music_asset_info"` +} + +type IgReelMusicAssetInfo struct { + DisplayArtist string `json:"display_artist"` + Title string + ProgressiveDownloadURL string `json:"progressive_download_url"` +} + +type IgReelOriginalSoundInfo struct { + ProgressiveDownloadURL string `json:"progressive_download_url"` +} From 6e069a0fc4afa7abadfcb9149469d99754c07837 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Sat, 9 Apr 2022 13:14:03 +0300 Subject: [PATCH 381/439] Save instagram json into file for debug reasons --- api/instagram_api.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/instagram_api.go b/api/instagram_api.go index e4e92be..c8f9119 100644 --- a/api/instagram_api.go +++ b/api/instagram_api.go @@ -54,7 +54,7 @@ func (api *InstagramAPI) GetReel(url string) (*IgReel, error) { return nil, fmt.Errorf("unexpected html") } - // os.WriteFile("instagram"+string(match[1])+".json", match[2], 0644) + // os.WriteFile("instagram-"+string(match[1])+".json", match[2], 0644) var reel IgReel err = json.Unmarshal([]byte(match[2]), &reel) From f1cda165b2ef15299348d09b4274df578930d37b Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Sat, 9 Apr 2022 13:14:32 +0300 Subject: [PATCH 382/439] Append music to instagram reel caption if any --- api/instagram_media_factory.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/api/instagram_media_factory.go b/api/instagram_media_factory.go index 91cd18d..f9f9240 100644 --- a/api/instagram_media_factory.go +++ b/api/instagram_media_factory.go @@ -28,5 +28,11 @@ func (factory *InstagramMediaFactory) CreateMedia(url string) ([]*core.Media, er } item := reel.Items[0] - return []*core.Media{{ResourceURL: item.VideoVersions[0].URL, URL: "https://www.instagram.com/reel/" + item.Code + "/", Title: item.User.FullName, Caption: item.Caption.Text}}, nil + caption := item.Caption.Text + + if info := item.ClipsMetadata.MusicInfo; info != nil { + caption = fmt.Sprintf("\n🎶 %s - %s\n\n%s", info.MusicAssetInfo.ProgressiveDownloadURL, info.MusicAssetInfo.DisplayArtist, info.MusicAssetInfo.Title, caption) + } + + return []*core.Media{{ResourceURL: item.VideoVersions[0].URL, URL: "https://www.instagram.com/reel/" + item.Code + "/", Title: item.User.FullName, Caption: caption}}, nil } From 96d127d6f3ca8b2fdeb1157ccfba12fe00b971fc Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Sat, 23 Apr 2022 16:57:14 +0300 Subject: [PATCH 383/439] Extend reels flow with UploadVideoStrategy --- pullanusbot.go | 2 +- usecases/reels_flow.go | 25 +++++++++++++++---------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/pullanusbot.go b/pullanusbot.go index 2524abe..e62f2f4 100644 --- a/pullanusbot.go +++ b/pullanusbot.go @@ -101,7 +101,7 @@ func main() { telebot.AddHandler(iDoNotCare) reelsAPI := api.CreateInstagramMediaFactory(logger, path.Join(getWorkingDir(), "cookies.json")) - reelsFlow := usecases.CreateReelsFlow(logger, reelsAPI, localMediaSender) + reelsFlow := usecases.CreateReelsFlow(logger, reelsAPI, localMediaSender, sendVideoStrategySplitDecorator) removeReelsSourceDecorator := usecases.CreateRemoveSourceDecorator(logger, reelsFlow) telebot.AddHandler(removeReelsSourceDecorator) diff --git a/usecases/reels_flow.go b/usecases/reels_flow.go index 3081021..1287354 100644 --- a/usecases/reels_flow.go +++ b/usecases/reels_flow.go @@ -7,14 +7,15 @@ import ( "github.com/ailinykh/pullanusbot/v2/core" ) -func CreateReelsFlow(l core.ILogger, mediaFactory core.IMediaFactory, sendMediaStrategy core.ISendMediaStrategy) core.ITextHandler { - return &ReelsFlow{l, mediaFactory, sendMediaStrategy} +func CreateReelsFlow(l core.ILogger, mediaFactory core.IMediaFactory, sendMedia core.ISendMediaStrategy, sendVideo core.ISendVideoStrategy) core.ITextHandler { + return &ReelsFlow{l, mediaFactory, sendMedia, sendVideo} } type ReelsFlow struct { - l core.ILogger - mediaFactory core.IMediaFactory - sendMediaStrategy core.ISendMediaStrategy + l core.ILogger + mediaFactory core.IMediaFactory + sendMedia core.ISendMediaStrategy + sendVideo core.ISendVideoStrategy } // HandleText is a core.ITextHandler protocol implementation @@ -22,11 +23,15 @@ func (flow *ReelsFlow) HandleText(message *core.Message, bot core.IBot) error { r := regexp.MustCompile(`https://www.instagram.com/reel/\S+`) match := r.FindAllStringSubmatch(message.Text, -1) - if len(match) < 1 { - return fmt.Errorf("not implemented") + if len(match) > 0 { + return flow.handleReel(match[0][0], message, bot) } - media, err := flow.mediaFactory.CreateMedia(match[0][0]) + return fmt.Errorf("not implemented") +} + +func (flow *ReelsFlow) handleReel(url string, message *core.Message, bot core.IBot) error { + media, err := flow.mediaFactory.CreateMedia(url) if err != nil { flow.l.Error(err) return err @@ -38,8 +43,8 @@ func (flow *ReelsFlow) HandleText(message *core.Message, bot core.IBot) error { m := &core.Media{ ResourceURL: media[0].ResourceURL, - Caption: fmt.Sprintf("📷 %s (by %s)\n%s", match[0][0], media[0].Title, message.Sender.DisplayName(), media[0].Caption), + Caption: fmt.Sprintf("📷 %s (by %s)\n%s", url, media[0].Title, message.Sender.DisplayName(), media[0].Caption), } - return flow.sendMediaStrategy.SendMedia([]*core.Media{m}, bot) + return flow.sendMedia.SendMedia([]*core.Media{m}, bot) } From a076b3e6ac81cda48de3b10ecfe8601b6bd5bade Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Sat, 23 Apr 2022 16:59:06 +0300 Subject: [PATCH 384/439] Rename `reels_flow` to more generic `instagram_flow` --- pullanusbot.go | 8 ++++---- usecases/{reels_flow.go => instagram_flow.go} | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) rename usecases/{reels_flow.go => instagram_flow.go} (69%) diff --git a/pullanusbot.go b/pullanusbot.go index e62f2f4..6cffd4e 100644 --- a/pullanusbot.go +++ b/pullanusbot.go @@ -100,10 +100,10 @@ func main() { iDoNotCare := usecases.CreateIDoNotCare() telebot.AddHandler(iDoNotCare) - reelsAPI := api.CreateInstagramMediaFactory(logger, path.Join(getWorkingDir(), "cookies.json")) - reelsFlow := usecases.CreateReelsFlow(logger, reelsAPI, localMediaSender, sendVideoStrategySplitDecorator) - removeReelsSourceDecorator := usecases.CreateRemoveSourceDecorator(logger, reelsFlow) - telebot.AddHandler(removeReelsSourceDecorator) + instaAPI := api.CreateInstagramMediaFactory(logger, path.Join(getWorkingDir(), "cookies.json")) + instaFlow := usecases.CreateInstagramFlow(logger, instaAPI, localMediaSender, sendVideoStrategySplitDecorator) + removeInstaSourceDecorator := usecases.CreateRemoveSourceDecorator(logger, instaFlow) + telebot.AddHandler(removeInstaSourceDecorator) commonLocalizer := infrastructure.CreateCommonLocalizer() startFlow := usecases.CreateStartFlow(logger, commonLocalizer, chatStorageDecorator, commandService) diff --git a/usecases/reels_flow.go b/usecases/instagram_flow.go similarity index 69% rename from usecases/reels_flow.go rename to usecases/instagram_flow.go index 1287354..c669abf 100644 --- a/usecases/reels_flow.go +++ b/usecases/instagram_flow.go @@ -7,11 +7,11 @@ import ( "github.com/ailinykh/pullanusbot/v2/core" ) -func CreateReelsFlow(l core.ILogger, mediaFactory core.IMediaFactory, sendMedia core.ISendMediaStrategy, sendVideo core.ISendVideoStrategy) core.ITextHandler { - return &ReelsFlow{l, mediaFactory, sendMedia, sendVideo} +func CreateInstagramFlow(l core.ILogger, mediaFactory core.IMediaFactory, sendMedia core.ISendMediaStrategy, sendVideo core.ISendVideoStrategy) core.ITextHandler { + return &InstagramFlow{l, mediaFactory, sendMedia, sendVideo} } -type ReelsFlow struct { +type InstagramFlow struct { l core.ILogger mediaFactory core.IMediaFactory sendMedia core.ISendMediaStrategy @@ -19,7 +19,7 @@ type ReelsFlow struct { } // HandleText is a core.ITextHandler protocol implementation -func (flow *ReelsFlow) HandleText(message *core.Message, bot core.IBot) error { +func (flow *InstagramFlow) HandleText(message *core.Message, bot core.IBot) error { r := regexp.MustCompile(`https://www.instagram.com/reel/\S+`) match := r.FindAllStringSubmatch(message.Text, -1) @@ -30,7 +30,7 @@ func (flow *ReelsFlow) HandleText(message *core.Message, bot core.IBot) error { return fmt.Errorf("not implemented") } -func (flow *ReelsFlow) handleReel(url string, message *core.Message, bot core.IBot) error { +func (flow *InstagramFlow) handleReel(url string, message *core.Message, bot core.IBot) error { media, err := flow.mediaFactory.CreateMedia(url) if err != nil { flow.l.Error(err) From 62b30715bf4c09cba2735ee0515ea632dac92f1b Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Sat, 15 Oct 2022 20:08:26 +0400 Subject: [PATCH 385/439] Rename TikTok to TikTokV1 --- api/tiktok.go | 55 ------------------------------------- api/tiktok_api_decorator.go | 2 +- api/tiktok_html_api.go | 4 +-- api/tiktok_json_api.go | 4 +-- api/tiktok_media_factory.go | 2 +- api/tiktok_v1.go | 55 +++++++++++++++++++++++++++++++++++++ 6 files changed, 61 insertions(+), 61 deletions(-) delete mode 100644 api/tiktok.go create mode 100644 api/tiktok_v1.go diff --git a/api/tiktok.go b/api/tiktok.go deleted file mode 100644 index 3182da4..0000000 --- a/api/tiktok.go +++ /dev/null @@ -1,55 +0,0 @@ -package api - -type TikTokJSONResponse struct { - ItemInfo TikTokItemInfo -} - -type TikTokHTMLResponse struct { - Props TikTokHTMLProps -} - -type TikTokHTMLProps struct { - PageProps TikTokResponse -} - -type TikTokResponse struct { - ServerCode int - StatusCode int - ItemInfo TikTokItemInfo -} - -type TikTokItemInfo struct { - ItemStruct TikTokItemStruct -} - -type TikTokItemStruct struct { - Desc string - Author TikTokAuthor - Music TikTokMusic - Video TikTokVideo - StickersOnItem []TikTokSticker -} - -type TikTokAuthor struct { - UniqueId string - Nickname string -} - -type TikTokMusic struct { - Id string - Title string - AuthorName string -} - -type TikTokVideo struct { - Id string - DownloadAddr string - ShareCover []string - Bitrate int - CodecType string -} - -type TikTokSticker struct { - StickerText []string - StickerType int -} diff --git a/api/tiktok_api_decorator.go b/api/tiktok_api_decorator.go index 951e1e4..73a52d3 100644 --- a/api/tiktok_api_decorator.go +++ b/api/tiktok_api_decorator.go @@ -9,7 +9,7 @@ type TikTokAPIDecorator struct { secondary ITikTokAPI } -func (api *TikTokAPIDecorator) GetItem(username string, videoId string) (*TikTokItemStruct, error) { +func (api *TikTokAPIDecorator) GetItem(username string, videoId string) (*TikTokV1ItemStruct, error) { item, err := api.primary.GetItem(username, videoId) if err != nil { return api.secondary.GetItem(username, videoId) diff --git a/api/tiktok_html_api.go b/api/tiktok_html_api.go index 1b13a7f..64af6c4 100644 --- a/api/tiktok_html_api.go +++ b/api/tiktok_html_api.go @@ -19,7 +19,7 @@ type TikTokHTMLAPI struct { r core.IRand } -func (api *TikTokHTMLAPI) GetItem(username string, videoId string) (*TikTokItemStruct, error) { +func (api *TikTokHTMLAPI) GetItem(username string, videoId string) (*TikTokV1ItemStruct, error) { url := "https://www.tiktok.com/" + username + "/video/" + videoId api.l.Infof("processing %s", url) api.hc.SetHeader("Cookie", "tt_webid_v2=69"+api.randomDigits(17)+"; Domain=tiktok.com; Path=/; Secure; hostOnly=false; hostOnly=false; aAge=4ms; cAge=4ms") @@ -36,7 +36,7 @@ func (api *TikTokHTMLAPI) GetItem(username string, videoId string) (*TikTokItemS return nil, fmt.Errorf("unexpected html") } - var resp TikTokHTMLResponse + var resp TikTokV1HTMLResponse err = json.Unmarshal([]byte(match[1]), &resp) if err != nil { return nil, err diff --git a/api/tiktok_json_api.go b/api/tiktok_json_api.go index e0a5e3e..44bbc5d 100644 --- a/api/tiktok_json_api.go +++ b/api/tiktok_json_api.go @@ -17,7 +17,7 @@ type TikTokJsonAPI struct { r core.IRand } -func (api *TikTokJsonAPI) GetItem(username string, videoId string) (*TikTokItemStruct, error) { +func (api *TikTokJsonAPI) GetItem(username string, videoId string) (*TikTokV1ItemStruct, error) { url := "https://www.tiktok.com/node/share/video/" + username + "/" + videoId api.l.Infof("processing %s", url) api.hc.SetHeader("Cookie", "tt_webid_v2=69"+api.randomDigits(17)+"; Domain=tiktok.com; Path=/; Secure; hostOnly=false; hostOnly=false; aAge=4ms; cAge=4ms") @@ -26,7 +26,7 @@ func (api *TikTokJsonAPI) GetItem(username string, videoId string) (*TikTokItemS return nil, err } - var resp TikTokJSONResponse + var resp TikTokV1JSONResponse err = json.Unmarshal([]byte(jsonString), &resp) if err != nil { return nil, err diff --git a/api/tiktok_media_factory.go b/api/tiktok_media_factory.go index 39ff034..1dc01de 100644 --- a/api/tiktok_media_factory.go +++ b/api/tiktok_media_factory.go @@ -8,7 +8,7 @@ import ( ) type ITikTokAPI interface { - GetItem(string, string) (*TikTokItemStruct, error) + GetItem(string, string) (*TikTokV1ItemStruct, error) } func CreateTikTokMediaFactory(l core.ILogger, api ITikTokAPI) core.IMediaFactory { diff --git a/api/tiktok_v1.go b/api/tiktok_v1.go new file mode 100644 index 0000000..994f071 --- /dev/null +++ b/api/tiktok_v1.go @@ -0,0 +1,55 @@ +package api + +type TikTokV1JSONResponse struct { + ItemInfo TikTokV1ItemInfo +} + +type TikTokV1HTMLResponse struct { + Props TikTokV1HTMLProps +} + +type TikTokV1HTMLProps struct { + PageProps TikTokV1Response +} + +type TikTokV1Response struct { + ServerCode int + StatusCode int + ItemInfo TikTokV1ItemInfo +} + +type TikTokV1ItemInfo struct { + ItemStruct TikTokV1ItemStruct +} + +type TikTokV1ItemStruct struct { + Desc string + Author TikTokV1Author + Music TikTokV1Music + Video TikTokV1Video + StickersOnItem []TikTokV1Sticker +} + +type TikTokV1Author struct { + UniqueId string + Nickname string +} + +type TikTokV1Music struct { + Id string + Title string + AuthorName string +} + +type TikTokV1Video struct { + Id string + DownloadAddr string + ShareCover []string + Bitrate int + CodecType string +} + +type TikTokV1Sticker struct { + StickerText []string + StickerType int +} From a7ef413688cf237d4391a9a93715caa9add08492 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Sat, 15 Oct 2022 22:16:24 +0400 Subject: [PATCH 386/439] TikTok HTML API v2 --- api/tiktok.go | 22 ++++++ api/tiktok_api_decorator.go | 2 +- ...ktok_html_api.go => tiktok_html_v1_api.go} | 34 ++++++-- api/tiktok_html_v2_api.go | 79 +++++++++++++++++++ api/tiktok_json_api.go | 23 +++++- api/tiktok_media_factory.go | 12 +-- api/tiktok_v2.go | 18 +++++ pullanusbot.go | 8 +- 8 files changed, 177 insertions(+), 21 deletions(-) create mode 100644 api/tiktok.go rename api/{tiktok_html_api.go => tiktok_html_v1_api.go} (63%) create mode 100644 api/tiktok_html_v2_api.go create mode 100644 api/tiktok_v2.go diff --git a/api/tiktok.go b/api/tiktok.go new file mode 100644 index 0000000..2f05780 --- /dev/null +++ b/api/tiktok.go @@ -0,0 +1,22 @@ +package api + +type ITikTokAPI interface { + GetItem(string, string) (*TikTokItem, error) +} + +type TikTokItem struct { + Author TikTokAuthor + Desc string + Music TikTokMusic + Stickers []string + VideoURL string +} + +type TikTokAuthor struct { + Nickname string + UniqueId string +} + +type TikTokMusic struct { + Title string +} diff --git a/api/tiktok_api_decorator.go b/api/tiktok_api_decorator.go index 73a52d3..7037e97 100644 --- a/api/tiktok_api_decorator.go +++ b/api/tiktok_api_decorator.go @@ -9,7 +9,7 @@ type TikTokAPIDecorator struct { secondary ITikTokAPI } -func (api *TikTokAPIDecorator) GetItem(username string, videoId string) (*TikTokV1ItemStruct, error) { +func (api *TikTokAPIDecorator) GetItem(username string, videoId string) (*TikTokItem, error) { item, err := api.primary.GetItem(username, videoId) if err != nil { return api.secondary.GetItem(username, videoId) diff --git a/api/tiktok_html_api.go b/api/tiktok_html_v1_api.go similarity index 63% rename from api/tiktok_html_api.go rename to api/tiktok_html_v1_api.go index 64af6c4..a59121b 100644 --- a/api/tiktok_html_api.go +++ b/api/tiktok_html_v1_api.go @@ -9,17 +9,17 @@ import ( "github.com/ailinykh/pullanusbot/v2/core" ) -func CreateTikTokHTMLAPI(l core.ILogger, hc core.IHttpClient, r core.IRand) ITikTokAPI { - return &TikTokHTMLAPI{l, hc, r} +func CreateTikTokHTMLV1API(l core.ILogger, hc core.IHttpClient, r core.IRand) ITikTokAPI { + return &TikTokHTMLV1API{l, hc, r} } -type TikTokHTMLAPI struct { +type TikTokHTMLV1API struct { l core.ILogger hc core.IHttpClient r core.IRand } -func (api *TikTokHTMLAPI) GetItem(username string, videoId string) (*TikTokV1ItemStruct, error) { +func (api *TikTokHTMLV1API) GetItem(username string, videoId string) (*TikTokItem, error) { url := "https://www.tiktok.com/" + username + "/video/" + videoId api.l.Infof("processing %s", url) api.hc.SetHeader("Cookie", "tt_webid_v2=69"+api.randomDigits(17)+"; Domain=tiktok.com; Path=/; Secure; hostOnly=false; hostOnly=false; aAge=4ms; cAge=4ms") @@ -50,10 +50,32 @@ func (api *TikTokHTMLAPI) GetItem(username string, videoId string) (*TikTokV1Ite return nil, fmt.Errorf("%d not equal to zero", resp.Props.PageProps.StatusCode) } - return &resp.Props.PageProps.ItemInfo.ItemStruct, nil + item := resp.Props.PageProps.ItemInfo.ItemStruct + + stickers := []string{} + for _, s := range item.StickersOnItem { + for _, t := range s.StickerText { + stickers = append(stickers, t) + } + } + + i := TikTokItem{ + Author: TikTokAuthor{ + Nickname: item.Author.Nickname, + UniqueId: item.Author.UniqueId, + }, + Desc: item.Desc, + Music: TikTokMusic{ + Title: item.Music.Title, + }, + Stickers: stickers, + VideoURL: item.Video.DownloadAddr, + } + + return &i, nil } -func (api *TikTokHTMLAPI) randomDigits(count int) string { +func (api *TikTokHTMLV1API) randomDigits(count int) string { rv := "" for i := 1; i < count; i++ { rv = rv + strconv.Itoa(api.r.GetRand(10)) diff --git a/api/tiktok_html_v2_api.go b/api/tiktok_html_v2_api.go new file mode 100644 index 0000000..77ef04b --- /dev/null +++ b/api/tiktok_html_v2_api.go @@ -0,0 +1,79 @@ +package api + +import ( + "encoding/json" + "fmt" + "regexp" + "strconv" + + "github.com/ailinykh/pullanusbot/v2/core" +) + +func CreateTikTokHTMLV2API(l core.ILogger, hc core.IHttpClient, r core.IRand) ITikTokAPI { + return &TikTokHTMLV2API{l, hc, r} +} + +type TikTokHTMLV2API struct { + l core.ILogger + hc core.IHttpClient + r core.IRand +} + +func (api *TikTokHTMLV2API) GetItem(username string, videoId string) (*TikTokItem, error) { + url := "https://www.tiktok.com/" + username + "/video/" + videoId + api.l.Infof("processing %s", url) + api.hc.SetHeader("Cookie", "tt_webid_v2=69"+api.randomDigits(17)+"; Domain=tiktok.com; Path=/; Secure; hostOnly=false; hostOnly=false; aAge=4ms; cAge=4ms") + htmlString, err := api.hc.GetContent(url) + if err != nil { + return nil, err + } + + // os.WriteFile("tiktok-"+strings.Split(url, "/")[5]+".html", []byte(htmlString), 0644) + r := regexp.MustCompile(``) - match := r.FindSubmatch(body) - if len(match) < 2 { - return nil, fmt.Errorf("unexpected html") - } - - // os.WriteFile("instagram-"+string(match[1])+".json", match[2], 0644) + // os.WriteFile("instagram-reel-"+data.mediaId+".json", body, 0644) var reel IgReel - err = json.Unmarshal([]byte(match[2]), &reel) + err = json.Unmarshal(body, &reel) if err != nil { api.l.Error(err) return nil, err @@ -66,48 +66,50 @@ func (api *InstagramAPI) GetReel(url string) (*IgReel, error) { return &reel, nil } -type IgReel struct { - Items []IgReelItem -} - -type IgReelUser struct { - Username string - FullName string `json:"full_name"` -} - -type IgReelItem struct { - Code string - User IgReelUser - Caption IgReelCaption - VideoVersions []IgReelVideo `json:"video_versions"` - ClipsMetadata IgReelClipsMetadata `json:"clips_metadata"` -} +func (api *InstagramAPI) getContent(urlString string, headers map[string]string) ([]byte, error) { + req, err := http.NewRequest("GET", urlString, nil) + req.Header.Set("user-agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36") + for k, v := range headers { + req.Header.Set(k, v) + } -type IgReelVideo struct { - Width int - Height int - URL string + if err != nil { + api.l.Error(err) + return nil, err + } + resp, err := api.client.Do(req) + if err != nil { + api.l.Error(err) + return nil, err + } + defer resp.Body.Close() + return ioutil.ReadAll(resp.Body) } -type IgReelCaption struct { - Text string -} +func (p *InstagramAPI) parseData(data []byte) (*InstagramHTMLData, error) { + appId, err := p.parse(data, `"app_id":"(\d+)"`) + if err != nil { + return nil, err + } -type IgReelClipsMetadata struct { - MusicInfo *IgReelMusicInfo `json:"music_info"` - OriginalSoundInfo *IgReelOriginalSoundInfo `json:"original_sound_info"` -} + csrfToken, err := p.parse(data, `"csrf_token":"(\w+)"`) + if err != nil { + return nil, err + } -type IgReelMusicInfo struct { - MusicAssetInfo IgReelMusicAssetInfo `json:"music_asset_info"` -} + mediaId, err := p.parse(data, `"media_id":"(\d+)"`) + if err != nil { + return nil, err + } -type IgReelMusicAssetInfo struct { - DisplayArtist string `json:"display_artist"` - Title string - ProgressiveDownloadURL string `json:"progressive_download_url"` + return &InstagramHTMLData{string(appId), string(csrfToken), string(mediaId)}, nil } -type IgReelOriginalSoundInfo struct { - ProgressiveDownloadURL string `json:"progressive_download_url"` +func (p *InstagramAPI) parse(data []byte, reg string) ([]byte, error) { + r := regexp.MustCompile(reg) + match := r.FindSubmatch(data) + if len(match) < 2 { + return nil, fmt.Errorf("parse `%s` failed", reg) + } + return match[1], nil } diff --git a/api/instagram_media_factory.go b/api/instagram_media_factory.go deleted file mode 100644 index f9f9240..0000000 --- a/api/instagram_media_factory.go +++ /dev/null @@ -1,38 +0,0 @@ -package api - -import ( - "fmt" - - "github.com/ailinykh/pullanusbot/v2/core" -) - -func CreateInstagramMediaFactory(l core.ILogger, cookiesFile string) *InstagramMediaFactory { - return &InstagramMediaFactory{l, CreateInstagramAPI(l, cookiesFile)} -} - -type InstagramMediaFactory struct { - l core.ILogger - api *InstagramAPI -} - -// CreateMedia is a core.IMediaFactory interface implementation -func (factory *InstagramMediaFactory) CreateMedia(url string) ([]*core.Media, error) { - reel, err := factory.api.GetReel(url) - if err != nil { - factory.l.Error(err) - return nil, err - } - - if len(reel.Items) < 1 { - return nil, fmt.Errorf("insufficient reel items") - } - - item := reel.Items[0] - caption := item.Caption.Text - - if info := item.ClipsMetadata.MusicInfo; info != nil { - caption = fmt.Sprintf("\n🎶 %s - %s\n\n%s", info.MusicAssetInfo.ProgressiveDownloadURL, info.MusicAssetInfo.DisplayArtist, info.MusicAssetInfo.Title, caption) - } - - return []*core.Media{{ResourceURL: item.VideoVersions[0].URL, URL: "https://www.instagram.com/reel/" + item.Code + "/", Title: item.User.FullName, Caption: caption}}, nil -} diff --git a/pullanusbot.go b/pullanusbot.go index c12871c..07af466 100644 --- a/pullanusbot.go +++ b/pullanusbot.go @@ -102,8 +102,11 @@ func main() { iDoNotCare := usecases.CreateIDoNotCare() telebot.AddHandler(iDoNotCare) - instaAPI := api.CreateInstagramMediaFactory(logger, path.Join(getWorkingDir(), "cookies.json")) - instaFlow := usecases.CreateInstagramFlow(logger, instaAPI, localMediaSender, sendVideoStrategySplitDecorator) + cookies := path.Join(getWorkingDir(), "instagram-cookies.json") + jar := api.CreateJsonCookieJar(logger, cookies) + instaAPI := api.CreateInstagramAPI(logger, jar) + downloadVideoFactory := helpers.CreateDownloadVideoFactory(logger, fileDownloader, converter) + instaFlow := usecases.CreateInstagramFlow(logger, instaAPI, downloadVideoFactory, localMediaSender, sendVideoStrategySplitDecorator) removeInstaSourceDecorator := usecases.CreateRemoveSourceDecorator(logger, instaFlow) telebot.AddHandler(removeInstaSourceDecorator) diff --git a/usecases/instagram_flow.go b/usecases/instagram_flow.go index c669abf..c210f9f 100644 --- a/usecases/instagram_flow.go +++ b/usecases/instagram_flow.go @@ -4,47 +4,100 @@ import ( "fmt" "regexp" + "github.com/ailinykh/pullanusbot/v2/api" "github.com/ailinykh/pullanusbot/v2/core" ) -func CreateInstagramFlow(l core.ILogger, mediaFactory core.IMediaFactory, sendMedia core.ISendMediaStrategy, sendVideo core.ISendVideoStrategy) core.ITextHandler { - return &InstagramFlow{l, mediaFactory, sendMedia, sendVideo} +func CreateInstagramFlow(l core.ILogger, api api.InstAPI, createVideo core.IVideoFactory, sendMedia core.ISendMediaStrategy, sendVideo core.ISendVideoStrategy) core.ITextHandler { + return &InstagramFlow{l, api, createVideo, sendMedia, sendVideo} } type InstagramFlow struct { - l core.ILogger - mediaFactory core.IMediaFactory - sendMedia core.ISendMediaStrategy - sendVideo core.ISendVideoStrategy + l core.ILogger + api api.InstAPI + createVideo core.IVideoFactory + sendMedia core.ISendMediaStrategy + sendVideo core.ISendVideoStrategy } // HandleText is a core.ITextHandler protocol implementation func (flow *InstagramFlow) HandleText(message *core.Message, bot core.IBot) error { r := regexp.MustCompile(`https://www.instagram.com/reel/\S+`) - match := r.FindAllStringSubmatch(message.Text, -1) + rmatch := r.FindAllString(message.Text, -1) - if len(match) > 0 { - return flow.handleReel(match[0][0], message, bot) + switch len(rmatch) { + case 0: + break + case 1: + return flow.handleReel(rmatch[0], message, bot) + default: + for _, reel := range rmatch { + err := flow.handleReel(reel, message, bot) + if err != nil { + flow.l.Error(err) + return err + } + } + // FIXME: temporal coupling + return fmt.Errorf("do not remove source message") + } + + t := regexp.MustCompile(`https://www.instagram.com/tv/\S+`) + tmatch := t.FindAllString(message.Text, -1) + + // TODO: multiple tv? + if len(tmatch) > 0 { + return flow.handleReel(tmatch[0], message, bot) } return fmt.Errorf("not implemented") } func (flow *InstagramFlow) handleReel(url string, message *core.Message, bot core.IBot) error { - media, err := flow.mediaFactory.CreateMedia(url) + flow.l.Infof("processing %s", url) + reel, err := flow.api.GetReel(url) if err != nil { flow.l.Error(err) return err } - if len(media) < 1 { - return fmt.Errorf("unexpected count of media") + if len(reel.Items) < 1 { + return fmt.Errorf("insufficient reel items") + } + + item := reel.Items[0] + + caption := item.Caption.Text + if info := item.ClipsMetadata.MusicInfo; info != nil { + caption = fmt.Sprintf("\n🎶 %s - %s\n\n%s", info.MusicAssetInfo.ProgressiveDownloadURL, info.MusicAssetInfo.DisplayArtist, info.MusicAssetInfo.Title, caption) + } + caption = fmt.Sprintf("📷 %s (by %s)\n%s", url, item.User.FullName, message.Sender.DisplayName(), caption) + + if item.VideoDuration < 360 { // apparently 6 min file takes less than 50 MB + return flow.sendAsMedia(item, caption, message, bot) + } + + video, err := flow.createVideo.CreateVideo(item.VideoVersions[0].URL) + if err != nil { + flow.l.Error(err) + return err } + defer video.Dispose() - m := &core.Media{ - ResourceURL: media[0].ResourceURL, - Caption: fmt.Sprintf("📷 %s (by %s)\n%s", url, media[0].Title, message.Sender.DisplayName(), media[0].Caption), + return flow.sendVideo.SendVideo(video, caption, bot) +} + +func (flow *InstagramFlow) sendAsMedia(item api.IgReelItem, caption string, message *core.Message, bot core.IBot) error { + media := &core.Media{ + ResourceURL: item.VideoVersions[0].URL, + URL: "https://www.instagram.com/reel/" + item.Code + "/", + Title: item.User.FullName, + Caption: caption, } - return flow.sendMedia.SendMedia([]*core.Media{m}, bot) + err := flow.sendMedia.SendMedia([]*core.Media{media}, bot) + if err != nil { + flow.l.Error(err) + } + return err } From d2c77fa4e4900955ebc92ffeb9b53e6d4a886add Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Mon, 24 Oct 2022 22:39:52 +0400 Subject: [PATCH 397/439] Move logging into file downloader --- api/youtube_api.go | 2 -- helpers/download_video_factory.go | 1 - infrastructure/file_downloader.go | 15 +++++++++++---- pullanusbot.go | 2 +- usecases/youtube_flow.go | 1 - 5 files changed, 12 insertions(+), 9 deletions(-) diff --git a/api/youtube_api.go b/api/youtube_api.go index 0388ac1..4d9f283 100644 --- a/api/youtube_api.go +++ b/api/youtube_api.go @@ -124,7 +124,6 @@ func (y *YoutubeAPI) getThumb(video *Video, vf *Format) (*core.Image, error) { thumb := video.thumb() filename := fmt.Sprintf("youtube-%s-%s.jpg", video.ID, vf.FormatID) thumbPath := path.Join(os.TempDir(), filename) - y.l.Infof("downloading thumb %s", thumb.URL) file, err := y.fd.Download(thumb.URL, thumbPath) if err != nil { return nil, err @@ -140,7 +139,6 @@ func (y *YoutubeAPI) getThumbV2(video *Video, vf *Format) (*core.Image, error) { filename := fmt.Sprintf("youtube-%s-%s-maxres.jpg", video.ID, vf.FormatID) originalThumbPath := path.Join(os.TempDir(), filename+"-original") thumbPath := path.Join(os.TempDir(), filename) - y.l.Infof("downloading thumb %s", video.Thumbnail) file, err := y.fd.Download(video.Thumbnail, originalThumbPath) if err != nil { y.l.Error(err) diff --git a/helpers/download_video_factory.go b/helpers/download_video_factory.go index be8ebc4..ca09b43 100644 --- a/helpers/download_video_factory.go +++ b/helpers/download_video_factory.go @@ -31,7 +31,6 @@ func (factory *DownloadVideoFactory) CreateVideo(url string) (*core.Video, error } videoPath := path.Join(os.TempDir(), filename) - factory.l.Infof("downloading %s %s", videoPath, url) file, err := factory.fileDownloader.Download(url, videoPath) if err != nil { factory.l.Error(err) diff --git a/infrastructure/file_downloader.go b/infrastructure/file_downloader.go index 456c5b6..039efa2 100644 --- a/infrastructure/file_downloader.go +++ b/infrastructure/file_downloader.go @@ -10,16 +10,19 @@ import ( ) // CreateFileDownloader is a default FileDownloader factory -func CreateFileDownloader() *FileDownloader { - return &FileDownloader{} +func CreateFileDownloader(l core.ILogger) *FileDownloader { + return &FileDownloader{l} } // FileDownloader is a default implementation for core.IFileDownloader -type FileDownloader struct{} +type FileDownloader struct { + l core.ILogger +} // Download is a core.IFileDownloader interface implementation -func (FileDownloader) Download(url core.URL, filepath string) (*core.File, error) { +func (downloader *FileDownloader) Download(url core.URL, filepath string) (*core.File, error) { name := path.Base(filepath) + downloader.l.Infof("downloading %s %s", url, filepath) // Get the data client := http.DefaultClient req, _ := http.NewRequest("GET", url, nil) @@ -27,6 +30,7 @@ func (FileDownloader) Download(url core.URL, filepath string) (*core.File, error req.Header.Set("Referer", url) res, err := client.Do(req) if err != nil { + downloader.l.Error(err) return nil, err } defer res.Body.Close() @@ -34,6 +38,7 @@ func (FileDownloader) Download(url core.URL, filepath string) (*core.File, error // Create the file out, err := os.Create(filepath) if err != nil { + downloader.l.Error(err) return nil, err } defer out.Close() @@ -41,12 +46,14 @@ func (FileDownloader) Download(url core.URL, filepath string) (*core.File, error // Write the body to file _, err = io.Copy(out, res.Body) if err != nil { + downloader.l.Error(err) return nil, err } // Retreive file size stat, err := os.Stat(filepath) if err != nil { + downloader.l.Error(err) return nil, err } return &core.File{Name: name, Path: filepath, Size: stat.Size()}, err diff --git a/pullanusbot.go b/pullanusbot.go index 07af466..b82dbd2 100644 --- a/pullanusbot.go +++ b/pullanusbot.go @@ -46,7 +46,7 @@ func main() { videoFlow := usecases.CreateVideoFlow(logger, converter, converter) telebot.AddHandler(videoFlow) - fileDownloader := infrastructure.CreateFileDownloader() + fileDownloader := infrastructure.CreateFileDownloader(logger) remoteMediaSender := helpers.CreateSendMediaStrategy(logger) localMediaSender := helpers.CreateUploadMediaStrategy(logger, remoteMediaSender, fileDownloader, converter) diff --git a/usecases/youtube_flow.go b/usecases/youtube_flow.go index a59dbb0..3905578 100644 --- a/usecases/youtube_flow.go +++ b/usecases/youtube_flow.go @@ -60,7 +60,6 @@ func (flow *YoutubeFlow) process(id string, message *core.Message, bot core.IBot } title := media[0].Title - flow.l.Infof("downloading %s", id) file, err := flow.videoFactory.CreateVideo(id) if err != nil { return err From bef1df5ae648065eecfba3a5c950a4191fdb09be Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Mon, 24 Oct 2022 23:28:17 +0400 Subject: [PATCH 398/439] Add size to core.Media struct --- core/media.go | 1 + infrastructure/ffmpeg_converter.go | 10 ++++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/core/media.go b/core/media.go index 6da23a3..1472bfd 100644 --- a/core/media.go +++ b/core/media.go @@ -24,6 +24,7 @@ type Media struct { Caption string Duration int // video only Codec string // video only + Size int Type MediaType } diff --git a/infrastructure/ffmpeg_converter.go b/infrastructure/ffmpeg_converter.go index cc34bf1..b5fc4e7 100644 --- a/infrastructure/ffmpeg_converter.go +++ b/infrastructure/ffmpeg_converter.go @@ -71,11 +71,17 @@ func (c *FfmpegConverter) CreateMedia(url string) ([]*core.Media, error) { return nil, err } + size, err := strconv.Atoi(ffprobe.Format.Size) + if err != nil { + c.l.Warning(err) + size = 0 + } + if ffprobe.Format.FormatName == "image2" { - return []*core.Media{{ResourceURL: url, URL: url, Codec: stream.CodecName, Type: core.TPhoto}}, nil + return []*core.Media{{ResourceURL: url, URL: url, Codec: stream.CodecName, Size: size, Type: core.TPhoto}}, nil } - return []*core.Media{{ResourceURL: url, URL: url, Codec: stream.CodecName, Type: core.TVideo}}, nil + return []*core.Media{{ResourceURL: url, URL: url, Codec: stream.CodecName, Size: size, Type: core.TVideo}}, nil } // CreateVideo is a core.IVideoSplitter interface implementation From c7b953cea3123022c0dd0f5babd4dbfc0d9728d9 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Mon, 24 Oct 2022 23:28:49 +0400 Subject: [PATCH 399/439] Add file size limit to upload media strategy --- helpers/upload_media_strategy.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/helpers/upload_media_strategy.go b/helpers/upload_media_strategy.go index ba138b9..1a23713 100644 --- a/helpers/upload_media_strategy.go +++ b/helpers/upload_media_strategy.go @@ -1,6 +1,7 @@ package helpers import ( + "fmt" "os" "path" "strings" @@ -33,6 +34,10 @@ func (ums *UploadMediaStrategy) SendMedia(media []*core.Media, bot core.IBot) er } func (ums *UploadMediaStrategy) fallbackToUploading(media *core.Media, bot core.IBot) error { + if media.Size/1024/1024 > 50 { + return fmt.Errorf("file size limit exceeded") + } + ums.l.Info("send by uploading") file, err := ums.downloadMedia(media) if err != nil { From b790f63a036db62bb1b665c9cbe37f3c398f588c Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Mon, 24 Oct 2022 23:35:18 +0400 Subject: [PATCH 400/439] Rename UploadMediaStrategy to UploadMediaStrategyDecorator --- helpers/upload_media_strategy.go | 89 ------------------- helpers/upload_media_strategy_decorator.go | 89 +++++++++++++++++++ ...> upload_media_strategy_decorator_test.go} | 10 +-- pullanusbot.go | 2 +- 4 files changed, 95 insertions(+), 95 deletions(-) delete mode 100644 helpers/upload_media_strategy.go create mode 100644 helpers/upload_media_strategy_decorator.go rename helpers/{upload_media_strategy_test.go => upload_media_strategy_decorator_test.go} (72%) diff --git a/helpers/upload_media_strategy.go b/helpers/upload_media_strategy.go deleted file mode 100644 index 1a23713..0000000 --- a/helpers/upload_media_strategy.go +++ /dev/null @@ -1,89 +0,0 @@ -package helpers - -import ( - "fmt" - "os" - "path" - "strings" - - "github.com/ailinykh/pullanusbot/v2/core" -) - -func CreateUploadMediaStrategy(l core.ILogger, sms core.ISendMediaStrategy, fd core.IFileDownloader, vf core.IVideoFactory) core.ISendMediaStrategy { - return &UploadMediaStrategy{l, sms, fd, vf} -} - -type UploadMediaStrategy struct { - l core.ILogger - sms core.ISendMediaStrategy - fd core.IFileDownloader - vf core.IVideoFactory -} - -// SendMedia is a core.ISendMediaStrategy interface implementation -func (ums *UploadMediaStrategy) SendMedia(media []*core.Media, bot core.IBot) error { - err := ums.sms.SendMedia(media, bot) - if err != nil { - ums.l.Error(err) - if strings.Contains(err.Error(), "failed to get HTTP URL content") || strings.Contains(err.Error(), "wrong file identifier/HTTP URL specified") { - return ums.fallbackToUploading(media[0], bot) - } - } - - return err -} - -func (ums *UploadMediaStrategy) fallbackToUploading(media *core.Media, bot core.IBot) error { - if media.Size/1024/1024 > 50 { - return fmt.Errorf("file size limit exceeded") - } - - ums.l.Info("send by uploading") - file, err := ums.downloadMedia(media) - if err != nil { - return err - } - defer file.Dispose() - - switch media.Type { - case core.TText: - ums.l.Warning("unexpected media type") - case core.TPhoto: - image := &core.Image{File: *file} - _, err = bot.SendImage(image, media.Caption) - return err - case core.TVideo: - vf, err := ums.vf.CreateVideo(file.Path) - if err != nil { - ums.l.Errorf("can't create video file for %s, %v", file.Path, err) - return err - } - _, err = bot.SendVideo(vf, media.Caption) - return err - } - return err -} - -func (ums *UploadMediaStrategy) downloadMedia(media *core.Media) (*core.File, error) { - //TODO: duplicated code - filename := path.Base(media.ResourceURL) - if strings.Contains(filename, "?") { - parts := strings.Split(media.ResourceURL, "?") - filename = path.Base(parts[0]) - } - - if !strings.HasSuffix(filename, ".mp4") { - filename = filename + ".mp4" - } - - mediaPath := path.Join(os.TempDir(), filename) - file, err := ums.fd.Download(media.ResourceURL, mediaPath) - if err != nil { - ums.l.Errorf("video download error: %v", err) - return nil, err - } - - ums.l.Infof("file downloaded: %s %0.2fMB", file.Name, float64(file.Size)/1024/1024) - - return file, nil -} diff --git a/helpers/upload_media_strategy_decorator.go b/helpers/upload_media_strategy_decorator.go new file mode 100644 index 0000000..c504b95 --- /dev/null +++ b/helpers/upload_media_strategy_decorator.go @@ -0,0 +1,89 @@ +package helpers + +import ( + "fmt" + "os" + "path" + "strings" + + "github.com/ailinykh/pullanusbot/v2/core" +) + +func CreateUploadMediaStrategyDecorator(l core.ILogger, decoratee core.ISendMediaStrategy, fileDownloader core.IFileDownloader, videoFactory core.IVideoFactory) core.ISendMediaStrategy { + return &UploadMediaStrategyDecorator{l, decoratee, fileDownloader, videoFactory} +} + +type UploadMediaStrategyDecorator struct { + l core.ILogger + decoratee core.ISendMediaStrategy + fileDownloader core.IFileDownloader + videoFactory core.IVideoFactory +} + +// SendMedia is a core.ISendMediaStrategy interface implementation +func (decorator *UploadMediaStrategyDecorator) SendMedia(media []*core.Media, bot core.IBot) error { + err := decorator.decoratee.SendMedia(media, bot) + if err != nil { + decorator.l.Error(err) + if strings.Contains(err.Error(), "failed to get HTTP URL content") || strings.Contains(err.Error(), "wrong file identifier/HTTP URL specified") { + return decorator.fallbackToUploading(media[0], bot) + } + } + + return err +} + +func (decorator *UploadMediaStrategyDecorator) fallbackToUploading(media *core.Media, bot core.IBot) error { + if media.Size/1024/1024 > 50 { + return fmt.Errorf("file size limit exceeded") + } + + decorator.l.Info("send by uploading") + file, err := decorator.downloadMedia(media) + if err != nil { + return err + } + defer file.Dispose() + + switch media.Type { + case core.TText: + decorator.l.Warning("unexpected media type") + case core.TPhoto: + image := &core.Image{File: *file} + _, err = bot.SendImage(image, media.Caption) + return err + case core.TVideo: + vf, err := decorator.videoFactory.CreateVideo(file.Path) + if err != nil { + decorator.l.Errorf("can't create video file for %s, %v", file.Path, err) + return err + } + _, err = bot.SendVideo(vf, media.Caption) + return err + } + return err +} + +func (decorator *UploadMediaStrategyDecorator) downloadMedia(media *core.Media) (*core.File, error) { + //TODO: duplicated code + filename := path.Base(media.ResourceURL) + if strings.Contains(filename, "?") { + parts := strings.Split(media.ResourceURL, "?") + filename = path.Base(parts[0]) + } + + if !strings.HasSuffix(filename, ".mp4") { + filename = filename + ".mp4" + } + + mediaPath := path.Join(os.TempDir(), filename) + file, err := decorator.fileDownloader.Download(media.ResourceURL, mediaPath) + if err != nil { + decorator.l.Errorf("video download error: %v", err) + return nil, err + } + + decorator.l.Infof("file downloaded: %s %0.2fMB", file.Name, float64(file.Size)/1024/1024) + + return file, nil +} diff --git a/helpers/upload_media_strategy_test.go b/helpers/upload_media_strategy_decorator_test.go similarity index 72% rename from helpers/upload_media_strategy_test.go rename to helpers/upload_media_strategy_decorator_test.go index 8a0019e..df5609f 100644 --- a/helpers/upload_media_strategy_test.go +++ b/helpers/upload_media_strategy_decorator_test.go @@ -11,7 +11,7 @@ import ( ) func Test_UploadMedia_DoesNotFailOnEmptyMedia(t *testing.T) { - strategy, _, bot := makeUploadMediaStrategySUT() + strategy, _, bot := makeUploadMediaStrategyDecoratorSUT() media := []*core.Media{} strategy.SendMedia(media, bot) @@ -20,7 +20,7 @@ func Test_UploadMedia_DoesNotFailOnEmptyMedia(t *testing.T) { } func Test_UploadMedia_DoesNotFallbackOnGenericError(t *testing.T) { - strategy, proxy, bot := makeUploadMediaStrategySUT() + strategy, proxy, bot := makeUploadMediaStrategyDecoratorSUT() media := []*core.Media{} proxy.Err = fmt.Errorf("an error") @@ -30,7 +30,7 @@ func Test_UploadMedia_DoesNotFallbackOnGenericError(t *testing.T) { } func Test_UploadMedia_FallbackOnSpecificError(t *testing.T) { - strategy, proxy, bot := makeUploadMediaStrategySUT() + strategy, proxy, bot := makeUploadMediaStrategyDecoratorSUT() media := []*core.Media{{ResourceURL: "https://a-url.com"}} proxy.Err = fmt.Errorf("failed to get HTTP URL content") @@ -40,12 +40,12 @@ func Test_UploadMedia_FallbackOnSpecificError(t *testing.T) { } // Helpers -func makeUploadMediaStrategySUT() (core.ISendMediaStrategy, *test_helpers.FakeSendMediaStrategy, *test_helpers.FakeBot) { +func makeUploadMediaStrategyDecoratorSUT() (core.ISendMediaStrategy, *test_helpers.FakeSendMediaStrategy, *test_helpers.FakeBot) { logger := test_helpers.CreateLogger() send_media_strategy := test_helpers.CreateSendMediaStrategy() file_downloader := test_helpers.CreateFileDownloader() video_factory := test_helpers.CreateVideoFactory() - strategy := helpers.CreateUploadMediaStrategy(logger, send_media_strategy, file_downloader, video_factory) + strategy := helpers.CreateUploadMediaStrategyDecorator(logger, send_media_strategy, file_downloader, video_factory) bot := test_helpers.CreateBot() return strategy, send_media_strategy, bot } diff --git a/pullanusbot.go b/pullanusbot.go index b82dbd2..0cc5075 100644 --- a/pullanusbot.go +++ b/pullanusbot.go @@ -48,7 +48,7 @@ func main() { fileDownloader := infrastructure.CreateFileDownloader(logger) remoteMediaSender := helpers.CreateSendMediaStrategy(logger) - localMediaSender := helpers.CreateUploadMediaStrategy(logger, remoteMediaSender, fileDownloader, converter) + localMediaSender := helpers.CreateUploadMediaStrategyDecorator(logger, remoteMediaSender, fileDownloader, converter) rabbit, close := infrastructure.CreateRabbitFactory(logger, os.Getenv("AMQP_URL")) defer close() From 1888b7ec669ab2b008141d68651450e94272fa32 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Mon, 24 Oct 2022 23:40:28 +0400 Subject: [PATCH 401/439] Rename UploadMediaStrategyDecorator to UploadMediaDecorator --- helpers/upload_media_strategy_decorator.go | 12 ++++++------ helpers/upload_media_strategy_decorator_test.go | 10 +++++----- pullanusbot.go | 2 +- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/helpers/upload_media_strategy_decorator.go b/helpers/upload_media_strategy_decorator.go index c504b95..076cdf4 100644 --- a/helpers/upload_media_strategy_decorator.go +++ b/helpers/upload_media_strategy_decorator.go @@ -9,11 +9,11 @@ import ( "github.com/ailinykh/pullanusbot/v2/core" ) -func CreateUploadMediaStrategyDecorator(l core.ILogger, decoratee core.ISendMediaStrategy, fileDownloader core.IFileDownloader, videoFactory core.IVideoFactory) core.ISendMediaStrategy { - return &UploadMediaStrategyDecorator{l, decoratee, fileDownloader, videoFactory} +func CreateUploadMediaDecorator(l core.ILogger, decoratee core.ISendMediaStrategy, fileDownloader core.IFileDownloader, videoFactory core.IVideoFactory) core.ISendMediaStrategy { + return &UploadMediaDecorator{l, decoratee, fileDownloader, videoFactory} } -type UploadMediaStrategyDecorator struct { +type UploadMediaDecorator struct { l core.ILogger decoratee core.ISendMediaStrategy fileDownloader core.IFileDownloader @@ -21,7 +21,7 @@ type UploadMediaStrategyDecorator struct { } // SendMedia is a core.ISendMediaStrategy interface implementation -func (decorator *UploadMediaStrategyDecorator) SendMedia(media []*core.Media, bot core.IBot) error { +func (decorator *UploadMediaDecorator) SendMedia(media []*core.Media, bot core.IBot) error { err := decorator.decoratee.SendMedia(media, bot) if err != nil { decorator.l.Error(err) @@ -33,7 +33,7 @@ func (decorator *UploadMediaStrategyDecorator) SendMedia(media []*core.Media, bo return err } -func (decorator *UploadMediaStrategyDecorator) fallbackToUploading(media *core.Media, bot core.IBot) error { +func (decorator *UploadMediaDecorator) fallbackToUploading(media *core.Media, bot core.IBot) error { if media.Size/1024/1024 > 50 { return fmt.Errorf("file size limit exceeded") } @@ -64,7 +64,7 @@ func (decorator *UploadMediaStrategyDecorator) fallbackToUploading(media *core.M return err } -func (decorator *UploadMediaStrategyDecorator) downloadMedia(media *core.Media) (*core.File, error) { +func (decorator *UploadMediaDecorator) downloadMedia(media *core.Media) (*core.File, error) { //TODO: duplicated code filename := path.Base(media.ResourceURL) if strings.Contains(filename, "?") { diff --git a/helpers/upload_media_strategy_decorator_test.go b/helpers/upload_media_strategy_decorator_test.go index df5609f..8541212 100644 --- a/helpers/upload_media_strategy_decorator_test.go +++ b/helpers/upload_media_strategy_decorator_test.go @@ -11,7 +11,7 @@ import ( ) func Test_UploadMedia_DoesNotFailOnEmptyMedia(t *testing.T) { - strategy, _, bot := makeUploadMediaStrategyDecoratorSUT() + strategy, _, bot := makeUploadMediaDecoratorSUT() media := []*core.Media{} strategy.SendMedia(media, bot) @@ -20,7 +20,7 @@ func Test_UploadMedia_DoesNotFailOnEmptyMedia(t *testing.T) { } func Test_UploadMedia_DoesNotFallbackOnGenericError(t *testing.T) { - strategy, proxy, bot := makeUploadMediaStrategyDecoratorSUT() + strategy, proxy, bot := makeUploadMediaDecoratorSUT() media := []*core.Media{} proxy.Err = fmt.Errorf("an error") @@ -30,7 +30,7 @@ func Test_UploadMedia_DoesNotFallbackOnGenericError(t *testing.T) { } func Test_UploadMedia_FallbackOnSpecificError(t *testing.T) { - strategy, proxy, bot := makeUploadMediaStrategyDecoratorSUT() + strategy, proxy, bot := makeUploadMediaDecoratorSUT() media := []*core.Media{{ResourceURL: "https://a-url.com"}} proxy.Err = fmt.Errorf("failed to get HTTP URL content") @@ -40,12 +40,12 @@ func Test_UploadMedia_FallbackOnSpecificError(t *testing.T) { } // Helpers -func makeUploadMediaStrategyDecoratorSUT() (core.ISendMediaStrategy, *test_helpers.FakeSendMediaStrategy, *test_helpers.FakeBot) { +func makeUploadMediaDecoratorSUT() (core.ISendMediaStrategy, *test_helpers.FakeSendMediaStrategy, *test_helpers.FakeBot) { logger := test_helpers.CreateLogger() send_media_strategy := test_helpers.CreateSendMediaStrategy() file_downloader := test_helpers.CreateFileDownloader() video_factory := test_helpers.CreateVideoFactory() - strategy := helpers.CreateUploadMediaStrategyDecorator(logger, send_media_strategy, file_downloader, video_factory) + strategy := helpers.CreateUploadMediaDecorator(logger, send_media_strategy, file_downloader, video_factory) bot := test_helpers.CreateBot() return strategy, send_media_strategy, bot } diff --git a/pullanusbot.go b/pullanusbot.go index 0cc5075..0c6c557 100644 --- a/pullanusbot.go +++ b/pullanusbot.go @@ -48,7 +48,7 @@ func main() { fileDownloader := infrastructure.CreateFileDownloader(logger) remoteMediaSender := helpers.CreateSendMediaStrategy(logger) - localMediaSender := helpers.CreateUploadMediaStrategyDecorator(logger, remoteMediaSender, fileDownloader, converter) + localMediaSender := helpers.CreateUploadMediaDecorator(logger, remoteMediaSender, fileDownloader, converter) rabbit, close := infrastructure.CreateRabbitFactory(logger, os.Getenv("AMQP_URL")) defer close() From 220bc47666e6b72d482af02a4d5c57b4b5111737 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Tue, 25 Oct 2022 01:18:55 +0400 Subject: [PATCH 402/439] Fallback to splitting in case of file size limit --- ...decorator.go => upload_media_decorator.go} | 14 ++++--------- ...test.go => upload_media_decorator_test.go} | 3 ++- pullanusbot.go | 6 +++--- test_helpers/send_video_strategy.go | 20 +++++++++++++++++++ 4 files changed, 29 insertions(+), 14 deletions(-) rename helpers/{upload_media_strategy_decorator.go => upload_media_decorator.go} (90%) rename helpers/{upload_media_strategy_decorator_test.go => upload_media_decorator_test.go} (93%) create mode 100644 test_helpers/send_video_strategy.go diff --git a/helpers/upload_media_strategy_decorator.go b/helpers/upload_media_decorator.go similarity index 90% rename from helpers/upload_media_strategy_decorator.go rename to helpers/upload_media_decorator.go index 076cdf4..50165a6 100644 --- a/helpers/upload_media_strategy_decorator.go +++ b/helpers/upload_media_decorator.go @@ -1,7 +1,6 @@ package helpers import ( - "fmt" "os" "path" "strings" @@ -9,8 +8,8 @@ import ( "github.com/ailinykh/pullanusbot/v2/core" ) -func CreateUploadMediaDecorator(l core.ILogger, decoratee core.ISendMediaStrategy, fileDownloader core.IFileDownloader, videoFactory core.IVideoFactory) core.ISendMediaStrategy { - return &UploadMediaDecorator{l, decoratee, fileDownloader, videoFactory} +func CreateUploadMediaDecorator(l core.ILogger, decoratee core.ISendMediaStrategy, fileDownloader core.IFileDownloader, videoFactory core.IVideoFactory, sendVideo core.ISendVideoStrategy) core.ISendMediaStrategy { + return &UploadMediaDecorator{l, decoratee, fileDownloader, videoFactory, sendVideo} } type UploadMediaDecorator struct { @@ -18,13 +17,13 @@ type UploadMediaDecorator struct { decoratee core.ISendMediaStrategy fileDownloader core.IFileDownloader videoFactory core.IVideoFactory + sendVideo core.ISendVideoStrategy } // SendMedia is a core.ISendMediaStrategy interface implementation func (decorator *UploadMediaDecorator) SendMedia(media []*core.Media, bot core.IBot) error { err := decorator.decoratee.SendMedia(media, bot) if err != nil { - decorator.l.Error(err) if strings.Contains(err.Error(), "failed to get HTTP URL content") || strings.Contains(err.Error(), "wrong file identifier/HTTP URL specified") { return decorator.fallbackToUploading(media[0], bot) } @@ -34,10 +33,6 @@ func (decorator *UploadMediaDecorator) SendMedia(media []*core.Media, bot core.I } func (decorator *UploadMediaDecorator) fallbackToUploading(media *core.Media, bot core.IBot) error { - if media.Size/1024/1024 > 50 { - return fmt.Errorf("file size limit exceeded") - } - decorator.l.Info("send by uploading") file, err := decorator.downloadMedia(media) if err != nil { @@ -58,8 +53,7 @@ func (decorator *UploadMediaDecorator) fallbackToUploading(media *core.Media, bo decorator.l.Errorf("can't create video file for %s, %v", file.Path, err) return err } - _, err = bot.SendVideo(vf, media.Caption) - return err + return decorator.sendVideo.SendVideo(vf, media.Caption, bot) } return err } diff --git a/helpers/upload_media_strategy_decorator_test.go b/helpers/upload_media_decorator_test.go similarity index 93% rename from helpers/upload_media_strategy_decorator_test.go rename to helpers/upload_media_decorator_test.go index 8541212..ebb290b 100644 --- a/helpers/upload_media_strategy_decorator_test.go +++ b/helpers/upload_media_decorator_test.go @@ -45,7 +45,8 @@ func makeUploadMediaDecoratorSUT() (core.ISendMediaStrategy, *test_helpers.FakeS send_media_strategy := test_helpers.CreateSendMediaStrategy() file_downloader := test_helpers.CreateFileDownloader() video_factory := test_helpers.CreateVideoFactory() - strategy := helpers.CreateUploadMediaDecorator(logger, send_media_strategy, file_downloader, video_factory) + send_video := test_helpers.CreateSendVideoStrategy() + strategy := helpers.CreateUploadMediaDecorator(logger, send_media_strategy, file_downloader, video_factory, send_video) bot := test_helpers.CreateBot() return strategy, send_media_strategy, bot } diff --git a/pullanusbot.go b/pullanusbot.go index 0c6c557..b846a99 100644 --- a/pullanusbot.go +++ b/pullanusbot.go @@ -48,7 +48,9 @@ func main() { fileDownloader := infrastructure.CreateFileDownloader(logger) remoteMediaSender := helpers.CreateSendMediaStrategy(logger) - localMediaSender := helpers.CreateUploadMediaDecorator(logger, remoteMediaSender, fileDownloader, converter) + sendVideoStrategy := helpers.CreateSendVideoStrategy(logger) + sendVideoStrategySplitDecorator := helpers.CreateSendVideoStrategySplitDecorator(logger, sendVideoStrategy, converter) + localMediaSender := helpers.CreateUploadMediaDecorator(logger, remoteMediaSender, fileDownloader, converter, sendVideoStrategySplitDecorator) rabbit, close := infrastructure.CreateRabbitFactory(logger, os.Getenv("AMQP_URL")) defer close() @@ -87,8 +89,6 @@ func main() { telebot.AddHandler("/loh666", publisherFlow.HandleRequest) youtubeAPI := api.CreateYoutubeAPI(logger, fileDownloader) - sendVideoStrategy := helpers.CreateSendVideoStrategy(logger) - sendVideoStrategySplitDecorator := helpers.CreateSendVideoStrategySplitDecorator(logger, sendVideoStrategy, converter) youtubeFlow := usecases.CreateYoutubeFlow(logger, youtubeAPI, youtubeAPI, sendVideoStrategySplitDecorator) removeYoutubeSourceDecorator := usecases.CreateRemoveSourceDecorator(logger, youtubeFlow) telebot.AddHandler(removeYoutubeSourceDecorator) diff --git a/test_helpers/send_video_strategy.go b/test_helpers/send_video_strategy.go new file mode 100644 index 0000000..e33a177 --- /dev/null +++ b/test_helpers/send_video_strategy.go @@ -0,0 +1,20 @@ +package test_helpers + +import ( + "github.com/ailinykh/pullanusbot/v2/core" +) + +func CreateSendVideoStrategy() *FakeSendVideoStrategy { + return &FakeSendVideoStrategy{[]string{}, nil} +} + +type FakeSendVideoStrategy struct { + SentVideos []string + Err error +} + +// SendVideo is a core.ISendVideoStrategy interface implementation +func (fsms *FakeSendVideoStrategy) SendVideo(video *core.Video, caption string, bot core.IBot) error { + fsms.SentVideos = append(fsms.SentVideos, video.Name) + return fsms.Err +} From 19a7e6263ef9b85766fb4a79613daf90cb1791a4 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Tue, 25 Oct 2022 21:41:58 +0400 Subject: [PATCH 403/439] More logs in TelebotAdapter SendVideo func --- api/telebot_adapter.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/telebot_adapter.go b/api/telebot_adapter.go index 67a7f57..e8130d8 100644 --- a/api/telebot_adapter.go +++ b/api/telebot_adapter.go @@ -157,7 +157,7 @@ func (a *TelebotAdapter) SendPhotoAlbum(medias []*core.Media) ([]*core.Message, // SendVideo is a core.IBot interface implementation func (a *TelebotAdapter) SendVideo(vf *core.Video, caption string) (*core.Message, error) { - a.t.logger.Infof("uploading video %s", vf.Name) + a.t.logger.Infof("uploading video %s (%.2f MB)", vf.Name, float64(vf.Size)/2014/2014) video := makeTbVideo(vf, caption) a.t.bot.Notify(a.m.Chat, tb.UploadingVideo) sent, err := video.Send(a.t.bot, a.m.Chat, &tb.SendOptions{ParseMode: tb.ModeHTML}) From f4126f7dc386675271f11fd256496734699564ef Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Tue, 25 Oct 2022 21:43:16 +0400 Subject: [PATCH 404/439] Fix logger closing file descriptor issue --- core/logger.go | 1 + pullanusbot.go | 13 ++++--------- test_helpers/logger.go | 1 + 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/core/logger.go b/core/logger.go index 79b6344..98bb1c4 100644 --- a/core/logger.go +++ b/core/logger.go @@ -2,6 +2,7 @@ package core // ILogger for logging type ILogger interface { + Close() Error(...interface{}) Errorf(string, ...interface{}) Info(...interface{}) diff --git a/pullanusbot.go b/pullanusbot.go index b846a99..f0ce403 100644 --- a/pullanusbot.go +++ b/pullanusbot.go @@ -13,8 +13,8 @@ import ( ) func main() { - logger, close := createLogger() - defer close() + logger := createLogger() + defer logger.Close() dbFile := path.Join(getWorkingDir(), "pullanusbot.db") @@ -118,19 +118,14 @@ func main() { telebot.Run() } -func createLogger() (core.ILogger, func()) { +func createLogger() core.ILogger { logFilePath := path.Join(getWorkingDir(), "pullanusbot.log") lf, err := os.OpenFile(logFilePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0660) if err != nil { panic(err) } - l := logger.Init("pullanusbot", true, false, lf) - close := func() { - lf.Close() - l.Close() - } - return l, close + return logger.Init("pullanusbot", true, false, lf) } func getWorkingDir() string { diff --git a/test_helpers/logger.go b/test_helpers/logger.go index b454420..d896afa 100644 --- a/test_helpers/logger.go +++ b/test_helpers/logger.go @@ -6,6 +6,7 @@ func CreateLogger() *FakeLogger { type FakeLogger struct{} +func (FakeLogger) Close() {} func (FakeLogger) Error(...interface{}) {} func (FakeLogger) Errorf(string, ...interface{}) {} func (FakeLogger) Info(...interface{}) {} From 6ae6acffede0dd253fb85aa9972d90d1e802621e Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Tue, 25 Oct 2022 21:44:09 +0400 Subject: [PATCH 405/439] Format telebot initialization signature --- api/telebot.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/api/telebot.go b/api/telebot.go index ebb9c9e..a19f5a5 100644 --- a/api/telebot.go +++ b/api/telebot.go @@ -42,7 +42,16 @@ func CreateTelebot(token string, logger core.ILogger, chatStorage core.IChatStor panic(err) } - telebot := &Telebot{bot, logger, &CoreFactory{chatStorage: chatStorage}, []string{}, []core.ITextHandler{}, []core.IDocumentHandler{}, []core.IImageHandler{}, []core.IVideoHandler{}} + telebot := &Telebot{ + bot, + logger, + &CoreFactory{chatStorage: chatStorage}, + []string{}, + []core.ITextHandler{}, + []core.IDocumentHandler{}, + []core.IImageHandler{}, + []core.IVideoHandler{}, + } bot.Handle(tb.OnText, func(c tb.Context) error { var err error From 65a3b51b07c0a89ff7a6cac395ded8f6844e59f5 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Tue, 25 Oct 2022 22:29:31 +0400 Subject: [PATCH 406/439] Add SendMultipartVideo to send video via http --- helpers/send_multipart_video.go | 96 +++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 helpers/send_multipart_video.go diff --git a/helpers/send_multipart_video.go b/helpers/send_multipart_video.go new file mode 100644 index 0000000..79863d2 --- /dev/null +++ b/helpers/send_multipart_video.go @@ -0,0 +1,96 @@ +package helpers + +import ( + "bytes" + "io" + "io/ioutil" + "mime/multipart" + "net/http" + "os" + "strconv" + "strings" + + "github.com/ailinykh/pullanusbot/v2/core" +) + +// FIXME: SendMultipartVideo should conform to core.ISendVideoStrategy +func CreateSendMultipartVideo(l core.ILogger, url core.URL) *SendMultipartVideo { + return &SendMultipartVideo{l, http.DefaultClient, url} +} + +type SendMultipartVideo struct { + l core.ILogger + client *http.Client + url core.URL +} + +func (strategy *SendMultipartVideo) SendVideo(video *core.Video, caption string, chatId int64) ([]byte, error) { + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + + strategy.addParams(writer, map[string]interface{}{ + "caption": caption, + "duration": video.Duration, + "width": video.Width, + "height": video.Height, + "supports_streaming": "true", + "parse_mode": "HTML", + "chat_id": chatId, + "video": video.File, + "thumb": video.Thumb.File, + }) + + writer.Close() + + strategy.l.Infof("uploading %s (%.2f MB)", video.Name, float64(video.Size)/1024/1024) + r, _ := http.NewRequest("POST", strategy.url, body) + r.Header.Add("Content-Type", writer.FormDataContentType()) + res, err := strategy.client.Do(r) + if err != nil { + strategy.l.Error(err) + return nil, err + } + defer res.Body.Close() + strategy.l.Infof("%s successfully sent", video.Name) + return ioutil.ReadAll(res.Body) +} + +func (strategy *SendMultipartVideo) addParams(writer *multipart.Writer, params map[string]interface{}) { + for key, param := range params { + var reader io.Reader + var part io.Writer + var err error + switch p := param.(type) { + case string: + part, err = writer.CreateFormField(key) + reader = strings.NewReader(p) + case int: + part, err = writer.CreateFormField(key) + reader = strings.NewReader(strconv.Itoa(p)) + case int64: + part, err = writer.CreateFormField(key) + reader = strings.NewReader(strconv.FormatInt(p, 10)) + case core.File: + file, err := os.Open(p.Path) + if err != nil { + strategy.l.Error(err) + continue + } + defer file.Close() + part, err = writer.CreateFormFile(key, file.Name()) + reader = file + default: + strategy.l.Errorf("unexpected param type %+v", p) + continue + } + + if err != nil { + strategy.l.Error(err) + continue + } + _, err = io.Copy(part, reader) + if err != nil { + strategy.l.Error(err) + } + } +} From c8ffb92da56e2842afa6507b0cdacb814643ad88 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Tue, 25 Oct 2022 22:32:46 +0400 Subject: [PATCH 407/439] Add `multipart` as fallback for large video uploads --- api/telebot.go | 9 +++++++++ api/telebot_adapter.go | 15 +++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/api/telebot.go b/api/telebot.go index a19f5a5..e9918b2 100644 --- a/api/telebot.go +++ b/api/telebot.go @@ -19,6 +19,7 @@ type Telebot struct { bot *tb.Bot logger core.ILogger coreFactory *CoreFactory + multipart *helpers.SendMultipartVideo commandHandlers []string textHandlers []core.ITextHandler documentHandlers []core.IDocumentHandler @@ -42,10 +43,18 @@ func CreateTelebot(token string, logger core.ILogger, chatStorage core.IChatStor panic(err) } + var multipart *helpers.SendMultipartVideo + apiURL := os.Getenv("BOT_API_URL") + if len(apiURL) > 0 { + apiURL = fmt.Sprintf("%s/bot%s/sendVideo", apiURL, token) + multipart = helpers.CreateSendMultipartVideo(logger, apiURL) + } + telebot := &Telebot{ bot, logger, &CoreFactory{chatStorage: chatStorage}, + multipart, []string{}, []core.ITextHandler{}, []core.IDocumentHandler{}, diff --git a/api/telebot_adapter.go b/api/telebot_adapter.go index e8130d8..bdd3f66 100644 --- a/api/telebot_adapter.go +++ b/api/telebot_adapter.go @@ -1,6 +1,7 @@ package api import ( + "encoding/json" "fmt" "github.com/ailinykh/pullanusbot/v2/core" @@ -157,6 +158,20 @@ func (a *TelebotAdapter) SendPhotoAlbum(medias []*core.Media) ([]*core.Message, // SendVideo is a core.IBot interface implementation func (a *TelebotAdapter) SendVideo(vf *core.Video, caption string) (*core.Message, error) { + if vf.Size > 50*1024*1024 && a.t.multipart != nil { + body, err := a.t.multipart.SendVideo(vf, caption, a.m.Chat.ID) + if err != nil { + return nil, err + } + var resp struct { + Result *tb.Message + } + err = json.Unmarshal(body, &resp) + if err != nil { + return nil, err + } + return a.t.coreFactory.makeMessage(resp.Result), err + } a.t.logger.Infof("uploading video %s (%.2f MB)", vf.Name, float64(vf.Size)/2014/2014) video := makeTbVideo(vf, caption) a.t.bot.Notify(a.m.Chat, tb.UploadingVideo) From 83fb32538130271c960c640b4b67e7a0375da2d3 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Wed, 26 Oct 2022 21:41:43 +0400 Subject: [PATCH 408/439] Support for youtube shorts --- usecases/youtube_flow.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/usecases/youtube_flow.go b/usecases/youtube_flow.go index 3905578..55c926a 100644 --- a/usecases/youtube_flow.go +++ b/usecases/youtube_flow.go @@ -23,10 +23,10 @@ type YoutubeFlow struct { // HandleText is a core.ITextHandler protocol implementation func (flow *YoutubeFlow) HandleText(message *core.Message, bot core.IBot) error { - r := regexp.MustCompile(`youtu\.?be(\.com)?\/(watch\?v=)?([\w\-_]+)`) + r := regexp.MustCompile(`youtu\.?be(\.com)?(\/shorts)?\/(watch\?v=)?([\w\-_]+)`) match := r.FindStringSubmatch(message.Text) - if len(match) == 4 { - err := flow.process(match[3], message, bot) + if len(match) == 5 { + err := flow.process(match[4], message, bot) if err != nil { return err } From 151afdd2ea1eb06dbd014cee0b1d6c9936785cbf Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Wed, 26 Oct 2022 22:08:08 +0400 Subject: [PATCH 409/439] add telegram-bot-api to development docker-compose file --- .docker/dev/docker-compose.yaml | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/.docker/dev/docker-compose.yaml b/.docker/dev/docker-compose.yaml index e057104..ba2c6f9 100644 --- a/.docker/dev/docker-compose.yaml +++ b/.docker/dev/docker-compose.yaml @@ -1,4 +1,4 @@ -version: '2' +version: "2" services: bot: build: ../.. @@ -10,12 +10,24 @@ services: - ./.directory/pullanusbot-data:/usr/local/share/pullanusbot-data restart: always + telegram-bot-api: + image: aiogram/telegram-bot-api:latest + environment: + TELEGRAM_API_ID: 1488 + TELEGRAM_API_HASH: XXXXXXXXxxxxxxxxXXXXXXXXxxxxxxxxXXX + TELEGRAM_VERBOSITY: 1 + volumes: + - ./.directory/telegram-bot-api-data:/var/lib/telegram-bot-api + ports: + - "8081:8081" + restart: always + # Create service with RabbitMQ. message-broker: image: rabbitmq:3-management-alpine container_name: message-broker ports: - - 5672:5672 # for sender and consumer connections + - 5672:5672 # for sender and consumer connections - 15672:15672 # for serve RabbitMQ GUI volumes: - ./.directory/rabbitmq-data/data/:/var/lib/rabbitmq @@ -27,4 +39,4 @@ services: networks: # Create a new Docker network. dev-network: - driver: bridge \ No newline at end of file + driver: bridge From a3d097b4957344c169375620d81052f14848ce00 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Thu, 27 Oct 2022 00:41:15 +0400 Subject: [PATCH 410/439] Add timestamp to multipart uploader --- helpers/send_multipart_video.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/helpers/send_multipart_video.go b/helpers/send_multipart_video.go index 79863d2..6095e4e 100644 --- a/helpers/send_multipart_video.go +++ b/helpers/send_multipart_video.go @@ -9,6 +9,7 @@ import ( "os" "strconv" "strings" + "time" "github.com/ailinykh/pullanusbot/v2/core" ) @@ -42,6 +43,7 @@ func (strategy *SendMultipartVideo) SendVideo(video *core.Video, caption string, writer.Close() + start := time.Now() strategy.l.Infof("uploading %s (%.2f MB)", video.Name, float64(video.Size)/1024/1024) r, _ := http.NewRequest("POST", strategy.url, body) r.Header.Add("Content-Type", writer.FormDataContentType()) @@ -51,7 +53,7 @@ func (strategy *SendMultipartVideo) SendVideo(video *core.Video, caption string, return nil, err } defer res.Body.Close() - strategy.l.Infof("%s successfully sent", video.Name) + strategy.l.Infof("successfully sent %s (%.2f MB) %s", video.Name, float64(video.Size)/1024/1024, time.Now().Sub(start)) return ioutil.ReadAll(res.Body) } From ec89ca5c6d9e6b2493e4b239c016dfe466184f7a Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Thu, 27 Oct 2022 19:59:25 +0400 Subject: [PATCH 411/439] Add ChatID type alias --- core/chat.go | 4 +++- core/command.go | 4 ++-- core/game_storage.go | 10 +++++----- core/vpn.go | 6 +++--- 4 files changed, 13 insertions(+), 11 deletions(-) diff --git a/core/chat.go b/core/chat.go index de3e484..8af3655 100644 --- a/core/chat.go +++ b/core/chat.go @@ -1,7 +1,9 @@ package core +type ChatID = int64 + type Chat struct { - ID int64 + ID ChatID Title string Type string Settings *Settings diff --git a/core/command.go b/core/command.go index a3d50ec..831e292 100644 --- a/core/command.go +++ b/core/command.go @@ -10,6 +10,6 @@ func DefaultCommands() []Command { } type ICommandService interface { - EnableCommands(int64, []Command, IBot) error - DisableCommands(int64, []Command, IBot) error + EnableCommands(ChatID, []Command, IBot) error + DisableCommands(ChatID, []Command, IBot) error } diff --git a/core/game_storage.go b/core/game_storage.go index 8386978..b71d0df 100644 --- a/core/game_storage.go +++ b/core/game_storage.go @@ -2,9 +2,9 @@ package core // IGameStorage is an abstract interface for game players and results handling type IGameStorage interface { - GetPlayers(int64) ([]*User, error) - GetRounds(int64) ([]*Round, error) - AddPlayer(int64, *User) error - UpdatePlayer(int64, *User) error - AddRound(int64, *Round) error + GetPlayers(ChatID) ([]*User, error) + GetRounds(ChatID) ([]*Round, error) + AddPlayer(ChatID, *User) error + UpdatePlayer(ChatID, *User) error + AddRound(ChatID, *Round) error } diff --git a/core/vpn.go b/core/vpn.go index 1538f6f..1dd52f3 100644 --- a/core/vpn.go +++ b/core/vpn.go @@ -2,13 +2,13 @@ package core type VpnKey struct { ID string - ChatID int64 + ChatID ChatID Title string Key string } type IVpnAPI interface { - GetKeys(int64) ([]*VpnKey, error) - CreateKey(int64, string) (*VpnKey, error) + GetKeys(ChatID) ([]*VpnKey, error) + CreateKey(ChatID, string) (*VpnKey, error) DeleteKey(*VpnKey) error } From 5533cd8c2d881de4c42b428b414c33f7c3a27af2 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Thu, 27 Oct 2022 23:09:09 +0400 Subject: [PATCH 412/439] Add ISettingsProvider protocol --- core/settings_provider.go | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 core/settings_provider.go diff --git a/core/settings_provider.go b/core/settings_provider.go new file mode 100644 index 0000000..7a24bd2 --- /dev/null +++ b/core/settings_provider.go @@ -0,0 +1,6 @@ +package core + +type ISettingsProvider interface { + GetData(ChatID, string) ([]byte, error) + SetData(ChatID, string, []byte) error +} From 93720a789cdf5ee4079c1649f49f03bfa08f2bd4 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Fri, 28 Oct 2022 00:06:30 +0400 Subject: [PATCH 413/439] Add settings storage as a default `ISettingsProvider` implementation --- infrastructure/settings_storage.go | 57 ++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 infrastructure/settings_storage.go diff --git a/infrastructure/settings_storage.go b/infrastructure/settings_storage.go new file mode 100644 index 0000000..645a177 --- /dev/null +++ b/infrastructure/settings_storage.go @@ -0,0 +1,57 @@ +package infrastructure + +import ( + "time" + + "github.com/ailinykh/pullanusbot/v2/core" + "gorm.io/driver/sqlite" + "gorm.io/gorm" + "gorm.io/gorm/logger" +) + +// Settings +type Settings struct { + ChatID int64 `gorm:"primaryKey"` + Key string `gorm:"primaryKey"` + Data []byte + CreatedAt time.Time `gorm:"autoUpdateTime"` + UpdatedAt time.Time `gorm:"autoCreateTime"` +} + +func CreateSettingsStorage(dbFile string) core.ISettingsProvider { + conn, err := gorm.Open(sqlite.Open(dbFile+"?cache=shared"), &gorm.Config{ + Logger: logger.Default.LogMode(logger.Error), + }) + if err != nil { + panic(err) + } + + conn.AutoMigrate(&Settings{}) + + return &SettingsStorage{conn} +} + +// SettingsStorage implements core.ISettingsProvider interface +type SettingsStorage struct { + conn *gorm.DB +} + +// GetData is a core.ISettingsProvider interface implementation +func (storage *SettingsStorage) GetData(chatID core.ChatID, key string) ([]byte, error) { + var sessings Settings + err := storage.conn.First(&sessings, chatID, key).Error + if err != nil { + return nil, err + } + return sessings.Data, nil +} + +// SetData is a core.ISettingsProvider interface implementation +func (storage *SettingsStorage) SetData(chatID core.ChatID, key string, data []byte) error { + settings := Settings{ + ChatID: chatID, + Key: key, + Data: data, + } + return storage.conn.Save(&settings).Error +} From b815ab8e5418b684cccade3c0f96104cde42c3e4 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Fri, 28 Oct 2022 00:07:51 +0400 Subject: [PATCH 414/439] Replace `IChatStorage` with `ISettingsProvider` in game --- pullanusbot.go | 3 +- test_helpers/settings_provider.go | 36 +++++++++++++++ usecases/faggot_game.go | 75 +++++++++++++++++++++---------- usecases/faggot_game_test.go | 4 +- 4 files changed, 91 insertions(+), 27 deletions(-) create mode 100644 test_helpers/settings_provider.go diff --git a/pullanusbot.go b/pullanusbot.go index f0ce403..afc5982 100644 --- a/pullanusbot.go +++ b/pullanusbot.go @@ -18,6 +18,7 @@ func main() { dbFile := path.Join(getWorkingDir(), "pullanusbot.db") + settingsProvider := infrastructure.CreateSettingsStorage(dbFile) databaseChatStorage := infrastructure.CreateChatStorage(dbFile, logger) inMemoryChatStorage := infrastructure.CreateInMemoryChatStorage() chatStorageDecorator := usecases.CreateChatStorageDecorator(logger, inMemoryChatStorage, databaseChatStorage) @@ -34,7 +35,7 @@ func main() { gameStorage := infrastructure.CreateGameStorage(dbFile) rand := infrastructure.CreateMathRand() commandService := usecases.CreateCommandService(logger) - gameFlow := usecases.CreateGameFlow(logger, localizer, gameStorage, rand, chatStorageDecorator, commandService) + gameFlow := usecases.CreateGameFlow(logger, localizer, gameStorage, rand, settingsProvider, commandService) telebot.AddHandler("/pidorules", gameFlow.Rules) telebot.AddHandler("/pidoreg", gameFlow.Add) telebot.AddHandler("/pidor", gameFlow.Play) diff --git a/test_helpers/settings_provider.go b/test_helpers/settings_provider.go new file mode 100644 index 0000000..6b25b01 --- /dev/null +++ b/test_helpers/settings_provider.go @@ -0,0 +1,36 @@ +package test_helpers + +import ( + "fmt" + + "github.com/ailinykh/pullanusbot/v2/core" +) + +func CreateSettingsProvider() core.ISettingsProvider { + return &FakeSettingsProvider{make(map[int64]map[string][]byte), nil} +} + +type FakeSettingsProvider struct { + Data map[core.ChatID]map[string][]byte + Err error +} + +// GetSettings is a core.ISettingsProvider interface implementation +func (s *FakeSettingsProvider) GetData(chatID core.ChatID, key string) ([]byte, error) { + if chat, ok := s.Data[chatID]; ok { + if settings, ok := chat[key]; ok { + return settings, nil + } + } + + return nil, fmt.Errorf("not found") +} + +// SetSettings is a core.ISettingsProvider interface implementation +func (s *FakeSettingsProvider) SetData(chatID core.ChatID, key string, data []byte) error { + if _, ok := s.Data[chatID]; !ok { + s.Data[chatID] = map[string][]byte{} + } + s.Data[chatID][key] = data + return nil +} diff --git a/usecases/faggot_game.go b/usecases/faggot_game.go index e020b44..a1b8607 100644 --- a/usecases/faggot_game.go +++ b/usecases/faggot_game.go @@ -1,6 +1,7 @@ package usecases import ( + "encoding/json" "fmt" "math/rand" "os" @@ -14,8 +15,8 @@ import ( ) // CreateGameFlow is a simple GameFlow factory -func CreateGameFlow(l core.ILogger, t core.ILocalizer, s core.IGameStorage, r core.IRand, chatStorage core.IChatStorage, commandService core.ICommandService) *GameFlow { - return &GameFlow{l, t, s, r, chatStorage, commandService, sync.Mutex{}} +func CreateGameFlow(l core.ILogger, t core.ILocalizer, s core.IGameStorage, r core.IRand, settings core.ISettingsProvider, commandService core.ICommandService) *GameFlow { + return &GameFlow{l, t, s, r, settings, commandService, sync.Mutex{}} } // GameFlow represents faggot game logic @@ -24,7 +25,7 @@ type GameFlow struct { t core.ILocalizer s core.IGameStorage r core.IRand - chatStorage core.IChatStorage + settings core.ISettingsProvider commandService core.ICommandService mutex sync.Mutex } @@ -76,27 +77,7 @@ func (flow *GameFlow) Play(message *core.Message, bot core.IBot) error { flow.mutex.Lock() defer flow.mutex.Unlock() - if !message.Chat.Settings.FaggotGameCommandsEnabled { - settings := message.Chat.Settings - settings.FaggotGameCommandsEnabled = true - err := flow.chatStorage.UpdateSettings(message.Chat.ID, settings) - if err != nil { - return err - } - - commands := []core.Command{ - {Text: "pidor", Description: "play the game, see /pidorules first"}, - {Text: "pidorules", Description: "POTD game rules"}, - {Text: "pidoreg", Description: "register for POTD game"}, - {Text: "pidorstats", Description: "POTD game stats for this year"}, - {Text: "pidorall", Description: "POTD game stats for all time"}, - {Text: "pidorme", Description: "POTD personal stats"}, - } - err = flow.commandService.EnableCommands(message.Chat.ID, commands, bot) - if err != nil { - return err - } - } + flow.checkSettings(message.Chat.ID, bot) flow.l.Infof("chat_id: %d, game started by %v", message.Chat.ID, message.Sender) @@ -281,3 +262,49 @@ func (flow *GameFlow) getStat(message *core.Message) ([]Stat, error) { return entries, nil } + +func (flow *GameFlow) checkSettings(chatID core.ChatID, bot core.IBot) error { + data, err := flow.settings.GetData(chatID, "faggot_game") + + if err != nil { + flow.l.Error(err) + } + + var settingsV1 struct { + Enabled bool + } + + err = json.Unmarshal(data, &settingsV1) + if err != nil { + flow.l.Error(err) + // TODO: perform a migration + } + + if settingsV1.Enabled { + return nil + } + + settingsV1.Enabled = true + data, err = json.Marshal(settingsV1) + if err != nil { + flow.l.Error(err) + return err + } + + err = flow.settings.SetData(chatID, "faggot_game", data) + if err != nil { + flow.l.Error(err) + return err + } + + commands := []core.Command{ + {Text: "pidor", Description: "play the game, see /pidorules first"}, + {Text: "pidorules", Description: "POTD game rules"}, + {Text: "pidoreg", Description: "register for POTD game"}, + {Text: "pidorstats", Description: "POTD game stats for this year"}, + {Text: "pidorall", Description: "POTD game stats for all time"}, + {Text: "pidorme", Description: "POTD personal stats"}, + } + + return flow.commandService.EnableCommands(chatID, commands, bot) +} diff --git a/usecases/faggot_game_test.go b/usecases/faggot_game_test.go index 1cd195b..1f98f6a 100644 --- a/usecases/faggot_game_test.go +++ b/usecases/faggot_game_test.go @@ -308,14 +308,14 @@ func makeSUT(args ...interface{}) (*usecases.GameFlow, *test_helpers.FakeBot, *G storage := &GameStorageMock{players: []*core.User{}, rounds: []*core.Round{}} bot := test_helpers.CreateBot() l := &test_helpers.FakeLogger{} - s := test_helpers.CreateChatStorage() + s := test_helpers.CreateSettingsProvider() for _, arg := range args { switch opt := arg.(type) { case map[string]string: dict = opt case *core.Message: - s.Chats[opt.Chat.ID] = opt.Chat + s.SetData(opt.Chat.ID, "key", []byte{}) } } From af868180749e7e5f7af7016b1b29c775c6bb0ad3 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Fri, 28 Oct 2022 00:27:55 +0400 Subject: [PATCH 415/439] Replace `IChatStorage` with `ISettingsProvider` in start flow --- pullanusbot.go | 2 +- test_helpers/settings_provider.go | 2 +- usecases/start_flow.go | 33 +++++++++++++++++++++++-------- usecases/start_flow_test.go | 19 ++++++++++-------- 4 files changed, 38 insertions(+), 18 deletions(-) diff --git a/pullanusbot.go b/pullanusbot.go index afc5982..5ba22ff 100644 --- a/pullanusbot.go +++ b/pullanusbot.go @@ -112,7 +112,7 @@ func main() { telebot.AddHandler(removeInstaSourceDecorator) commonLocalizer := infrastructure.CreateCommonLocalizer() - startFlow := usecases.CreateStartFlow(logger, commonLocalizer, chatStorageDecorator, commandService) + startFlow := usecases.CreateStartFlow(logger, commonLocalizer, settingsProvider, commandService) telebot.AddHandler("/start", startFlow.Start) telebot.AddHandler("/help", startFlow.Help) // Start endless loop diff --git a/test_helpers/settings_provider.go b/test_helpers/settings_provider.go index 6b25b01..5b5c7ae 100644 --- a/test_helpers/settings_provider.go +++ b/test_helpers/settings_provider.go @@ -6,7 +6,7 @@ import ( "github.com/ailinykh/pullanusbot/v2/core" ) -func CreateSettingsProvider() core.ISettingsProvider { +func CreateSettingsProvider() *FakeSettingsProvider { return &FakeSettingsProvider{make(map[int64]map[string][]byte), nil} } diff --git a/usecases/start_flow.go b/usecases/start_flow.go index 26cf776..97f5639 100644 --- a/usecases/start_flow.go +++ b/usecases/start_flow.go @@ -1,20 +1,21 @@ package usecases import ( + "encoding/json" "strings" "sync" "github.com/ailinykh/pullanusbot/v2/core" ) -func CreateStartFlow(l core.ILogger, loc core.ILocalizer, chatStorage core.IChatStorage, commandService core.ICommandService) *StartFlow { - return &StartFlow{l, loc, chatStorage, commandService, sync.Mutex{}} +func CreateStartFlow(l core.ILogger, loc core.ILocalizer, settings core.ISettingsProvider, commandService core.ICommandService) *StartFlow { + return &StartFlow{l, loc, settings, commandService, sync.Mutex{}} } type StartFlow struct { l core.ILogger loc core.ILocalizer - chatStorage core.IChatStorage + settings core.ISettingsProvider commandService core.ICommandService lock sync.Mutex } @@ -51,18 +52,34 @@ func (flow *StartFlow) Help(message *core.Message, bot core.IBot) error { } func (flow *StartFlow) handlePayload(payload string, chatID int64) error { - chat, err := flow.chatStorage.GetChatByID(chatID) + data, err := flow.settings.GetData(chatID, "payload") + if err != nil { flow.l.Error(err) - return err } - if flow.contains(payload, chat.Settings.Payload) { + var settingsV1 struct { + Payload []string + } + + err = json.Unmarshal(data, &settingsV1) + if err != nil { + flow.l.Error(err) + // TODO: perform a migration + } + + if flow.contains(payload, settingsV1.Payload) { return nil } - chat.Settings.Payload = append(chat.Settings.Payload, payload) - return flow.chatStorage.UpdateSettings(chat.ID, chat.Settings) + settingsV1.Payload = append(settingsV1.Payload, payload) + data, err = json.Marshal(settingsV1) + if err != nil { + flow.l.Error(err) + return err + } + + return flow.settings.SetData(chatID, "payload", data) } func (flow *StartFlow) contains(payload string, current []string) bool { diff --git a/usecases/start_flow_test.go b/usecases/start_flow_test.go index 04da375..2fc7f1c 100644 --- a/usecases/start_flow_test.go +++ b/usecases/start_flow_test.go @@ -1,6 +1,7 @@ package usecases_test import ( + "encoding/json" "sync" "testing" @@ -13,12 +14,10 @@ import ( func Test_HandleText_CreateChatPayload(t *testing.T) { logger := test_helpers.CreateLogger() loc := test_helpers.CreateLocalizer(map[string]string{}) - chatStorage := test_helpers.CreateChatStorage() + settingsProvider := test_helpers.CreateSettingsProvider() commandService := test_helpers.CreateCommandService(logger) - startFlow := usecases.CreateStartFlow(logger, loc, chatStorage, commandService) + startFlow := usecases.CreateStartFlow(logger, loc, settingsProvider, commandService) - settings := core.DefaultSettings() - chatStorage.CreateChat(1488, "Paul Durov", "private", &settings) bot := test_helpers.CreateBot() messages := []string{ @@ -38,12 +37,16 @@ func Test_HandleText_CreateChatPayload(t *testing.T) { wg.Wait() - assert.Equal(t, 1, len(chatStorage.Chats)) + assert.Equal(t, 1, len(settingsProvider.Data)) message := makeMessage("/start") - chat, _ := chatStorage.GetChatByID(message.Chat.ID) - assert.Equal(t, true, contains("payload", chat.Settings.Payload)) - assert.Equal(t, true, contains("another_payload", chat.Settings.Payload)) + data, _ := settingsProvider.GetData(message.Chat.ID, "payload") + var settingsV1 struct { + Payload []string + } + _ = json.Unmarshal(data, &settingsV1) + assert.Equal(t, true, contains("payload", settingsV1.Payload)) + assert.Equal(t, true, contains("another_payload", settingsV1.Payload)) expected := []string{ "enable commands 1488 [{help show help message}]", From 3fd03c6cee461870dfb82881e988f9bfa5c905b5 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Fri, 28 Oct 2022 01:05:49 +0400 Subject: [PATCH 416/439] Remove `core.Settinge` and replace it with ISettingsProvider --- api/telebot.go | 17 +++------ core/chat.go | 7 ++-- core/chat_storage.go | 3 +- core/settings.go | 11 ------ infrastructure/chat_storage.go | 46 +++--------------------- infrastructure/in_memory_chat_storage.go | 19 +++------- pullanusbot.go | 10 +++--- test_helpers/chat_storage.go | 14 ++------ test_helpers/settings_storage.go | 29 --------------- usecases/bootstrap_flow.go | 3 +- usecases/chat_storage_decorator.go | 14 +++----- usecases/faggot_game_test.go | 3 +- usecases/remove_source_decorator.go | 27 +++++++++++--- usecases/start_flow_test.go | 3 +- 14 files changed, 53 insertions(+), 153 deletions(-) delete mode 100644 core/settings.go delete mode 100644 test_helpers/settings_storage.go diff --git a/api/telebot.go b/api/telebot.go index e9918b2..01bedb5 100644 --- a/api/telebot.go +++ b/api/telebot.go @@ -28,7 +28,7 @@ type Telebot struct { } // CreateTelebot is a default Telebot factory -func CreateTelebot(token string, logger core.ILogger, chatStorage core.IChatStorage) *Telebot { +func CreateTelebot(token string, logger core.ILogger) *Telebot { poller := tb.NewMiddlewarePoller(&tb.LongPoller{Timeout: 10 * time.Second}, func(upd *tb.Update) bool { return true }) @@ -53,7 +53,7 @@ func CreateTelebot(token string, logger core.ILogger, chatStorage core.IChatStor telebot := &Telebot{ bot, logger, - &CoreFactory{chatStorage: chatStorage}, + &CoreFactory{}, multipart, []string{}, []core.ITextHandler{}, @@ -242,7 +242,6 @@ func (t *Telebot) reportError(m *tb.Message, e error) { } type CoreFactory struct { - chatStorage core.IChatStorage } func (factory *CoreFactory) makeMessage(m *tb.Message) *core.Message { @@ -270,20 +269,14 @@ func (factory *CoreFactory) makeMessage(m *tb.Message) *core.Message { } func (factory *CoreFactory) makeChat(c *tb.Chat) *core.Chat { - settings := core.DefaultSettings() - chat, err := factory.chatStorage.GetChatByID(c.ID) - if err == nil { - settings = *chat.Settings - } title := c.Title if c.Type == tb.ChatPrivate { title = c.FirstName + " " + c.LastName } return &core.Chat{ - ID: c.ID, - Title: title, - Type: string(c.Type), - Settings: &settings, + ID: c.ID, + Title: title, + Type: string(c.Type), } } diff --git a/core/chat.go b/core/chat.go index 8af3655..487f519 100644 --- a/core/chat.go +++ b/core/chat.go @@ -3,8 +3,7 @@ package core type ChatID = int64 type Chat struct { - ID ChatID - Title string - Type string - Settings *Settings + ID ChatID + Title string + Type string } diff --git a/core/chat_storage.go b/core/chat_storage.go index 1d66f45..e55d890 100644 --- a/core/chat_storage.go +++ b/core/chat_storage.go @@ -2,6 +2,5 @@ package core type IChatStorage interface { GetChatByID(int64) (*Chat, error) - CreateChat(int64, string, string, *Settings) error - UpdateSettings(int64, *Settings) error + CreateChat(int64, string, string) error } diff --git a/core/settings.go b/core/settings.go deleted file mode 100644 index 3673708..0000000 --- a/core/settings.go +++ /dev/null @@ -1,11 +0,0 @@ -package core - -type Settings struct { - FaggotGameCommandsEnabled bool `json:"faggot_game_enabled"` - Payload []string `json:"payload"` - RemoveSourceOnSucccess bool `json:"remove_source_on_success"` -} - -func DefaultSettings() Settings { - return Settings{false, []string{}, true} -} diff --git a/infrastructure/chat_storage.go b/infrastructure/chat_storage.go index c14b9d5..09a31a1 100644 --- a/infrastructure/chat_storage.go +++ b/infrastructure/chat_storage.go @@ -1,7 +1,6 @@ package infrastructure import ( - "encoding/json" "time" "github.com/ailinykh/pullanusbot/v2/core" @@ -33,7 +32,6 @@ type Chat struct { ID int64 `gorm:"primaryKey"` Title string Type string - Settings []byte CreatedAt time.Time `gorm:"autoUpdateTime"` UpdatedAt time.Time `gorm:"autoCreateTime"` } @@ -48,27 +46,13 @@ func (s *ChatStorage) GetChatByID(chatID int64) (*core.Chat, error) { return nil, err } - var settings core.Settings - err = json.Unmarshal(chat.Settings, &settings) - - if err != nil { - s.l.Error(err) - return nil, err - } - - return &core.Chat{ID: chat.ID, Title: chat.Title, Type: chat.Type, Settings: &settings}, nil + return &core.Chat{ID: chat.ID, Title: chat.Title, Type: chat.Type}, nil } // CreateChat is a core.IChatStorage interface implementation -func (s *ChatStorage) CreateChat(chatID int64, title string, type_ string, settings *core.Settings) error { - data, err := json.Marshal(&settings) - if err != nil { - s.l.Error(err) - return err - } - - chat := Chat{ID: chatID, Title: title, Type: type_, Settings: data} - err = s.conn.Create(&chat).Error +func (s *ChatStorage) CreateChat(chatID int64, title string, type_ string) error { + chat := Chat{ID: chatID, Title: title, Type: type_} + err := s.conn.Create(&chat).Error if err != nil { s.l.Error(err) return err @@ -77,25 +61,3 @@ func (s *ChatStorage) CreateChat(chatID int64, title string, type_ string, setti s.l.Infof("chat created: {%d %s %s}", chat.ID, chat.Title, chat.Type) return nil } - -// UpdateSettings is a core.IChatStorage interface implementation -func (s *ChatStorage) UpdateSettings(chatID int64, settings *core.Settings) error { - chat, err := s.GetChatByID(chatID) - if err != nil { - return err - } - - data, err := json.Marshal(&settings) - if err != nil { - s.l.Error(err) - return err - } - - c := Chat{ - ID: chat.ID, - Title: chat.Title, - Type: chat.Type, - Settings: data, - } - return s.conn.Save(&c).Error -} diff --git a/infrastructure/in_memory_chat_storage.go b/infrastructure/in_memory_chat_storage.go index e91bade..6d99c05 100644 --- a/infrastructure/in_memory_chat_storage.go +++ b/infrastructure/in_memory_chat_storage.go @@ -23,22 +23,11 @@ func (storage *InMemoryChatStorage) GetChatByID(chatID int64) (*core.Chat, error } // CreateChat is a core.IChatStorage interface implementation -func (storage *InMemoryChatStorage) CreateChat(chatID int64, title string, type_ string, settings *core.Settings) error { +func (storage *InMemoryChatStorage) CreateChat(chatID int64, title string, type_ string) error { storage.cache[chatID] = &core.Chat{ - ID: chatID, - Title: title, - Type: type_, - Settings: settings, + ID: chatID, + Title: title, + Type: type_, } return nil } - -// UpdateSettings is a core.IChatStorage interface implementation -func (storage *InMemoryChatStorage) UpdateSettings(chatID int64, settings *core.Settings) error { - chat, err := storage.GetChatByID(chatID) - if err != nil { - return err - } - chat.Settings = settings - return nil -} diff --git a/pullanusbot.go b/pullanusbot.go index 5ba22ff..1305273 100644 --- a/pullanusbot.go +++ b/pullanusbot.go @@ -22,7 +22,7 @@ func main() { databaseChatStorage := infrastructure.CreateChatStorage(dbFile, logger) inMemoryChatStorage := infrastructure.CreateInMemoryChatStorage() chatStorageDecorator := usecases.CreateChatStorageDecorator(logger, inMemoryChatStorage, databaseChatStorage) - telebot := api.CreateTelebot(os.Getenv("BOT_TOKEN"), logger, chatStorageDecorator) + telebot := api.CreateTelebot(os.Getenv("BOT_TOKEN"), logger) telebot.SetupInfo() databaseUserStorage := infrastructure.CreateUserStorage(dbFile, logger) @@ -61,13 +61,13 @@ func main() { twitterFlow := usecases.CreateTwitterFlow(logger, twitterMediaFactory, localMediaSender) twitterTimeout := usecases.CreateTwitterTimeout(logger, twitterFlow) twitterParser := usecases.CreateTwitterParser(logger, twitterTimeout) - twitterRemoveSourceDecorator := usecases.CreateRemoveSourceDecorator(logger, twitterParser) + twitterRemoveSourceDecorator := usecases.CreateRemoveSourceDecorator(logger, twitterParser, "remove_source_twitter", settingsProvider) telebot.AddHandler(twitterRemoveSourceDecorator) httpClient := api.CreateHttpClient() convertMediaSender := helpers.CreateConvertMediaStrategy(logger, localMediaSender, fileDownloader, converter, converter) linkFlow := usecases.CreateLinkFlow(logger, httpClient, converter, convertMediaSender) - removeLinkSourceDecorator := usecases.CreateRemoveSourceDecorator(logger, linkFlow) + removeLinkSourceDecorator := usecases.CreateRemoveSourceDecorator(logger, linkFlow, "remove_source_link", settingsProvider) telebot.AddHandler(removeLinkSourceDecorator) tiktokHttpClient := api.CreateHttpClient() // domain specific headers and cookies @@ -91,7 +91,7 @@ func main() { youtubeAPI := api.CreateYoutubeAPI(logger, fileDownloader) youtubeFlow := usecases.CreateYoutubeFlow(logger, youtubeAPI, youtubeAPI, sendVideoStrategySplitDecorator) - removeYoutubeSourceDecorator := usecases.CreateRemoveSourceDecorator(logger, youtubeFlow) + removeYoutubeSourceDecorator := usecases.CreateRemoveSourceDecorator(logger, youtubeFlow, "remove_source_youtube", settingsProvider) telebot.AddHandler(removeYoutubeSourceDecorator) telebot.AddHandler("/proxy", func(m *core.Message, bot core.IBot) error { @@ -108,7 +108,7 @@ func main() { instaAPI := api.CreateInstagramAPI(logger, jar) downloadVideoFactory := helpers.CreateDownloadVideoFactory(logger, fileDownloader, converter) instaFlow := usecases.CreateInstagramFlow(logger, instaAPI, downloadVideoFactory, localMediaSender, sendVideoStrategySplitDecorator) - removeInstaSourceDecorator := usecases.CreateRemoveSourceDecorator(logger, instaFlow) + removeInstaSourceDecorator := usecases.CreateRemoveSourceDecorator(logger, instaFlow, "remove_source_instagram", settingsProvider) telebot.AddHandler(removeInstaSourceDecorator) commonLocalizer := infrastructure.CreateCommonLocalizer() diff --git a/test_helpers/chat_storage.go b/test_helpers/chat_storage.go index bc986db..0438b15 100644 --- a/test_helpers/chat_storage.go +++ b/test_helpers/chat_storage.go @@ -24,17 +24,7 @@ func (storage *FakeChatStorage) GetChatByID(chatID int64) (*core.Chat, error) { } // CreateChat is a core.IChatStorage interface implementation -func (s *FakeChatStorage) CreateChat(chatID int64, title string, type_ string, settings *core.Settings) error { - s.Chats[chatID] = &core.Chat{ID: chatID, Title: title, Type: type_, Settings: settings} - return nil -} - -// UpdateSettings is a core.IChatStorage interface implementation -func (s *FakeChatStorage) UpdateSettings(chatID int64, settings *core.Settings) error { - chat, err := s.GetChatByID(chatID) - if err != nil { - return err - } - chat.Settings = settings +func (s *FakeChatStorage) CreateChat(chatID int64, title string, type_ string) error { + s.Chats[chatID] = &core.Chat{ID: chatID, Title: title, Type: type_} return nil } diff --git a/test_helpers/settings_storage.go b/test_helpers/settings_storage.go deleted file mode 100644 index 956d1a2..0000000 --- a/test_helpers/settings_storage.go +++ /dev/null @@ -1,29 +0,0 @@ -package test_helpers - -import "github.com/ailinykh/pullanusbot/v2/core" - -func CreateSettingsStorage() *FakeSettingsStorage { - return &FakeSettingsStorage{make(map[int64]*core.Settings), nil} -} - -type FakeSettingsStorage struct { - Data map[int64]*core.Settings - Err error -} - -// GetSettings is a core.ISettingsStorage interface implementation -func (s *FakeSettingsStorage) GetSettings(chatID int64) (*core.Settings, error) { - if settings, ok := s.Data[chatID]; ok { - return settings, nil - } - - settings := core.DefaultSettings() - s.Data[chatID] = &settings - return &settings, nil -} - -// SetSettings is a core.ISettingsStorage interface implementation -func (s *FakeSettingsStorage) SetSettings(chatID int64, settings *core.Settings) error { - s.Data[chatID] = settings - return nil -} diff --git a/usecases/bootstrap_flow.go b/usecases/bootstrap_flow.go index 5189aa0..b30e4e1 100644 --- a/usecases/bootstrap_flow.go +++ b/usecases/bootstrap_flow.go @@ -41,8 +41,7 @@ func (flow *BootstrapFlow) ensureChatExists(chat *core.Chat) error { _, err := flow.chatStorage.GetChatByID(chat.ID) if err != nil { if err.Error() == "record not found" { - settings := core.DefaultSettings() - return flow.chatStorage.CreateChat(chat.ID, chat.Title, chat.Type, &settings) + return flow.chatStorage.CreateChat(chat.ID, chat.Title, chat.Type) } flow.l.Error(err) } diff --git a/usecases/chat_storage_decorator.go b/usecases/chat_storage_decorator.go index 045b46e..0767ba6 100644 --- a/usecases/chat_storage_decorator.go +++ b/usecases/chat_storage_decorator.go @@ -20,20 +20,14 @@ func (decorator *ChatStorageDecorator) GetChatByID(chatID int64) (*core.Chat, er if err != nil { return nil, err } - _ = decorator.cache.CreateChat(chat.ID, chat.Title, chat.Type, chat.Settings) + _ = decorator.cache.CreateChat(chat.ID, chat.Title, chat.Type) return chat, nil } return chat, nil } // CreateChat is a core.IChatStorage interface implementation -func (decorator *ChatStorageDecorator) CreateChat(chatID int64, title string, type_ string, settings *core.Settings) error { - _ = decorator.cache.CreateChat(chatID, title, type_, settings) - return decorator.db.CreateChat(chatID, title, type_, settings) -} - -// UpdateSettings is a core.IChatStorage interface implementation -func (decorator *ChatStorageDecorator) UpdateSettings(chatID int64, settings *core.Settings) error { - _ = decorator.cache.UpdateSettings(chatID, settings) - return decorator.db.UpdateSettings(chatID, settings) +func (decorator *ChatStorageDecorator) CreateChat(chatID int64, title string, type_ string) error { + _ = decorator.cache.CreateChat(chatID, title, type_) + return decorator.db.CreateChat(chatID, title, type_) } diff --git a/usecases/faggot_game_test.go b/usecases/faggot_game_test.go index 1f98f6a..b9d0107 100644 --- a/usecases/faggot_game_test.go +++ b/usecases/faggot_game_test.go @@ -299,8 +299,7 @@ func makeGameMessage(id int64, username string) *core.Message { LastName: "LastName" + fmt.Sprint(id), Username: username, } - settings := core.DefaultSettings() - return &core.Message{ID: 0, Chat: &core.Chat{ID: 0, Settings: &settings}, Sender: player} + return &core.Message{ID: 0, Chat: &core.Chat{ID: 0}, Sender: player} } func makeSUT(args ...interface{}) (*usecases.GameFlow, *test_helpers.FakeBot, *GameStorageMock) { diff --git a/usecases/remove_source_decorator.go b/usecases/remove_source_decorator.go index 572ddb0..93a763b 100644 --- a/usecases/remove_source_decorator.go +++ b/usecases/remove_source_decorator.go @@ -1,16 +1,20 @@ package usecases import ( + "encoding/json" + "github.com/ailinykh/pullanusbot/v2/core" ) -func CreateRemoveSourceDecorator(l core.ILogger, decoratee core.ITextHandler) *RemoveSourceDecorator { - return &RemoveSourceDecorator{l, decoratee} +func CreateRemoveSourceDecorator(l core.ILogger, decoratee core.ITextHandler, settingsKey string, settingsProvider core.ISettingsProvider) *RemoveSourceDecorator { + return &RemoveSourceDecorator{l, decoratee, settingsKey, settingsProvider} } type RemoveSourceDecorator struct { - l core.ILogger - decoratee core.ITextHandler + l core.ILogger + decoratee core.ITextHandler + settingsKey string + settingsProvider core.ISettingsProvider } // HandleText is a core.ITextHandler protocol implementation @@ -26,7 +30,20 @@ func (decorator *RemoveSourceDecorator) HandleText(message *core.Message, bot co return err } - if message.Chat.Settings.RemoveSourceOnSucccess { + data, _ := decorator.settingsProvider.GetData(message.Chat.ID, decorator.settingsKey) + + var settingsV1 struct { + Enabled bool + } + + err = json.Unmarshal(data, &settingsV1) + if err != nil { + decorator.l.Error(err) + // TODO: perform a migration + return nil + } + + if settingsV1.Enabled { decorator.l.Infof("removing chat %d message %d", message.Chat.ID, message.ID) return bot.Delete(message) } diff --git a/usecases/start_flow_test.go b/usecases/start_flow_test.go index 2fc7f1c..fb945e6 100644 --- a/usecases/start_flow_test.go +++ b/usecases/start_flow_test.go @@ -57,8 +57,7 @@ func Test_HandleText_CreateChatPayload(t *testing.T) { } func makeMessage(text string) *core.Message { - settings := core.DefaultSettings() - chat := core.Chat{ID: 1488, Title: "Paul Durov", Type: "private", Settings: &settings} + chat := core.Chat{ID: 1488, Title: "Paul Durov", Type: "private"} sender := core.User{ID: 1, FirstName: "Paul", LastName: "Durov"} return &core.Message{Text: text, Chat: &chat, Sender: &sender} } From 8efd46d1e05429f4afbae680d46f7964febe1471 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Sat, 29 Oct 2022 21:15:27 +0400 Subject: [PATCH 417/439] Replace int64 with ChatID in `core` package --- core/bot.go | 6 +++--- core/chat_storage.go | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/core/bot.go b/core/bot.go index 5bc5e07..bc951f1 100644 --- a/core/bot.go +++ b/core/bot.go @@ -10,7 +10,7 @@ type IBot interface { SendMedia(*Media) (*Message, error) SendPhotoAlbum([]*Media) ([]*Message, error) SendVideo(*Video, string) (*Message, error) - IsUserMemberOfChat(*User, int64) bool - GetCommands(int64) ([]Command, error) - SetCommands(int64, []Command) error + IsUserMemberOfChat(*User, ChatID) bool + GetCommands(ChatID) ([]Command, error) + SetCommands(ChatID, []Command) error } diff --git a/core/chat_storage.go b/core/chat_storage.go index e55d890..6e26190 100644 --- a/core/chat_storage.go +++ b/core/chat_storage.go @@ -1,6 +1,6 @@ package core type IChatStorage interface { - GetChatByID(int64) (*Chat, error) - CreateChat(int64, string, string) error + GetChatByID(ChatID) (*Chat, error) + CreateChat(ChatID, string, string) error } From de303715cfcc2c156dc772de1b3b307bb1996e8e Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Sat, 29 Oct 2022 22:55:18 +0400 Subject: [PATCH 418/439] Use `core.SettingKey` instead of `string` --- core/settings.go | 16 ++++++++++++++++ core/settings_provider.go | 4 ++-- infrastructure/settings_storage.go | 6 +++--- pullanusbot.go | 8 ++++---- test_helpers/settings_provider.go | 10 +++++----- usecases/faggot_game.go | 4 ++-- usecases/remove_source_decorator.go | 4 ++-- usecases/start_flow.go | 4 ++-- usecases/start_flow_test.go | 2 +- 9 files changed, 37 insertions(+), 21 deletions(-) create mode 100644 core/settings.go diff --git a/core/settings.go b/core/settings.go new file mode 100644 index 0000000..1833c86 --- /dev/null +++ b/core/settings.go @@ -0,0 +1,16 @@ +package core + +type SettingKey string + +const ( + SFaggotGameEnabled SettingKey = "faggot_game" + SInstagramFlowEnabled SettingKey = "instagram_flow" + SInstagramFlowRemoveSource SettingKey = "instagram_flow_remove_source" + SLinkFlowEnabled SettingKey = "link_flow" + SLinkFlowRemoveSource SettingKey = "link_flow_remove_source" + SPayloadList SettingKey = "payload_list" + STwitterFlowEnabled SettingKey = "twitter_flo" + STwitterFlowRemoveSource SettingKey = "twitter_flow_remove_source" + SYoutubeFlowEnabled SettingKey = "youtube_flow" + SYoutubeFlowRemoveSource SettingKey = "youtube_flow_remove_source" +) diff --git a/core/settings_provider.go b/core/settings_provider.go index 7a24bd2..9f84bff 100644 --- a/core/settings_provider.go +++ b/core/settings_provider.go @@ -1,6 +1,6 @@ package core type ISettingsProvider interface { - GetData(ChatID, string) ([]byte, error) - SetData(ChatID, string, []byte) error + GetData(ChatID, SettingKey) ([]byte, error) + SetData(ChatID, SettingKey, []byte) error } diff --git a/infrastructure/settings_storage.go b/infrastructure/settings_storage.go index 645a177..c527882 100644 --- a/infrastructure/settings_storage.go +++ b/infrastructure/settings_storage.go @@ -37,7 +37,7 @@ type SettingsStorage struct { } // GetData is a core.ISettingsProvider interface implementation -func (storage *SettingsStorage) GetData(chatID core.ChatID, key string) ([]byte, error) { +func (storage *SettingsStorage) GetData(chatID core.ChatID, key core.SettingKey) ([]byte, error) { var sessings Settings err := storage.conn.First(&sessings, chatID, key).Error if err != nil { @@ -47,10 +47,10 @@ func (storage *SettingsStorage) GetData(chatID core.ChatID, key string) ([]byte, } // SetData is a core.ISettingsProvider interface implementation -func (storage *SettingsStorage) SetData(chatID core.ChatID, key string, data []byte) error { +func (storage *SettingsStorage) SetData(chatID core.ChatID, key core.SettingKey, data []byte) error { settings := Settings{ ChatID: chatID, - Key: key, + Key: string(key), Data: data, } return storage.conn.Save(&settings).Error diff --git a/pullanusbot.go b/pullanusbot.go index 1305273..e708fb6 100644 --- a/pullanusbot.go +++ b/pullanusbot.go @@ -61,13 +61,13 @@ func main() { twitterFlow := usecases.CreateTwitterFlow(logger, twitterMediaFactory, localMediaSender) twitterTimeout := usecases.CreateTwitterTimeout(logger, twitterFlow) twitterParser := usecases.CreateTwitterParser(logger, twitterTimeout) - twitterRemoveSourceDecorator := usecases.CreateRemoveSourceDecorator(logger, twitterParser, "remove_source_twitter", settingsProvider) + twitterRemoveSourceDecorator := usecases.CreateRemoveSourceDecorator(logger, twitterParser, core.STwitterFlowRemoveSource, settingsProvider) telebot.AddHandler(twitterRemoveSourceDecorator) httpClient := api.CreateHttpClient() convertMediaSender := helpers.CreateConvertMediaStrategy(logger, localMediaSender, fileDownloader, converter, converter) linkFlow := usecases.CreateLinkFlow(logger, httpClient, converter, convertMediaSender) - removeLinkSourceDecorator := usecases.CreateRemoveSourceDecorator(logger, linkFlow, "remove_source_link", settingsProvider) + removeLinkSourceDecorator := usecases.CreateRemoveSourceDecorator(logger, linkFlow, core.SLinkFlowRemoveSource, settingsProvider) telebot.AddHandler(removeLinkSourceDecorator) tiktokHttpClient := api.CreateHttpClient() // domain specific headers and cookies @@ -91,7 +91,7 @@ func main() { youtubeAPI := api.CreateYoutubeAPI(logger, fileDownloader) youtubeFlow := usecases.CreateYoutubeFlow(logger, youtubeAPI, youtubeAPI, sendVideoStrategySplitDecorator) - removeYoutubeSourceDecorator := usecases.CreateRemoveSourceDecorator(logger, youtubeFlow, "remove_source_youtube", settingsProvider) + removeYoutubeSourceDecorator := usecases.CreateRemoveSourceDecorator(logger, youtubeFlow, core.SYoutubeFlowRemoveSource, settingsProvider) telebot.AddHandler(removeYoutubeSourceDecorator) telebot.AddHandler("/proxy", func(m *core.Message, bot core.IBot) error { @@ -108,7 +108,7 @@ func main() { instaAPI := api.CreateInstagramAPI(logger, jar) downloadVideoFactory := helpers.CreateDownloadVideoFactory(logger, fileDownloader, converter) instaFlow := usecases.CreateInstagramFlow(logger, instaAPI, downloadVideoFactory, localMediaSender, sendVideoStrategySplitDecorator) - removeInstaSourceDecorator := usecases.CreateRemoveSourceDecorator(logger, instaFlow, "remove_source_instagram", settingsProvider) + removeInstaSourceDecorator := usecases.CreateRemoveSourceDecorator(logger, instaFlow, core.SInstagramFlowRemoveSource, settingsProvider) telebot.AddHandler(removeInstaSourceDecorator) commonLocalizer := infrastructure.CreateCommonLocalizer() diff --git a/test_helpers/settings_provider.go b/test_helpers/settings_provider.go index 5b5c7ae..b64d439 100644 --- a/test_helpers/settings_provider.go +++ b/test_helpers/settings_provider.go @@ -7,16 +7,16 @@ import ( ) func CreateSettingsProvider() *FakeSettingsProvider { - return &FakeSettingsProvider{make(map[int64]map[string][]byte), nil} + return &FakeSettingsProvider{make(map[int64]map[core.SettingKey][]byte), nil} } type FakeSettingsProvider struct { - Data map[core.ChatID]map[string][]byte + Data map[core.ChatID]map[core.SettingKey][]byte Err error } // GetSettings is a core.ISettingsProvider interface implementation -func (s *FakeSettingsProvider) GetData(chatID core.ChatID, key string) ([]byte, error) { +func (s *FakeSettingsProvider) GetData(chatID core.ChatID, key core.SettingKey) ([]byte, error) { if chat, ok := s.Data[chatID]; ok { if settings, ok := chat[key]; ok { return settings, nil @@ -27,9 +27,9 @@ func (s *FakeSettingsProvider) GetData(chatID core.ChatID, key string) ([]byte, } // SetSettings is a core.ISettingsProvider interface implementation -func (s *FakeSettingsProvider) SetData(chatID core.ChatID, key string, data []byte) error { +func (s *FakeSettingsProvider) SetData(chatID core.ChatID, key core.SettingKey, data []byte) error { if _, ok := s.Data[chatID]; !ok { - s.Data[chatID] = map[string][]byte{} + s.Data[chatID] = map[core.SettingKey][]byte{} } s.Data[chatID][key] = data return nil diff --git a/usecases/faggot_game.go b/usecases/faggot_game.go index a1b8607..c563c2b 100644 --- a/usecases/faggot_game.go +++ b/usecases/faggot_game.go @@ -264,7 +264,7 @@ func (flow *GameFlow) getStat(message *core.Message) ([]Stat, error) { } func (flow *GameFlow) checkSettings(chatID core.ChatID, bot core.IBot) error { - data, err := flow.settings.GetData(chatID, "faggot_game") + data, err := flow.settings.GetData(chatID, core.SFaggotGameEnabled) if err != nil { flow.l.Error(err) @@ -291,7 +291,7 @@ func (flow *GameFlow) checkSettings(chatID core.ChatID, bot core.IBot) error { return err } - err = flow.settings.SetData(chatID, "faggot_game", data) + err = flow.settings.SetData(chatID, core.SFaggotGameEnabled, data) if err != nil { flow.l.Error(err) return err diff --git a/usecases/remove_source_decorator.go b/usecases/remove_source_decorator.go index 93a763b..b8c805f 100644 --- a/usecases/remove_source_decorator.go +++ b/usecases/remove_source_decorator.go @@ -6,14 +6,14 @@ import ( "github.com/ailinykh/pullanusbot/v2/core" ) -func CreateRemoveSourceDecorator(l core.ILogger, decoratee core.ITextHandler, settingsKey string, settingsProvider core.ISettingsProvider) *RemoveSourceDecorator { +func CreateRemoveSourceDecorator(l core.ILogger, decoratee core.ITextHandler, settingsKey core.SettingKey, settingsProvider core.ISettingsProvider) *RemoveSourceDecorator { return &RemoveSourceDecorator{l, decoratee, settingsKey, settingsProvider} } type RemoveSourceDecorator struct { l core.ILogger decoratee core.ITextHandler - settingsKey string + settingsKey core.SettingKey settingsProvider core.ISettingsProvider } diff --git a/usecases/start_flow.go b/usecases/start_flow.go index 97f5639..b8c192e 100644 --- a/usecases/start_flow.go +++ b/usecases/start_flow.go @@ -52,7 +52,7 @@ func (flow *StartFlow) Help(message *core.Message, bot core.IBot) error { } func (flow *StartFlow) handlePayload(payload string, chatID int64) error { - data, err := flow.settings.GetData(chatID, "payload") + data, err := flow.settings.GetData(chatID, core.SPayloadList) if err != nil { flow.l.Error(err) @@ -79,7 +79,7 @@ func (flow *StartFlow) handlePayload(payload string, chatID int64) error { return err } - return flow.settings.SetData(chatID, "payload", data) + return flow.settings.SetData(chatID, core.SPayloadList, data) } func (flow *StartFlow) contains(payload string, current []string) bool { diff --git a/usecases/start_flow_test.go b/usecases/start_flow_test.go index fb945e6..fd20026 100644 --- a/usecases/start_flow_test.go +++ b/usecases/start_flow_test.go @@ -40,7 +40,7 @@ func Test_HandleText_CreateChatPayload(t *testing.T) { assert.Equal(t, 1, len(settingsProvider.Data)) message := makeMessage("/start") - data, _ := settingsProvider.GetData(message.Chat.ID, "payload") + data, _ := settingsProvider.GetData(message.Chat.ID, core.SPayloadList) var settingsV1 struct { Payload []string } From 685d35f081efbea71735a9dd05c7465098d5fe4f Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Sun, 30 Oct 2022 14:57:25 +0400 Subject: [PATCH 419/439] Add `IBoolSettingProvider` interface which represent a boolean setting --- core/settings_provider.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/core/settings_provider.go b/core/settings_provider.go index 9f84bff..1933585 100644 --- a/core/settings_provider.go +++ b/core/settings_provider.go @@ -4,3 +4,8 @@ type ISettingsProvider interface { GetData(ChatID, SettingKey) ([]byte, error) SetData(ChatID, SettingKey, []byte) error } + +type IBoolSettingProvider interface { + GetBool(ChatID, SettingKey) bool + SetBool(ChatID, SettingKey, bool) error +} From 349183d799592e8510013d263b6c28372628ef74 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Sun, 30 Oct 2022 14:57:47 +0400 Subject: [PATCH 420/439] Add default `BoolSettingProvider` implementation --- helpers/bool_setting_provider.go | 43 ++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 helpers/bool_setting_provider.go diff --git a/helpers/bool_setting_provider.go b/helpers/bool_setting_provider.go new file mode 100644 index 0000000..5f08180 --- /dev/null +++ b/helpers/bool_setting_provider.go @@ -0,0 +1,43 @@ +package helpers + +import ( + "encoding/json" + + "github.com/ailinykh/pullanusbot/v2/core" +) + +func CreateBoolSettingProvider(settingsProvider core.ISettingsProvider) core.IBoolSettingProvider { + return &BoolSettingProvider{settingsProvider} +} + +type BoolSettingProvider struct { + settingsProvider core.ISettingsProvider +} + +func (provider *BoolSettingProvider) GetBool(chatID core.ChatID, key core.SettingKey) bool { + data, _ := provider.settingsProvider.GetData(chatID, key) + + var settings struct { + Enabled bool + } + + err := json.Unmarshal(data, &settings) + if err != nil { + return false + } + + return settings.Enabled +} + +func (provider *BoolSettingProvider) SetBool(chatID core.ChatID, key core.SettingKey, value bool) error { + var settings struct { + Enabled bool + } + settings.Enabled = value + data, err := json.Marshal(settings) + if err != nil { + return err + } + + return provider.settingsProvider.SetData(chatID, key, data) +} From d17acc33667736c7d464ea317d4a77e5f02a369e Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Sun, 30 Oct 2022 14:58:21 +0400 Subject: [PATCH 421/439] Replace `ISettingsProvider` with `IBoolSettingProvider` in remove source decorator --- pullanusbot.go | 9 +++++---- usecases/remove_source_decorator.go | 29 ++++++++--------------------- 2 files changed, 13 insertions(+), 25 deletions(-) diff --git a/pullanusbot.go b/pullanusbot.go index e708fb6..17a26da 100644 --- a/pullanusbot.go +++ b/pullanusbot.go @@ -19,6 +19,7 @@ func main() { dbFile := path.Join(getWorkingDir(), "pullanusbot.db") settingsProvider := infrastructure.CreateSettingsStorage(dbFile) + boolSettingProvider := helpers.CreateBoolSettingProvider(settingsProvider) databaseChatStorage := infrastructure.CreateChatStorage(dbFile, logger) inMemoryChatStorage := infrastructure.CreateInMemoryChatStorage() chatStorageDecorator := usecases.CreateChatStorageDecorator(logger, inMemoryChatStorage, databaseChatStorage) @@ -61,13 +62,13 @@ func main() { twitterFlow := usecases.CreateTwitterFlow(logger, twitterMediaFactory, localMediaSender) twitterTimeout := usecases.CreateTwitterTimeout(logger, twitterFlow) twitterParser := usecases.CreateTwitterParser(logger, twitterTimeout) - twitterRemoveSourceDecorator := usecases.CreateRemoveSourceDecorator(logger, twitterParser, core.STwitterFlowRemoveSource, settingsProvider) + twitterRemoveSourceDecorator := usecases.CreateRemoveSourceDecorator(logger, twitterParser, core.STwitterFlowRemoveSource, boolSettingProvider) telebot.AddHandler(twitterRemoveSourceDecorator) httpClient := api.CreateHttpClient() convertMediaSender := helpers.CreateConvertMediaStrategy(logger, localMediaSender, fileDownloader, converter, converter) linkFlow := usecases.CreateLinkFlow(logger, httpClient, converter, convertMediaSender) - removeLinkSourceDecorator := usecases.CreateRemoveSourceDecorator(logger, linkFlow, core.SLinkFlowRemoveSource, settingsProvider) + removeLinkSourceDecorator := usecases.CreateRemoveSourceDecorator(logger, linkFlow, core.SLinkFlowRemoveSource, boolSettingProvider) telebot.AddHandler(removeLinkSourceDecorator) tiktokHttpClient := api.CreateHttpClient() // domain specific headers and cookies @@ -91,7 +92,7 @@ func main() { youtubeAPI := api.CreateYoutubeAPI(logger, fileDownloader) youtubeFlow := usecases.CreateYoutubeFlow(logger, youtubeAPI, youtubeAPI, sendVideoStrategySplitDecorator) - removeYoutubeSourceDecorator := usecases.CreateRemoveSourceDecorator(logger, youtubeFlow, core.SYoutubeFlowRemoveSource, settingsProvider) + removeYoutubeSourceDecorator := usecases.CreateRemoveSourceDecorator(logger, youtubeFlow, core.SYoutubeFlowRemoveSource, boolSettingProvider) telebot.AddHandler(removeYoutubeSourceDecorator) telebot.AddHandler("/proxy", func(m *core.Message, bot core.IBot) error { @@ -108,7 +109,7 @@ func main() { instaAPI := api.CreateInstagramAPI(logger, jar) downloadVideoFactory := helpers.CreateDownloadVideoFactory(logger, fileDownloader, converter) instaFlow := usecases.CreateInstagramFlow(logger, instaAPI, downloadVideoFactory, localMediaSender, sendVideoStrategySplitDecorator) - removeInstaSourceDecorator := usecases.CreateRemoveSourceDecorator(logger, instaFlow, core.SInstagramFlowRemoveSource, settingsProvider) + removeInstaSourceDecorator := usecases.CreateRemoveSourceDecorator(logger, instaFlow, core.SInstagramFlowRemoveSource, boolSettingProvider) telebot.AddHandler(removeInstaSourceDecorator) commonLocalizer := infrastructure.CreateCommonLocalizer() diff --git a/usecases/remove_source_decorator.go b/usecases/remove_source_decorator.go index b8c805f..78cb720 100644 --- a/usecases/remove_source_decorator.go +++ b/usecases/remove_source_decorator.go @@ -1,20 +1,18 @@ package usecases import ( - "encoding/json" - "github.com/ailinykh/pullanusbot/v2/core" ) -func CreateRemoveSourceDecorator(l core.ILogger, decoratee core.ITextHandler, settingsKey core.SettingKey, settingsProvider core.ISettingsProvider) *RemoveSourceDecorator { - return &RemoveSourceDecorator{l, decoratee, settingsKey, settingsProvider} +func CreateRemoveSourceDecorator(l core.ILogger, decoratee core.ITextHandler, settingsKey core.SettingKey, settingProvider core.IBoolSettingProvider) *RemoveSourceDecorator { + return &RemoveSourceDecorator{l, decoratee, settingsKey, settingProvider} } type RemoveSourceDecorator struct { - l core.ILogger - decoratee core.ITextHandler - settingsKey core.SettingKey - settingsProvider core.ISettingsProvider + l core.ILogger + decoratee core.ITextHandler + settingsKey core.SettingKey + settingProvider core.IBoolSettingProvider } // HandleText is a core.ITextHandler protocol implementation @@ -30,20 +28,9 @@ func (decorator *RemoveSourceDecorator) HandleText(message *core.Message, bot co return err } - data, _ := decorator.settingsProvider.GetData(message.Chat.ID, decorator.settingsKey) - - var settingsV1 struct { - Enabled bool - } - - err = json.Unmarshal(data, &settingsV1) - if err != nil { - decorator.l.Error(err) - // TODO: perform a migration - return nil - } + enabled := decorator.settingProvider.GetBool(message.Chat.ID, decorator.settingsKey) - if settingsV1.Enabled { + if enabled { decorator.l.Infof("removing chat %d message %d", message.Chat.ID, message.ID) return bot.Delete(message) } From 94bbeee67f1b07aed82356e3518cb9ba49cb0817 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Sun, 30 Oct 2022 22:21:27 +0400 Subject: [PATCH 422/439] Add `User` to `ButtonPressed` func to be able to identify who pressed a button --- api/telebot.go | 7 ++++++- core/button.go | 2 +- usecases/outline_vpn_flow.go | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/api/telebot.go b/api/telebot.go index 01bedb5..707daf9 100644 --- a/api/telebot.go +++ b/api/telebot.go @@ -205,7 +205,12 @@ func (t *Telebot) AddHandler(handler ...interface{}) { t.bot.Handle("\f"+id, func(c tb.Context) error { m := c.Message() cb := c.Callback() - err := h.ButtonPressed(cb.Data, t.coreFactory.makeMessage(m), t.coreFactory.makeIBot(m, t)) + err := h.ButtonPressed( + cb.Data, + t.coreFactory.makeMessage(m), + t.coreFactory.makeUser(c.Sender()), + t.coreFactory.makeIBot(m, t), + ) if err != nil { t.logger.Error(err) t.reportError(m, err) diff --git a/core/button.go b/core/button.go index 6317b4b..0cd631f 100644 --- a/core/button.go +++ b/core/button.go @@ -4,7 +4,7 @@ type Keyboard = [][]*Button type IButtonHandler interface { GetButtonIds() []string - ButtonPressed(string, *Message, IBot) error + ButtonPressed(string, *Message, *User, IBot) error } type Button struct { diff --git a/usecases/outline_vpn_flow.go b/usecases/outline_vpn_flow.go index da7bdc6..1bb9971 100644 --- a/usecases/outline_vpn_flow.go +++ b/usecases/outline_vpn_flow.go @@ -45,7 +45,7 @@ func (flow *OutlineVpnFlow) GetButtonIds() []string { } // ButtonPressed is a core.IButtonHandler protocol implementation -func (flow *OutlineVpnFlow) ButtonPressed(id string, message *core.Message, bot core.IBot) error { +func (flow *OutlineVpnFlow) ButtonPressed(id string, message *core.Message, _ *core.User, bot core.IBot) error { if callback, ok := flow.callbacks[id]; ok { return callback(message, bot) } From 03c21e721d7b88aeb0ef03e9e940868dbcb7fefd Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Thu, 5 Jan 2023 23:35:23 +0400 Subject: [PATCH 423/439] Remove TikTok `Referrer` header --- api/http_client.go | 2 +- api/tiktok_html_v2_api.go | 4 ++++ api/tiktok_v2.go | 5 +++++ usecases/tiktok_flow.go | 1 - 4 files changed, 10 insertions(+), 2 deletions(-) diff --git a/api/http_client.go b/api/http_client.go index efe8ebd..05b1456 100644 --- a/api/http_client.go +++ b/api/http_client.go @@ -9,7 +9,7 @@ import ( ) func CreateHttpClient() *HttpClient { - return &HttpClient{map[string]string{"User-Agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:59.0) Gecko/20100101 Firefox/59.0"}} + return &HttpClient{map[string]string{"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36"}} } type HttpClient struct { diff --git a/api/tiktok_html_v2_api.go b/api/tiktok_html_v2_api.go index 77ef04b..a200f37 100644 --- a/api/tiktok_html_v2_api.go +++ b/api/tiktok_html_v2_api.go @@ -42,6 +42,10 @@ func (api *TikTokHTMLV2API) GetItem(username string, videoId string) (*TikTokIte return nil, err } + if resp.VideoPage.StatusCode != 0 { + return nil, fmt.Errorf("unextected status code %d", resp.VideoPage.StatusCode) + } + // os.WriteFile("tiktok-"+strings.Split(url, "/")[5]+".json", []byte(match[1]), 0644) item := resp.ItemModule[videoId] stickers := []string{} diff --git a/api/tiktok_v2.go b/api/tiktok_v2.go index 199aec2..0eea6e3 100644 --- a/api/tiktok_v2.go +++ b/api/tiktok_v2.go @@ -3,6 +3,7 @@ package api type TikTokV2HTMLNResponse struct { ItemModule map[string]*TikTokV2Item UserModule TikTokV2UserModule + VideoPage TikTokV2VideoPage } type TikTokV2Item struct { @@ -16,3 +17,7 @@ type TikTokV2Item struct { type TikTokV2UserModule struct { Users map[string]*TikTokAuthor } + +type TikTokV2VideoPage struct { + StatusCode int +} diff --git a/usecases/tiktok_flow.go b/usecases/tiktok_flow.go index dbe90d9..816225e 100644 --- a/usecases/tiktok_flow.go +++ b/usecases/tiktok_flow.go @@ -8,7 +8,6 @@ import ( ) func CreateTikTokFlow(l core.ILogger, httpClient core.IHttpClient, mediaFactory core.IMediaFactory, sendMediaStrategy core.ISendMediaStrategy) *TikTokFlow { - httpClient.SetHeader("Referrer", "https://www.tiktok.com/") return &TikTokFlow{l, httpClient, mediaFactory, sendMediaStrategy} } From af13cd2bb8d98e94faeb9b61d10c83a5a63d1d95 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Wed, 8 Feb 2023 22:56:56 +0400 Subject: [PATCH 424/439] passthrough tiktok api error --- api/tiktok_api_decorator.go | 5 +++++ api/tiktok_html_v2_api.go | 4 +++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/api/tiktok_api_decorator.go b/api/tiktok_api_decorator.go index 7037e97..3ab1199 100644 --- a/api/tiktok_api_decorator.go +++ b/api/tiktok_api_decorator.go @@ -1,5 +1,7 @@ package api +import "strings" + func CreateTikTokAPIDecorator(primary ITikTokAPI, secondary ITikTokAPI) ITikTokAPI { return &TikTokAPIDecorator{primary, secondary} } @@ -12,6 +14,9 @@ type TikTokAPIDecorator struct { func (api *TikTokAPIDecorator) GetItem(username string, videoId string) (*TikTokItem, error) { item, err := api.primary.GetItem(username, videoId) if err != nil { + if strings.HasPrefix(err.Error(), "tiktok:") { + return nil, err + } return api.secondary.GetItem(username, videoId) } return item, nil diff --git a/api/tiktok_html_v2_api.go b/api/tiktok_html_v2_api.go index a200f37..5ef8cec 100644 --- a/api/tiktok_html_v2_api.go +++ b/api/tiktok_html_v2_api.go @@ -25,6 +25,7 @@ func (api *TikTokHTMLV2API) GetItem(username string, videoId string) (*TikTokIte api.hc.SetHeader("Cookie", "tt_webid_v2=69"+api.randomDigits(17)+"; Domain=tiktok.com; Path=/; Secure; hostOnly=false; hostOnly=false; aAge=4ms; cAge=4ms") htmlString, err := api.hc.GetContent(url) if err != nil { + api.l.Error(err) return nil, err } @@ -39,11 +40,12 @@ func (api *TikTokHTMLV2API) GetItem(username string, videoId string) (*TikTokIte var resp TikTokV2HTMLNResponse err = json.Unmarshal([]byte(match[1]), &resp) if err != nil { + api.l.Error(err) return nil, err } if resp.VideoPage.StatusCode != 0 { - return nil, fmt.Errorf("unextected status code %d", resp.VideoPage.StatusCode) + return nil, fmt.Errorf("tiktok: unexpected video page status code %d", resp.VideoPage.StatusCode) } // os.WriteFile("tiktok-"+strings.Split(url, "/")[5]+".json", []byte(match[1]), 0644) item := resp.ItemModule[videoId] From 16b11f4c82fa00db49978b028e548dabbb072923 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Wed, 8 Feb 2023 22:57:20 +0400 Subject: [PATCH 425/439] add more logs fro instagram html --- api/instagram_api.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/api/instagram_api.go b/api/instagram_api.go index d40a92f..f0960a4 100644 --- a/api/instagram_api.go +++ b/api/instagram_api.go @@ -41,6 +41,8 @@ func (api *InstagramAPI) GetReel(urlString string) (*IgReel, error) { return nil, err } + // os.WriteFile("instagram-reel.html", body, 0644) + data, err := api.parseData(body) if err != nil { api.l.Error(err) From 5576cbe94d2682cae6e2a0b695c8a2b3416d90b8 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Sat, 18 Mar 2023 18:57:57 +0400 Subject: [PATCH 426/439] Add payload to `Button` --- api/telebot.go | 14 ++++++++++++-- api/telebot_factory.go | 2 +- core/button.go | 7 ++++--- usecases/outline_vpn_flow.go | 4 ++-- 4 files changed, 19 insertions(+), 8 deletions(-) diff --git a/api/telebot.go b/api/telebot.go index 707daf9..8b4bebf 100644 --- a/api/telebot.go +++ b/api/telebot.go @@ -144,7 +144,7 @@ func CreateTelebot(token string, logger core.ILogger) *Telebot { Width: m.Video.Width, Height: m.Video.Height, } - + logger.Info(video) for _, h := range telebot.videoHandlers { err = h.HandleVideo(video, telebot.coreFactory.makeMessage(m), telebot.coreFactory.makeIBot(m, telebot)) if err != nil { @@ -205,8 +205,13 @@ func (t *Telebot) AddHandler(handler ...interface{}) { t.bot.Handle("\f"+id, func(c tb.Context) error { m := c.Message() cb := c.Callback() + button := core.Button{ + ID: cb.Unique, + Text: c.Text(), + Payload: c.Data(), + } err := h.ButtonPressed( - cb.Data, + &button, t.coreFactory.makeMessage(m), t.coreFactory.makeUser(c.Sender()), t.coreFactory.makeIBot(m, t), @@ -214,6 +219,11 @@ func (t *Telebot) AddHandler(handler ...interface{}) { if err != nil { t.logger.Error(err) t.reportError(m, err) + resp := tb.CallbackResponse{ + CallbackID: cb.ID, + Text: err.Error(), + } + return t.bot.Respond(cb, &resp) } return t.bot.Respond(cb, &tb.CallbackResponse{CallbackID: cb.ID}) }) diff --git a/api/telebot_factory.go b/api/telebot_factory.go index 6dc90d7..fa0bb81 100644 --- a/api/telebot_factory.go +++ b/api/telebot_factory.go @@ -65,7 +65,7 @@ func makeInlineKeyboard(k core.Keyboard) [][]tb.InlineButton { for _, buttons := range k { btns := []tb.InlineButton{} for _, b := range buttons { - btn := tb.InlineButton{Unique: b.ID, Text: b.Text, Data: b.ID} + btn := tb.InlineButton{Unique: b.ID, Text: b.Text, Data: b.Payload} btns = append(btns, btn) } keyboard = append(keyboard, btns) diff --git a/core/button.go b/core/button.go index 0cd631f..cc2102a 100644 --- a/core/button.go +++ b/core/button.go @@ -4,10 +4,11 @@ type Keyboard = [][]*Button type IButtonHandler interface { GetButtonIds() []string - ButtonPressed(string, *Message, *User, IBot) error + ButtonPressed(*Button, *Message, *User, IBot) error } type Button struct { - ID string - Text string + ID string + Text string + Payload string } diff --git a/usecases/outline_vpn_flow.go b/usecases/outline_vpn_flow.go index 1bb9971..e17227d 100644 --- a/usecases/outline_vpn_flow.go +++ b/usecases/outline_vpn_flow.go @@ -45,8 +45,8 @@ func (flow *OutlineVpnFlow) GetButtonIds() []string { } // ButtonPressed is a core.IButtonHandler protocol implementation -func (flow *OutlineVpnFlow) ButtonPressed(id string, message *core.Message, _ *core.User, bot core.IBot) error { - if callback, ok := flow.callbacks[id]; ok { +func (flow *OutlineVpnFlow) ButtonPressed(button *core.Button, message *core.Message, _ *core.User, bot core.IBot) error { + if callback, ok := flow.callbacks[button.ID]; ok { return callback(message, bot) } return fmt.Errorf("not implemented") From a59f55d96320d7d0faf481f319d0523a18359fd7 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Sat, 18 Mar 2023 19:04:11 +0400 Subject: [PATCH 427/439] Fix for `i do not care` flow --- usecases/i_do_not_care.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/usecases/i_do_not_care.go b/usecases/i_do_not_care.go index 0c7c9a6..fa0ac57 100644 --- a/usecases/i_do_not_care.go +++ b/usecases/i_do_not_care.go @@ -15,7 +15,14 @@ type IDoNotCare struct{} // HandleText is a core.ITextHandler protocol implementation func (IDoNotCare) HandleText(message *core.Message, bot core.IBot) error { if strings.Contains(strings.ToLower(message.Text), "мне всё равно") { - _, err := bot.SendVideo(&core.Video{ID: "BAACAgIAAxkBAAIfmmNP8h0AAbIFnYs3RhAVnHQQxq3DjgAC9CAAAkxOgUriVsO_AcngZSoE"}, "") + _, err := bot.SendVideo(&core.Video{ID: "BAACAgIAAxkBAAEDfu1kFdKdAAHM4iO92LOC3muxi2yyvosAAgQoAAIZvLFIVaKgRXqfmVgvBA"}, "") + if err != nil { + media := &core.Media{ + ResourceURL: "https://telegra.ph/file/182c624365bea4df6842a.mp4", + Type: core.TVideo, + } + _, err = bot.SendMedia(media) + } return err } if strings.Contains(strings.ToLower(message.Text), "привет, андрей") { From db2d2248c3dddb25b8cf358309ecc13f7e327de9 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Sun, 19 Mar 2023 14:04:23 +0400 Subject: [PATCH 428/439] Log incoming video with chatid --- api/telebot.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/telebot.go b/api/telebot.go index 8b4bebf..8db94ff 100644 --- a/api/telebot.go +++ b/api/telebot.go @@ -144,7 +144,7 @@ func CreateTelebot(token string, logger core.ILogger) *Telebot { Width: m.Video.Width, Height: m.Video.Height, } - logger.Info(video) + logger.Info(m, video) for _, h := range telebot.videoHandlers { err = h.HandleVideo(video, telebot.coreFactory.makeMessage(m), telebot.coreFactory.makeIBot(m, telebot)) if err != nil { From 0d60de865d855b4b6db372f71bf45d6d5d2b48a7 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Sun, 19 Mar 2023 14:05:08 +0400 Subject: [PATCH 429/439] Replace `youtube-dl` with `yt-dlp` --- Dockerfile | 4 ++-- api/youtube_api.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index ddcfb84..d2397d0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,8 +9,8 @@ RUN CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -a -installsuffix cgo -ldflag FROM jrottenberg/ffmpeg:4.4-alpine313 RUN apk update && apk add tzdata python3 --no-cache && \ - wget https://yt-dl.org/downloads/latest/youtube-dl -O /usr/local/bin/youtube-dl && \ - chmod a+rx /usr/local/bin/youtube-dl && \ + wget https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp -O /usr/local/bin/yt-dlp && \ + chmod a+rx /usr/local/bin/yt-dlp && \ ln -s /usr/bin/python3 /usr/bin/python COPY --from=builder /go/src/github.com/ailinykh/pullanusbot/pullanusbot /usr/local/bin/pullanusbot WORKDIR /usr/local/share diff --git a/api/youtube_api.go b/api/youtube_api.go index 4d9f283..6a8cf23 100644 --- a/api/youtube_api.go +++ b/api/youtube_api.go @@ -59,7 +59,7 @@ func (y *YoutubeAPI) CreateVideo(id string) (*core.Video, error) { name := fmt.Sprintf("youtube-%s-%s-%s.mp4", id, vf.FormatID, af.FormatID) videoPath := path.Join(os.TempDir(), name) - cmd := fmt.Sprintf("youtube-dl -f %s+%s https://youtu.be/%s -o %s", vf.FormatID, af.FormatID, id, videoPath) + cmd := fmt.Sprintf("yt-dlp -f %s+%s https://youtu.be/%s -o %s", vf.FormatID, af.FormatID, id, videoPath) y.l.Info(strings.ReplaceAll(cmd, os.TempDir(), "$TMPDIR/")) out, err := exec.Command("/bin/sh", "-c", cmd).CombinedOutput() if err != nil { @@ -84,7 +84,7 @@ func (y *YoutubeAPI) CreateVideo(id string) (*core.Video, error) { } func (y *YoutubeAPI) getInfo(id string) (*Video, error) { - cmd := fmt.Sprintf(`youtube-dl -j https://youtu.be/%s`, id) // id might start with dash, ex: -bdUoHZCf24 + cmd := fmt.Sprintf(`yt-dlp -j https://youtu.be/%s`, id) // id might start with dash, ex: -bdUoHZCf24 out, err := exec.Command("/bin/sh", "-c", cmd).CombinedOutput() if err != nil { y.l.Error(err) From 140c59d9ffc2c3389a05535f768b5aff251e380b Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Sun, 28 May 2023 21:58:51 +0400 Subject: [PATCH 430/439] update twitter tokens --- api/twitter_api.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/api/twitter_api.go b/api/twitter_api.go index 111d405..b71134f 100644 --- a/api/twitter_api.go +++ b/api/twitter_api.go @@ -15,9 +15,7 @@ import ( func CreateTwitterAPI(l core.ILogger, t core.ITask) *TwitterAPI { return &TwitterAPI{l, t, []string{ "AAAAAAAAAAAAAAAAAAAAAPYXBAAAAAAACLXUNDekMxqa8h%2F40K4moUkGsoc%3DTYfbDKbT3jJPCEVnMYqilB28NHfOPqkca3qaAxGfsyKCs0wRbw", - "AAAAAAAAAAAAAAAAAAAAAPAh2AAAAAAAoInuXrJ%2BcqfgfR5PlJGnQsOniNY%3Dn9galDg4iUr7KyRAU47JGDbQz2q7sdwXRTkonzBX2uLxXRgNv0", - "AAAAAAAAAAAAAAAAAAAAAA4JLwEAAAAAXIyoETwtg%2BiTlR1VTNxGXnphfu4%3D6iSv0IXHo4NWGndWWLC8Bk3XuPkLMyATMxM0h6CfomnfRbGpgK", - "AAAAAAAAAAAAAAAAAAAAAAnuQQEAAAAAkV36hXt9HP5m5Qake9ffdXZMNTI%3DaF9mA4ZreVb938IeW8vfpTpT8HxDYOi0WYi5i4B8Cce9UVpwi6", + "AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA", }} } @@ -40,9 +38,10 @@ func (api *TwitterAPI) getTweetByID(tweetID string) (*Tweet, error) { return tweet, err } -func (TwitterAPI) getTweetByIdAndToken(tweetID string, token string) (*Tweet, error) { +func (api *TwitterAPI) getTweetByIdAndToken(tweetID string, token string) (*Tweet, error) { client := http.DefaultClient - req, _ := http.NewRequest("GET", fmt.Sprintf("https://api.twitter.com/1.1/statuses/show.json?id=%s&tweet_mode=extended", tweetID), nil) + url := fmt.Sprintf("https://api.twitter.com/1.1/statuses/show.json?id=%s&tweet_mode=extended", tweetID) + req, _ := http.NewRequest("GET", url, nil) req.Header.Add("Authorization", "Bearer "+token) res, err := client.Do(req) if err != nil { @@ -64,6 +63,7 @@ func (TwitterAPI) getTweetByIdAndToken(tweetID string, token string) (*Tweet, er if tweet.Errors[0].Code == 88 { // "Rate limit exceeded 88" return nil, fmt.Errorf("%s %s", tweet.Errors[0].Message, res.Header["X-Rate-Limit-Reset"][0]) } + api.l.Errorf("%s %s", tweet.Errors, token) return nil, fmt.Errorf(tweet.Errors[0].Message) } From eb2532f1d423c9f294c0ef636e4f6f81acd464cf Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Wed, 12 Jul 2023 17:16:58 +0400 Subject: [PATCH 431/439] Attempt to fix twitter parsing error --- api/tweet.go | 82 +++++++++++++++++++- api/twitter_api.go | 143 ++++++++++++++++++++++++++++++----- api/twitter_media_factory.go | 23 +++++- 3 files changed, 225 insertions(+), 23 deletions(-) diff --git a/api/tweet.go b/api/tweet.go index 3615788..f4244ba 100644 --- a/api/tweet.go +++ b/api/tweet.go @@ -6,7 +6,7 @@ type Tweet struct { FullText string `json:"full_text"` Entities Entity `json:"entities"` ExtendedEntities Entity `json:"extended_entities,omitempty"` - User User `json:"user"` + User User `json:"user,omitempty"` QuotedStatus *Tweet `json:"quoted_status,omitempty"` Errors []Error `json:"errors,omitempty"` } @@ -25,8 +25,7 @@ type Entity struct { // Media ... type Media struct { - MediaURL string `json:"media_url"` - MediaURLHTTPS string `json:"media_url_https"` + MediaUrlHttps string `json:"media_url_https"` Type string `json:"type"` VideoInfo VideoInfo `json:"video_info,omitempty"` } @@ -69,3 +68,80 @@ type TweetScreenshot struct { Username string `json:"username"` URL string `json:"url"` } + +// GraphQL types +type GraphQLRequest struct { + Variables GraphQLVariables `json:"variables"` + Features GraphQLFeatures `json:"features"` + FieldToggles GraphQLFieldToggles `json:"fieldToggles"` +} + +type GraphQLVariables struct { + TweetId string `json:"tweetId"` + WithCommunity bool `json:"withCommunity"` + IncludePromotedContent bool `json:"includePromotedContent"` + WithVoice bool `json:"withVoice"` +} + +type GraphQLFeatures struct { + CreatorSubscriptionsTweetPreviewApiEnabled bool `json:"creator_subscriptions_tweet_preview_api_enabled"` + FreedomOfSpeechNotReachFetceEnabled bool `json:"freedom_of_speech_not_reach_fetch_enabled"` + GraphqlIsTranslatableRwebTweetIsTranslatableEnabled bool `json:"graphql_is_translatable_rweb_tweet_is_translatable_enabled"` + LongformNotetweetsConsumptionEnabled bool `json:"longform_notetweets_consumption_enabled"` + LongformNotetweetsInlineMediaEnabled bool `json:"longform_notetweets_inline_media_enabled"` + LongformNotetweetsRichTextReadEnabled bool `json:"longform_notetweets_rich_text_read_enabled"` + ResponsiveWebGraphqlSkipUserProfileImageExtensionsEnabled bool `json:"responsive_web_graphql_skip_user_profile_image_extensions_enabled"` + ResponsiveWebEditTweetApiEnabled bool `json:"responsive_web_edit_tweet_api_enabled"` + ResponsiveWebEnhanceCardsEnabled bool `json:"responsive_web_enhance_cards_enabled"` + ResponsiveWebMediaDownloadVideoEnabled bool `json:"responsive_web_media_download_video_enabled"` + ResponsiveWebGraphqlTimelineNavigationEnabled bool `json:"responsive_web_graphql_timeline_navigation_enabled"` + ResponsiveWebGraphqlExcludeDirectiveEnabled bool `json:"responsive_web_graphql_exclude_directive_enabled"` + ResponsiveWebTwitterArticleTweetConsumptionEnabled bool `json:"responsive_web_twitter_article_tweet_consumption_enabled"` + StandardizedNudgesMisinfo bool `json:"standardized_nudges_misinfo"` + TweetAwardsWebTippingEnabled bool `json:"tweet_awards_web_tipping_enabled"` + TweetWithVisibilityResultsPreferGqlLimitedActionsPolicyEnabled bool `json:"tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled"` + TweetypieUnmentionOptimizationEnabled bool `json:"tweetypie_unmention_optimization_enabled"` + VerifiedPhoneLabelEnabled bool `json:"verified_phone_label_enabled"` + ViewCountsEverywhereApiEnabled bool `json:"view_counts_everywhere_api_enabled"` +} + +type GraphQLFieldToggles struct { + WithArticleRichContentState bool `json:"withArticleRichContentState"` +} + +type GraphQLResponse struct { + Errors []Error `json:"errors,omitempty"` + Data GraphQLResponseData `json:"data"` +} + +type GraphQLResponseData struct { + TweetResult GraphQLResponseTweetResult `json:"tweetResult"` +} + +type GraphQLResponseTweetResult struct { + Result GraphQLResponseTweetResultResult `json:"result"` +} + +type GraphQLResponseTweetResultResult struct { + Core GraphQLResponseCore `json:"core"` + Legacy Tweet `json:"legacy"` + RestId string `json:"rest_id"` +} + +type GraphQLResponseCore struct { + UserResults GraphQLResponseUserResults `json:"user_results"` +} + +type GraphQLResponseUserResults struct { + Result GraphQLResponseUserResult `json:"result"` +} + +type GraphQLResponseUserResult struct { + Legacy User `json:"legacy"` + RestId string `json:"rest_id"` + Verified bool `json:"is_blue_verified"` +} + +type GuestTokenResponse struct { + GuestToken string `json:"guest_token"` +} diff --git a/api/twitter_api.go b/api/twitter_api.go index b71134f..cdb793d 100644 --- a/api/twitter_api.go +++ b/api/twitter_api.go @@ -5,7 +5,7 @@ import ( "fmt" "io/ioutil" "net/http" - "strings" + "net/url" "time" "github.com/ailinykh/pullanusbot/v2/core" @@ -13,36 +13,145 @@ import ( // CreateTwitterAPI is a default Twitter factory func CreateTwitterAPI(l core.ILogger, t core.ITask) *TwitterAPI { - return &TwitterAPI{l, t, []string{ - "AAAAAAAAAAAAAAAAAAAAAPYXBAAAAAAACLXUNDekMxqa8h%2F40K4moUkGsoc%3DTYfbDKbT3jJPCEVnMYqilB28NHfOPqkca3qaAxGfsyKCs0wRbw", - "AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA", - }} + return &TwitterAPI{l, t, TwitterApiCredentials{ + bearer_token: "AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA", + guest_token: "1679397394880888834", + }, + } +} + +type TwitterApiCredentials struct { + bearer_token string + guest_token string } // Twitter API type TwitterAPI struct { - l core.ILogger - task core.ITask - tokens []string + l core.ILogger + task core.ITask + credentials TwitterApiCredentials } func (api *TwitterAPI) getTweetByID(tweetID string) (*Tweet, error) { - var tweet *Tweet - var err = fmt.Errorf("tokens not set") - for _, t := range api.tokens { - tweet, err = api.getTweetByIdAndToken(tweetID, t) - if err == nil || !strings.HasPrefix(err.Error(), "Rate limit exceeded") { - return tweet, err + tweet, err := api.getTweetFromGraphQL(tweetID) + if err == nil { + return tweet, err + } + + if err.Error() == "Bad guest token" { + resp, err := api.getGuestToken() + if err != nil { + return nil, err } + + api.l.Infof("guest token received %s", resp.GuestToken) + + api.credentials = TwitterApiCredentials{ + bearer_token: api.credentials.bearer_token, + guest_token: resp.GuestToken, + } + + return api.getTweetFromGraphQL(tweetID) } + return tweet, err } -func (api *TwitterAPI) getTweetByIdAndToken(tweetID string, token string) (*Tweet, error) { +func (api *TwitterAPI) getGuestToken() (*GuestTokenResponse, error) { + api.l.Info("updating guest token") + + req, _ := http.NewRequest("POST", "https://api.twitter.com/1.1/guest/activate.json", nil) + req.Header.Add("Authorization", "Bearer "+api.credentials.bearer_token) + + res, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + defer res.Body.Close() + + var response GuestTokenResponse + body, _ := ioutil.ReadAll(res.Body) + + err = json.Unmarshal(body, &response) + if err != nil { + return nil, err + } + + return &response, nil +} + +func (api *TwitterAPI) getTweetFromGraphQL(tweetID string) (*Tweet, error) { + data, _ := json.Marshal(GraphQLVariables{tweetID, false, false, false}) + variables := url.QueryEscape(string(data)) + + data, _ = json.Marshal(GraphQLFeatures{ + CreatorSubscriptionsTweetPreviewApiEnabled: true, + FreedomOfSpeechNotReachFetceEnabled: true, + GraphqlIsTranslatableRwebTweetIsTranslatableEnabled: true, + LongformNotetweetsConsumptionEnabled: true, + LongformNotetweetsInlineMediaEnabled: true, + LongformNotetweetsRichTextReadEnabled: true, + ResponsiveWebGraphqlSkipUserProfileImageExtensionsEnabled: false, + ResponsiveWebEditTweetApiEnabled: true, + ResponsiveWebEnhanceCardsEnabled: false, + ResponsiveWebMediaDownloadVideoEnabled: true, + ResponsiveWebGraphqlTimelineNavigationEnabled: true, + ResponsiveWebGraphqlExcludeDirectiveEnabled: true, + ResponsiveWebTwitterArticleTweetConsumptionEnabled: false, + StandardizedNudgesMisinfo: true, + TweetAwardsWebTippingEnabled: false, + TweetWithVisibilityResultsPreferGqlLimitedActionsPolicyEnabled: true, + TweetypieUnmentionOptimizationEnabled: true, + VerifiedPhoneLabelEnabled: false, + ViewCountsEverywhereApiEnabled: true, + }) + features := url.QueryEscape(string(data)) + + data, _ = json.Marshal(GraphQLFieldToggles{false}) + field_toggles := url.QueryEscape(string(data)) + + url := fmt.Sprintf("https://twitter.com/i/api/graphql/2ICDjqPd81tulZcYrtpTuQ/TweetResultByRestId?variables=%s&features=%s&fieldToggles=%s", variables, features, field_toggles) + + req, _ := http.NewRequest("GET", url, nil) + req.Header.Add("authorization", "Bearer "+api.credentials.bearer_token) + req.Header.Add("x-guest-token", api.credentials.guest_token) + + res, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + defer res.Body.Close() + + var response GraphQLResponse + body, _ := ioutil.ReadAll(res.Body) + + // os.WriteFile("tweet-"+tweetID+".json", body, 0644) + + err = json.Unmarshal(body, &response) + if err != nil { + return nil, err + } + + if len(response.Errors) > 0 { + if response.Errors[0].Code == 88 { // "Rate limit exceeded 88" + return nil, fmt.Errorf("%s %s", response.Errors[0].Message, res.Header["X-Rate-Limit-Reset"][0]) + } + return nil, fmt.Errorf(response.Errors[0].Message) + } + + // TODO: combine `twitter_api` with `twitter_media_factory` + tweet := response.Data.TweetResult.Result.Legacy + user := response.Data.TweetResult.Result.Core.UserResults.Result.Legacy + tweet.User = User{Name: user.Name, ScreenName: user.ScreenName} + + return &tweet, nil +} + +func (api *TwitterAPI) getTweetByIdAndToken(tweetID string, creds TwitterApiCredentials) (*Tweet, error) { client := http.DefaultClient url := fmt.Sprintf("https://api.twitter.com/1.1/statuses/show.json?id=%s&tweet_mode=extended", tweetID) req, _ := http.NewRequest("GET", url, nil) - req.Header.Add("Authorization", "Bearer "+token) + req.Header.Add("Authorization", "Bearer "+creds.bearer_token) res, err := client.Do(req) if err != nil { return nil, err @@ -63,7 +172,7 @@ func (api *TwitterAPI) getTweetByIdAndToken(tweetID string, token string) (*Twee if tweet.Errors[0].Code == 88 { // "Rate limit exceeded 88" return nil, fmt.Errorf("%s %s", tweet.Errors[0].Message, res.Header["X-Rate-Limit-Reset"][0]) } - api.l.Errorf("%s %s", tweet.Errors, token) + api.l.Errorf("%s %s", tweet.Errors, creds.guest_token) return nil, fmt.Errorf(tweet.Errors[0].Message) } diff --git a/api/twitter_media_factory.go b/api/twitter_media_factory.go index 13bd7cf..0274e51 100644 --- a/api/twitter_media_factory.go +++ b/api/twitter_media_factory.go @@ -19,6 +19,7 @@ type TwitterMediaFactory struct { func (tmf *TwitterMediaFactory) CreateMedia(tweetID string) ([]*core.Media, error) { tweet, err := tmf.api.getTweetByID(tweetID) if err != nil { + tmf.l.Error(err) return nil, err } @@ -41,9 +42,19 @@ func (tmf *TwitterMediaFactory) CreateMedia(tweetID string) ([]*core.Media, erro case 1: if media[0].Type == "video" || media[0].Type == "animated_gif" { //TODO: Codec ?? - return []*core.Media{{ResourceURL: media[0].VideoInfo.best().URL, URL: url, Title: tweet.User.Name, Description: tweet.FullText, Type: core.TVideo}}, nil + return []*core.Media{{ + ResourceURL: media[0].VideoInfo.best().URL, + URL: url, Title: tweet.User.Name, + Description: tweet.FullText, + Type: core.TVideo, + }}, nil } else if media[0].Type == "photo" { - return []*core.Media{{ResourceURL: media[0].MediaURL, URL: url, Title: tweet.User.Name, Description: tweet.FullText, Type: core.TPhoto}}, nil + return []*core.Media{{ + ResourceURL: media[0].MediaUrlHttps, + URL: url, Title: tweet.User.Name, + Description: tweet.FullText, + Type: core.TPhoto, + }}, nil } else { return nil, fmt.Errorf("unexpected type: %s", media[0].Type) } @@ -51,7 +62,13 @@ func (tmf *TwitterMediaFactory) CreateMedia(tweetID string) ([]*core.Media, erro // t.sendAlbum(media, tweet, m) medias := []*core.Media{} for _, m := range media { - medias = append(medias, &core.Media{ResourceURL: m.MediaURL, URL: url, Title: tweet.User.Name, Description: tweet.FullText, Type: core.TPhoto}) + medias = append(medias, &core.Media{ + ResourceURL: m.MediaUrlHttps, + URL: url, + Title: tweet.User.Name, + Description: tweet.FullText, + Type: core.TPhoto, + }) } return medias, nil } From 2a361a7ca801d90534b5d9459aec3eec6b7edba4 Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Fri, 14 Jul 2023 00:09:01 +0400 Subject: [PATCH 432/439] Replace `TikTok` api with `yt-dlp` api --- api/tiktok.go | 22 ---------- api/tiktok_api_decorator.go | 23 ---------- api/tiktok_html_v1_api.go | 84 ------------------------------------ api/tiktok_html_v2_api.go | 85 ------------------------------------- api/tiktok_json_api.go | 63 --------------------------- api/tiktok_media_factory.go | 29 +++---------- api/tiktok_v1.go | 55 ------------------------ api/tiktok_v2.go | 23 ---------- api/ytdlp_api.go | 57 +++++++++++++++++++++++++ pullanusbot.go | 8 +--- 10 files changed, 66 insertions(+), 383 deletions(-) delete mode 100644 api/tiktok.go delete mode 100644 api/tiktok_api_decorator.go delete mode 100644 api/tiktok_html_v1_api.go delete mode 100644 api/tiktok_html_v2_api.go delete mode 100644 api/tiktok_json_api.go delete mode 100644 api/tiktok_v1.go delete mode 100644 api/tiktok_v2.go create mode 100644 api/ytdlp_api.go diff --git a/api/tiktok.go b/api/tiktok.go deleted file mode 100644 index 2f05780..0000000 --- a/api/tiktok.go +++ /dev/null @@ -1,22 +0,0 @@ -package api - -type ITikTokAPI interface { - GetItem(string, string) (*TikTokItem, error) -} - -type TikTokItem struct { - Author TikTokAuthor - Desc string - Music TikTokMusic - Stickers []string - VideoURL string -} - -type TikTokAuthor struct { - Nickname string - UniqueId string -} - -type TikTokMusic struct { - Title string -} diff --git a/api/tiktok_api_decorator.go b/api/tiktok_api_decorator.go deleted file mode 100644 index 3ab1199..0000000 --- a/api/tiktok_api_decorator.go +++ /dev/null @@ -1,23 +0,0 @@ -package api - -import "strings" - -func CreateTikTokAPIDecorator(primary ITikTokAPI, secondary ITikTokAPI) ITikTokAPI { - return &TikTokAPIDecorator{primary, secondary} -} - -type TikTokAPIDecorator struct { - primary ITikTokAPI - secondary ITikTokAPI -} - -func (api *TikTokAPIDecorator) GetItem(username string, videoId string) (*TikTokItem, error) { - item, err := api.primary.GetItem(username, videoId) - if err != nil { - if strings.HasPrefix(err.Error(), "tiktok:") { - return nil, err - } - return api.secondary.GetItem(username, videoId) - } - return item, nil -} diff --git a/api/tiktok_html_v1_api.go b/api/tiktok_html_v1_api.go deleted file mode 100644 index a59121b..0000000 --- a/api/tiktok_html_v1_api.go +++ /dev/null @@ -1,84 +0,0 @@ -package api - -import ( - "encoding/json" - "fmt" - "regexp" - "strconv" - - "github.com/ailinykh/pullanusbot/v2/core" -) - -func CreateTikTokHTMLV1API(l core.ILogger, hc core.IHttpClient, r core.IRand) ITikTokAPI { - return &TikTokHTMLV1API{l, hc, r} -} - -type TikTokHTMLV1API struct { - l core.ILogger - hc core.IHttpClient - r core.IRand -} - -func (api *TikTokHTMLV1API) GetItem(username string, videoId string) (*TikTokItem, error) { - url := "https://www.tiktok.com/" + username + "/video/" + videoId - api.l.Infof("processing %s", url) - api.hc.SetHeader("Cookie", "tt_webid_v2=69"+api.randomDigits(17)+"; Domain=tiktok.com; Path=/; Secure; hostOnly=false; hostOnly=false; aAge=4ms; cAge=4ms") - htmlString, err := api.hc.GetContent(url) - if err != nil { - return nil, err - } - - // os.WriteFile("tiktok-"+strings.Split(url, "/")[5]+".html", []byte(htmlString), 0644) - r := regexp.MustCompile(`