Compare commits

...

8 commits

Author SHA1 Message Date
b0d68f3a83 Don't log every received message
Some checks failed
Build and Push to Docker Hub on push to any branch / docker (push) Has been cancelled
2024-12-22 08:21:02 +01:00
b0d49ab3c4 gitignore vscode config 2024-12-22 08:20:51 +01:00
a2c4c43eaa add build script 2024-12-22 07:41:48 +01:00
6be403fc58 v1.0
- Fixed SLEEPTIME not working (always 0)
- Refined logging
- Added metadata to /info command
- Bot now automatically sets own commands for autocompletion
2024-12-22 07:28:00 +01:00
eb0482b8e0 Build when Dockerfile changes 2024-12-22 04:13:37 +01:00
c800be186f fix typos 2024-12-22 04:12:36 +01:00
877852aa67 Change from ghcr to dockerhub 2024-12-22 04:06:08 +01:00
96db61831c add cross-compile 2024-12-22 03:52:10 +01:00
16 changed files with 213 additions and 95 deletions

View file

@ -1,38 +0,0 @@
name: Build and Push to Docker Hub on changes to master branch
on:
push:
branches:
- master
jobs:
docker:
runs-on: ubuntu-latest
steps:
- name: Check out the repo
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{github.actor}}
password: ${{secrets.GHCR_TOKEN}} # needs read:packages, write:packages. delete:packages
- name: Build and push
uses: docker/build-push-action@v6
with:
platforms: linux/amd64,linux/arm64
push: true
tags: |
ghcr.io/${{ github.repository }}:master
ghcr.io/${{ github.repository }}:commit-${{ github.sha }}

34
.github/workflows/on-push.yml vendored Normal file
View file

@ -0,0 +1,34 @@
name: Build and Push to Docker Hub on push to any branch
on:
push:
paths:
- '**.go'
- 'go.mod'
- 'go.sum'
- 'Dockerfile'
- '.github/workflows/**'
jobs:
docker:
runs-on: ubuntu-latest
steps:
- name: Check out the repo
uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{secrets.DOCKERHUB_USERNAME}}
password: ${{secrets.DOCKERHUB_TOKEN}}
- name: Build and push
uses: docker/build-push-action@v6
with:
platforms: linux/amd64,linux/386,linux/arm64,linux/arm/v7,linux/riscv64
push: true
tags: |
${{ github.repository }}:${{ github.ref_name }}
${{ github.repository }}:commit-${{ github.sha }}

6
.gitignore vendored
View file

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

View file

@ -1,12 +1,15 @@
FROM golang:alpine AS builder
FROM --platform=$BUILDPLATFORM golang:1.23.4-alpine AS builder
WORKDIR /steamsalty
RUN apk update && apk add --no-cache ca-certificates
COPY . .
COPY go.mod go.sum ./
RUN go mod download
RUN go build -o /app/steamsalty
COPY . .
ARG TARGETOS TARGETARCH
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o ./steamsalty
@ -16,6 +19,6 @@ WORKDIR /app
COPY --from=builder /etc/ssl/certs /etc/ssl/certs
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"]

View file

@ -1,2 +1,40 @@
# steamsalty
Get notifications about Steam Comments on Telegram
# SteamSalty
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 |

5
bot.go
View file

