Merge pull request #1 from watn3y/v1.2
Some checks failed
Build and Push to Docker Hub on push to any branch / docker (push) Has been cancelled

V1.2
This commit is contained in:
Noah 2025-08-31 04:15:13 +02:00 committed by GitHub
commit c6c7480493
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 53 additions and 39 deletions

View file

@ -27,15 +27,18 @@ services:
## Running on Linux ## Running on Linux
Grab a release from the [releases page](https://github.com/watn3y/steamsalty/releases). Make sure to set your **environment variables** accordingly. Grab a release from the [releases page](https://github.com/watn3y/steamsalty/releases). Make sure to set your [environment variables](#environment-variables) accordingly.
## Environment Variables ## Environment Variables
| Variable | Description | Default | > [!NOTE]
| ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------| ------------------ | > For development purposes, SteamSalty supports loading environment variables from a .env file placed in the project root directory.
| `STEAMSALTY_LOGLEVEL` | LogLevel as described [in the zerolog documentation](https://pkg.go.dev/github.com/rs/zerolog@v1.34.0#readme-simple-leveled-logging-example) | 1 (Info) |
| `STEAMSALTY_TELEGRAMAPITOKEN` | Telegram Bot Token, get from @BotFather | None, **required** | | Variable | Description | Default | Required |
| `STEAMSALTY_STEAMAPIKEY` | Steam API Key, get from [steamcommunity.com/dev/apikey](https://steamcommunity.com/dev/apikey) | None, **required** | | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------| ------------------ | -------------- |
| `STEAMSALTY_CHATID` | Chat to notify about new Comments | None, **required** | | `STEAMSALTY_LOGLEVEL` | LogLevel as described [in the zerolog documentation](https://pkg.go.dev/github.com/rs/zerolog@v1.34.0#readme-simple-leveled-logging-example) | 1 (Info) | ❌ |
| `STEAMSALTY_WATCHERS` | SteamIDs (in SteamID64 format) to check for new Profile Comments | None, **required** | | `STEAMSALTY_TELEGRAMAPITOKEN` | Telegram BotToken, get it from [@BotFather on Telegram](https://t.me/BotFather) | None | ✅ |
| `STEAMSALTY_SLEEPINTERVAL` | Amount of time to wait between requests to Steam in seconds | 60 | | `STEAMSALTY_STEAMAPIKEY` | Steam API Key, get it from [steamcommunity.com/dev/apikey](https://steamcommunity.com/dev/apikey) | None | ✅ |
| `STEAMSALTY_CHATID` | Chat to notify about new comments | None | ✅ |
| `STEAMSALTY_WATCHERS` | SteamIDs (in SteamID64 format) to check for new profile comments | None | ✅ |
| `STEAMSALTY_SLEEPINTERVAL` | Amount of time to wait between requests to Steam in seconds | 60 | ❌ |

6
bot.go
View file

@ -18,10 +18,10 @@ func bot() {
go steam.StartWatchers(bot) go steam.StartWatchers(bot)
for update := range updates { for update := range updates {
log.Debug().Interface("update", update).Msg("Received update") log.Debug().Interface("update", update).Msg("Update received")
if update.Message == nil || update.Message.Text == "" { if update.Message == nil || update.Message.Text == "" {
log.Debug().Int("UpdateID", update.UpdateID).Msg("Unable to parse update") log.Debug().Int("UpdateID", update.UpdateID).Msg("Failed to parse update")
continue continue
} }
if update.Message.Time().UTC().Unix() < time.Now().UTC().Unix() { if update.Message.Time().UTC().Unix() < time.Now().UTC().Unix() {
@ -30,7 +30,7 @@ func bot() {
} }
if update.Message.IsCommand() { if update.Message.IsCommand() {
log.Info().Int64("ChatID", update.Message.Chat.ID).Int64("UserID", update.Message.From.ID).Str("Text", update.Message.Text).Msg("Received Command") log.Info().Int64("ChatID", update.Message.Chat.ID).Int64("UserID", update.Message.From.ID).Str("Text", update.Message.Text).Msg("Command received")
commands.Commands(update, bot) commands.Commands(update, bot)
} }
} }

View file

@ -9,7 +9,7 @@ import (
func Authenticate() (tgbotapi.UpdatesChannel, *tgbotapi.BotAPI) { func Authenticate() (tgbotapi.UpdatesChannel, *tgbotapi.BotAPI) {
bot, err := tgbotapi.NewBotAPI(config.BotConfig.TelegramAPIToken) bot, err := tgbotapi.NewBotAPI(config.BotConfig.TelegramAPIToken)
if err != nil { if err != nil {
log.Panic().Err(err).Msg("Failed to authenticate") log.Panic().Err(err).Msg("Failed to authenticate to Telegram")
} }
bot.Debug = false bot.Debug = false
@ -20,7 +20,7 @@ func Authenticate() (tgbotapi.UpdatesChannel, *tgbotapi.BotAPI) {
updates := tgbotapi.NewUpdate(0) updates := tgbotapi.NewUpdate(0)
updates.Timeout = 60 updates.Timeout = 60
log.Info().Int64("ID", bot.Self.ID).Str("username", bot.Self.UserName).Msg("Authenticated to Telegram API") log.Info().Int64("ID", bot.Self.ID).Str("username", bot.Self.UserName).Msg("Authenticated to Telegram successfully")
return bot.GetUpdatesChan(updates), bot return bot.GetUpdatesChan(updates), bot

View file

@ -12,7 +12,7 @@ func SendMessage(message tgbotapi.MessageConfig, bot *tgbotapi.BotAPI) (result t
return return
} }
log.Info().Int64("chat", result.Chat.ID).Str("msg", result.Text).Msg("Sent message") log.Info().Int64("chat", result.Chat.ID).Str("msg", result.Text).Msg("Sent message successfully")
log.Debug().Interface("msg", result).Msg("") log.Debug().Interface("msg", result).Msg("")
return result return result
@ -25,7 +25,7 @@ func EditMessage(message tgbotapi.EditMessageTextConfig, bot *tgbotapi.BotAPI) (
return return
} }
log.Info().Int64("chat", result.Chat.ID).Str("msg", result.Text).Msg("Edited message") log.Info().Int64("chat", result.Chat.ID).Str("msg", result.Text).Msg("Edited message successfully")
log.Debug().Interface("msg", result).Msg("") log.Debug().Interface("msg", result).Msg("")
return result return result
@ -38,7 +38,7 @@ func SendVideo(message tgbotapi.VideoConfig, bot *tgbotapi.BotAPI) (result tgbot
return return
} }
log.Info().Int64("chat", result.Chat.ID).Msg("Sent video") log.Info().Int64("chat", result.Chat.ID).Msg("Sent video successfully")
log.Debug().Interface("video", result).Msg("") log.Debug().Interface("video", result).Msg("")
return result return result
@ -51,7 +51,7 @@ func SendPhoto(message tgbotapi.PhotoConfig, bot *tgbotapi.BotAPI) (result tgbot
return return
} }
log.Info().Int64("chat", result.Chat.ID).Msg("Sent photo") log.Info().Int64("chat", result.Chat.ID).Msg("Sent photo successfully")
log.Debug().Interface("photo", result).Msg("") log.Debug().Interface("photo", result).Msg("")
return result return result
@ -64,7 +64,7 @@ func SendSticker(message tgbotapi.StickerConfig, bot *tgbotapi.BotAPI) (result t
return return
} }
log.Info().Int64("chat", result.Chat.ID).Msg("Sent sticker") log.Info().Int64("chat", result.Chat.ID).Msg("Sent sticker successfully")
log.Debug().Interface("sticker", result).Msg("") log.Debug().Interface("sticker", result).Msg("")
return result return result

View file

@ -22,11 +22,11 @@ func SetBotCommands(bot *tgbotapi.BotAPI) {
result, err := bot.Request(commands) result, err := bot.Request(commands)
if err != nil { if err != nil {
log.Error().Err(err).Msg("Failed to set own commands") log.Error().Err(err).Msg("Failed to publish commands to Telegram")
return return
} }
log.Debug().Interface("commands", result).Msg("Set own commands") log.Debug().Interface("commands", result).Msg("Published commands to Telegram successfully")
} }
func Commands(update tgbotapi.Update, bot *tgbotapi.BotAPI) { func Commands(update tgbotapi.Update, bot *tgbotapi.BotAPI) {

View file

@ -5,18 +5,26 @@ import (
"github.com/rs/zerolog" "github.com/rs/zerolog"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/joho/godotenv"
envconfig "github.com/sethvargo/go-envconfig" envconfig "github.com/sethvargo/go-envconfig"
) )
var BotConfig config var BotConfig config
func LoadConfig() { func LoadConfig() {
if err := godotenv.Load(); err != nil {
log.Info().Err(err).Msg("Failed to load .env file, using the system environment")
} else {
log.Info().Err(err).Msg(".env file loaded successfully")
}
if err := envconfig.Process(context.Background(), &BotConfig); err != nil { if err := envconfig.Process(context.Background(), &BotConfig); err != nil {
log.Panic().Err(err).Msg("Error parsing config from env variables") log.Panic().Err(err).Msg("Failed to parse config from env variables")
} }
zerolog.SetGlobalLevel(zerolog.Level(BotConfig.LogLevel)) zerolog.SetGlobalLevel(zerolog.Level(BotConfig.LogLevel))
log.Info().Msg("Loaded config") log.Info().Msg("Config loaded successfully")
log.Debug().Interface("config", BotConfig).Msg("") log.Debug().Interface("config", BotConfig).Msg("")
} }

1
go.mod
View file

@ -12,6 +12,7 @@ require (
require ( require (
github.com/andybalholm/cascadia v1.3.3 // indirect github.com/andybalholm/cascadia v1.3.3 // indirect
github.com/joho/godotenv v1.5.1 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
golang.org/x/net v0.43.0 // indirect golang.org/x/net v0.43.0 // indirect

2
go.sum
View file

@ -10,6 +10,8 @@ github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=

View file

@ -38,6 +38,6 @@ func configureLogger() {
output := zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.DateTime} output := zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.DateTime}
log.Logger = zerolog.New(output).With().Timestamp().Caller().Logger() log.Logger = zerolog.New(output).With().Timestamp().Caller().Logger()
log.Info().Msg("Started Logger") log.Info().Msg("Logger started successfully")
} }

View file

@ -11,8 +11,8 @@ func GetPlayerDetails(steamID uint64) (summary steamapi.PlayerSummary) {
response, err := steamapi.GetPlayerSummaries([]uint64{steamID}, config.BotConfig.SteamAPIKey) response, err := steamapi.GetPlayerSummaries([]uint64{steamID}, config.BotConfig.SteamAPIKey)
if err != nil { if err != nil {
log.Error().Err(err).Msg("Failed to get Player Summary") log.Error().Err(err).Msg("Failed to retrive player summary")
} }
log.Debug().Interface("Player", response[0]).Msg("Got PlayerSummary from Steam API") log.Debug().Interface("Player", response[0]).Msg("Retrived player summary from SteamAPI successfully")
return response[0] return response[0]
} }

View file

@ -19,7 +19,7 @@ func GetComments(steamID uint64, start int, count int) (page CommentsPage) {
url, err := url.Parse(baseURL + strconv.FormatUint(steamID, 10)) url, err := url.Parse(baseURL + strconv.FormatUint(steamID, 10))
if err != nil { if err != nil {
log.Error().Err(err).Msg("Unable to Parse SteamID into URL") log.Error().Err(err).Msg("Failed to parse SteamID into URL")
return return
} }
@ -30,7 +30,7 @@ func GetComments(steamID uint64, start int, count int) (page CommentsPage) {
resp, err := http.Get(url.String()) resp, err := http.Get(url.String())
if err != nil || resp.StatusCode != http.StatusOK { if err != nil || resp.StatusCode != http.StatusOK {
log.Error().Err(err).Int("Response Code", resp.StatusCode).Msg("Failed to get Comments") log.Error().Err(err).Int("Response Code", resp.StatusCode).Msg("Failed to retrieve comments")
} }
defer resp.Body.Close() defer resp.Body.Close()
@ -38,16 +38,16 @@ func GetComments(steamID uint64, start int, count int) (page CommentsPage) {
// Read the response body // Read the response body
body, err := io.ReadAll(resp.Body) body, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
log.Error().Err(err).Msg("Failed to parse Comments") log.Error().Err(err).Msg("Failed to parse comments")
log.Trace().Interface("Body", resp.Body) log.Trace().Interface("Body", resp.Body)
} }
err = json.Unmarshal(body, &page) err = json.Unmarshal(body, &page)
if err != nil { if err != nil {
log.Error().Err(err).Msg("Failed to parse Comments as JSON") log.Error().Err(err).Msg("Failed to parse comments as JSON")
} }
log.Trace().Interface("CommentPage", page).Uint64("ProfileID", steamID).Msg("Got Comment Page") log.Trace().Interface("CommentPage", page).Uint64("ProfileID", steamID).Msg("Retrieved Comment Page successfully")
return page return page
} }
@ -56,7 +56,7 @@ func parseComments(rawComments CommentsPage) (comments []Comment) {
doc, err := goquery.NewDocumentFromReader(strings.NewReader(rawComments.CommentsHTML)) doc, err := goquery.NewDocumentFromReader(strings.NewReader(rawComments.CommentsHTML))
if err != nil { if err != nil {
log.Error().Err(err).Msg("Error while parsing CommentsHTML") log.Error().Err(err).Msg("Failed to parse CommentsHTML")
return return
} }
doc.Find(".commentthread_comment.responsive_body_text").Each(func(i int, s *goquery.Selection) { doc.Find(".commentthread_comment.responsive_body_text").Each(func(i int, s *goquery.Selection) {
@ -64,14 +64,14 @@ func parseComments(rawComments CommentsPage) (comments []Comment) {
parsedID, err := strconv.ParseUint(strings.TrimPrefix(s.AttrOr("id", ""), "comment_"), 10, 64) parsedID, err := strconv.ParseUint(strings.TrimPrefix(s.AttrOr("id", ""), "comment_"), 10, 64)
if err != nil { if err != nil {
log.Error().Err(err).Msg("Error while parsing Comment ID") log.Error().Err(err).Msg("Failed to parse Comment ID")
return return
} }
c.ID = parsedID c.ID = parsedID
c.Timestamp, err = strconv.ParseInt(s.Find(".commentthread_comment_timestamp").AttrOr("data-timestamp", ""), 10, 64) c.Timestamp, err = strconv.ParseInt(s.Find(".commentthread_comment_timestamp").AttrOr("data-timestamp", ""), 10, 64)
if err != nil { if err != nil {
log.Error().Err(err).Msg("Error while parsing Comment Timestamp") log.Error().Err(err).Msg("Failed to parse Comment Timestamp")
return return
} }
@ -85,6 +85,6 @@ func parseComments(rawComments CommentsPage) (comments []Comment) {
}) })
slices.Reverse(comments) slices.Reverse(comments)
log.Trace().Interface("Comments", comments).Msg("Parsed Comment Page") log.Trace().Interface("Comments", comments).Msg("Parsed comment page successfully")
return comments return comments
} }

View file

@ -33,7 +33,7 @@ func StartWatchers(bot *tgbotapi.BotAPI) {
} }
func watcher(bot *tgbotapi.BotAPI, steamID uint64, sleeptime time.Duration) { func watcher(bot *tgbotapi.BotAPI, steamID uint64, sleeptime time.Duration) {
log.Info().Uint64("SteamID", steamID).Msg("Started Watcher") log.Info().Uint64("SteamID", steamID).Msg("Starting Watcher")
var newestProcessedComment int64 = 0 var newestProcessedComment int64 = 0
@ -56,9 +56,9 @@ func watcher(bot *tgbotapi.BotAPI, steamID uint64, sleeptime time.Duration) {
profileOwner := GetPlayerDetails(steamID) profileOwner := GetPlayerDetails(steamID)
for _, comment := range parseComments(currentCommentsPage) { for _, comment := range parseComments(currentCommentsPage) {
log.Debug().Interface("Comment", comment).Msg("Processing Comment") log.Debug().Interface("Comment", comment).Msg("Processing comment")
if comment.Timestamp <= newestProcessedComment { if comment.Timestamp <= newestProcessedComment {
log.Debug().Uint64("CommentID", comment.ID).Msg("Skipping Comment") log.Debug().Uint64("CommentID", comment.ID).Msg("Skipping comment")
continue continue
} }
@ -69,7 +69,7 @@ func watcher(bot *tgbotapi.BotAPI, steamID uint64, sleeptime time.Duration) {
Text: fmt.Sprintf(`<b><a href="%s">%s</a> just commented on <a href="%s">%s</a>'s profile:</b>`, comment.AuthorProfileURL, comment.Author, profileOwner.ProfileURL, profileOwner.PersonaName) + "\n" + Text: fmt.Sprintf(`<b><a href="%s">%s</a> just commented on <a href="%s">%s</a>'s profile:</b>`, comment.AuthorProfileURL, comment.Author, profileOwner.ProfileURL, profileOwner.PersonaName) + "\n" +
"<blockquote>" + comment.Text + "</blockquote>", "<blockquote>" + comment.Text + "</blockquote>",
} }
log.Info().Interface("Comment", comment).Msg("Notifying about new Comment") log.Info().Interface("Comment", comment).Msg("Notifying about new comment")
botIO.SendMessage(msg, bot) botIO.SendMessage(msg, bot)
time.Sleep(time.Minute / 20) time.Sleep(time.Minute / 20)
} }