[WIP] Initial commit

This commit is contained in:
eleksir
2025-05-08 18:12:52 +03:00
commit 5379ae6eb3
1516 changed files with 648142 additions and 0 deletions

82
internal/moon/config.go Normal file
View File

@@ -0,0 +1,82 @@
package moon
import (
"encoding/json"
"fmt"
"os"
"github.com/hjson/hjson-go"
"go.uber.org/zap/zapcore"
)
// parseConfig разбирает и валидирует даденный конфиг.
func ParseConfig(path string) (MyConfig, error) {
fileInfo, err := os.Stat(path)
// Предполагаем, что файла либо нет, либо мы не можем его прочитать, второе надо бы логгировать, но пока забьём.
if err != nil {
return MyConfig{}, err
}
// Конфиг-файл длинноват для конфига, попробуем следующего кандидата.
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)
// Не удалось прочитать.
if err != nil {
return MyConfig{}, err
}
// Исходя из документации, hjson какбы умеет парсить "кривой" json, но парсит его в map-ку.
// Интереснее на выходе получить структурку: то есть мы вначале конфиг преобразуем в map-ку, затем эту map-ку
// сериализуем в json, а потом json превращааем в структурку. Не очень эффективно, но он и не часто требуется.
var (
sampleConfig MyConfig
tmp map[string]interface{}
)
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
}
return sampleConfig, err
}

122
internal/moon/db.go Normal file
View File

@@ -0,0 +1,122 @@
package moon
import (
"crypto/sha256"
"errors"
"fmt"
"io/fs"
"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)
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 достаёт настройку из БД с настройками.
func (cfg *MyConfig) GetValue(chatID string, setting string) string {
var err error
chatHash := sha256.Sum256([]byte(chatID))
database := fmt.Sprintf("db/%x", chatHash)
// Если БД не открыта, откроем её
if _, ok := DB[database]; !ok {
var options pebble.Options
// По дефолту ограничение ставится на мегабайты данных, а не на количество файлов, поэтому с дефолтными
// настройками порождается огромное количество файлов. Умолчальное ограничение на количество файлов - 500 штук,
// что нас не устраивает, поэтому немного снизим эту цифру до более приемлемых значений.
options.L0CompactionFileThreshold = 8
DB[database], err = pebble.Open(cfg.DataDir+"/"+database, &options)
if err != nil {
Log.Errorf("Unable to open db, %s: %s\n", database, err)
return ""
}
}
value, err := FetchV(DB[database], 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, database)
case errors.Is(err, fs.ErrNotExist):
Log.Debugf("Unable to get value for %s: db dir %s does not exist", setting, database)
case errors.Is(err, oserror.ErrNotExist):
Log.Debugf("Unable to get value for %s: db dir %s does not exist", setting, database)
default:
Log.Errorf("Unable to get value for %s in db dir %s: %s", setting, database, err)
}
return ""
}
return value
}
// SaveSetting сохраняет настройку в БД с настройками.
func (cfg *MyConfig) SaveKeyValue(chatID string, setting string, value string) error {
var (
chatHash = sha256.Sum256([]byte(chatID))
database = fmt.Sprintf("db/%x", chatHash)
err error
)
// Если БД не открыта, откроем её.
if _, ok := DB[database]; !ok {
var options pebble.Options
// По дефолту ограничение ставится на мегабайты данных, а не на количество файлов, поэтому с дефолтными
// настройками порождается огромное количество файлов. Умолчальное ограничение на количество файлов - 500 штук,
// что нас не устраивает, поэтому немного снизим эту цифру до более приемлемых значений.
options.L0CompactionFileThreshold = 8
DB[database], err = pebble.Open(cfg.DataDir+"/"+database, &options)
if err != nil {
Log.Errorf("Unable to open settings db, %s: %s\n", database, err)
return err
}
}
if err := StoreKV(DB[database], setting, value); err != nil {
Log.Errorf("Unable to save setting %s in database %s: %s", setting, database, err)
}
return err
}
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */

18
internal/moon/globals.go Normal file
View File

@@ -0,0 +1,18 @@
package moon
import (
"os"
"github.com/NicoNex/echotron/v3"
"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)
)

View 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: */

135
internal/moon/telegram.go Normal file
View File

@@ -0,0 +1,135 @@
package moon
import (
"github.com/NicoNex/echotron/v3"
"github.com/davecgh/go-spew/spew"
)
// Telegram основная горутинка, реализующая бота.
func Telegram(cfg MyConfig) {
Tg = echotron.NewAPI(cfg.Token)
for u := range echotron.PollingUpdates(cfg.Token) {
telegramMsgParser(u)
}
}
// telegramMsgParser parses events generated by echotron.
func telegramMsgParser(msg *echotron.Update) {
// We not interested in text message events, yet at least.
if msg.Message.Text != "" {
if msg.Message.ReplyToMessage == nil {
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 {
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,
)
}
Log.Info(spew.Sdump(msg.Message))
return
}
/*
Итак, что нам интересно?
1. Приход chat members
2. Обновление chat members
*/
// обновление
if msg.ChatMember != nil {
if msg.ChatMember.NewChatMember.User != nil {
Log.Infof(
"User %s %s (username = %s, id = %d) joined %s %s (%d)",
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,
)
} else {
Log.Info(spew.Sdump(msg))
}
return
}
// новые люди (по документации)
if msg.Message.NewChatMembers != nil {
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,
)
resp, err := Tg.SendMessage(
"Say forty two in numbers",
msg.ChatID(),
&echotron.MessageOptions{
DisableNotification: true,
ReplyParameters: echotron.ReplyParameters{
AllowSendingWithoutReply: false,
},
},
)
if err != nil {
Log.Errorf("Unable to send greet for new user: %s", err)
}
Log.Infof(spew.Sdump(resp))
}
return
}
// Chat member leaves.
if msg.Message.LeftChatMember != nil {
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,
)
return
}
Log.Info(spew.Sdump(msg))
}

10
internal/moon/types.go Normal file
View File

@@ -0,0 +1,10 @@
package moon
type MyConfig struct {
Name string `json:"name,omitempty"`
Token string `json:"token,omitempty"`
LogLevel string `json:"loglevel,omitempty"`
LogFile string `json:"logfile,omitempty"`
CSign string `json:"csign,omitempty"`
DataDir string `json:"datadir,omitempty"`
}