- Fixed SLEEPTIME not working (always 0)
- Refined logging
- Added metadata to /info command
- Bot now automatically sets own commands for autocompletion
This commit is contained in:
Noah 2024-12-22 07:13:19 +01:00
parent eb0482b8e0
commit 6be403fc58
14 changed files with 116 additions and 53 deletions

View file

@ -27,7 +27,7 @@ jobs:
- name: Build and push - name: Build and push
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
with: with:
platforms: linux/amd64,linux/386,linux/arm64,linux/arm/v7 platforms: linux/amd64,linux/386,linux/arm64,linux/arm/v7,linux/riscv64
push: true push: true
tags: | tags: |
${{ github.repository }}:${{ github.ref_name }} ${{ github.repository }}:${{ github.ref_name }}

3
.gitignore vendored
View file

@ -23,3 +23,6 @@ go.work.sum
# env file # env file
.env .env
# Build ouput
build/

View file

@ -9,7 +9,7 @@ RUN go mod download
COPY . . COPY . .
ARG TARGETOS TARGETARCH ARG TARGETOS TARGETARCH
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o /app/steamsalty RUN GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o ./steamsalty
@ -19,6 +19,6 @@ WORKDIR /app
COPY --from=builder /etc/ssl/certs /etc/ssl/certs COPY --from=builder /etc/ssl/certs /etc/ssl/certs
COPY --from=builder /usr/share/ca-certificates /usr/share/ca-certificates COPY --from=builder /usr/share/ca-certificates /usr/share/ca-certificates
COPY --from=builder /app/steamsalty /app/steamsalty COPY --from=builder /steamsalty/steamsalty /app/steamsalty
ENTRYPOINT ["/app/steamsalty"] ENTRYPOINT ["/app/steamsalty"]

View file

