From 469997e7b0953852bb39387cb52373c06704401c Mon Sep 17 00:00:00 2001 From: Anthony Ilinykh Date: Tue, 4 May 2021 14:48:20 +0300 Subject: [PATCH 001/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] =?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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] "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/295] 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/295] "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/295] 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/295] 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/295] 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/295] 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/295] [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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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/295] 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(`