@ -13,6 +13,8 @@ import (
func bot() {
updates, bot := botIO.Authenticate()
go commands.SetBotCommands(bot)
go steam.StartWatchers(bot)
for update := range updates {
@ -27,9 +29,8 @@ func bot() {
continue
}
log.Info().Int64("ChatID", update.Message.Chat.ID).Int64("UserID", update.Message.From.ID).Str("Text", update.Message.Text).Msg("Recieved Message")
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")
commands.Commands(update, bot)
}
}

View file

@ -12,12 +12,15 @@ func Authenticate() (tgbotapi.UpdatesChannel, *tgbotapi.BotAPI) {
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.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

54
build.sh Normal file
View file

@ -0,0 +1,54 @@
#!/usr/bin/env bash
# Get the current directory name as the project name
PROJECT_NAME=$(basename "$PWD")
# Change to the Go project directory (current directory)
cd "$PWD" || exit
# List of target architectures
architectures=(
"linux/amd64"
"linux/386"
"linux/arm64"
"linux/arm/v7"
"linux/riscv64"
"windows/amd64"
"darwin/amd64"
"darwin/arm64"
)
# Create a build directory if it doesn't exist
mkdir -p build
# Loop over architectures and build for each one
for arch in "${architectures[@]}"; do
os=$(echo $arch | cut -d '/' -f 1)
arch_type=$(echo $arch | cut -d '/' -f 2)
# Set the output file name using the project name, os, and arch
output_file="build/$PROJECT_NAME-$os-$arch_type"
# Build the app for the specific architecture
GOARCH=$arch_type GOOS=$os go build -o "$output_file"
# Provide feedback to the user
if [ $? -eq 0 ]; then
echo "Successfully built for $arch: $output_file"
# Create a tar.gz archive for the individual build
tar -czf "$output_file.tar.gz" -C build $(basename "$output_file")
if [ $? -eq 0 ]; then
echo "Successfully created archive: $output_file.tar.gz"
# Delete the executable after archiving
rm "$output_file"
echo "Deleted executable: $output_file"
else
echo "Failed to create archive for $output_file"
fi
else
echo "Failed to build for $arch"
fi
done

View file

@ -2,7 +2,10 @@ package commands
import (
"fmt"
"strconv"
"strings"
"time"
"watn3y/steamsalty/botIO"
"watn3y/steamsalty/config"
"watn3y/steamsalty/steam"
@ -11,26 +14,41 @@ import (
"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) {
cmd := strings.ToLower(update.Message.Command())
log.Debug().Str("cmd", cmd).Msg("Matching command")
switch cmd {
case "start":
start(update, bot)
startGithub(update, bot)
case "github":
startGithub(update, bot)
case "info":
info(update, bot)
}
}
func start(update tgbotapi.Update, bot *tgbotapi.BotAPI) {
func startGithub(update tgbotapi.Update, bot *tgbotapi.BotAPI) {
message := tgbotapi.MessageConfig{
BaseChat: tgbotapi.BaseChat{ChatID: update.Message.Chat.ID, ReplyToMessageID: update.Message.MessageID},
ParseMode: "html",
DisableWebPagePreview: false,
Text: "https://github.com/watn3y/steamsalty",
Text: "Check out: https://github.com/watn3y/steamsalty",
}
botIO.SendMessage(message, bot)
}
@ -44,9 +62,16 @@ func info(update tgbotapi.Update, bot *tgbotapi.BotAPI) {
for _, steamID := range config.BotConfig.Watchers {
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{

View file

@ -2,6 +2,7 @@ package config
import (
"context"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
envconfig "github.com/sethvargo/go-envconfig"
@ -11,12 +12,9 @@ var BotConfig config
func LoadConfig() {
if err := envconfig.Process(context.Background(), &BotConfig); err != nil {
log.Panic().Err(err).Msg("error parsing config from env variables")
}
if !BotConfig.DebugMode {
zerolog.SetGlobalLevel(zerolog.InfoLevel)
log.Panic().Err(err).Msg("Error parsing config from env variables")
}
zerolog.SetGlobalLevel(zerolog.Level(BotConfig.LogLevel))
log.Info().Msg("Loaded config")
log.Debug().Interface("config", BotConfig).Msg("")

View file

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

View file

@ -1,14 +0,0 @@
services:
steamsalty:
image: ghcr.io/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
import (
"fmt"
"os"
"strconv"
"strings"
"time"
"github.com/rs/zerolog"
@ -12,7 +13,7 @@ import (
)
func main() {
fmt.Println("Starting SteamSalty...")
println("Starting SteamSalty...")
configureLogger()
@ -23,13 +24,20 @@ func main() {
}
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}
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")
}

View file

@ -13,6 +13,6 @@ func GetPlayerDetails(steamID uint64) (summary steamapi.PlayerSummary) {
if err != nil {
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]
}

View file

@ -13,7 +13,7 @@ import (
"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/"
@ -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.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
}
@ -85,6 +85,6 @@ func parseComments(rawComments CommentsPage) (comments []Comment) {
})
slices.Reverse(comments)
log.Debug().Interface("Comments", comments).Msg("Successfully parsed Comment Page")
log.Trace().Interface("Comments", comments).Msg("Parsed Comment Page")
return comments
}

View file

@ -15,30 +15,30 @@ import (
"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)."
func StartWatchers(bot *tgbotapi.BotAPI) {
var wg sync.WaitGroup
for _, steamID := range config.BotConfig.Watchers {
wg.Add(1)
go func(steamID uint64) {
defer wg.Done()
watcher(bot, steamID)
watcher(bot, steamID, time.Duration(config.BotConfig.SleepInterval)*time.Second)
}(steamID)
}
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")
var newestProcessedComment int64 = 0
for {
currentCommentsPage := getComments(steamID, 0, math.MaxInt32)
currentCommentsPage := GetComments(steamID, 0, math.MaxInt32)
if newestProcessedComment == 0 || newestProcessedComment == currentCommentsPage.TimeLastPost {
newestProcessedComment = currentCommentsPage.TimeLastPost
time.Sleep(sleeptime)