0

[WIP] update linter rules, fix many linter warnings

This commit is contained in:
eleksir
2025-11-01 02:15:08 +03:00
parent c31547ce4a
commit fbd0196a48
17 changed files with 961 additions and 251 deletions

View File

@@ -1,8 +1,10 @@
// Package tg implements most of aleesa-telegram-go functionality.
package tg
import (
"fmt"
"regexp"
"strconv"
"github.com/NicoNex/echotron/v3"
"github.com/davecgh/go-spew/spew"
@@ -18,6 +20,7 @@ func Admin(msg *echotron.Update) (bool, error) {
switch {
case c == "admin" || c == "админ":
answer = "```\n"
answer += fmt.Sprintf("%sadmin censor - показать список состояния типов сообщений\n", Config.Csign)
answer += fmt.Sprintf("%sадмин ценз - показать список состояния типов сообщений\n", Config.Csign)
answer += fmt.Sprintf("%sadmin censor type # - где 1 - вкл, 0 - выкл цензуры для означенного типа сообщений\n", Config.Csign)
@@ -64,67 +67,67 @@ func Admin(msg *echotron.Update) (bool, error) {
answer += "```"
case regexp.MustCompile("^(admin|админ)[[:space:]](censor|ценз)$").MatchString(c):
if GetSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "VoiceMsg") == "1" {
if GetSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "VoiceMsg") == "1" {
answer += "Тип сообщения voice удаляется\n"
} else {
answer += "Тип сообщения voice не удаляется\n"
}
if GetSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "AudioMsg") == "1" {
if GetSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "AudioMsg") == "1" {
answer += "Тип сообщения audio удаляется\n"
} else {
answer += "Тип сообщения audio не удаляется\n"
}
if GetSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "PhotoMsg") == "1" {
if GetSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "PhotoMsg") == "1" {
answer += "Тип сообщения photo удаляется\n"
} else {
answer += "Тип сообщения photo не удаляется\n"
}
if GetSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "VideoMsg") == "1" {
if GetSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "VideoMsg") == "1" {
answer += "Тип сообщения video удаляется\n"
} else {
answer += "Тип сообщения video не удаляется\n"
}
if GetSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "VideoNoteMsg") == "1" {
if GetSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "VideoNoteMsg") == "1" {
answer += "Тип сообщения video_note удаляется\n"
} else {
answer += "Тип сообщения video_note не удаляется\n"
}
if GetSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "AnimationMsg") == "1" {
if GetSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "AnimationMsg") == "1" {
answer += "Тип сообщения animation удаляется\n"
} else {
answer += "Тип сообщения animation не удаляется\n"
}
if GetSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "StickerMsg") == "1" {
if GetSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "StickerMsg") == "1" {
answer += "Тип сообщения sticker удаляется\n"
} else {
answer += "Тип сообщения sticker не удаляется\n"
}
if GetSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "DiceMsg") == "1" {
if GetSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "DiceMsg") == "1" {
answer += "Тип сообщения dice удаляется\n"
} else {
answer += "Тип сообщения dice не удаляется\n"
}
if GetSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "GameMsg") == "1" {
if GetSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "GameMsg") == "1" {
answer += "Тип сообщения game удаляется\n"
} else {
answer += "Тип сообщения game не удаляется\n"
}
if GetSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "PollMsg") == "1" {
if GetSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "PollMsg") == "1" {
answer += "Тип сообщения poll удаляется\n"
} else {
answer += "Тип сообщения poll не удаляется\n"
}
if GetSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "DocumentMsg") == "1" {
if GetSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "DocumentMsg") == "1" {
answer += "Тип сообщения document удаляется\n"
} else {
answer += "Тип сообщения document не удаляется\n"
@@ -139,14 +142,14 @@ func Admin(msg *echotron.Update) (bool, error) {
case "voice":
switch cmdArray[3] {
case "1":
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "VoiceMsg", "1"); err != nil {
if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "VoiceMsg", "1"); err != nil {
return true, err
}
answer += "Теперь сообщения с voice будут оставаться"
case "0":
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "VoiceMsg", "0"); err != nil {
if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "VoiceMsg", "0"); err != nil {
return true, err
}
@@ -159,14 +162,14 @@ func Admin(msg *echotron.Update) (bool, error) {
case "audio":
switch cmdArray[3] {
case "1":
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "AudioMsg", "1"); err != nil {
if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "AudioMsg", "1"); err != nil {
return true, err
}
answer += "Теперь сообщения с audio будут оставаться"
case "0":
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "AudioMsg", "0"); err != nil {
if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "AudioMsg", "0"); err != nil {
return true, err
}
@@ -179,14 +182,14 @@ func Admin(msg *echotron.Update) (bool, error) {
case "photo":
switch cmdArray[3] {
case "1":
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "PhotoMsg", "1"); err != nil {
if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "PhotoMsg", "1"); err != nil {
return true, err
}
answer += "Теперь сообщения с photo будут оставаться"
case "0":
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "PhotoMsg", "0"); err != nil {
if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "PhotoMsg", "0"); err != nil {
return true, err
}
@@ -199,14 +202,14 @@ func Admin(msg *echotron.Update) (bool, error) {
case "video":
switch cmdArray[3] {
case "1":
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "VideoMsg", "1"); err != nil {
if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "VideoMsg", "1"); err != nil {
return true, err
}
answer += "Теперь сообщения с video будут оставаться"
case "0":
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "VideoMsg", "0"); err != nil {
if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "VideoMsg", "0"); err != nil {
return true, err
}
@@ -219,14 +222,14 @@ func Admin(msg *echotron.Update) (bool, error) {
case "video_note":
switch cmdArray[3] {
case "1":
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "VideoNoteMsg", "1"); err != nil {
if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "VideoNoteMsg", "1"); err != nil {
return true, err
}
answer += "Теперь сообщения с video_note будут оставаться"
case "0":
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "VideoNoteMsg", "0"); err != nil {
if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "VideoNoteMsg", "0"); err != nil {
return true, err
}
@@ -239,14 +242,14 @@ func Admin(msg *echotron.Update) (bool, error) {
case "animation":
switch cmdArray[3] {
case "1":
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "AnimationMsg", "1"); err != nil {
if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "AnimationMsg", "1"); err != nil {
return true, err
}
answer += "Теперь сообщения с animation будут оставаться"
case "0":
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "AnimationMsg", "0"); err != nil {
if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "AnimationMsg", "0"); err != nil {
return true, err
}
@@ -259,14 +262,14 @@ func Admin(msg *echotron.Update) (bool, error) {
case "sticker":
switch cmdArray[3] {
case "1":
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "StickerMsg", "1"); err != nil {
if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "StickerMsg", "1"); err != nil {
return true, err
}
answer += "Теперь сообщения с sticker будут оставаться"
case "0":
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "StickerMsg", "0"); err != nil {
if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "StickerMsg", "0"); err != nil {
return true, err
}
@@ -279,14 +282,14 @@ func Admin(msg *echotron.Update) (bool, error) {
case "dice":
switch cmdArray[3] {
case "1":
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "DiceMsg", "1"); err != nil {
if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "DiceMsg", "1"); err != nil {
return true, err
}
answer += "Теперь сообщения с dice будут оставаться"
case "0":
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "diceMsg", "0"); err != nil {
if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "diceMsg", "0"); err != nil {
return true, err
}
@@ -299,14 +302,14 @@ func Admin(msg *echotron.Update) (bool, error) {
case "game":
switch cmdArray[3] {
case "1":
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "GameMsg", "1"); err != nil {
if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "GameMsg", "1"); err != nil {
return true, err
}
answer += "Теперь сообщения с game будут оставаться"
case "0":
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "GameMsg", "0"); err != nil {
if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "GameMsg", "0"); err != nil {
return true, err
}
@@ -319,14 +322,14 @@ func Admin(msg *echotron.Update) (bool, error) {
case "poll":
switch cmdArray[3] {
case "1":
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "PollMsg", "1"); err != nil {
if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "PollMsg", "1"); err != nil {
return true, err
}
answer += "Теперь сообщения с poll будут оставаться"
case "0":
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "PollMsg", "0"); err != nil {
if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "PollMsg", "0"); err != nil {
return true, err
}
@@ -339,14 +342,14 @@ func Admin(msg *echotron.Update) (bool, error) {
case "document":
switch cmdArray[3] {
case "1":
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "DocumentMsg", "1"); err != nil {
if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "DocumentMsg", "1"); err != nil {
return true, err
}
answer += "Теперь сообщения с document будут оставаться"
case "0":
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "DocumentMsg", "0"); err != nil {
if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "DocumentMsg", "0"); err != nil {
return true, err
}
@@ -361,7 +364,7 @@ func Admin(msg *echotron.Update) (bool, error) {
}
case regexp.MustCompile("^(admin|админ)[[:space:]](fortune|фортунка)$").MatchString(c):
if GetSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "FortuneMsg") == "1" {
if GetSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "FortuneMsg") == "1" {
answer += "Фортунки с утра показываются"
} else {
answer += "Фортунки с утра не показываются"
@@ -375,14 +378,14 @@ func Admin(msg *echotron.Update) (bool, error) {
// мы не сможем вытащить список чятиков.
switch cmdArray[2] {
case "1":
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "FortuneMsg", "1"); err != nil {
if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "FortuneMsg", "1"); err != nil {
return true, err
}
answer += "Теперь сообщения с фортункой будут отправляться с утра"
case "0":
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "FortuneMsg", "0"); err != nil {
if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "FortuneMsg", "0"); err != nil {
return true, err
}
@@ -393,7 +396,7 @@ func Admin(msg *echotron.Update) (bool, error) {
}
case regexp.MustCompile("^(admin|админ)[[:space:]](greet|приветствие)$").MatchString(c):
if GetSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "GreetMsg") == "1" {
if GetSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "GreetMsg") == "1" {
answer += "Приветствуем вновьприбывших участников чатика"
} else {
answer += "Не приветствуем вновьприбывших участников чатика"
@@ -407,14 +410,14 @@ func Admin(msg *echotron.Update) (bool, error) {
// мы не сможем вытащить список чятиков.
switch cmdArray[2] {
case "1":
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "GreetMsg", "1"); err != nil {
if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "GreetMsg", "1"); err != nil {
return true, err
}
answer += "Будем приветствовать вновьприбывших участников чата"
case "0":
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "GreetMsg", "0"); err != nil {
if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "GreetMsg", "0"); err != nil {
return true, err
}
@@ -425,7 +428,7 @@ func Admin(msg *echotron.Update) (bool, error) {
}
case regexp.MustCompile("^(admin|админ)[[:space:]](goodbye|прощание)$").MatchString(c):
if GetSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "GoodbyeMsg") == "1" {
if GetSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "GoodbyeMsg") == "1" {
answer += "Прощаемся с ушедшими участниками чатика"
} else {
answer += "Не прощаемся с ушедшими участниками чатика"
@@ -437,14 +440,14 @@ func Admin(msg *echotron.Update) (bool, error) {
switch cmdArray[2] {
case "1":
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "GoodbyeMsg", "1"); err != nil {
if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "GoodbyeMsg", "1"); err != nil {
return true, err
}
answer += "Будем прощаться с ушедшими участниками чата"
case "0":
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "GoodbyeMsg", "0"); err != nil {
if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "GoodbyeMsg", "0"); err != nil {
return true, err
}
@@ -460,14 +463,14 @@ func Admin(msg *echotron.Update) (bool, error) {
switch cmdArray[2] {
case "1":
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "ChanMsg", "1"); err != nil {
if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "ChanMsg", "1"); err != nil {
return true, err
}
answer += "Будем удалять сообщения, написанные от имени канала"
case "0":
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "ChanMsg", "0"); err != nil {
if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "ChanMsg", "0"); err != nil {
return true, err
}

View File

@@ -1,7 +1,7 @@
package tg
import (
"fmt"
"strconv"
"github.com/NicoNex/echotron/v3"
)
@@ -10,10 +10,13 @@ import (
// Непотребными могут быть аудиосообщения, аудиофайлы, видеосообщения, сообщения от имени других каналов итп.
// Это могут настроить админы чятика через команду !admin censor.
func Censor(msg *echotron.Update) bool {
result := false
var (
result bool
chatID = msg.ChatID()
)
switch {
case GetSetting(fmt.Sprintf("%d", msg.ChatID()), "VoiceMsg") == "1":
case GetSetting(strconv.FormatInt(chatID, 10), "VoiceMsg") == "1":
result = true
// Предполагаем, что у voice-ов здесь всегда не ноль.
@@ -23,7 +26,7 @@ func Censor(msg *echotron.Update) bool {
result = true
}
case GetSetting(fmt.Sprintf("%d", msg.ChatID()), "AudioMsg") == "1":
case GetSetting(strconv.FormatInt(chatID, 10), "AudioMsg") == "1":
// Предполагаем, что у аудио здесь всегда не ноль.
if msg.Message.Audio.Duration != 0 {
delMsg(msg)
@@ -31,7 +34,7 @@ func Censor(msg *echotron.Update) bool {
result = true
}
case GetSetting(fmt.Sprintf("%d", msg.ChatID()), "PhotoMsg") == "1":
case GetSetting(strconv.FormatInt(chatID, 10), "PhotoMsg") == "1":
// Обычное сообщение не содержит фоток.
if len(msg.Message.Photo) != 0 {
delMsg(msg)
@@ -39,7 +42,7 @@ func Censor(msg *echotron.Update) bool {
result = true
}
case GetSetting(fmt.Sprintf("%d", msg.ChatID()), "VideoMsg") == "1":
case GetSetting(strconv.FormatInt(chatID, 10), "VideoMsg") == "1":
// Предполагаем, что у видео здесь всегда не ноль.
if msg.Message.Video.Duration != 0 {
delMsg(msg)
@@ -47,7 +50,7 @@ func Censor(msg *echotron.Update) bool {
result = true
}
case GetSetting(fmt.Sprintf("%d", msg.ChatID()), "VideoNoteMsg") == "1":
case GetSetting(strconv.FormatInt(chatID, 10), "VideoNoteMsg") == "1":
// Предполагаем, что у видео-заметки здесь всегда не ноль.
if msg.Message.VideoNote.Duration != 0 {
delMsg(msg)
@@ -55,7 +58,7 @@ func Censor(msg *echotron.Update) bool {
result = true
}
case GetSetting(fmt.Sprintf("%d", msg.ChatID()), "AnimationMsg") == "1":
case GetSetting(strconv.FormatInt(chatID, 10), "AnimationMsg") == "1":
// Предполагаем, что у анимации здесь всегда не ноль.
if msg.Message.Animation.Duration != 0 {
delMsg(msg)
@@ -63,7 +66,7 @@ func Censor(msg *echotron.Update) bool {
result = true
}
case GetSetting(fmt.Sprintf("%d", msg.ChatID()), "StickerMsg") == "1":
case GetSetting(strconv.FormatInt(chatID, 10), "StickerMsg") == "1":
// Предполагаем, что FileID не пустое только у стикера.
if msg.Message.Sticker.FileID != "" {
delMsg(msg)
@@ -71,7 +74,7 @@ func Censor(msg *echotron.Update) bool {
result = true
}
case GetSetting(fmt.Sprintf("%d", msg.ChatID()), "DiceMsg") == "1":
case GetSetting(strconv.FormatInt(chatID, 10), "DiceMsg") == "1":
// Предполагаем, что Value > 0 только у дайса.
if msg.Message.Dice.Value != 0 {
delMsg(msg)
@@ -79,7 +82,7 @@ func Censor(msg *echotron.Update) bool {
result = true
}
case GetSetting(fmt.Sprintf("%d", msg.ChatID()), "GameMsg") == "1":
case GetSetting(strconv.FormatInt(chatID, 10), "GameMsg") == "1":
// Предполагаем, что title только у game-а.
if msg.Message.Game.Title != "" {
delMsg(msg)
@@ -87,7 +90,7 @@ func Censor(msg *echotron.Update) bool {
result = true
}
case GetSetting(fmt.Sprintf("%d", msg.ChatID()), "PollMsg") == "1":
case GetSetting(strconv.FormatInt(chatID, 10), "PollMsg") == "1":
// Предполагаем, что title только у game-а.
if msg.Message.Poll.Question != "" {
delMsg(msg)
@@ -95,7 +98,7 @@ func Censor(msg *echotron.Update) bool {
result = true
}
case GetSetting(fmt.Sprintf("%d", msg.ChatID()), "DocumentMsg") == "1":
case GetSetting(strconv.FormatInt(chatID, 10), "DocumentMsg") == "1":
// Предполагаем, что FileID есть только у document-а.
if msg.Message.Document.FileID != "" {
delMsg(msg)
@@ -107,7 +110,7 @@ func Censor(msg *echotron.Update) bool {
// пидорство он будет удалять asap.
// 136817688 - это специальный id пользователя, который принимает облик канала, на него можно нажать и попасть
// на рекламируемый канал.
case GetSetting(fmt.Sprintf("%d", msg.ChatID()), "ChanMsg") == "1":
case GetSetting(strconv.FormatInt(chatID, 10), "ChanMsg") == "1":
if msg.Message.From.ID == 136817688 {
delMsg(msg)

View File

@@ -5,6 +5,8 @@ import (
"encoding/json"
"fmt"
"regexp"
"slices"
"strconv"
"github.com/NicoNex/echotron/v3"
)
@@ -20,8 +22,8 @@ func cmdParser(me echotron.APIResponseUser, cmd *echotron.Update) (bool, error)
smsg.From = "telegram"
smsg.Plugin = "telegram"
smsg.Chatid = fmt.Sprintf("%d", cmd.Message.Chat.ID)
smsg.Userid = fmt.Sprintf("%d", cmd.Message.From.ID)
smsg.Chatid = strconv.FormatInt(cmd.Message.Chat.ID, 10)
smsg.Userid = strconv.FormatInt(cmd.Message.From.ID, 10)
smsg.Message = cmd.Message.Text
smsg.Misc.Answer = 1
@@ -60,12 +62,8 @@ func cmdParser(me echotron.APIResponseUser, cmd *echotron.Update) (bool, error)
"рыба", "рыбка", "рыбалка", "karma", "карма", "fuck", "weather", "погода", "w", "п", "погодка", "погадка",
}
for _, c := range cmds {
if command == c {
bingo = true
break
}
if slices.Contains(cmds, command) {
bingo = true
}
// Не нашлось команды в одно слово. Поищем команды с одним аргументом.
@@ -103,20 +101,14 @@ func cmdParser(me echotron.APIResponseUser, cmd *echotron.Update) (bool, error)
"dig", "копать", "fish", "fishing", "рыба", "рыбка", "рыбалка",
}
for _, c := range cmds {
// В vscode возникает ошибка при использовании if-а.
switch {
case command == c:
smsg.Misc.Username = ConstructTelegramHighlightName(cmd.Message.From)
smsg.Misc.Msgformat = 1
break //nolint: gosimple
}
if slices.Contains(cmds, command) {
smsg.Misc.Username = ConstructTelegramHighlightName(cmd.Message.From)
smsg.Misc.Msgformat = 1
}
// Если у нас супергруппа с тредами, надо проставить thread id.
if cmd.Message.Chat.Type == "supergroup" && cmd.Message.IsTopicMessage && cmd.Message.ThreadID != 0 {
smsg.ThreadID = fmt.Sprintf("%d", cmd.Message.ThreadID)
smsg.ThreadID = strconv.Itoa(cmd.Message.ThreadID)
}
// Отправляем сообщение в редиску.
@@ -129,7 +121,7 @@ func cmdParser(me echotron.APIResponseUser, cmd *echotron.Update) (bool, error)
}
// Заталкиваем наш json в редиску.
if err := RedisClient.Publish(ctx, Config.Redis.Channel, data).Err(); err != nil {
if err := RedisClient.Publish(Ctx, Config.Redis.Channel, data).Err(); err != nil {
log.Warnf("Unable to send data to redis channel %s: %s", Config.Redis.Channel, err)
} else {
log.Debugf("Sent msg to redis channel %s: %s", Config.Redis.Channel, string(data))

View File

@@ -9,37 +9,42 @@ import (
"github.com/go-redis/redis/v8"
)
// Config - это у нас глобальная штука.
var Config myConfig
var (
// Config - это у нас глобальная штука.
Config MyConfig
// To break circular message forwarding we must set some sane default, it can be overridden via config.
var forwardMax int64 = 5
// forwardMax is used to break circular message forwarding we must set some sane default, it can be overridden via config.
forwardMax int64 = 5
// Объектики клиента-редиски.
var RedisClient *redis.Client
var Subscriber *redis.PubSub
// RedisClient объектик клиента-редиски.
RedisClient *redis.Client
// Main context.
var ctx = context.Background()
// Subscriber is redis PubSub object.
Subscriber *redis.PubSub
// Ставится в true, если мы получили сигнал на выключение.
var Shutdown = false
// Ctx is main context.
Ctx = context.Background()
// Канал, в который приходят уведомления для хэндлера сигналов от траппера сигналов.
var SigChan = make(chan os.Signal, 1)
// Shutdown ставится в true, если мы получили сигнал на выключение.
Shutdown = false
// Мапка с открытыми дескрипторами баз с настройками.
var settingsDB = make(map[string]*pebble.DB)
// SigChan канал, в который приходят уведомления для хэндлера сигналов от траппера сигналов.
SigChan = make(chan os.Signal, 1)
var tg echotron.API
// settingsDB мапка с открытыми дескрипторами баз с настройками.
settingsDB = make(map[string]*pebble.DB)
// Приветсвенные фразы для новых участников чата.
var introduceGreet = [...]string{
"Дратути",
"Дарована",
"Доброе утро, день или вечер",
"Добро пожаловать в наше скромное коммунити",
"Наше вам с кисточкой тут, на канальчике",
}
// tg is var for telegram api.
tg echotron.API
// introduceGreet приветсвенные фразы для новых участников чата.
introduceGreet = [...]string{
"Дратути",
"Дарована",
"Доброе утро, день или вечер",
"Добро пожаловать в наше скромное коммунити",
"Наше вам с кисточкой тут, на канальчике",
}
)
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */

View File

@@ -10,6 +10,7 @@ import (
// Help Выводит в чат сообщение с основными командами бота.
func Help(cmd *echotron.Update) (bool, error) {
help := "```\n"
help += fmt.Sprintf("%shelp | %sпомощь - список команд", Config.Csign, Config.Csign)
help += fmt.Sprintf("%sanek | %sанек | %sанекдот - рандомный анекдот с anekdot.ru", Config.Csign, Config.Csign, Config.Csign)
help += fmt.Sprintf("%sbuni - рандомный стрип hapi buni", Config.Csign)

View File

@@ -11,52 +11,52 @@ import (
)
// ParseConfig разбирает и валидирует даденный конфиг.
func ParseConfig(path string) (myConfig, error) {
func ParseConfig(path string) (MyConfig, error) {
fileInfo, err := os.Stat(path)
// Предполагаем, что файла либо нет, либо мы не можем его прочитать, второе надо бы логгировать, но пока забьём.
if err != nil {
return myConfig{}, err
return MyConfig{}, err
}
// Конфиг-файл длинноват для конфига, попробуем следующего кандидата.
if fileInfo.Size() > 65535 {
err := fmt.Errorf("config file %s is too long for config, skipping", path)
return myConfig{}, err
return MyConfig{}, err
}
buf, err := os.ReadFile(path)
// Не удалось прочитать.
if err != nil {
return myConfig{}, err
return MyConfig{}, err
}
// Исходя из документации, hjson какбы умеет парсить "кривой" json, но парсит его в map-ку.
// Интереснее на выходе получить структурку: то есть мы вначале конфиг преобразуем в map-ку, затем эту map-ку
// сериализуем в json, а потом json превращааем в структурку. Не очень эффективно, но он и не часто требуется.
var (
sampleConfig myConfig
tmp map[string]interface{}
sampleConfig MyConfig
tmp map[string]any
)
err = hjson.Unmarshal(buf, &tmp)
// Не удалось распарсить.
if err != nil {
return myConfig{}, err
return MyConfig{}, err
}
tmpjson, err := json.Marshal(tmp)
// Не удалось преобразовать map-ку в json.
if err != nil {
return myConfig{}, err
return MyConfig{}, err
}
if err := json.Unmarshal(tmpjson, &sampleConfig); err != nil {
return myConfig{}, err
return MyConfig{}, err
}
// Валидируем значения из конфига.
@@ -73,29 +73,29 @@ func ParseConfig(path string) (myConfig, error) {
if sampleConfig.Redis.Channel == "" {
err := fmt.Errorf("channel field in config file %s must be set", path)
return myConfig{}, err
return MyConfig{}, err
}
if sampleConfig.Redis.MyChannel == "" {
err := fmt.Errorf("my_channel field in config file %s must be set", path)
return myConfig{}, err
return MyConfig{}, err
}
if sampleConfig.Telegram.Token == "" {
return myConfig{}, fmt.Errorf("telegram.token field in config file %s must be set", path)
return MyConfig{}, fmt.Errorf("telegram.token field in config file %s must be set", path)
}
if sampleConfig.Loglevel == "" {
sampleConfig.Loglevel = "info"
}
// sampleConfig.Log = "" if not set
// sampleConfig.Log = "" if not set..
if sampleConfig.Csign == "" {
err := fmt.Errorf("csign field in config file %s must be set", path)
return myConfig{}, err
return MyConfig{}, err
}
if sampleConfig.ForwardsMax == 0 {
@@ -103,7 +103,7 @@ func ParseConfig(path string) (myConfig, error) {
}
if sampleConfig.DataDir == "" {
return myConfig{}, fmt.Errorf("data_dir field in config file %s must be set", path)
return MyConfig{}, fmt.Errorf("data_dir field in config file %s must be set", path)
}
return sampleConfig, err

View File

@@ -26,7 +26,7 @@ func RedisMsgParser(msg string) {
return
}
// Validate our j
// Validate our j.
if exist := j.From; exist == "" {
log.Warnf("Incorrect msg from redis, no from field: %s", msg)
@@ -63,7 +63,7 @@ func RedisMsgParser(msg string) {
return
}
// j.Misc.Answer может и не быть, тогда ответа на такое сообщение не будет
// j.Misc.Answer может и не быть, тогда ответа на такое сообщение не будет.
if j.Misc.Answer == 0 {
log.Debug("Field Misc->Answer = 0, skipping message")
@@ -76,14 +76,14 @@ func RedisMsgParser(msg string) {
j.Misc.Csign = Config.Csign
}
// j.Misc.FwdCnt если нам его не передали, то будет 0
// j.Misc.FwdCnt если нам его не передали, то будет 0.
if exist := j.Misc.Fwdcnt; exist == 0 {
j.Misc.Fwdcnt = 1
}
// j.Misc.GoodMorning может быть 1 или 0, по-умолчанию 0
// j.Misc.MsgFormat может быть 1 или 0, по-умолчанию 0
// j.Misc.Username можно не передавать, тогда будет пустая строка
// j.Misc.GoodMorning может быть 1 или 0, по-умолчанию 0.
// j.Misc.MsgFormat может быть 1 или 0, по-умолчанию 0.
// j.Misc.Username можно не передавать, тогда будет пустая строка.
// Отвалидировались, теперь вернёмся к нашим баранам.

View File

@@ -52,7 +52,7 @@ func GetSetting(chatID string, setting string) string {
chatHash := sha256.Sum256([]byte(chatID))
database := fmt.Sprintf("settings_db/%x", chatHash)
// Если БД не открыта, откроем её
// Если БД не открыта, откроем её.
if _, ok := settingsDB[database]; !ok {
var options pebble.Options
// По дефолту ограничение ставится на мегабайты данных, а не на количество файлов, поэтому с дефолтными

View File

@@ -30,7 +30,7 @@ func SigHandler() {
Shutdown = true
// Отпишемся от всех каналов и закроем коннект к редиске.
if err = Subscriber.Unsubscribe(ctx); err != nil {
if err = Subscriber.Unsubscribe(Ctx); err != nil {
log.Errorf("Unable to unsubscribe from redis channels cleanly: %s", err)
} else {
log.Debug("Unsubscribe from all redis channels")

View File

@@ -2,6 +2,7 @@ package tg
import (
"fmt"
"strconv"
"aleesa-telegram-go/internal/log"
@@ -9,7 +10,7 @@ import (
)
// Telega основная горутинка, реализующая бота.
func Telega(c myConfig) {
func Telega(c MyConfig) {
tg = echotron.NewAPI(c.Telegram.Token)
for u := range echotron.PollingUpdates(c.Telegram.Token) {
@@ -86,11 +87,11 @@ func ConstructPartialUserUsername(u *echotron.User) string {
return u.LastName
default:
return fmt.Sprintf("%d", u.ID)
return strconv.FormatInt(u.ID, 10)
}
}
// ConstructPartialUserUsername пытается найти и вытащить username, если такового нет, вытаскивает First/Last Name, если
// ConstructPartialChatUsername пытается найти и вытащить username, если такового нет, вытаскивает First/Last Name, если
// такового нет, то возвращает ID.
func ConstructPartialChatUsername(c *echotron.Chat) string {
switch {
@@ -107,7 +108,7 @@ func ConstructPartialChatUsername(c *echotron.Chat) string {
return c.LastName
default:
return fmt.Sprintf("%d", c.ID)
return strconv.FormatInt(c.ID, 10)
}
}
@@ -126,7 +127,7 @@ func ConstructUserFirstLastName(u *echotron.User) string {
case u.Username != "":
user = fmt.Sprintf("@%s", u.Username)
default:
user = fmt.Sprintf("%d", u.ID)
user = strconv.FormatInt(u.ID, 10)
}
return user

View File

@@ -5,6 +5,7 @@ import (
"fmt"
"math/rand/v2"
"regexp"
"strconv"
"strings"
"aleesa-telegram-go/internal/log"
@@ -21,10 +22,9 @@ func telegramMsgParser(msg *echotron.Update) {
}
// Сообщение о том, что этот чятик изменился, например, превратился в супергруппу.
if (msg.Message.MigrateFromChatID < 0) && (msg.Message.MigrateToChatID < 0) { //nolint: revive,staticcheck
if (msg.Message.MigrateFromChatID < 0) && (msg.Message.MigrateToChatID < 0) {
// TODO: поддержать миграцию настроек чата на новый chatId.
// TODO: подумать, что можно сделать с настройками бэкэндов. Кажись, ничего, но надо глянуть.
return
}
@@ -56,16 +56,17 @@ func telegramMsgParser(msg *echotron.Update) {
}
// Люди пришли в чят.
if msg.Message.NewChatMembers != nil { //nolint: revive,staticcheck
if msg.Message.NewChatMembers != nil {
// TODO: проверить, надо ли приветствовать, выбрать одну из приветственных фраз, потянуть время, типа, мы пишем
// текст и поприветствовать вновь прибывшего.
if GetSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "GreetMsg") == "1" {
if GetSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "GreetMsg") == "1" {
// Пользователи, которых мы приветсвуем, одной строкой.
var users string
if len(msg.Message.NewChatMembers) > 1 {
lastOne := msg.Message.NewChatMembers[len(msg.Message.NewChatMembers)-1]
theRest := msg.Message.NewChatMembers[:len(msg.Message.NewChatMembers)-1]
var usersSlice []string
for _, user := range theRest {
@@ -73,6 +74,7 @@ func telegramMsgParser(msg *echotron.Update) {
}
users = strings.Join(usersSlice, ", ")
users += fmt.Sprintf(" and %s", ConstructTelegramHighlightName(lastOne))
} else {
users = ConstructTelegramHighlightName(msg.Message.NewChatMembers[0])
@@ -88,7 +90,7 @@ func telegramMsgParser(msg *echotron.Update) {
resp, err := tg.SendChatAction(
echotron.Typing,
msg.Message.From.ID,
&echotron.ChatActionOptions{MessageThreadID: int(msg.Message.ThreadID)},
&echotron.ChatActionOptions{MessageThreadID: msg.Message.ThreadID},
)
// TODO: вероятно, ошибку надо засылать в обработчик ошибок, так как в ней может быть миграция чятика или
@@ -123,7 +125,7 @@ func telegramMsgParser(msg *echotron.Update) {
// Человек ушёл из чата.
if msg.Message.LeftChatMember != nil {
if GetSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "GoodbyeMsg") == "1" {
if GetSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "GoodbyeMsg") == "1" {
user := ConstructUserFirstLastName(msg.Message.From)
goodbye := fmt.Sprintf("Прощаемся с %s", user)
@@ -160,8 +162,8 @@ func telegramMsgParser(msg *echotron.Update) {
rmsg.Misc.Answer = 1
// Засылаем фразу в misc-канал (в роутер).
rmsg.Chatid = fmt.Sprintf("%d", msg.Message.Chat.ID)
rmsg.Userid = fmt.Sprintf("%d", msg.Message.From.ID)
rmsg.Chatid = strconv.FormatInt(msg.Message.Chat.ID, 10)
rmsg.Userid = strconv.FormatInt(msg.Message.From.ID, 10)
rmsg.Message = msg.Message.Text
rmsg.Mode = "private"
rmsg.Plugin = "telegram"
@@ -198,7 +200,7 @@ func telegramMsgParser(msg *echotron.Update) {
}
// Заталкиваем наш json в редиску.
if err := RedisClient.Publish(ctx, Config.Redis.Channel, data).Err(); err != nil {
if err := RedisClient.Publish(Ctx, Config.Redis.Channel, data).Err(); err != nil {
log.Warnf("Unable to send data to redis channel %s: %s", Config.Redis.Channel, err)
} else {
log.Debugf("Sent msg to redis channel %s: %s", Config.Redis.Channel, string(data))
@@ -209,7 +211,7 @@ func telegramMsgParser(msg *echotron.Update) {
// Обрабатываем сообщения для групп и супергрупп. По сути это примерно одинаковые вещи/сущности.
// Цензор удаляет сообщения из чятика, если они "неправильные" - от имени других каналов, аудиосообщения,
// содержат картинки, видео аудио итп, это настраивается через команду !admin censor.
if GetSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "censor") == "1" {
if GetSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "censor") == "1" {
// Если цензор отработал, больше делать нечего, сообщения уже нету.
if Censor(msg) {
return
@@ -239,7 +241,7 @@ func telegramMsgParser(msg *echotron.Update) {
return
}
// Здесь же, если фраза обращена к боту (ник или имя), выставляем флажок, что надо ответить
// Здесь же, если фраза обращена к боту (ник или имя), выставляем флажок, что надо ответить.
quotedBotUsername := regexp.QuoteMeta(me.Result.Username)
quotedBotFirstName := regexp.QuoteMeta(me.Result.FirstName)
quotedBotLastName := regexp.QuoteMeta(me.Result.LastName)
@@ -250,7 +252,9 @@ func telegramMsgParser(msg *echotron.Update) {
return
} else if messageContainsBotUsername {
rmsg.Misc.Answer = 1
m := regexp.MustCompile(quotedBotUsername)
msg.Message.Text = m.ReplaceAllString(msg.Message.Text, "")
}
@@ -273,7 +277,9 @@ func telegramMsgParser(msg *echotron.Update) {
if messageContainsBotName {
rmsg.Misc.Answer = 1
m := regexp.MustCompile(quotedBotFirstName + "[[:space:]]+" + quotedBotLastName)
msg.Message.Text = m.ReplaceAllString(msg.Message.Text, "")
}
// Если у нас есть только имя, то попробуем его вырезать из входящей фразы.
@@ -288,7 +294,9 @@ func telegramMsgParser(msg *echotron.Update) {
if messageContainsBotName {
rmsg.Misc.Answer = 1
m := regexp.MustCompile(quotedBotFirstName)
msg.Message.Text = m.ReplaceAllString(msg.Message.Text, "")
}
// Если у нас есть только фамилия, то попробуем его вырезать из входящей фразы.
@@ -303,14 +311,16 @@ func telegramMsgParser(msg *echotron.Update) {
if messageContainsBotName {
rmsg.Misc.Answer = 1
m := regexp.MustCompile(quotedBotLastName)
msg.Message.Text = m.ReplaceAllString(msg.Message.Text, "")
}
}
// Засылаем фразу в misc-канал (в роутер).
rmsg.Chatid = fmt.Sprintf("%d", msg.Message.Chat.ID)
rmsg.Userid = fmt.Sprintf("%d", msg.Message.From.ID)
rmsg.Chatid = strconv.FormatInt(msg.Message.Chat.ID, 10)
rmsg.Userid = strconv.FormatInt(msg.Message.From.ID, 10)
rmsg.Message = msg.Message.Text
rmsg.Mode = "public"
rmsg.Plugin = "telegram"
@@ -335,7 +345,7 @@ func telegramMsgParser(msg *echotron.Update) {
}
// Заталкиваем наш json в редиску.
if err := RedisClient.Publish(ctx, Config.Redis.Channel, data).Err(); err != nil {
if err := RedisClient.Publish(Ctx, Config.Redis.Channel, data).Err(); err != nil {
log.Warnf("Unable to send data to redis channel %s: %s", Config.Redis.Channel, err)
} else {
log.Debugf("Sent msg to redis channel %s: %s", Config.Redis.Channel, string(data))

View File

@@ -1,68 +1,71 @@
package tg
// myConfig структура, описывающая формат Конфига.
type myConfig struct {
Redis struct {
Server string `json:"server,omitempty"`
Port int `json:"port,omitempty"`
Channel string `json:"channel,omitempty"`
MyChannel string `json:"my_channel,omitempty"`
} `json:"redis"`
type (
// MyConfig структура, описывающая формат Конфига.
MyConfig struct {
Csign string `json:"csign,omitempty"`
DataDir string `json:"data_dir,omitempty"`
Log string `json:"log,omitempty"`
Loglevel string `json:"loglevel,omitempty"`
Telegram struct {
Token string `json:"token,omitempty"`
Telegram struct {
Token string `json:"token,omitempty"`
} `json:"telegram"`
Redis struct {
Channel string `json:"channel,omitempty"`
MyChannel string `json:"my_channel,omitempty"`
Server string `json:"server,omitempty"`
Port int `json:"port,omitempty"`
} `json:"redis"`
ForwardsMax int64 `json:"forwards_max,omitempty"`
}
Loglevel string `json:"loglevel,omitempty"`
Log string `json:"log,omitempty"`
Csign string `json:"csign,omitempty"`
ForwardsMax int64 `json:"forwards_max,omitempty"`
DataDir string `json:"data_dir,omitempty"`
}
// rMsg структура, описывающая формат входящего сообщения из pubsub-канала redis-ки.
rMsg struct {
From string `json:"from,omitempty"`
Chatid string `json:"chatid,omitempty"`
Userid string `json:"userid,omitempty"`
ThreadID string `json:"threadid,omitempty"`
Message string `json:"message,omitempty"`
Plugin string `json:"plugin,omitempty"`
Mode string `json:"mode,omitempty"`
Misc struct {
Botnick string `json:"bot_nick,omitempty"`
Csign string `json:"csign,omitempty"`
// Судя по перловой реализации, сюда всегда попадает [$fullname](tg://user?id=$userid).
// Согласно докуметации, и перловой реализации бота, мы проставляем это поле тогда, когда мы точно знаем, что в
// бэкэнде оно используется.
Username string `json:"username,omitempty"`
Answer int64 `json:"answer,omitempty"`
Fwdcnt int64 `json:"fwd_cnt,omitempty"`
GoodMorning int64 `json:"good_morning,omitempty"`
// В этом поле мы пишем 1, если бэкэнд должен в ответе форматировать сообщение в MarkdownV2, что какбэ
// не оч хорошо, так как мы выносим особенности реализации бэкэнда сюда.
Msgformat int64 `json:"msg_format,omitempty"`
} `json:"Misc"`
}
// rMsg структура, описывающая формат входящего сообщения из pubsub-канала redis-ки.
type rMsg struct {
From string `json:"from,omitempty"`
Chatid string `json:"chatid,omitempty"`
Userid string `json:"userid,omitempty"`
ThreadID string `json:"threadid,omitempty"`
Message string `json:"message,omitempty"`
Plugin string `json:"plugin,omitempty"`
Mode string `json:"mode,omitempty"`
Misc struct {
Answer int64 `json:"answer,omitempty"`
Botnick string `json:"bot_nick,omitempty"`
Csign string `json:"csign,omitempty"`
Fwdcnt int64 `json:"fwd_cnt,omitempty"`
GoodMorning int64 `json:"good_morning,omitempty"`
// В этом поле мы пишем 1, если бэкэнд должен в ответе форматировать сообщение в MarkdownV2, что какбэ
// не оч хорошо, так как мы выносим особенности реализации бэкэнда сюда.
Msgformat int64 `json:"msg_format,omitempty"`
// Судя по перловой реализации, сюда всегда попадает [$fullname](tg://user?id=$userid).
// Согласно докуметации, и перловой реализации бота, мы проставляем это поле тогда, когда мы точно знаем, что в
// бэкэнде оно используется.
Username string `json:"username,omitempty"`
} `json:"Misc"`
}
// sMsg структура, описывающая исходящее сообщение в pubsub-канал redis-ки.
type sMsg struct {
From string `json:"from"`
Chatid string `json:"chatid"`
Userid string `json:"userid"`
ThreadID string `json:"threadid,omitempty"`
Message string `json:"message"`
Plugin string `json:"plugin"`
Mode string `json:"mode"`
Misc struct {
Answer int64 `json:"answer"`
Botnick string `json:"bot_nick"`
Csign string `json:"csign"`
Fwdcnt int64 `json:"fwd_cnt"`
GoodMorning int64 `json:"good_morning"`
Msgformat int64 `json:"msg_format"`
Username string `json:"username"`
} `json:"misc"`
}
// sMsg структура, описывающая исходящее сообщение в pubsub-канал redis-ки.
sMsg struct {
From string `json:"from"`
Chatid string `json:"chatid"`
Userid string `json:"userid"`
ThreadID string `json:"threadid,omitempty"`
Message string `json:"message"`
Plugin string `json:"plugin"`
Mode string `json:"mode"`
Misc struct {
Botnick string `json:"bot_nick"`
Csign string `json:"csign"`
Username string `json:"username"`
Answer int64 `json:"answer"`
Fwdcnt int64 `json:"fwd_cnt"`
GoodMorning int64 `json:"good_morning"`
Msgformat int64 `json:"msg_format"`
} `json:"misc"`
}
)
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */