Initial commit
This commit is contained in:
27
internal/moon/animation.go
Normal file
27
internal/moon/animation.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package moon
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/NicoNex/echotron/v3"
|
||||
)
|
||||
|
||||
func animation(msg *echotron.Update) {
|
||||
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: */
|
||||
100
internal/moon/casChatAPI.go
Normal file
100
internal/moon/casChatAPI.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package moon
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/hjson/hjson-go"
|
||||
)
|
||||
|
||||
// CasCheckID check given telegram user id vs CAS spammers base.
|
||||
// https://api.cas.chat/check?user_id=821871410
|
||||
func CasCheckID(id int64) (bool, error) {
|
||||
var (
|
||||
result bool
|
||||
err error
|
||||
url = fmt.Sprintf("https://api.cas.chat/check?user_id=%d", id)
|
||||
c = &http.Client{
|
||||
Timeout: 15 * time.Second,
|
||||
}
|
||||
banned CasTrue
|
||||
notBanned CasFalse
|
||||
)
|
||||
|
||||
resp, err := c.Get(url) //nolint: bodyclose, noctx
|
||||
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("unable to query api.cas.chat, %w", err)
|
||||
}
|
||||
|
||||
// Looks like we should clse body as it it recommends in docs: https://pkg.go.dev/net/http .
|
||||
defer func(Body io.ReadCloser) {
|
||||
err := Body.Close()
|
||||
|
||||
if err != nil {
|
||||
Log.Errorf("Unable to close response body for request to %s: %s", url, err)
|
||||
}
|
||||
}(resp.Body)
|
||||
|
||||
// Client should not return with http status code other than 200.
|
||||
if resp.StatusCode > 200 {
|
||||
return result, fmt.Errorf( //nolint: err113
|
||||
"api.cas.chat returns non-200 http status code: %d %s",
|
||||
resp.StatusCode, resp.Status)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("unable to read response from api.cas.chat, %w", err)
|
||||
}
|
||||
|
||||
// looks like go native json decoder is pretty shitty, so we'll take hjson as less restrictive parser, parse json
|
||||
// encode it back to struct, then parse struct as encode/json
|
||||
|
||||
var tmp map[string]any
|
||||
err = hjson.Unmarshal(body, &tmp)
|
||||
|
||||
// Не удалось распарсить.
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("unable to parse response from api.cas.chat via hjson: %w", err)
|
||||
}
|
||||
|
||||
tmpjson, err := json.Marshal(tmp)
|
||||
|
||||
// Не удалось преобразовать map-ку в json.
|
||||
if err != nil {
|
||||
return result, fmt.Errorf(
|
||||
"unable to parse back to json response from api.cas.chat parsed by hjson: %w", err)
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(tmpjson, ¬Banned); err == nil {
|
||||
// if ok field in json == false, then user is not in ban. (records about id not found)
|
||||
if !notBanned.Ok {
|
||||
return false, err
|
||||
}
|
||||
} // At this point we just ignore err != nil, because it *can be* other json type.
|
||||
|
||||
if err := json.Unmarshal(body, &banned); err == nil {
|
||||
// if ok field in json == true, then user is banned. (records about id are found)
|
||||
if banned.Ok {
|
||||
Log.Debugf(
|
||||
"api.cas.chat reported user with id=%d as banned: offences=%d ban_time=%s",
|
||||
id,
|
||||
banned.Result.Offenses,
|
||||
banned.Result.TimeAdded,
|
||||
)
|
||||
|
||||
return true, err
|
||||
}
|
||||
|
||||
return result, fmt.Errorf("api.cas.chat returns strange structure: %s", string(body)) //nolint: err113
|
||||
} // At this point we can't ignore err != nil because we do not know (yet?) other forms of json reply from cas api.
|
||||
|
||||
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: */
|
||||
208
internal/moon/chatMemberUpdated.go
Normal file
208
internal/moon/chatMemberUpdated.go
Normal file
@@ -0,0 +1,208 @@
|
||||
package moon
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/NicoNex/echotron/v3"
|
||||
)
|
||||
|
||||
// newChatMember parses one of types ChatMemberUpdated event.
|
||||
// https://core.telegram.org/bots/api#chatmemberupdated
|
||||
func chatMemberUpdated(msg *echotron.Update) {
|
||||
AddChat(&msg.ChatMember.Chat)
|
||||
AddUser(msg.ChatMember.NewChatMember.User)
|
||||
|
||||
Log.Infof(
|
||||
"User %s %s (username = %s, id = %d) in chat %s %s (%d) now %s was %s",
|
||||
msg.ChatMember.NewChatMember.User.FirstName,
|
||||
msg.ChatMember.NewChatMember.User.LastName,
|
||||
msg.ChatMember.NewChatMember.User.Username,
|
||||
msg.ChatMember.NewChatMember.User.ID,
|
||||
msg.ChatMember.Chat.Type,
|
||||
msg.ChatMember.Chat.Title,
|
||||
msg.ChatMember.Chat.ID,
|
||||
msg.ChatMember.NewChatMember.Status,
|
||||
msg.ChatMember.OldChatMember.Status,
|
||||
)
|
||||
|
||||
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.ChatMember.Chat.ID)
|
||||
slices.Sort(ChatList)
|
||||
ChatList = slices.Compact(ChatList)
|
||||
|
||||
// Check some other attributes.
|
||||
switch {
|
||||
case msg.ChatMember.OldChatMember.Status == "left" && msg.ChatMember.NewChatMember.Status == "member":
|
||||
Log.Infof(
|
||||
"Getting info about user %s %s (username = %s, id = %d) in chat %s %s (%d) from LeftMembers DB",
|
||||
msg.ChatMember.NewChatMember.User.FirstName,
|
||||
msg.ChatMember.NewChatMember.User.LastName,
|
||||
msg.ChatMember.NewChatMember.User.Username,
|
||||
msg.ChatMember.NewChatMember.User.ID,
|
||||
msg.ChatMember.Chat.Type,
|
||||
msg.ChatMember.Chat.Title,
|
||||
msg.ChatMember.Chat.ID,
|
||||
)
|
||||
|
||||
leftTimestampString := Cfg.GetValue(
|
||||
"LeftMembers",
|
||||
fmt.Sprintf("%d", msg.ChatMember.Chat.ID),
|
||||
fmt.Sprintf("%d", msg.ChatMember.NewChatMember.User.ID),
|
||||
)
|
||||
|
||||
// Yes, this member definitely left chat at some point, we have no question to him/her.
|
||||
if leftTimestampString != "" {
|
||||
Log.Infof(
|
||||
"User %s %s (username = %s, id = %d) in chat %s %s (%d) found in from LeftMembers DB",
|
||||
msg.ChatMember.NewChatMember.User.FirstName,
|
||||
msg.ChatMember.NewChatMember.User.LastName,
|
||||
msg.ChatMember.NewChatMember.User.Username,
|
||||
msg.ChatMember.NewChatMember.User.ID,
|
||||
msg.ChatMember.Chat.Type,
|
||||
msg.ChatMember.Chat.Title,
|
||||
msg.ChatMember.Chat.ID,
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
Log.Infof(
|
||||
"User %s %s (username = %s, id = %d) in chat %s %s (%d) not found in LeftMembers DB",
|
||||
msg.ChatMember.NewChatMember.User.FirstName,
|
||||
msg.ChatMember.NewChatMember.User.LastName,
|
||||
msg.ChatMember.NewChatMember.User.Username,
|
||||
msg.ChatMember.NewChatMember.User.ID,
|
||||
msg.ChatMember.Chat.Type,
|
||||
msg.ChatMember.Chat.Title,
|
||||
msg.ChatMember.Chat.ID,
|
||||
)
|
||||
|
||||
// Spam bots come with these attributes.
|
||||
// msg.ChatMember.NewChatMember.Status = "member"
|
||||
// msg.ChatMember.OldChatMember.User != nil
|
||||
// msg.ChatMember.OldChatMember.Status = "left"
|
||||
|
||||
casBanned, err := CasCheckID(msg.ChatMember.NewChatMember.User.ID)
|
||||
|
||||
if err != nil {
|
||||
Log.Errorf(
|
||||
"Unable to check user %s %s (username = %s, id = %d) in chat %s %s (%d): %s",
|
||||
msg.ChatMember.NewChatMember.User.FirstName,
|
||||
msg.ChatMember.NewChatMember.User.LastName,
|
||||
msg.ChatMember.NewChatMember.User.Username,
|
||||
msg.ChatMember.NewChatMember.User.ID,
|
||||
msg.ChatMember.Chat.Type,
|
||||
msg.ChatMember.Chat.Title,
|
||||
msg.ChatMember.Chat.ID,
|
||||
err,
|
||||
)
|
||||
} else if casBanned {
|
||||
// Ban user if cas report it as banned.
|
||||
Log.Infof(
|
||||
"Ban user %s %s (username = %s, id = %d) in chat %s %s (%d) by CAS blacklist",
|
||||
msg.ChatMember.NewChatMember.User.FirstName,
|
||||
msg.ChatMember.NewChatMember.User.LastName,
|
||||
msg.ChatMember.NewChatMember.User.Username,
|
||||
msg.ChatMember.NewChatMember.User.ID,
|
||||
msg.ChatMember.Chat.Type,
|
||||
msg.ChatMember.Chat.Title,
|
||||
msg.ChatMember.Chat.ID,
|
||||
)
|
||||
|
||||
squash(
|
||||
msg.ChatMember.Chat.ID,
|
||||
msg.ChatMember.NewChatMember.User.ID,
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
Log.Infof(
|
||||
"Storing info about user %s %s (username = %s, id = %d) in chat %s %s (%d) to AppearedMember DB",
|
||||
msg.ChatMember.NewChatMember.User.FirstName,
|
||||
msg.ChatMember.NewChatMember.User.LastName,
|
||||
msg.ChatMember.NewChatMember.User.Username,
|
||||
msg.ChatMember.NewChatMember.User.ID,
|
||||
msg.ChatMember.Chat.Type,
|
||||
msg.ChatMember.Chat.Title,
|
||||
msg.ChatMember.Chat.ID,
|
||||
)
|
||||
|
||||
key := fmt.Sprintf("%d+%d", msg.ChatMember.Chat.ID, msg.ChatMember.NewChatMember.User.ID)
|
||||
AppearedMembers.Set(key, time.Now().Unix(), 1*time.Minute)
|
||||
PendingCASMembers.Set(key, time.Now().Unix(), 30*time.Minute)
|
||||
|
||||
case msg.ChatMember.OldChatMember.Status == "member" && msg.ChatMember.NewChatMember.Status == "kicked":
|
||||
Log.Infof(
|
||||
"User %s %s (username = %s, id = %d) in chat %s %s (%d) was banned",
|
||||
msg.ChatMember.NewChatMember.User.FirstName,
|
||||
msg.ChatMember.NewChatMember.User.LastName,
|
||||
msg.ChatMember.NewChatMember.User.Username,
|
||||
msg.ChatMember.NewChatMember.User.ID,
|
||||
msg.ChatMember.Chat.Type,
|
||||
msg.ChatMember.Chat.Title,
|
||||
msg.ChatMember.Chat.ID,
|
||||
)
|
||||
|
||||
key := fmt.Sprintf("%d+%d", msg.ChatMember.Chat.ID, msg.ChatMember.NewChatMember.User.ID)
|
||||
|
||||
Log.Debugf(
|
||||
"Put user %s %s (username = %s, id = %d) in %s %s (%d) into SquashedMembers to debounce ban.",
|
||||
msg.ChatMember.NewChatMember.User.FirstName,
|
||||
msg.ChatMember.NewChatMember.User.LastName,
|
||||
msg.ChatMember.NewChatMember.User.Username,
|
||||
msg.ChatMember.NewChatMember.User.ID,
|
||||
msg.ChatMember.Chat.Type,
|
||||
msg.ChatMember.Chat.Title,
|
||||
msg.ChatMember.Chat.ID,
|
||||
)
|
||||
|
||||
SquashedMembers.Set(key, true, 30*time.Second)
|
||||
|
||||
Log.Debugf(
|
||||
"Storing info about user %s %s (username = %s, id = %d) in chat %s %s (%d) to BannedMembers DB",
|
||||
msg.ChatMember.NewChatMember.User.FirstName,
|
||||
msg.ChatMember.NewChatMember.User.LastName,
|
||||
msg.ChatMember.NewChatMember.User.Username,
|
||||
msg.ChatMember.NewChatMember.User.ID,
|
||||
msg.ChatMember.Chat.Type,
|
||||
msg.ChatMember.Chat.Title,
|
||||
msg.ChatMember.Chat.ID,
|
||||
)
|
||||
|
||||
if err := Cfg.SaveKeyValue(
|
||||
"BannedMembers",
|
||||
fmt.Sprintf("%d", msg.ChatMember.Chat.ID),
|
||||
fmt.Sprintf("%d", msg.ChatMember.NewChatMember.User.ID),
|
||||
fmt.Sprintf("%d", time.Now().Unix()),
|
||||
); err != nil {
|
||||
Log.Error(err)
|
||||
} else {
|
||||
Log.Infof("Info about banned userid %d saved to BannedMembers DB", msg.ChatMember.Chat.ID)
|
||||
}
|
||||
|
||||
default:
|
||||
Log.Infof(
|
||||
"User %s %s (username = %s, id = %d) in chat %s %s (%d) now %s was %s",
|
||||
msg.ChatMember.NewChatMember.User.FirstName,
|
||||
msg.ChatMember.NewChatMember.User.LastName,
|
||||
msg.ChatMember.NewChatMember.User.Username,
|
||||
msg.ChatMember.NewChatMember.User.ID,
|
||||
msg.ChatMember.Chat.Type,
|
||||
msg.ChatMember.Chat.Title,
|
||||
msg.ChatMember.Chat.ID,
|
||||
msg.ChatMember.NewChatMember.Status,
|
||||
msg.ChatMember.OldChatMember.Status,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||
49
internal/moon/chatlog.go
Normal file
49
internal/moon/chatlog.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package moon
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// ChatLog logs message to separate file, placed in basedir(Cfg.LogFile).
|
||||
func ChatLog(chatid int64, message string) {
|
||||
var (
|
||||
filename string
|
||||
)
|
||||
|
||||
if Cfg.LogFile != "" {
|
||||
filename = fmt.Sprintf("%s/%s", filepath.Dir(Cfg.LogFile), strconv.FormatInt(chatid, 10))
|
||||
} else {
|
||||
filename = os.DevNull
|
||||
}
|
||||
|
||||
f, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644)
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("Unable to open file %s for logging: %s", filename, err)
|
||||
|
||||
// TODO: Should we try harder?
|
||||
return
|
||||
}
|
||||
|
||||
defer f.Close()
|
||||
|
||||
log.Logger = log.Output(
|
||||
zerolog.ConsoleWriter{
|
||||
Out: f,
|
||||
NoColor: true,
|
||||
TimeFormat: time.DateTime,
|
||||
PartsOrder: []string{"time", "message"},
|
||||
},
|
||||
)
|
||||
|
||||
log.Info().Msg(message)
|
||||
}
|
||||
|
||||
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||
100
internal/moon/config.go
Normal file
100
internal/moon/config.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package moon
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/hjson/hjson-go"
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
// parseConfig разбирает и валидирует даденный конфиг.
|
||||
func ParseConfig(path string) (MyConfig, error) {
|
||||
fileInfo, err := os.Stat(path)
|
||||
|
||||
// Assume that no such file or directory error occurs.
|
||||
if err != nil {
|
||||
return MyConfig{}, fmt.Errorf("unable to find config: %w", err)
|
||||
}
|
||||
|
||||
// Config file looks too long for config.
|
||||
if fileInfo.Size() > 65535 {
|
||||
err := fmt.Errorf("config file %s is too long for config, skipping", path) //nolint: err113
|
||||
|
||||
return MyConfig{}, err
|
||||
}
|
||||
|
||||
buf, err := os.ReadFile(path)
|
||||
|
||||
// Noluck reading file.
|
||||
if err != nil {
|
||||
return MyConfig{}, err
|
||||
}
|
||||
|
||||
/*
|
||||
* According to docs hjson kinda can patse quirky json, but unmarshalls it to map. *
|
||||
* Struct on output look more interesting: so we unmarshall config to map, then mashall back to json and then *
|
||||
* unmarshall it via ecodinf/json to struct. Not very effective way but we do this not too often. *
|
||||
*/
|
||||
var (
|
||||
sampleConfig MyConfig
|
||||
tmp map[string]any
|
||||
)
|
||||
|
||||
err = hjson.Unmarshal(buf, &tmp)
|
||||
|
||||
// Не удалось распарсить.
|
||||
if err != nil {
|
||||
return MyConfig{}, err
|
||||
}
|
||||
|
||||
tmpjson, err := json.Marshal(tmp)
|
||||
|
||||
// Не удалось преобразовать map-ку в json.
|
||||
if err != nil {
|
||||
return MyConfig{}, err
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(tmpjson, &sampleConfig); err != nil {
|
||||
return MyConfig{}, err
|
||||
}
|
||||
|
||||
if sampleConfig.Token == "" {
|
||||
return MyConfig{}, fmt.Errorf("token field in config file %s must be set", path) //nolint: err113
|
||||
}
|
||||
|
||||
if _, err := zapcore.ParseLevel(sampleConfig.LogLevel); err != nil {
|
||||
sampleConfig.LogLevel = "info"
|
||||
}
|
||||
|
||||
// sampleConfig.Log = "" if not set
|
||||
|
||||
if sampleConfig.CSign == "" {
|
||||
err := fmt.Errorf("csign field in config file %s must be set", path) //nolint: err113
|
||||
|
||||
return MyConfig{}, err
|
||||
}
|
||||
|
||||
if sampleConfig.DataDir == "" {
|
||||
return MyConfig{}, fmt.Errorf("data_dir field in config file %s must be set", path) //nolint: err113
|
||||
}
|
||||
|
||||
if strings.Split(sampleConfig.DataDir, "")[0] != "/" {
|
||||
executablePath, err := os.Executable()
|
||||
|
||||
if err != nil {
|
||||
err := fmt.Errorf("unable to get current executable path: %w", err)
|
||||
|
||||
return MyConfig{}, err
|
||||
}
|
||||
|
||||
sampleConfig.DataDir = fmt.Sprintf("%s/%s", filepath.Dir(executablePath), sampleConfig.DataDir)
|
||||
}
|
||||
|
||||
return sampleConfig, err
|
||||
}
|
||||
|
||||
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||
139
internal/moon/db.go
Normal file
139
internal/moon/db.go
Normal file
@@ -0,0 +1,139 @@
|
||||
package moon
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
|
||||
"github.com/cockroachdb/errors/oserror"
|
||||
"github.com/cockroachdb/pebble"
|
||||
)
|
||||
|
||||
// StoreKV сохраняет в указанной бд ключ и значение.
|
||||
func StoreKV(db *pebble.DB, key string, value string) error {
|
||||
var (
|
||||
kArray = []byte(key)
|
||||
vArray = []byte(value)
|
||||
)
|
||||
|
||||
err := db.Set(kArray, vArray, pebble.Sync)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = db.Flush()
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// FetchV достаёт значение по ключу.
|
||||
func FetchV(db *pebble.DB, key string) (string, error) {
|
||||
var (
|
||||
kArray = []byte(key)
|
||||
|
||||
vArray []byte
|
||||
valueString = ""
|
||||
)
|
||||
|
||||
vArray, closer, err := db.Get(kArray)
|
||||
|
||||
if err != nil {
|
||||
return valueString, err
|
||||
}
|
||||
|
||||
valueString = string(vArray)
|
||||
err = closer.Close()
|
||||
|
||||
return valueString, err
|
||||
}
|
||||
|
||||
// GetValue достаёт настройку из БД с настройками.
|
||||
// TODO: should return string, error and should not log error internally.
|
||||
func (cfg *MyConfig) GetValue(dataBaseName string, chatID string, setting string) string {
|
||||
var err error
|
||||
|
||||
chatHash := sha256.Sum256([]byte(chatID))
|
||||
dataBaseDir := fmt.Sprintf("%s/db/%s/%x", cfg.DataDir, dataBaseName, chatHash)
|
||||
|
||||
// Если БД не открыта, откроем её
|
||||
if _, ok := DB[dataBaseDir]; !ok {
|
||||
var options pebble.Options
|
||||
// По дефолту ограничение ставится на мегабайты данных, а не на количество файлов, поэтому с дефолтными
|
||||
// настройками порождается огромное количество файлов. Умолчальное ограничение на количество файлов - 500 штук,
|
||||
// что нас не устраивает, поэтому немного снизим эту цифру до более приемлемых значений.
|
||||
options.L0CompactionFileThreshold = 8
|
||||
|
||||
if err := os.MkdirAll(dataBaseDir, os.ModePerm); err != nil {
|
||||
Log.Errorf("Unable to create db dir %s: %s", dataBaseDir, err)
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
DB[dataBaseDir], err = pebble.Open(dataBaseDir, &options)
|
||||
|
||||
if err != nil {
|
||||
Log.Errorf("Unable to open db, %s: %s\n", dataBaseDir, err)
|
||||
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
value, err := FetchV(DB[dataBaseDir], setting)
|
||||
|
||||
// Если из базы ничего не вынулось, по каким-то причинам, то просто вернём пустую строку.
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, pebble.ErrNotFound):
|
||||
Log.Debugf("Unable to get value for %s: no record found in db %s", setting, dataBaseDir)
|
||||
case errors.Is(err, fs.ErrNotExist):
|
||||
Log.Debugf("Unable to get value for %s: db dir %s does not exist", setting, dataBaseDir)
|
||||
case errors.Is(err, oserror.ErrNotExist):
|
||||
Log.Debugf("Unable to get value for %s: db dir %s does not exist", setting, dataBaseDir)
|
||||
default:
|
||||
Log.Errorf("Unable to get value for %s in db dir %s: %s", setting, dataBaseDir, err)
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
// SaveSetting сохраняет настройку в БД с настройками.
|
||||
func (cfg *MyConfig) SaveKeyValue(dataBaseName string, chatID string, setting string, value string) error {
|
||||
var (
|
||||
chatHash = sha256.Sum256([]byte(chatID))
|
||||
dataBaseDir = fmt.Sprintf("%s/db/%s/%x", cfg.DataDir, dataBaseName, chatHash)
|
||||
err error
|
||||
)
|
||||
|
||||
// Если БД не открыта, откроем её.
|
||||
if _, ok := DB[dataBaseDir]; !ok {
|
||||
var options pebble.Options
|
||||
// По дефолту ограничение ставится на мегабайты данных, а не на количество файлов, поэтому с дефолтными
|
||||
// настройками порождается огромное количество файлов. Умолчальное ограничение на количество файлов - 500 штук,
|
||||
// что нас не устраивает, поэтому немного снизим эту цифру до более приемлемых значений.
|
||||
options.L0CompactionFileThreshold = 8
|
||||
|
||||
if err := os.MkdirAll(dataBaseDir, os.ModePerm); err != nil {
|
||||
return fmt.Errorf("unable to create db dir %s: %w", dataBaseDir, err)
|
||||
}
|
||||
|
||||
DB[dataBaseDir], err = pebble.Open(dataBaseDir, &options)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to open settings db, %s: %w", dataBaseDir, err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := StoreKV(DB[dataBaseDir], setting, value); err != nil {
|
||||
return fmt.Errorf("unable to save setting %s in database %s: %w", setting, dataBaseDir, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||
50
internal/moon/editedMessage.go
Normal file
50
internal/moon/editedMessage.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package moon
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/NicoNex/echotron/v3"
|
||||
)
|
||||
|
||||
// editedMessage handles editedMessage type of update.
|
||||
func editedMessage(msg *echotron.Update) {
|
||||
AddUser(msg.EditedMessage.From)
|
||||
AddChat(&msg.EditedMessage.Chat)
|
||||
|
||||
// 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,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||
49
internal/moon/globals.go
Normal file
49
internal/moon/globals.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package moon
|
||||
|
||||
import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/NicoNex/echotron/v3"
|
||||
"github.com/akyoto/cache"
|
||||
"github.com/cockroachdb/pebble"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
var (
|
||||
Cfg MyConfig
|
||||
SigChan = make(chan os.Signal, 1)
|
||||
Log *zap.SugaredLogger
|
||||
Tg echotron.API
|
||||
|
||||
// Мапка с открытыми дескрипторами баз настроек.
|
||||
DB = make(map[string]*pebble.DB)
|
||||
|
||||
// SquashedMembers struct with banned members.
|
||||
SquashedMembers = cache.New(1 * time.Minute)
|
||||
|
||||
// NewMembers struct with new members appeared via message with new_chat_members field set.
|
||||
NewMembers = cache.New(1 * time.Minute)
|
||||
|
||||
// AppearedMembers struct with new members appeared via ChatMemberUpdated event.
|
||||
AppearedMembers = cache.New(60 * time.Minute)
|
||||
|
||||
// PendingCASMembers contains newly comed members that pass first cas check, but still waiting second cas check.
|
||||
PendingCASMembers = cache.New(60 * time.Minute)
|
||||
|
||||
// GreetPending contains people that should answer to bot greet.
|
||||
GreetPending = cache.New(1 * time.Minute)
|
||||
|
||||
// NoMessagePeople contains people that are never say something in chat.
|
||||
NoMessagePeople = cache.New(1 * time.Minute)
|
||||
|
||||
// People cache with users info.
|
||||
People = cache.New(8 * time.Hour)
|
||||
|
||||
// Groups cache with groups info.
|
||||
Groups = cache.New(8 * time.Hour)
|
||||
|
||||
ChatList = make([]int64, 0)
|
||||
)
|
||||
|
||||
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||
37
internal/moon/leftChatMember.go
Normal file
37
internal/moon/leftChatMember.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package moon
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/NicoNex/echotron/v3"
|
||||
)
|
||||
|
||||
// leftChatMember handles left_chat_member event.
|
||||
func leftChatMember(msg *echotron.Update) {
|
||||
Log.Infof(
|
||||
"User %s %s (username = %s, id = %d) left %s %s (%d)",
|
||||
msg.Message.LeftChatMember.FirstName,
|
||||
msg.Message.LeftChatMember.LastName,
|
||||
msg.Message.LeftChatMember.Username,
|
||||
msg.Message.LeftChatMember.ID,
|
||||
msg.Message.Chat.Type,
|
||||
msg.Message.Chat.Title,
|
||||
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),
|
||||
fmt.Sprintf("%d", msg.Message.LeftChatMember.ID),
|
||||
fmt.Sprintf("%d", time.Now().Unix()),
|
||||
); err != nil {
|
||||
Log.Error(err)
|
||||
} else {
|
||||
Log.Infof("Info about left userid %d saved to left users db", msg.Message.LeftChatMember.ID)
|
||||
}
|
||||
}
|
||||
|
||||
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||
96
internal/moon/newChatMembers.go
Normal file
96
internal/moon/newChatMembers.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package moon
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/NicoNex/echotron/v3"
|
||||
)
|
||||
|
||||
// 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)
|
||||
|
||||
AddChat(&msg.Message.Chat)
|
||||
|
||||
for _, user := range msg.Message.NewChatMembers {
|
||||
Log.Infof(
|
||||
"User %s %s (username = %s, id = %d) joined %s %s (%d)",
|
||||
user.FirstName,
|
||||
user.LastName,
|
||||
user.Username,
|
||||
user.ID,
|
||||
msg.Message.Chat.Type,
|
||||
msg.Message.Chat.Title,
|
||||
msg.Message.Chat.ID,
|
||||
)
|
||||
|
||||
AddUser(user)
|
||||
|
||||
casBanned, err := CasCheckID(user.ID)
|
||||
|
||||
if err != nil {
|
||||
Log.Error(err)
|
||||
}
|
||||
|
||||
// Ban user if cas report it as banned.
|
||||
if casBanned {
|
||||
Log.Infof(
|
||||
"Ban user %s %s (username = %s, id = %d) in %s %s (%d) by CAS blacklist",
|
||||
user.FirstName,
|
||||
user.LastName,
|
||||
user.Username,
|
||||
user.ID,
|
||||
msg.Message.Chat.Type,
|
||||
msg.Message.Chat.Title,
|
||||
msg.Message.Chat.ID,
|
||||
)
|
||||
|
||||
squash(
|
||||
msg.Message.Chat.ID,
|
||||
user.ID,
|
||||
)
|
||||
}
|
||||
|
||||
// Do not return on prev. step.
|
||||
|
||||
Log.Infof(
|
||||
"Skip banning user %s %s (username = %s, id = %d) in %s %s (%d) because it was not found in CAS blacklist",
|
||||
user.FirstName,
|
||||
user.LastName,
|
||||
user.Username,
|
||||
user.ID,
|
||||
msg.Message.Chat.Type,
|
||||
msg.Message.Chat.Title,
|
||||
msg.Message.Chat.ID,
|
||||
)
|
||||
|
||||
Log.Infof(
|
||||
"Add user %s %s (username = %s, id = %d) in %s %s (%d) to NewMember DB",
|
||||
user.FirstName,
|
||||
user.LastName,
|
||||
user.Username,
|
||||
user.ID,
|
||||
msg.Message.Chat.Type,
|
||||
msg.Message.Chat.Title,
|
||||
msg.Message.Chat.ID,
|
||||
)
|
||||
|
||||
key := fmt.Sprintf("%d+%d", msg.Message.Chat.ID, user.ID)
|
||||
NewMembers.Set(key, time.Now().Unix(), 1*time.Minute)
|
||||
PendingCASMembers.Set(key, time.Now().Unix(), 30*time.Minute)
|
||||
}
|
||||
}
|
||||
|
||||
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||
49
internal/moon/photo.go
Normal file
49
internal/moon/photo.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package moon
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/NicoNex/echotron/v3"
|
||||
)
|
||||
|
||||
func photo(msg *echotron.Update) {
|
||||
AddChat(&msg.Message.Chat)
|
||||
AddUser(msg.Message.From)
|
||||
|
||||
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: */
|
||||
68
internal/moon/reaction.go
Normal file
68
internal/moon/reaction.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package moon
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/NicoNex/echotron/v3"
|
||||
)
|
||||
|
||||
func reaction(msg *echotron.Update) {
|
||||
AddChat(&msg.MessageReaction.Chat)
|
||||
AddUser(&msg.MessageReaction.User)
|
||||
|
||||
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: */
|
||||
23
internal/moon/reactionCount.go
Normal file
23
internal/moon/reactionCount.go
Normal file
@@ -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: */
|
||||
293
internal/moon/runAndBan.go
Normal file
293
internal/moon/runAndBan.go
Normal file
@@ -0,0 +1,293 @@
|
||||
package moon
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func RunAndBan() {
|
||||
for {
|
||||
var (
|
||||
ListAppearedMembers, ListNewMembers []string
|
||||
)
|
||||
|
||||
AppearedMembers.Range(
|
||||
func(k, _ any) bool {
|
||||
ListAppearedMembers = append(ListAppearedMembers, k.(string))
|
||||
|
||||
return true
|
||||
},
|
||||
)
|
||||
|
||||
NewMembers.Range(
|
||||
func(k, _ any) bool {
|
||||
ListNewMembers = append(ListNewMembers, k.(string))
|
||||
|
||||
return true
|
||||
},
|
||||
)
|
||||
|
||||
for _, AppearedMember := range ListAppearedMembers {
|
||||
for _, NewMember := range ListNewMembers {
|
||||
if AppearedMember == NewMember {
|
||||
AppearedMembers.Delete(AppearedMember)
|
||||
NewMembers.Delete(NewMember)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
currentTime := time.Now().Unix()
|
||||
|
||||
ListAppearedMembers = ListAppearedMembers[:0]
|
||||
ListNewMembers = ListNewMembers[:0]
|
||||
ListAppearedMembers = slices.Clip(ListAppearedMembers)
|
||||
ListNewMembers = slices.Clip(ListNewMembers)
|
||||
|
||||
AppearedMembers.Range(
|
||||
func(k, _ any) bool {
|
||||
ListAppearedMembers = append(ListAppearedMembers, k.(string))
|
||||
|
||||
return true
|
||||
},
|
||||
)
|
||||
|
||||
NewMembers.Range(
|
||||
func(k, _ any) bool {
|
||||
ListNewMembers = append(ListNewMembers, k.(string))
|
||||
|
||||
return true
|
||||
},
|
||||
)
|
||||
|
||||
for _, AppearedMember := range ListAppearedMembers {
|
||||
eventTimestamp, found := AppearedMembers.Get(AppearedMember)
|
||||
|
||||
if !found {
|
||||
continue
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
userid, err := strconv.ParseInt(item[1], 10, 64)
|
||||
|
||||
if err != nil {
|
||||
Log.Errorf("Got broken key in AppearedMembers cache: %s", AppearedMember)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
user := GetUser(userid)
|
||||
chat := GetChat(chatid)
|
||||
|
||||
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",
|
||||
user.FirstName,
|
||||
user.LastName,
|
||||
user.Username,
|
||||
userid,
|
||||
chat.Type,
|
||||
chat.Title,
|
||||
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
|
||||
}
|
||||
|
||||
user := GetUser(userid)
|
||||
chat := GetChat(chatid)
|
||||
|
||||
Log.Debugf("Clean newChatMember event record older than 10 seconds for user user %s %s (username = %s, id = %d) in chat %s %s (%d)",
|
||||
user.FirstName,
|
||||
user.LastName,
|
||||
user.Username,
|
||||
userid,
|
||||
chat.Type,
|
||||
chat.Title,
|
||||
chatid,
|
||||
)
|
||||
|
||||
NewMembers.Delete(NewMember)
|
||||
}
|
||||
}
|
||||
|
||||
time.Sleep(10 * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
func PeriodicCASCheck() {
|
||||
for {
|
||||
var (
|
||||
ListPendingMembers []string
|
||||
)
|
||||
|
||||
PendingCASMembers.Range(
|
||||
func(k, _ any) bool {
|
||||
ListPendingMembers = append(ListPendingMembers, k.(string))
|
||||
|
||||
return true
|
||||
},
|
||||
)
|
||||
|
||||
currentTime := time.Now().Unix()
|
||||
|
||||
for _, PendingMember := range ListPendingMembers {
|
||||
eventTimestamp, found := PendingCASMembers.Get(PendingMember)
|
||||
|
||||
if !found {
|
||||
continue
|
||||
}
|
||||
|
||||
eventTimeDelta := currentTime - eventTimestamp.(int64)
|
||||
|
||||
// It's time to check for second time this id if it was banned in CAS.
|
||||
if eventTimeDelta > 300 {
|
||||
item := strings.Split(PendingMember, "+")
|
||||
chatid, err := strconv.ParseInt(item[0], 10, 64)
|
||||
|
||||
if err != nil {
|
||||
Log.Errorf("Got broken key in PendingMembers cache: %s", PendingMember)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
userid, err := strconv.ParseInt(item[1], 10, 64)
|
||||
|
||||
if err != nil {
|
||||
Log.Errorf("Got broken key in PendingMembers cache: %s", PendingMember)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
value := Cfg.GetValue(
|
||||
"BannedMembers",
|
||||
fmt.Sprintf("%d", chatid),
|
||||
fmt.Sprintf("%d", userid),
|
||||
)
|
||||
|
||||
user := GetUser(userid)
|
||||
chat := GetChat(chatid)
|
||||
|
||||
if value != "" {
|
||||
Log.Debugf("User %s %s (username = %s, id = %d) in %s %s (%d) is already banned, skip cas check.",
|
||||
user.FirstName,
|
||||
user.LastName,
|
||||
user.Username,
|
||||
userid,
|
||||
chat.Type,
|
||||
chat.Title,
|
||||
chatid,
|
||||
)
|
||||
|
||||
Log.Debugf(
|
||||
"Remove user %s %s (username = %s, id = %d) in %s %s (%d) from PendingCASMembers",
|
||||
user.FirstName,
|
||||
user.LastName,
|
||||
user.Username,
|
||||
userid,
|
||||
chat.Type,
|
||||
chat.Title,
|
||||
chatid,
|
||||
)
|
||||
|
||||
key := fmt.Sprintf("%d+%d", chatid, userid)
|
||||
PendingCASMembers.Delete(key)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
Log.Debugf("Checking user %s %s (username = %s, id = %d) in %s %s (%d) for second time in CAS",
|
||||
user.FirstName,
|
||||
user.LastName,
|
||||
user.Username,
|
||||
userid,
|
||||
chat.Type,
|
||||
chat.Title,
|
||||
chatid,
|
||||
)
|
||||
|
||||
banned, err := CasCheckID(userid)
|
||||
|
||||
if err != nil {
|
||||
Log.Error(err)
|
||||
}
|
||||
|
||||
if banned {
|
||||
Log.Infof(
|
||||
"Ban user %s %s (username = %s, id = %d) in %s %s (%d) by CAS blacklist",
|
||||
user.FirstName,
|
||||
user.LastName,
|
||||
user.Username,
|
||||
userid,
|
||||
chat.Type,
|
||||
chat.Title,
|
||||
chatid,
|
||||
)
|
||||
|
||||
squash(chatid, userid)
|
||||
}
|
||||
|
||||
Log.Debugf(
|
||||
"Remove user %s %s (username = %s, id = %d) in %s %s (%d) from PendingCASMembers",
|
||||
user.FirstName,
|
||||
user.LastName,
|
||||
user.Username,
|
||||
userid,
|
||||
chat.Type,
|
||||
chat.Title,
|
||||
chatid,
|
||||
)
|
||||
|
||||
PendingCASMembers.Delete(PendingMember)
|
||||
}
|
||||
}
|
||||
|
||||
time.Sleep(10 * time.Minute)
|
||||
}
|
||||
}
|
||||
|
||||
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||
40
internal/moon/sighandler.go
Normal file
40
internal/moon/sighandler.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package moon
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// SigHandler хэндлер сигналов закрывает все бд, все сетевые соединения и сваливает из приложения.
|
||||
func SigHandler() {
|
||||
for {
|
||||
var s = <-SigChan
|
||||
switch s {
|
||||
case syscall.SIGINT:
|
||||
Log.Infoln("Got SIGINT, quitting")
|
||||
case syscall.SIGTERM:
|
||||
Log.Infoln("Got SIGTERM, quitting")
|
||||
case syscall.SIGQUIT:
|
||||
Log.Infoln("Got SIGQUIT, quitting")
|
||||
|
||||
// Заходим на новую итерацию, если у нас "неинтересный" сигнал.
|
||||
default:
|
||||
continue
|
||||
}
|
||||
|
||||
if len(DB) > 0 {
|
||||
Log.Debug("Closing runtime bot settings db")
|
||||
|
||||
for _, db := range DB {
|
||||
_ = db.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// Since we shutting down, we have noting to do with log sync.
|
||||
_ = Log.Sync()
|
||||
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||
140
internal/moon/squash.go
Normal file
140
internal/moon/squash.go
Normal file
@@ -0,0 +1,140 @@
|
||||
package moon
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/NicoNex/echotron/v3"
|
||||
)
|
||||
|
||||
// TODO: refactor to return bool, error
|
||||
|
||||
// squash bans given chat member.
|
||||
func squash(chatid int64, userid int64) {
|
||||
banUntilDate := time.Now().Unix() + 10
|
||||
|
||||
user := GetUser(userid)
|
||||
chat := GetChat(chatid)
|
||||
|
||||
// If there is no such chatid, means that no such cache too.
|
||||
if len(ChatList) == 0 || !slices.Contains(ChatList, chatid) {
|
||||
Log.Debugf("Add %s %s (%d) to list of known chats", chat.Type, chat.Title, chat.ID)
|
||||
|
||||
ChatList = append(ChatList, chatid)
|
||||
}
|
||||
|
||||
key := fmt.Sprintf("%d+%d", chatid, userid)
|
||||
if _, exist := SquashedMembers.Get(key); exist {
|
||||
Log.Infof(
|
||||
"Skip banning user %s %s (username = %s, id = %d) in %s %s (%d), already banned",
|
||||
user.FirstName,
|
||||
user.LastName,
|
||||
user.Username,
|
||||
user.ID,
|
||||
chat.Type,
|
||||
chat.Title,
|
||||
chat.ID,
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if res, err := Tg.BanChatMember(
|
||||
chatid,
|
||||
userid,
|
||||
&echotron.BanOptions{
|
||||
UntilDate: int(banUntilDate),
|
||||
},
|
||||
); err != nil {
|
||||
Log.Errorf(
|
||||
"Unable to ban user user %s %s (username = %s, id = %d) in %s %s (%d): %s",
|
||||
user.FirstName,
|
||||
user.LastName,
|
||||
user.Username,
|
||||
user.ID,
|
||||
chat.Type,
|
||||
chat.Title,
|
||||
chat.ID,
|
||||
res.Description,
|
||||
)
|
||||
} else {
|
||||
if !res.Ok {
|
||||
Log.Errorf(
|
||||
"Unable to ban user %s %s (username = %s, id = %d) in %s %s (%d): %s",
|
||||
user.FirstName,
|
||||
user.LastName,
|
||||
user.Username,
|
||||
user.ID,
|
||||
chat.Type,
|
||||
chat.Title,
|
||||
chat.ID,
|
||||
res.Description,
|
||||
)
|
||||
} else {
|
||||
Log.Debugf(
|
||||
"Banned user %s %s (username = %s, id = %d) in %s %s (%d).",
|
||||
user.FirstName,
|
||||
user.LastName,
|
||||
user.Username,
|
||||
user.ID,
|
||||
chat.Type,
|
||||
chat.Title,
|
||||
chat.ID,
|
||||
)
|
||||
|
||||
key := fmt.Sprintf("%d+%d", chatid, userid)
|
||||
|
||||
Log.Debugf(
|
||||
"Put user %s %s (username = %s, id = %d) in %s %s (%d) into SquashedMembers to debounce ban.",
|
||||
user.FirstName,
|
||||
user.LastName,
|
||||
user.Username,
|
||||
user.ID,
|
||||
chat.Type,
|
||||
chat.Title,
|
||||
chat.ID,
|
||||
)
|
||||
|
||||
SquashedMembers.Set(key, true, 30*time.Second)
|
||||
|
||||
Log.Debugf(
|
||||
"Remove user %s %s (username = %s, id = %d) in %s %s (%d) from NewMembers.",
|
||||
user.FirstName,
|
||||
user.LastName,
|
||||
user.Username,
|
||||
user.ID,
|
||||
chat.Type,
|
||||
chat.Title,
|
||||
chat.ID,
|
||||
)
|
||||
|
||||
NewMembers.Delete(key)
|
||||
|
||||
Log.Debugf(
|
||||
"Remove user %s %s (username = %s, id = %d) in %s %s (%d) from AppearedMembers.",
|
||||
user.FirstName,
|
||||
user.LastName,
|
||||
user.Username,
|
||||
user.ID,
|
||||
chat.Type,
|
||||
chat.Title,
|
||||
chat.ID,
|
||||
)
|
||||
AppearedMembers.Delete(key)
|
||||
|
||||
if err := Cfg.SaveKeyValue(
|
||||
"BannedMembers",
|
||||
fmt.Sprintf("%d", chatid),
|
||||
fmt.Sprintf("%d", userid),
|
||||
fmt.Sprintf("%d", time.Now().Unix()),
|
||||
); err != nil {
|
||||
Log.Error(err)
|
||||
} else {
|
||||
Log.Infof("Info about banned userid %d saved to BannedMembers db", userid)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||
75
internal/moon/telegram.go
Normal file
75
internal/moon/telegram.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package moon
|
||||
|
||||
import (
|
||||
"github.com/NicoNex/echotron/v3"
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
)
|
||||
|
||||
// Telegram основная горутинка, реализующая бота.
|
||||
func Telegram(cfg MyConfig) {
|
||||
Tg = echotron.NewAPI(cfg.Token)
|
||||
|
||||
Log.Infof("Starting api update poller loop")
|
||||
|
||||
for u := range echotron.PollingUpdatesOptions(cfg.Token, true, echotron.UpdateOptions{
|
||||
AllowedUpdates: []echotron.UpdateType{
|
||||
"message",
|
||||
"edited_message",
|
||||
"channel_post",
|
||||
"edited_channel_post",
|
||||
"message_reaction",
|
||||
"message_reaction_count",
|
||||
"inline_query",
|
||||
"chosen_inline_result",
|
||||
"callback_query",
|
||||
"poll",
|
||||
"poll_answer",
|
||||
"my_chat_member",
|
||||
"chat_member",
|
||||
"chat_join_request",
|
||||
"chat_boost",
|
||||
"removed_chat_boost",
|
||||
},
|
||||
}) {
|
||||
telegramMsgParser(u)
|
||||
}
|
||||
}
|
||||
|
||||
// telegramMsgParser parses events generated by echotron.
|
||||
func telegramMsgParser(msg *echotron.Update) {
|
||||
switch {
|
||||
case msg.Message != nil && msg.Message.Text != "":
|
||||
textMessage(msg)
|
||||
|
||||
case msg.EditedMessage != nil:
|
||||
editedMessage(msg)
|
||||
|
||||
case msg.ChatMember != nil:
|
||||
chatMemberUpdated(msg)
|
||||
|
||||
// New members entered to chat.
|
||||
case msg.Message != nil && msg.Message.NewChatMembers != nil:
|
||||
newChatMembers(msg)
|
||||
|
||||
// Chat member leaves chat.
|
||||
case msg.Message != nil && msg.Message.LeftChatMember != nil:
|
||||
leftChatMember(msg)
|
||||
|
||||
case msg.Message != nil && msg.Message.Animation != nil:
|
||||
animation(msg)
|
||||
|
||||
case msg.MessageReaction != nil:
|
||||
reaction(msg)
|
||||
|
||||
case msg.MessageReactionCount != nil:
|
||||
reactionCount(msg)
|
||||
|
||||
case msg.Message != nil && msg.Message.Photo != nil:
|
||||
photo(msg)
|
||||
|
||||
default:
|
||||
Log.Debug(spew.Sdump(msg))
|
||||
}
|
||||
}
|
||||
|
||||
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||
58
internal/moon/textMessage.go
Normal file
58
internal/moon/textMessage.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package moon
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/NicoNex/echotron/v3"
|
||||
)
|
||||
|
||||
// textMessage handles text message events.
|
||||
func textMessage(msg *echotron.Update) {
|
||||
AddChat(&msg.Message.Chat)
|
||||
AddUser(msg.Message.From)
|
||||
|
||||
// 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.
|
||||
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,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||
40
internal/moon/types.go
Normal file
40
internal/moon/types.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package moon
|
||||
|
||||
import (
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
// MyConfig describes bot config file format.
|
||||
type MyConfig struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
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"`
|
||||
}
|
||||
|
||||
// CasFalse describes response from api.cas.chat when user is not blacklisted.
|
||||
type CasFalse struct {
|
||||
Ok bool `json:"ok"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
// CasFalse describes response from api.cas.chat when user is blacklisted.
|
||||
type CasTrue struct {
|
||||
Ok bool `json:"ok"`
|
||||
Result struct {
|
||||
Reasons []int `json:"reasons"`
|
||||
Offenses int `json:"offenses"`
|
||||
TimeAdded time.Time `json:"time_added"`
|
||||
} `json:"result"`
|
||||
}
|
||||
|
||||
type Chatlog struct {
|
||||
LogFilePath string
|
||||
File *os.File
|
||||
}
|
||||
|
||||
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||
76
internal/moon/usersGroups.go
Normal file
76
internal/moon/usersGroups.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package moon
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/NicoNex/echotron/v3"
|
||||
)
|
||||
|
||||
// AddUser remebers user info in People cache.
|
||||
func AddUser(u *echotron.User) {
|
||||
bytes, err := json.Marshal(u)
|
||||
|
||||
if err != nil {
|
||||
Log.Errorf("Unable to encode user info to json string: %s", err)
|
||||
}
|
||||
|
||||
People.Set(u.ID, bytes, 24*time.Hour)
|
||||
}
|
||||
|
||||
// GetUser returns user info structure.
|
||||
func GetUser(id int64) *echotron.User {
|
||||
var u *echotron.User
|
||||
|
||||
bytes, ok := People.Get(id)
|
||||
|
||||
if !ok {
|
||||
return u
|
||||
}
|
||||
|
||||
err := json.Unmarshal(bytes.([]byte), &u)
|
||||
|
||||
if err != nil {
|
||||
Log.Errorf("Unable to decode user info json: %s", err)
|
||||
|
||||
return u
|
||||
}
|
||||
|
||||
return u
|
||||
}
|
||||
|
||||
// AddChat remebers chat info in Groups cache.
|
||||
func AddChat(c *echotron.Chat) {
|
||||
bytes, err := json.Marshal(c)
|
||||
|
||||
if err != nil {
|
||||
Log.Errorf("Unable to encode chat info to json string: %s", err)
|
||||
}
|
||||
|
||||
Groups.Set(c.ID, bytes, 24*time.Hour)
|
||||
}
|
||||
|
||||
// GetChat returns chat info structure.
|
||||
func GetChat(id int64) *echotron.Chat {
|
||||
var c *echotron.Chat
|
||||
|
||||
bytes, ok := Groups.Get(id)
|
||||
|
||||
if !ok {
|
||||
Log.Errorf("No group %d in Groups cache", id)
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
err := json.Unmarshal(bytes.([]byte), &c)
|
||||
|
||||
if err != nil {
|
||||
Log.Errorf("Unable to decode chat info json: %s", err)
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||
Reference in New Issue
Block a user