[WIP] Initial commit
This commit is contained in:
82
internal/moon/config.go
Normal file
82
internal/moon/config.go
Normal 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
122
internal/moon/db.go
Normal 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
18
internal/moon/globals.go
Normal 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)
|
||||
)
|
||||
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: */
|
||||
135
internal/moon/telegram.go
Normal file
135
internal/moon/telegram.go
Normal 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
10
internal/moon/types.go
Normal 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"`
|
||||
}
|
||||
Reference in New Issue
Block a user