From 0ed09563e2144d6de8ec4ae424762a73ba190179 Mon Sep 17 00:00:00 2001 From: eleksir Date: Sun, 8 Jun 2025 04:32:16 +0300 Subject: [PATCH] Initial commit --- .golangci.yml | 62 ++++---- Makefile | 72 ++++++++- cmd/bunny-bot/main.go | 5 +- data/config-example.json | 3 +- internal/moon/animation.go | 33 ++-- internal/moon/casChatAPI.go | 2 + internal/moon/chatMemberUpdated.go | 34 ++-- internal/moon/chatlog.go | 56 +++++++ internal/moon/editedMessage.go | 45 ++++++ internal/moon/globals.go | 9 +- internal/moon/leftChatMember.go | 2 + internal/moon/newChatMembers.go | 40 ++--- internal/moon/photo.go | 46 ++++++ internal/moon/reaction.go | 65 ++++++++ internal/moon/reactionCount.go | 23 +++ internal/moon/runAndBan.go | 245 ++++++++++++++--------------- internal/moon/squash.go | 19 +-- internal/moon/telegram.go | 24 +++ internal/moon/textMessage.go | 88 +++++------ internal/moon/types.go | 7 + 20 files changed, 600 insertions(+), 280 deletions(-) create mode 100644 internal/moon/chatlog.go create mode 100644 internal/moon/editedMessage.go create mode 100644 internal/moon/photo.go create mode 100644 internal/moon/reaction.go create mode 100644 internal/moon/reactionCount.go diff --git a/.golangci.yml b/.golangci.yml index 3ef85c2..f6d8c8d 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,30 +1,12 @@ +version: "2" run: - tests: false + go: "1.24" modules-download-mode: readonly + tests: false allow-parallel-runners: true - go: '1.24' -output: - sort-results: true -linters-settings: - gosec: - excludes: - # Оно не используется для повышения безопасности или каких-то таких секъюрых вещей, оно используется для выборки - # произвольного элемента из массива, так что ИДИ НАХУЙ с этой проверкой и не еби мозги липовой безопасностью. - - G404 # Insecure random number source (rand) -issues: - exclude-dirs: - - data - - vendor - linters: - disable-all: true + default: none enable: - - errcheck - - gosimple - - govet - - ineffassign - - staticcheck - - unused - asasalint - asciicheck - bodyclose @@ -34,6 +16,7 @@ linters: - decorder - durationcheck - err113 + - errcheck - errchkjson - errorlint - exhaustive @@ -41,12 +24,12 @@ linters: - gocheckcompilerdirectives - gocritic - godot - - gofmt - - goimports - gosec - gosmopolitan + - govet - grouper - importas + - ineffassign - ireturn - makezero - mirror @@ -62,10 +45,35 @@ linters: - reassign - revive - rowserrcheck - - stylecheck + - staticcheck - unconvert + - unused - wastedassign - whitespace - wsl - -# vim: set ft=yaml et ai ts=2 sw=2 sts=2: + settings: + exclusions: + generated: lax + presets: + - comments + - common-false-positives + - legacy + - std-error-handling + paths: + - data + - vendor + - third_party$ + - builtin$ + - examples$ +formatters: + enable: + - gofmt + - goimports + exclusions: + generated: lax + paths: + - data + - vendor + - third_party$ + - builtin$ + - examples$ diff --git a/Makefile b/Makefile index a90fcb8..01f594a 100644 --- a/Makefile +++ b/Makefile @@ -2,18 +2,82 @@ GOOPTS=CGO_ENABLED=0 BUILDOPTS=-ldflags="-s -w" -a -gcflags=all=-l -trimpath -buildvcs=false +MYNAME=bunny-bot +BINARY=$MYNAME +RMCMD=rm -rf + +# На windows имя бинарника может зависеть не только от платформы, но и от выбранной цели, для linux-а суффикс .exe +# не нужен +ifeq ($(OS),Windows_NT) +ifdef GOOS +ifeq ($(GOOS),windows) +BINARY=${MYNAME}.exe +else # not ifeq ($(GOOS),windows) +BINARY=${MYNAME} +endif # ifeq ($(GOOS),windows) +else # not ifdef GOOS +BINARY=${MYNAME}.exe +endif # ifdef GOOS +ifeq ($(SHELL), sh.exe) +RMCMD=DEL /Q /F +endif +endif + +# Явно определяем символ новой строки, чтобы избежать неоднозначности на windows +define IFS + +endef + all: clean build + build: - ${GOOPTS} go build ${BUILDOPTS} -o bunny-bot ./cmd/bunny-bot +ifeq ($(OS),Windows_NT) +# Looks like on windows gnu make explicitly set SHELL to sh.exe, if it was not set. +ifeq ($(SHELL), sh.exe) +# # Vanilla cmd.exe / powershell. + SET "CGO_ENABLED=0" + go build ${BUILDOPTS} -o ${BINARY} ./cmd/${MYNAME} +else ifeq (,$(findstring(Git/usr/bin/sh.exe, $(SHELL)))) +# # git-bash + CGO_ENABLED=0 go build ${BUILDOPTS} -o ${BINARY} ./cmd/${MYNAME} +else # not ifeq (,$(findstring(Git/usr/bin/sh.exe, $(SHELL)))) +# # Some other shell. +# # TODO: handle it. + $(info "-- Dunno how to handle this ${SHELL}") +endif # ifeq (,$(findstring(Git/usr/bin/sh.exe, $(SHELL)))) +else # not ($(OS),Windows_NT) + CGO_ENABLED=0 go build ${BUILDOPTS} -o ${BINARY} ./cmd/${MYNAME} +endif # ifeq ($(OS),Windows_NT) + clean: - rm -rf bunny-bot +ifeq ($(OS),Windows_NT) +ifeq ($(SHELL),sh.exe) +# # Vanilla cmd.exe / powershell. + if exist ${BINARY} ${RMCMD} ${BINARY} +else # not ifeq ($(SHELL),sh.exe) + ${RMCMD} ./${BINARY} +endif # ifeq ($(SHELL),sh.exe) +else # not ifeq ($(OS),Windows_NT) + ${RMCMD} ./${BINARY} +endif + upgrade: - rm -rf vendor - go get -u -t -tool ./... +ifeq ($(OS),Windows_NT) +ifeq ($(SHELL),sh.exe) +# # Vanilla cmd.exe / powershell. + if exist vendor DEL /F /S /Q vendor >nul +else # not ifeq ($(SHELL),sh.exe) +# # git-bash + $(RM) -r vendor +endif # ifeq ($(SHELL),sh.exe) +else # not ifeq ($(OS),Windows_NT) + $(RM) -r vendor +endif + go get -d -u ./... go mod tidy go mod vendor diff --git a/cmd/bunny-bot/main.go b/cmd/bunny-bot/main.go index 9be9c53..0447867 100644 --- a/cmd/bunny-bot/main.go +++ b/cmd/bunny-bot/main.go @@ -13,6 +13,9 @@ import ( "go.uber.org/zap/zapcore" ) +// TODO: use https://dev.to/gholami1313/saving-log-messages-to-a-custom-log-file-in-golang-ce5 for +// +// chat messages. func main() { var err error @@ -32,7 +35,7 @@ func main() { } // Config for Zap logger facility. - var logCfg zap.Config = zap.NewDevelopmentConfig() + var logCfg = zap.NewDevelopmentConfig() // Define loglevel. var l zapcore.Level diff --git a/data/config-example.json b/data/config-example.json index ca945df..7c15697 100644 --- a/data/config-example.json +++ b/data/config-example.json @@ -2,7 +2,8 @@ "name": "Bunny", "token": "get_it_from_botfather", "loglevel": "debug", - "logfile": "bunny-bot.log", + "logfile": "bunny-bot.log", + "logchats": false, "csign": ".", "datadir": "data" } diff --git a/internal/moon/animation.go b/internal/moon/animation.go index 7102a0d..75cc1f8 100644 --- a/internal/moon/animation.go +++ b/internal/moon/animation.go @@ -1,16 +1,27 @@ package moon -import "github.com/NicoNex/echotron/v3" +import ( + "fmt" + + "github.com/NicoNex/echotron/v3" +) func animation(msg *echotron.Update) { - Log.Infof( - "User %s %s (username = %s, id = %d) sent animation to chat %s %s (%d)", - msg.Message.From.FirstName, - msg.Message.From.LastName, - msg.Message.From.Username, - msg.Message.From.ID, - msg.Message.Chat.Type, - msg.Message.Chat.Title, - msg.Message.Chat.ID, - ) + if Cfg.LogChats { + ChatLog( + msg.Message.Chat.ID, + fmt.Sprintf( + "User %s %s (username = %s, id = %d) sent animation to chat %s %s (%d)", + msg.Message.From.FirstName, + msg.Message.From.LastName, + msg.Message.From.Username, + msg.Message.From.ID, + msg.Message.Chat.Type, + msg.Message.Chat.Title, + msg.Message.Chat.ID, + ), + ) + } } + +/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */ diff --git a/internal/moon/casChatAPI.go b/internal/moon/casChatAPI.go index badd3f7..aeccb6c 100644 --- a/internal/moon/casChatAPI.go +++ b/internal/moon/casChatAPI.go @@ -93,3 +93,5 @@ func CasCheckID(id int64) (bool, error) { return result, fmt.Errorf("unable to parse response from api.cas.chat: %s", string(body)) //nolint: err113 } + +/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */ diff --git a/internal/moon/chatMemberUpdated.go b/internal/moon/chatMemberUpdated.go index 500b8c5..e6a7211 100644 --- a/internal/moon/chatMemberUpdated.go +++ b/internal/moon/chatMemberUpdated.go @@ -6,7 +6,6 @@ import ( "time" "github.com/NicoNex/echotron/v3" - "github.com/akyoto/cache" ) // newChatMember parses one of types ChatMemberUpdated event. @@ -25,28 +24,16 @@ func newChatMember(msg *echotron.Update) { msg.ChatMember.OldChatMember.Status, ) - // If there is no such chatid, means that no such cache too. - if len(ChatList) == 0 || !slices.Contains(ChatList, msg.Message.Chat.ID) { - Log.Debugf( - "Creating caches for new %s %s (%d)", - msg.ChatMember.Chat.Type, - msg.ChatMember.Chat.Title, - msg.ChatMember.Chat.ID, - ) + Log.Debugf( + "Add %s %s (%d) to list of known chats", + msg.ChatMember.Chat.Type, + msg.ChatMember.Chat.Title, + msg.ChatMember.Chat.ID, + ) - NewMembers[msg.Message.Chat.ID] = cache.New(1 * time.Minute) - AppearedMembers[msg.Message.Chat.ID] = cache.New(1 * time.Minute) - SquashedMembers[msg.Message.Chat.ID] = cache.New(60 * time.Minute) - - Log.Debugf( - "Add %s %s (%d) to list of known chats", - msg.ChatMember.Chat.Type, - msg.ChatMember.Chat.Title, - msg.ChatMember.Chat.ID, - ) - - ChatList = append(ChatList, msg.Message.Chat.ID) - } + ChatList = append(ChatList, msg.ChatMember.Chat.ID) + slices.Sort(ChatList) + ChatList = slices.Compact(ChatList) // Check some other attributes. if msg.ChatMember.OldChatMember.Status == "left" && msg.ChatMember.NewChatMember.Status == "member" { @@ -150,7 +137,8 @@ func newChatMember(msg *echotron.Update) { msg.ChatMember.Chat.ID, ) - AppearedMembers[msg.ChatMember.Chat.ID].Set(msg.ChatMember.NewChatMember.User.ID, time.Now().Unix(), 1*time.Minute) + key := fmt.Sprintf("%d+%d", msg.ChatMember.Chat.ID, msg.ChatMember.NewChatMember.User.ID) + AppearedMembers.Set(key, time.Now().Unix(), 1*time.Minute) } } diff --git a/internal/moon/chatlog.go b/internal/moon/chatlog.go new file mode 100644 index 0000000..fd87da0 --- /dev/null +++ b/internal/moon/chatlog.go @@ -0,0 +1,56 @@ +package moon + +import ( + "fmt" + "log" + "os" + "path/filepath" + "strconv" +) + +// ChatLog logs message to separate file, placed in basedir(Cfg.LogFile). +func ChatLog(chatid int64, text string) { + file := os.DevNull + + if Cfg.LogFile != "" { + file = fmt.Sprintf("%s/%s", filepath.Dir(Cfg.LogFile), strconv.FormatInt(chatid, 10)) + } + + var ( + isOpened = false + ) + + for _, l := range chatlogs { + if l.LogFilePath == file { + isOpened = true + + break + } + } + + if !isOpened { + // open log file + logFile, err := os.OpenFile(file, os.O_APPEND|os.O_RDWR|os.O_CREATE, 0644) + + if err != nil { + fmt.Printf("Unable to open log file %s: %s", file, err) + } + + l := Chatlog{LogFilePath: file, File: logFile} + + chatlogs = append(chatlogs, l) + } + + for _, l := range chatlogs { + if l.LogFilePath == file { + log.SetOutput(l.File) + // optional: log date-time, filename, and line number + log.SetFlags(log.Lshortfile | log.LstdFlags) + log.Println(text) + + break + } + } +} + +/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */ diff --git a/internal/moon/editedMessage.go b/internal/moon/editedMessage.go new file mode 100644 index 0000000..f29a80e --- /dev/null +++ b/internal/moon/editedMessage.go @@ -0,0 +1,45 @@ +package moon + +import ( + "fmt" + + "github.com/NicoNex/echotron/v3" +) + +// editedMessage hadles editedMessage type of update. +func editedMessage(msg *echotron.Update) { + // Edited message can contain all kinds of edited attributes that telegram allows to change in message. + // Not sure is we should handle all of them. + if Cfg.LogChats { + if msg.EditedMessage.Text != "" { + ChatLog( + msg.EditedMessage.Chat.ID, + fmt.Sprintf( + "User %s %s (username = %s, id = %d) in %s %s (%d) edited message: %s", + msg.EditedMessage.From.FirstName, + msg.EditedMessage.From.LastName, + msg.EditedMessage.From.Username, + msg.EditedMessage.From.ID, + msg.EditedMessage.Chat.Type, + msg.EditedMessage.Chat.Title, + msg.EditedMessage.Chat.ID, + msg.EditedMessage.Text, + ), + ) + } else { + ChatLog( + msg.EditedMessage.Chat.ID, + fmt.Sprintf( + "User %s %s (username = %s, id = %d) in %s %s (%d) edited message", + msg.EditedMessage.From.FirstName, + msg.EditedMessage.From.LastName, + msg.EditedMessage.From.Username, + msg.EditedMessage.From.ID, + msg.EditedMessage.Chat.Type, + msg.EditedMessage.Chat.Title, + msg.EditedMessage.Chat.ID, + ), + ) + } + } +} diff --git a/internal/moon/globals.go b/internal/moon/globals.go index 5a61e57..1228cc6 100644 --- a/internal/moon/globals.go +++ b/internal/moon/globals.go @@ -2,6 +2,7 @@ package moon import ( "os" + "time" "github.com/NicoNex/echotron/v3" "github.com/akyoto/cache" @@ -19,15 +20,17 @@ var ( DB = make(map[string]*pebble.DB) // SquashedMembers struct with banned members. - SquashedMembers map[int64]*cache.Cache + SquashedMembers = cache.New(1 * time.Minute) // NewMembers struct with new members appeared via message with new_chat_members field set. - NewMembers map[int64]*cache.Cache + NewMembers = cache.New(1 * time.Minute) // AppearedMembers struct with new members appeared via ChatMemberUpdated event. - AppearedMembers map[int64]*cache.Cache + AppearedMembers = cache.New(60 * time.Minute) ChatList = make([]int64, 0) + + chatlogs []Chatlog ) /* vim: set ft=go noet ai ts=4 sw=4 sts=4: */ diff --git a/internal/moon/leftChatMember.go b/internal/moon/leftChatMember.go index 6179549..0ecafa9 100644 --- a/internal/moon/leftChatMember.go +++ b/internal/moon/leftChatMember.go @@ -19,6 +19,8 @@ func leftChatMember(msg *echotron.Update) { msg.Message.Chat.ID, ) + // Save people that left chat to pebbledb. To preserve their state. + // Left folks can eventually return and in chat_mamber_updated event their state will be left->member. if err := Cfg.SaveKeyValue( "LeftMembers", fmt.Sprintf("%d", msg.Message.Chat.ID), diff --git a/internal/moon/newChatMembers.go b/internal/moon/newChatMembers.go index 57553e3..26bc09d 100644 --- a/internal/moon/newChatMembers.go +++ b/internal/moon/newChatMembers.go @@ -1,15 +1,27 @@ package moon import ( + "fmt" "slices" "time" "github.com/NicoNex/echotron/v3" - cache "github.com/akyoto/cache" ) // newChatMembers parses NewChatMember event. func newChatMembers(msg *echotron.Update) { + // If there is no such chatid, means that no such cache too. + Log.Debugf( + "Add %s %s (%d) to list of known chats", + msg.Message.Chat.Type, + msg.Message.Chat.Title, + msg.Message.Chat.ID, + ) + + ChatList = append(ChatList, msg.Message.Chat.ID) + slices.Sort(ChatList) + ChatList = slices.Compact(ChatList) + for _, user := range msg.Message.NewChatMembers { Log.Infof( "User %s %s (username = %s, id = %d) joined %s %s (%d)", @@ -22,29 +34,6 @@ func newChatMembers(msg *echotron.Update) { msg.Message.Chat.ID, ) - // If there is no such chatid, means that no such cache too. - if !slices.Contains(ChatList, msg.Message.Chat.ID) { - Log.Debugf( - "Creating caches for new %s %s (%d)", - msg.Message.Chat.Type, - msg.Message.Chat.Title, - msg.Message.Chat.ID, - ) - - NewMembers[msg.Message.Chat.ID] = cache.New(1 * time.Minute) - AppearedMembers[msg.Message.Chat.ID] = cache.New(1 * time.Minute) - SquashedMembers[msg.Message.Chat.ID] = cache.New(60 * time.Minute) - - Log.Debugf( - "Add %s %s (%d) to list of known chats", - msg.Message.Chat.Type, - msg.Message.Chat.Title, - msg.Message.Chat.ID, - ) - - ChatList = append(ChatList, msg.Message.Chat.ID) - } - casBanned, err := CasCheckID(user.ID) if err != nil { @@ -99,7 +88,8 @@ func newChatMembers(msg *echotron.Update) { msg.Message.Chat.ID, ) - NewMembers[msg.Message.Chat.ID].Set(user.ID, time.Now().Unix(), 1*time.Minute) + key := fmt.Sprintf("%d+%d", msg.Message.Chat.ID, user.ID) + NewMembers.Set(key, time.Now().Unix(), 1*time.Minute) } } diff --git a/internal/moon/photo.go b/internal/moon/photo.go new file mode 100644 index 0000000..60e7db5 --- /dev/null +++ b/internal/moon/photo.go @@ -0,0 +1,46 @@ +package moon + +import ( + "fmt" + + "github.com/NicoNex/echotron/v3" +) + +func photo(msg *echotron.Update) { + if Cfg.LogChats { + if msg.Message.Text != "" { + ChatLog( + msg.Message.Chat.ID, + fmt.Sprintf( + "User %s %s (username = %s, id = %d) sent %d photos to %s %s (%d) with text: %s", + msg.Message.From.FirstName, + msg.Message.From.LastName, + msg.Message.From.Username, + msg.Message.From.ID, + len(msg.Message.Photo), + msg.Message.Chat.Type, + msg.Message.Chat.Title, + msg.Message.Chat.ID, + msg.Message.Text, + ), + ) + } else { + ChatLog( + msg.Message.Chat.ID, + fmt.Sprintf( + "User %s %s (username = %s, id = %d) sent %d photos to %s %s (%d)", + msg.Message.From.FirstName, + msg.Message.From.LastName, + msg.Message.From.Username, + msg.Message.From.ID, + len(msg.Message.Photo), + msg.Message.Chat.Type, + msg.Message.Chat.Title, + msg.Message.Chat.ID, + ), + ) + } + } +} + +/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */ diff --git a/internal/moon/reaction.go b/internal/moon/reaction.go new file mode 100644 index 0000000..ee42c69 --- /dev/null +++ b/internal/moon/reaction.go @@ -0,0 +1,65 @@ +package moon + +import ( + "fmt" + + "github.com/NicoNex/echotron/v3" +) + +func reaction(msg *echotron.Update) { + if Cfg.LogChats { + var reactionString string + + for _, item := range msg.MessageReaction.NewReaction { + switch item.Type { + case "Emoji": + reactionString += item.Emoji + case "CustomEmoji": + reactionString += item.CustomEmoji + } + } + + if reactionString != "" { + ChatLog( + msg.MessageReaction.Chat.ID, + fmt.Sprintf( + "User %s %s (username = %s, id = %d) set reaction %s to chat %s %s (%d)", + msg.MessageReaction.User.FirstName, + msg.MessageReaction.User.LastName, + msg.MessageReaction.User.Username, + msg.MessageReaction.User.ID, + reactionString, + msg.MessageReaction.Chat.Type, + msg.MessageReaction.Chat.Title, + msg.MessageReaction.Chat.ID, + ), + ) + } else { + for _, item := range msg.MessageReaction.OldReaction { + switch item.Type { + case "Emoji": + reactionString += item.Emoji + case "CustomEmoji": + reactionString += item.CustomEmoji + } + } + + ChatLog( + msg.MessageReaction.Chat.ID, + fmt.Sprintf( + "User %s %s (username = %s, id = %d) removed reaction %s to chat %s %s (%d)", + msg.MessageReaction.User.FirstName, + msg.MessageReaction.User.LastName, + msg.MessageReaction.User.Username, + msg.MessageReaction.User.ID, + reactionString, + msg.MessageReaction.Chat.Type, + msg.MessageReaction.Chat.Title, + msg.MessageReaction.Chat.ID, + ), + ) + } + } +} + +/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */ diff --git a/internal/moon/reactionCount.go b/internal/moon/reactionCount.go new file mode 100644 index 0000000..200ead4 --- /dev/null +++ b/internal/moon/reactionCount.go @@ -0,0 +1,23 @@ +package moon + +import ( + "fmt" + + "github.com/NicoNex/echotron/v3" + "github.com/davecgh/go-spew/spew" +) + +func reactionCount(msg *echotron.Update) { + ChatLog( + msg.MessageReaction.Chat.ID, + fmt.Sprintf( + "Reaction count event in %s %s (%d): %s", + msg.MessageReaction.Chat.Type, + msg.MessageReaction.Chat.Title, + msg.MessageReaction.Chat.ID, + spew.Sdump(msg), + ), + ) +} + +/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */ diff --git a/internal/moon/runAndBan.go b/internal/moon/runAndBan.go index 4629ffe..fb99495 100644 --- a/internal/moon/runAndBan.go +++ b/internal/moon/runAndBan.go @@ -1,163 +1,156 @@ package moon import ( + "slices" + "strconv" + "strings" "time" ) func RunAndBan() { for { - // If user have both events, he is not spammer. So wipe him from unchecked events list, just for sure. - for _, chatid := range ChatList { - Log.Infof("RunAndBan by chatid: %d", chatid) + var ListAppearedMembers, ListNewMembers []string - var ListAppearedMembers, ListNewMembers []int64 + AppearedMembers.Range( + func(k, _ any) bool { + ListAppearedMembers = append(ListAppearedMembers, k.(string)) - AppearedMembers[chatid].Range( - func(k, _ any) bool { - ListAppearedMembers = append(ListAppearedMembers, k.(int64)) + return true + }, + ) - return true - }, - ) + NewMembers.Range( + func(k, _ any) bool { + ListNewMembers = append(ListNewMembers, k.(string)) - NewMembers[chatid].Range( - func(k, _ any) bool { - ListNewMembers = append(ListNewMembers, k.(int64)) + return true + }, + ) - return true - }, - ) - - for _, AppearedMember := range ListAppearedMembers { - for _, NewMember := range ListNewMembers { - if AppearedMember == NewMember { - // TODO: Store also additional attributes in temporary database. - Log.Infof( - "Skip banning user %s %s (username = %s, id = %d) in %s %s (%d), because it has both events newMember and updateMember", - "", - "", - "", - NewMember, - "", - "", - chatid, - ) - - Log.Infof( - "Deleting user %s %s (username = %s, id = %d) in %s %s (%d), from AppearedMember DB", - "", - "", - "", - NewMember, - "", - "", - chatid, - ) - - AppearedMembers[chatid].Delete(AppearedMember) - - Log.Infof( - "Deleting user %s %s (username = %s, id = %d) in %s %s (%d), from NewMember DB", - "", - "", - "", - NewMember, - "", - "", - chatid, - ) - - NewMembers[chatid].Delete(NewMember) - } + for _, AppearedMember := range ListAppearedMembers { + for _, NewMember := range ListNewMembers { + if AppearedMember == NewMember { + AppearedMembers.Delete(AppearedMember) + NewMembers.Delete(NewMember) } } } currentTime := time.Now().Unix() - // We would like to process only events that definitely older than 10 seconds! - for _, chatid := range ChatList { - var ( - ListAppearedMembers, ListNewMembers []int64 - ) + ListAppearedMembers = ListAppearedMembers[:0] + ListNewMembers = ListNewMembers[:0] + ListAppearedMembers = slices.Clip(ListAppearedMembers) + ListNewMembers = slices.Clip(ListNewMembers) - AppearedMembers[chatid].Range( - func(k, v interface{}) bool { - ListAppearedMembers = append(ListAppearedMembers, k.(int64)) + AppearedMembers.Range( + func(k, _ any) bool { + ListAppearedMembers = append(ListAppearedMembers, k.(string)) - return true - }, - ) + return true + }, + ) - NewMembers[chatid].Range( - func(k, v interface{}) bool { - ListNewMembers = append(ListNewMembers, k.(int64)) + NewMembers.Range( + func(k, _ any) bool { + ListNewMembers = append(ListNewMembers, k.(string)) - return true - }, - ) + return true + }, + ) - for _, AppearedMember := range ListAppearedMembers { - eventTimestamp, found := AppearedMembers[chatid].Get(AppearedMember) + for _, AppearedMember := range ListAppearedMembers { + eventTimestamp, found := AppearedMembers.Get(AppearedMember) - if !found { - continue - } - - eventTimeDelta := currentTime - eventTimestamp.(int64) - - // Looks like it is spammer! - if eventTimeDelta > 10 { - // TODO: Store also additional attributes in temporary database. - Log.Infof( - "Ban user %s %s (username = %s, id = %d) in chat %s %s (%d) that have no newChatMember message since chatMemberUpdated for more than 10 seconds", - "", - "", - "", - AppearedMember, - "", - "", - chatid, - ) - - squash( - chatid, - "", - "", - AppearedMember, - "", - "", - "", - ) - - AppearedMembers[chatid].Delete(AppearedMember) - } + if !found { + continue } - for _, NewMember := range ListNewMembers { - eventTimestamp, found := NewMembers[chatid].Get(NewMember) + eventTimeDelta := currentTime - eventTimestamp.(int64) + + // Spammer? + if eventTimeDelta > 10 { + item := strings.Split(AppearedMember, "+") + chatid, err := strconv.ParseInt(item[0], 10, 64) + + if err != nil { + Log.Errorf("Got broken key in AppearedMembers cache: %s", AppearedMember) - if !found { continue } - eventTimeDelta := currentTime - eventTimestamp.(int64) + userid, err := strconv.ParseInt(item[1], 10, 64) - // Totally new members should not have chatMemberUpdated events. - if eventTimeDelta > 15 { - // TODO: Store also additional attributes in temporary database. - Log.Debugf("Clean newChatMember event record older than 10 seconds for user user %s %s (username = %s, id = %d) in chat %s %s (%d)", - "", - "", - "", - NewMember, - "", - "", - chatid, - ) + if err != nil { + Log.Errorf("Got broken key in AppearedMembers cache: %s", AppearedMember) - NewMembers[chatid].Delete(NewMember) + continue } + + Log.Infof( + "Ban user %s %s (username = %s, id = %d) in chat %s %s (%d) that have no newChatMember message since chatMemberUpdated for more than 10 seconds", + "", + "", + "", + userid, + "", + "", + chatid, + ) + + squash( + chatid, + "", + "", + userid, + "", + "", + "", + ) + + AppearedMembers.Delete(AppearedMember) + } + } + + for _, NewMember := range ListNewMembers { + eventTimestamp, found := NewMembers.Get(NewMember) + + if !found { + continue + } + + eventTimeDelta := currentTime - eventTimestamp.(int64) + + // Spammer? + if eventTimeDelta > 10 { + item := strings.Split(NewMember, "+") + chatid, err := strconv.ParseInt(item[0], 10, 64) + + if err != nil { + Log.Errorf("Got broken key in AppearedMembers cache: %s", NewMember) + + continue + } + + userid, err := strconv.ParseInt(item[1], 10, 64) + + if err != nil { + Log.Errorf("Got broken key in AppearedMembers cache: %s", NewMember) + + continue + } + + Log.Debugf("Clean newChatMember event record older than 10 seconds for user user %s %s (username = %s, id = %d) in chat %s %s (%d)", + "", + "", + "", + userid, + "", + "", + chatid, + ) + + NewMembers.Delete(NewMember) } } diff --git a/internal/moon/squash.go b/internal/moon/squash.go index 4fbe0fb..789104f 100644 --- a/internal/moon/squash.go +++ b/internal/moon/squash.go @@ -1,11 +1,11 @@ package moon import ( + "fmt" "slices" "time" "github.com/NicoNex/echotron/v3" - "github.com/akyoto/cache" ) // TODO: refactor to return bool, error @@ -16,18 +16,12 @@ func squash(chatid int64, chattype, chattitle string, userid int64, firstname, l // If there is no such chatid, means that no such cache too. if len(ChatList) == 0 || !slices.Contains(ChatList, chatid) { - Log.Debugf("Creating caches for new %s %s (%d)", chattype, chattitle, chatid) - - NewMembers[chatid] = cache.New(1 * time.Minute) - AppearedMembers[chatid] = cache.New(1 * time.Minute) - SquashedMembers[chatid] = cache.New(5 * time.Minute) - Log.Debugf("Add %s %s (%d) to list of known chats", chattype, chattitle, chatid) ChatList = append(ChatList, chatid) } - if found, exist := SquashedMembers[chatid].Get(userid); exist && found.(bool) { + if _, exist := SquashedMembers.Get(userid); exist { Log.Infof( "Skip banning user %s %s (username = %s, id = %d) in %s %s (%d), already banned", firstname, @@ -85,9 +79,12 @@ func squash(chatid int64, chattype, chattitle string, userid int64, firstname, l chatid, ) - SquashedMembers[chatid].Set(userid, true, 30*time.Second) - NewMembers[chatid].Delete(userid) - AppearedMembers[chatid].Delete(userid) + key := fmt.Sprintf("%d+%d", chatid, userid) + SquashedMembers.Set(key, true, 30*time.Second) + NewMembers.Delete(key) + AppearedMembers.Delete(key) } } } + +/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */ diff --git a/internal/moon/telegram.go b/internal/moon/telegram.go index aecfa73..8fa9639 100644 --- a/internal/moon/telegram.go +++ b/internal/moon/telegram.go @@ -43,6 +43,12 @@ func telegramMsgParser(msg *echotron.Update) { return } + if msg.EditedMessage != nil && msg.EditedMessage.Text != "" { + editedMessage(msg) + + return + } + // ChatMember updated event. It goes to chatMemberUpdated.go. if msg.ChatMember != nil { if msg.ChatMember.NewChatMember.User != nil { @@ -72,6 +78,24 @@ func telegramMsgParser(msg *echotron.Update) { return } + if msg.MessageReaction != nil { + reaction(msg) + + return + } + + if msg.MessageReactionCount != nil { + reactionCount(msg) + + return + } + + if msg.Message != nil && msg.Message.Photo != nil { + photo(msg) + + return + } + Log.Debug(spew.Sdump(msg)) } diff --git a/internal/moon/textMessage.go b/internal/moon/textMessage.go index caa68ef..99729cf 100644 --- a/internal/moon/textMessage.go +++ b/internal/moon/textMessage.go @@ -2,61 +2,53 @@ package moon import ( "fmt" - "time" "github.com/NicoNex/echotron/v3" ) +// textMessage handles text message events. func textMessage(msg *echotron.Update) { - if msg.Message.ReplyToMessage == nil { + // Not sure if we must handle all kinds of these events. + if Cfg.LogChats { + switch { // Just log it, it seems later here should be ban/mute command set. - Log.Infof( - "User %s %s (username = %s, id = %d) in %s %s (%d) says: %s", - msg.Message.From.FirstName, - msg.Message.From.LastName, - msg.Message.From.Username, - msg.Message.From.ID, - msg.Message.Chat.Type, - msg.Message.Chat.Title, - msg.Message.Chat.ID, - msg.Message.Text, - ) - } else { - // Just log it. - Log.Infof( - "User %s %s (username = %s, id = %d) in %s %s (%d) replies to message %d by %s %s (username = %s, id = %d): %s", - msg.Message.From.FirstName, - msg.Message.From.LastName, - msg.Message.From.Username, - msg.Message.From.ID, - msg.Message.Chat.Type, - msg.Message.Chat.Title, - msg.Message.Chat.ID, - msg.Message.ReplyToMessage.ID, - msg.Message.ReplyToMessage.From.FirstName, - msg.Message.ReplyToMessage.From.LastName, - msg.Message.ReplyToMessage.From.Username, - msg.Message.ReplyToMessage.From.ID, - msg.Message.Text, - ) - } + case msg.Message != nil && msg.Message.ReplyToMessage == nil: + ChatLog( + msg.Message.Chat.ID, + fmt.Sprintf( + "User %s %s (username = %s, id = %d) in %s %s (%d) says: %s", + msg.Message.From.FirstName, + msg.Message.From.LastName, + msg.Message.From.Username, + msg.Message.From.ID, + msg.Message.Chat.Type, + msg.Message.Chat.Title, + msg.Message.Chat.ID, + msg.Message.Text, + ), + ) + + case msg.Message != nil && msg.Message.ReplyToMessage != nil: + ChatLog( + msg.Message.Chat.ID, + fmt.Sprintf( + "User %s %s (username = %s, id = %d) in %s %s (%d) replies to message %d by %s %s (username = %s, id = %d): %s", + msg.Message.From.FirstName, + msg.Message.From.LastName, + msg.Message.From.Username, + msg.Message.From.ID, + msg.Message.Chat.Type, + msg.Message.Chat.Title, + msg.Message.Chat.ID, + msg.Message.ReplyToMessage.ID, + msg.Message.ReplyToMessage.From.FirstName, + msg.Message.ReplyToMessage.From.LastName, + msg.Message.ReplyToMessage.From.Username, + msg.Message.ReplyToMessage.From.ID, + msg.Message.Text, + ), + ) - // Update database of new members :) - // Try to get timestamp when this member joined to chat - if joinTimestampString := Cfg.GetValue( - "NewMembers", - fmt.Sprintf("%d", msg.Message.Chat.ID), - fmt.Sprintf("%d", msg.Message.From.ID), - ); joinTimestampString == "" { - if err := Cfg.SaveKeyValue( - "NewMembers", - fmt.Sprintf("%d", msg.Message.Chat.ID), - fmt.Sprintf("%d", msg.Message.From.ID), - fmt.Sprintf("%d", time.Now().Unix()), - ); err != nil { - Log.Error(err) - } else { - Log.Infof("Info about new userid %d saved to new users db", msg.Message.From.ID) } } } diff --git a/internal/moon/types.go b/internal/moon/types.go index a85f6ec..96a542e 100644 --- a/internal/moon/types.go +++ b/internal/moon/types.go @@ -1,6 +1,7 @@ package moon import ( + "os" "time" ) @@ -10,6 +11,7 @@ type MyConfig struct { Token string `json:"token,omitempty"` LogLevel string `json:"loglevel,omitempty"` LogFile string `json:"logfile,omitempty"` + LogChats bool `json:"logchats,omitempty"` CSign string `json:"csign,omitempty"` DataDir string `json:"datadir,omitempty"` } @@ -30,4 +32,9 @@ type CasTrue struct { } `json:"result"` } +type Chatlog struct { + LogFilePath string + File *os.File +} + /* vim: set ft=go noet ai ts=4 sw=4 sts=4: */