@ -1,2 +1,40 @@
# steamsalty # SteamSalty
Get notifications about Steam Comments on Telegram SteamSalty notifies you on telegram about new comments on any steam profile.
## Running with Docker Compose
Docker image: https://hub.docker.com/r/watn3y/steamsalty
Example compose file:
```yaml
services:
steamsalty:
image: watn3y/steamsalty:latest # use :<branchname> to be up-to-date with any branch
container_name: steamsalty
restart: unless-stopped
volumes:
- /etc/localtime:/etc/localtime:ro
environment:
#- STEAMSALTY_LOGLEVEL=
- STEAMSALTY_TELEGRAMAPITOKEN=
- STEAMSALTY_STEAMAPIKEY=
- STEAMSALTY_CHATID=
- STEAMSALTY_WATCHERS=
#- STEAMSALTY_SLEEPINTERVAL=
```
## Running on Linux
Grab a release from the [releases page](https://github.com/watn3y/steamsalty/releases). Make sure to set your **environment variables** accordingly.
## Environment Variables
| Variable | Description | Default |
| ----------------------------- | -------------------------------------------------------------------------------------------------------------------- | ------------------ |
| `STEAMSALTY_LOGLEVEL` | LogLevel as described [here](https://pkg.go.dev/github.com/rs/zerolog@v1.33.0#readme-simple-leveled-logging-example) | 1 (Info) |
| `STEAMSALTY_TELEGRAMAPITOKEN` | Telegram Bot Token, get from @BotFather | None, **required** |
| `STEAMSALTY_STEAMAPIKEY` | Steam API Key, get from https://steamcommunity.com/dev/apikey | None, **required** |
| `STEAMSALTY_CHATID` | Chat to notify about new Comments | None, **required** |
| `STEAMSALTY_WATCHERS` | SteamIDs (in SteamID64 format) to check for new Profile Comments | None, **required** |
| `STEAMSALTY_SLEEPINTERVAL` | Amount of time to wait between requests to Steam in seconds | 60 |

2
bot.go
View file

@ -13,6 +13,8 @@ import (
func bot() { func bot() {
updates, bot := botIO.Authenticate() updates, bot := botIO.Authenticate()
go commands.SetBotCommands(bot)
go steam.StartWatchers(bot) go steam.StartWatchers(bot)
for update := range updates { for update := range updates {

View file

@ -12,12 +12,15 @@ func Authenticate() (tgbotapi.UpdatesChannel, *tgbotapi.BotAPI) {
log.Panic().Err(err).Msg("Failed to authenticate") log.Panic().Err(err).Msg("Failed to authenticate")
} }
bot.Debug = config.BotConfig.DebugMode bot.Debug = false
if config.BotConfig.LogLevel == -1 {
bot.Debug = true
}
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("Successfully authenticated to Telegram API") log.Info().Int64("ID", bot.Self.ID).Str("username", bot.Self.UserName).Msg("Authenticated to Telegram API")
return bot.GetUpdatesChan(updates), bot return bot.GetUpdatesChan(updates), bot

View file

@ -2,7 +2,10 @@ package commands
import ( import (
"fmt" "fmt"
"strconv"
"strings" "strings"
"time"
"watn3y/steamsalty/botIO" "watn3y/steamsalty/botIO"
"watn3y/steamsalty/config" "watn3y/steamsalty/config"
"watn3y/steamsalty/steam" "watn3y/steamsalty/steam"
@ -11,26 +14,41 @@ import (
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
func SetBotCommands(bot *tgbotapi.BotAPI) {
github := tgbotapi.BotCommand{Command: "github", Description: "Source GitHub repo"}
info := tgbotapi.BotCommand{Command: "info", Description: "Summary of watched profiles"}
commands := tgbotapi.NewSetMyCommands(github, info)
result, err := bot.Request(commands)
if err != nil {
log.Error().Err(err).Msg("Failed to set own commands")
return
}
log.Debug().Interface("commands", result).Msg("Set own commands")
}
func Commands(update tgbotapi.Update, bot *tgbotapi.BotAPI) { func Commands(update tgbotapi.Update, bot *tgbotapi.BotAPI) {
cmd := strings.ToLower(update.Message.Command()) cmd := strings.ToLower(update.Message.Command())
log.Debug().Str("cmd", cmd).Msg("Matching command")
switch cmd { switch cmd {
case "start": case "start":
start(update, bot) startGithub(update, bot)
case "github":
startGithub(update, bot)
case "info": case "info":
info(update, bot) info(update, bot)
} }
} }
func start(update tgbotapi.Update, bot *tgbotapi.BotAPI) { func startGithub(update tgbotapi.Update, bot *tgbotapi.BotAPI) {
message := tgbotapi.MessageConfig{ message := tgbotapi.MessageConfig{
BaseChat: tgbotapi.BaseChat{ChatID: update.Message.Chat.ID, ReplyToMessageID: update.Message.MessageID}, BaseChat: tgbotapi.BaseChat{ChatID: update.Message.Chat.ID, ReplyToMessageID: update.Message.MessageID},
ParseMode: "html", ParseMode: "html",
DisableWebPagePreview: false, DisableWebPagePreview: false,
Text: "https://github.com/watn3y/steamsalty", Text: "Check out: https://github.com/watn3y/steamsalty",
} }
botIO.SendMessage(message, bot) botIO.SendMessage(message, bot)
} }
@ -44,9 +62,16 @@ func info(update tgbotapi.Update, bot *tgbotapi.BotAPI) {
for _, steamID := range config.BotConfig.Watchers { for _, steamID := range config.BotConfig.Watchers {
profile := steam.GetPlayerDetails(steamID) profile := steam.GetPlayerDetails(steamID)
comments := steam.GetComments(steamID, 0, 0)
textInfo += fmt.Sprintf(`- <a href="%s">%s</a>`, profile.ProfileURL, profile.PersonaName) + "\n" lastComment := "never :("
if comments.TimeLastPost > 0 {
lastComment = time.Unix(comments.TimeLastPost, 0).Format(time.RFC1123)
}
textInfo += fmt.Sprintf(`<b><a href="%s">%s</a></b>:`, profile.ProfileURL, profile.PersonaName) + "\n" +
fmt.Sprintf(`Last Comment: %s`, lastComment) + "\n" +
fmt.Sprintf(`Number of Comments: %s`, strconv.Itoa(comments.TotalCount)) + "\n\n"
} }
message := tgbotapi.MessageConfig{ message := tgbotapi.MessageConfig{

View file

@ -2,6 +2,7 @@ package config
import ( import (
"context" "context"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
envconfig "github.com/sethvargo/go-envconfig" envconfig "github.com/sethvargo/go-envconfig"
@ -11,12 +12,9 @@ var BotConfig config
func LoadConfig() { func LoadConfig() {
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("Error parsing config from env variables")
}
if !BotConfig.DebugMode {
zerolog.SetGlobalLevel(zerolog.InfoLevel)
} }
zerolog.SetGlobalLevel(zerolog.Level(BotConfig.LogLevel))
log.Info().Msg("Loaded config") log.Info().Msg("Loaded config")
log.Debug().Interface("config", BotConfig).Msg("") log.Debug().Interface("config", BotConfig).Msg("")

View file

@ -1,10 +1,10 @@
package config package config
type config struct { type config struct {
TelegramAPIToken string `env:"TELEGRAMAPITOKEN, required"` LogLevel int `env:"STEAMSALTY_LOGLEVEL, default=1"`
SteamAPIKey string `env:"STEAMAPIKEY, required"` TelegramAPIToken string `env:"STEAMSALTY_TELEGRAMAPITOKEN, required"`
DebugMode bool `env:"DEBUGMODE, default=false"` SteamAPIKey string `env:"STEAMSALTY_STEAMAPIKEY, required"`
ChatID int64 `env:"CHATID"` ChatID int64 `env:"STEAMSALTY_CHATID, required"`
Watchers []uint64 `env:"WATCHERS"` Watchers []uint64 `env:"STEAMSALTY_WATCHERS, required"`
SleepInterval int `env:"SLEEPINTERVAL"` SleepInterval int `env:"STEAMSALTY_SLEEPINTERVAL, default=60"`
} }

View file

@ -1,14 +0,0 @@
services:
steamsalty:
image: watn3y/steamsalty:master
container_name: steamsalty
restart: unless-stopped
volumes:
- /etc/localtime:/etc/localtime:ro
environment:
- TELEGRAMAPITOKEN=
- STEAMAPIKEY=
- DebugMode=false
- CHATID=123
- WATCHERS=123,456,789
- SLEEPINTERVAL=60

20
main.go
View file

@ -1,8 +1,9 @@
package main package main
import ( import (
"fmt"
"os" "os"
"strconv"
"strings"
"time" "time"
"github.com/rs/zerolog" "github.com/rs/zerolog"
@ -12,7 +13,7 @@ import (
) )
func main() { func main() {
fmt.Println("Starting SteamSalty...") println("Starting SteamSalty...")
configureLogger() configureLogger()
@ -23,13 +24,20 @@ func main() {
} }
func configureLogger() { func configureLogger() {
zerolog.CallerMarshalFunc = func(pc uintptr, file string, line int) string {
const prefix = "steamsalty/"
index := strings.Index(file, prefix)
if index != -1 {
return file[index+len(prefix):] + ":" + strconv.Itoa(line)
}
return file + ":" + strconv.Itoa(line)
}
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()
//! note that we overwrite the loglevel after loading the config in config/config.go. This is just the default
zerolog.SetGlobalLevel(zerolog.TraceLevel)
log.Info().Msg("Started Logger") log.Info().Msg("Started Logger")
} }

View file

@ -13,6 +13,6 @@ func GetPlayerDetails(steamID uint64) (summary steamapi.PlayerSummary) {
if err != nil { if err != nil {
log.Error().Err(err).Msg("Failed to get Player Summary") log.Error().Err(err).Msg("Failed to get Player Summary")
} }
log.Debug().Interface("Player", response[0]).Msg("Successfully got PlayerSummary from Steam API") log.Debug().Interface("Player", response[0]).Msg("Got PlayerSummary from Steam API")
return response[0] return response[0]
} }

View file

@ -13,7 +13,7 @@ import (
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
func getComments(steamID uint64, start int, count int) (page CommentsPage) { func GetComments(steamID uint64, start int, count int) (page CommentsPage) {
baseURL := "https://steamcommunity.com/comment/Profile/render/" baseURL := "https://steamcommunity.com/comment/Profile/render/"
@ -47,7 +47,7 @@ func getComments(steamID uint64, start int, count int) (page CommentsPage) {
log.Error().Err(err).Msg("Failed to parse Comments as JSON") log.Error().Err(err).Msg("Failed to parse Comments as JSON")
} }
log.Debug().Interface("CommentPage", page).Uint64("ProfileID", steamID).Msg("Successfully got Comment Page") log.Trace().Interface("CommentPage", page).Uint64("ProfileID", steamID).Msg("Got Comment Page")
return page return page
} }
@ -85,6 +85,6 @@ func parseComments(rawComments CommentsPage) (comments []Comment) {
}) })
slices.Reverse(comments) slices.Reverse(comments)
log.Debug().Interface("Comments", comments).Msg("Successfully parsed Comment Page") log.Trace().Interface("Comments", comments).Msg("Parsed Comment Page")
return comments return comments
} }

View file

@ -15,30 +15,30 @@ import (
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
var sleeptime time.Duration = time.Duration(config.BotConfig.SleepInterval) * time.Second
var steamContentCheckText string = "This comment is awaiting analysis by our automated content check system. It will be temporarily hidden until we verify that it does not contain harmful content (e.g. links to websites that attempt to steal information)." var steamContentCheckText string = "This comment is awaiting analysis by our automated content check system. It will be temporarily hidden until we verify that it does not contain harmful content (e.g. links to websites that attempt to steal information)."
func StartWatchers(bot *tgbotapi.BotAPI) { func StartWatchers(bot *tgbotapi.BotAPI) {
var wg sync.WaitGroup var wg sync.WaitGroup
for _, steamID := range config.BotConfig.Watchers { for _, steamID := range config.BotConfig.Watchers {
wg.Add(1) wg.Add(1)
go func(steamID uint64) { go func(steamID uint64) {
defer wg.Done() defer wg.Done()
watcher(bot, steamID) watcher(bot, steamID, time.Duration(config.BotConfig.SleepInterval)*time.Second)
}(steamID) }(steamID)
} }
wg.Wait() wg.Wait()
} }
func watcher(bot *tgbotapi.BotAPI, steamID uint64) { func watcher(bot *tgbotapi.BotAPI, steamID uint64, sleeptime time.Duration) {
log.Info().Uint64("SteamID", steamID).Msg("Started Watcher") log.Info().Uint64("SteamID", steamID).Msg("Started Watcher")
var newestProcessedComment int64 = 0 var newestProcessedComment int64 = 0
for { for {
currentCommentsPage := getComments(steamID, 0, math.MaxInt32) currentCommentsPage := GetComments(steamID, 0, math.MaxInt32)
if newestProcessedComment == 0 || newestProcessedComment == currentCommentsPage.TimeLastPost { if newestProcessedComment == 0 || newestProcessedComment == currentCommentsPage.TimeLastPost {
newestProcessedComment = currentCommentsPage.TimeLastPost newestProcessedComment = currentCommentsPage.TimeLastPost
time.Sleep(sleeptime) time.Sleep(sleeptime)