From 900673a59d147778ef70a002cfdf193f3604980c Mon Sep 17 00:00:00 2001 From: Nefrace Date: Thu, 15 Jun 2023 17:03:57 +0300 Subject: [PATCH] RSS feed, optimized docs handler --- commands.go | 4 ++- go.mod | 12 ++++++++ go.sum | 31 ++++++++++++++++++++ handle-docs.go | 40 +++++++++++++++++++------- handle-karma.go | 2 +- handlers.go | 44 ++++++++++++++++++++++++++++ main.go | 1 + rss.go | 76 +++++++++++++++++++++++++++++++++++++++++++++++++ states.go | 4 ++- types.go | 17 +++++++++++ 10 files changed, 217 insertions(+), 14 deletions(-) create mode 100644 rss.go diff --git a/commands.go b/commands.go index 0e931a7..a09e365 100644 --- a/commands.go +++ b/commands.go @@ -8,7 +8,9 @@ var commandMe = neco.NewCommand("me", "Пишу ваш текст о вас в var commandHelp = neco.NewCommand("help", "Показываю данный текст", false) var commandSay = neco.NewCommand("say", "Пишу ваш текст от своего имени.", true) var commandWarn = neco.NewCommand("warn", "Делаю предупреждение пользователю", true) +var commandFeed = neco.NewCommand("subscribe", "Регистрирую данный чат в качестве получателя рассылки", true) +var commandFeedUnsub = neco.NewCommand("unsubscribe", "Удаляю данный чат из рассылки", true) var defaultCommands = []*neco.Command{commandHelp, commandMe} -var adminCommands = []*neco.Command{commandSay, commandWarn} +var adminCommands = []*neco.Command{commandSay, commandWarn, commandFeed} var allCommands = append(defaultCommands, adminCommands...) diff --git a/go.mod b/go.mod index 0158ad6..8d28193 100644 --- a/go.mod +++ b/go.mod @@ -8,3 +8,15 @@ require ( github.com/google/uuid v1.3.0 github.com/joho/godotenv v1.5.1 ) + +require ( + github.com/PuerkitoBio/goquery v1.8.0 // indirect + github.com/andybalholm/cascadia v1.3.1 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/mmcdole/gofeed v1.2.1 // indirect + github.com/mmcdole/goxpp v1.1.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + golang.org/x/net v0.4.0 // indirect + golang.org/x/text v0.5.0 // indirect +) diff --git a/go.sum b/go.sum index a90ef60..8ab4623 100644 --- a/go.sum +++ b/go.sum @@ -2,9 +2,40 @@ git.nefrace.ru/nefrace/nechotron v0.0.0-20230119201747-5842815c958c h1:3vYJhrChr git.nefrace.ru/nefrace/nechotron v0.0.0-20230119201747-5842815c958c/go.mod h1:PiYTWTy1SMXKdsxNSrQOqUQRffw4DXI32PjjxVMJuOA= github.com/NicoNex/echotron/v3 v3.22.0 h1:2ymJcjKqtZ/rfD5CveR2VKqQob7JmRgJmoLOJ3sim/4= github.com/NicoNex/echotron/v3 v3.22.0/go.mod h1:LpP5IyHw0y+DZUZMBgXEDAF9O8feXrQu7w7nlJzzoZI= +github.com/PuerkitoBio/goquery v1.8.0 h1:PJTF7AmFCFKk1N6V6jmKfrNH9tV5pNE6lZMkG0gta/U= +github.com/PuerkitoBio/goquery v1.8.0/go.mod h1:ypIiRMtY7COPGk+I/YbZLbxsxn9g5ejnI2HSMtkjZvI= +github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c= +github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/mmcdole/gofeed v1.2.1 h1:tPbFN+mfOLcM1kDF1x2c/N68ChbdBatkppdzf/vDe1s= +github.com/mmcdole/gofeed v1.2.1/go.mod h1:2wVInNpgmC85q16QTTuwbuKxtKkHLCDDtf0dCmnrNr4= +github.com/mmcdole/goxpp v1.1.0 h1:WwslZNF7KNAXTFuzRtn/OKZxFLJAAyOA9w82mDz2ZGI= +github.com/mmcdole/goxpp v1.1.0/go.mod h1:v+25+lT2ViuQ7mVxcncQ8ch1URund48oH+jhjiwEgS8= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU= +golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM= +golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/handle-docs.go b/handle-docs.go index 025e9b4..f154f58 100644 --- a/handle-docs.go +++ b/handle-docs.go @@ -9,10 +9,11 @@ import ( "strings" "git.nefrace.ru/nefrace/nechotron" + "github.com/NicoNex/echotron/v3" ) var docApiURL = "https://docs.godotengine.org/_/api/v2/search/?q=%s&project=godot&version=%s&language=en" -var docURL = "https://docs.godotengine.org/ru/stable/search.html?q=%s" +var docURL = "https://docs.godotengine.org/en/%s/search.html?q=%s" type DocResponse struct { Count uint `json:"count"` @@ -62,13 +63,21 @@ func getDocs(topic string, version string) (string, error) { return text, nil } +var doc_variants []string = []string{"latest", "3.5"} + func handleDocRequest(u *nechotron.Update) error { topic := u.Ctx.Value(nechotron.FilteredValue("docTopic")).(string) + currentVersion := doc_variants[0] + versionButtons := []echotron.InlineKeyboardButton{} + for _, variant := range doc_variants { + if variant == currentVersion { + continue + } + versionButtons = append(versionButtons, nechotron.InButtonCallback("Для "+variant, fmt.Sprintf("docs:%s:%s", variant, topic))) + } kb := nechotron.NewInlineKeyboard(). - Row( - nechotron.InButtonCallback("Дай для 3.5", fmt.Sprintf("docs3:%s", topic)), - nechotron.InButtonCallback("А на русском можно?", "rudocs")). - Row(nechotron.InButtonURL("Поищу сам", fmt.Sprintf(docURL, url.QueryEscape(topic)))). + Row(versionButtons...). + Row(nechotron.InButtonURL("Поищу сам", fmt.Sprintf(docURL, currentVersion, url.QueryEscape(topic)))). Row(nechotron.InButtonCallback("Спасибо, не надо", "delete")) opts := nechotron.NewOptions(). MarkdownV2(). @@ -84,19 +93,28 @@ func handleDocRequest(u *nechotron.Update) error { return err } -func handleDocRequest3(u *nechotron.Update) error { - topic := strings.TrimPrefix(u.Callback(), "docs3:") +func handleDocCallback(u *nechotron.Update) error { + text := strings.TrimPrefix(u.Callback(), "docs:") + items := strings.Split(text, ":") + version := items[0] + topic := items[1] + versionButtons := []echotron.InlineKeyboardButton{} + for _, variant := range doc_variants { + if variant == version { + continue + } + versionButtons = append(versionButtons, nechotron.InButtonCallback("Для "+variant, fmt.Sprintf("docs:%s:%s", variant, topic))) + } kb := nechotron.NewInlineKeyboard(). - Row( - nechotron.InButtonCallback("А на русском можно?", "rudocs")). - Row(nechotron.InButtonURL("Поищу сам", fmt.Sprintf(docURL, url.QueryEscape(topic)))). + Row(versionButtons...). + Row(nechotron.InButtonURL("Поищу сам", fmt.Sprintf(docURL, version, url.QueryEscape(topic)))). Row(nechotron.InButtonCallback("Спасибо, не надо", "delete")) opts := nechotron.NewOptions(). MarkdownV2(). ReplyMarkup(kb.Markup()). MessageTextOptions() - text, docerr := getDocs(topic, "3.5") + text, docerr := getDocs(topic, version) if docerr != nil { log.Println("Can't get docs: ", docerr) } diff --git a/handle-karma.go b/handle-karma.go index 0549b31..44ab9e5 100644 --- a/handle-karma.go +++ b/handle-karma.go @@ -36,7 +36,7 @@ func handleKarma(u *nechotron.Update) error { store := tongo.NewStore[KarmaShot](db) fromKarma, _ := store.Count(u.Ctx, tongo.E("to", from.ID)) totalFromKarma := from.KarmaOffset + fromKarma - if totalFromKarma < 0 { + if totalFromKarma <= 0 { res, err := u.AnswerText( fmt.Sprintf("У тебя слишком маленькая карма *\\(%d\\), чтобы менять её другим\\.", totalFromKarma), &echotron.MessageOptions{ParseMode: echotron.MarkdownV2, ReplyToMessageID: u.MessageID()}) diff --git a/handlers.go b/handlers.go index f7c6291..cb935dc 100644 --- a/handlers.go +++ b/handlers.go @@ -4,6 +4,7 @@ import ( "fmt" "log" "math/rand" + "time" "git.nefrace.ru/nefrace/nechotron" "git.nefrace.ru/nefrace/tongo" @@ -31,6 +32,9 @@ func handleHelp(u *nechotron.Update, text string) error { func handleSay(u *nechotron.Update, text string) error { u.DeleteMessage() + if text == "" { + return nil + } _, err := u.AnswerMarkdown(fmt.Sprintf("*_%s_*", nechotron.EscapeMd2(text))) return err } @@ -70,3 +74,43 @@ func handleUsersImport(u *nechotron.Update) error { log.Println(string(file)) return nil } + +func handleRegisterFeed(u *nechotron.Update, _ string) error { + feedChats := tongo.NewStore[FeedChat](db) + chat, err := feedChats.GetOne(u.Ctx, tongo.E("chatid", u.ChatID())) + if err != nil { + chat = &FeedChat{ + Item: tongo.NewID(), + ChatID: u.ChatID(), + Title: u.Message.Chat.Title, + ThreadID: u.Message.ThreadID, + } + feedChats.InsertOne(u.Ctx, chat) + res, _ := u.AnswerPlain("Чат зарегистрирован для рассылки") + go func() { + time.Sleep(10 * time.Second) + u.Bot.DeleteMessage(u.ChatID(), res.Result.ID) + }() + } + return nil +} + +func handleDeleteFeed(u *nechotron.Update, _ string) error { + feedChats := tongo.NewStore[FeedChat](db) + chat, err := feedChats.GetOne(u.Ctx, tongo.E("chatid", u.ChatID())) + if err != nil { + res, _ := u.AnswerPlain("Чат не подписан на рассылку") + go func() { + time.Sleep(10 * time.Second) + u.Bot.DeleteMessage(u.ChatID(), res.Result.ID) + }() + return nil + } + feedChats.DeleteByID(u.Ctx, chat.Id) + res, _ := u.AnswerPlain("Чат удалён из рассылки") + go func() { + time.Sleep(10 * time.Second) + u.Bot.DeleteMessage(u.ChatID(), res.Result.ID) + }() + return nil +} diff --git a/main.go b/main.go index fd5b853..c0cb965 100644 --- a/main.go +++ b/main.go @@ -34,6 +34,7 @@ func main() { api := echotron.NewAPI(token) nechotron.SetMyCommands(api, "", echotron.BotCommandScope{Type: echotron.Any}, defaultCommands...) nechotron.SetMyCommands(api, "", echotron.BotCommandScope{Type: echotron.BCSTAllChatAdministrators}, allCommands...) + go RSSTask(&api) log.Fatal(neco.DispatchPoll()) } diff --git a/rss.go b/rss.go new file mode 100644 index 0000000..7b51177 --- /dev/null +++ b/rss.go @@ -0,0 +1,76 @@ +package main + +import ( + "context" + "fmt" + "log" + "time" + + "git.nefrace.ru/nefrace/nechotron" + "git.nefrace.ru/nefrace/tongo" + "github.com/NicoNex/echotron/v3" + "github.com/mmcdole/gofeed" +) + +var text_template = ` +[*%s*](%s) +_by %s_ + +%s +` + +func RSSTask(api *echotron.API) { + for { + ParseRSS(api) + time.Sleep(5 * time.Minute) + } +} + +func ParseRSS(api *echotron.API) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + feedChats := tongo.NewStore[FeedChat](db) + feeds := tongo.NewStore[Feeds](db) + lastFeed, err := feeds.GetOne(ctx) + if err != nil || lastFeed == nil { + lastFeed = &Feeds{ + Item: tongo.NewID(), + } + feeds.ReplaceItem(ctx, *lastFeed, true) + } + fp := gofeed.NewParser() + feed, err := fp.ParseURL("https://godotengine.org/rss.xml") + if err != nil { + log.Println("Can't get feed: ", err) + return + } + item := feed.Items[0] + if lastFeed.Published.Compare(*item.PublishedParsed) == -1 { + chats, _ := feedChats.GetMany(ctx) + if len(chats) == 0 { + return + } + text := fmt.Sprintf(text_template, + nechotron.EscapeMd2(item.Title), + item.Link, + nechotron.EscapeMd2(item.Authors[0].Name), + nechotron.EscapeMd2(item.Description), + ) + sent := false + for _, chat := range chats { + _, err := api.SendPhoto(echotron.NewInputFileURL(item.Custom["image"]), chat.ChatID, nechotron.NewOptions().MarkdownV2().Caption(text).PhotoOptions()) + if err != nil { + log.Println("Can't send message to", chat.Title, ": ", err) + } else { + sent = true + } + time.Sleep(1 * time.Second) + } + if !sent { + return + } + lastFeed.Published = *item.PublishedParsed + lastFeed.Title = item.Title + feeds.ReplaceItem(ctx, *lastFeed, true) + } +} diff --git a/states.go b/states.go index fd6ad4c..a157d13 100644 --- a/states.go +++ b/states.go @@ -9,6 +9,8 @@ var MainState = neco.State{ adminDispatcher := neco.NewDispatcher(). HandleCallback(neco.CallbackExact("delete"), handleDeleteCallback). HandleCommand(commandSay, handleSay). + HandleCommand(commandFeed, handleRegisterFeed). + HandleCommand(commandFeedUnsub, handleDeleteFeed). HandleFilter(isFile, handleUsersImport) adminOnly := neco.NewDispatcher(). HandleFilter(neco.IsUserAdmin, adminDispatcher.Run) @@ -24,7 +26,7 @@ var MainState = neco.State{ docs := neco.NewDispatcher(). HandleFilter(docRequest, handleDocRequest). - HandleCallback(neco.CallbackPrefix("docs3"), handleDocRequest3) + HandleCallback(neco.CallbackPrefix("docs"), handleDocCallback) triggers := neco.NewDispatcher(). HandleFilter(offtopTrigger, handleOfftop) diff --git a/types.go b/types.go index 0ce3d60..4c00f21 100644 --- a/types.go +++ b/types.go @@ -97,3 +97,20 @@ type Config struct { } func (Config) Coll() string { return "config" } + +type Feeds struct { + tongo.Item `bson:",inline"` + Title string + Published time.Time +} + +func (Feeds) Coll() string { return "feeds" } + +type FeedChat struct { + tongo.Item `bson:",inline"` + Title string + ChatID int64 + ThreadID int +} + +func (FeedChat) Coll() string { return "feed_chats" }