Compare commits

..

No commits in common. "b0d68f3a8371ea39aab2dca2bdf4e40b1e05525c" and "453f03d832e09c076b1d891c5c927420f3ddf46f" have entirely different histories.

16 changed files with 95 additions and 213 deletions

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

@ -0,0 +1,38 @@
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 }}

View file

@ -1,34 +0,0 @@
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,9 +23,3 @@ go.work.sum
# env file # env file
.env .env
# Build ouput
build/
# vscode config
.vscode/

View file

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

View file

@ -1,40 +1,2 @@
# SteamSalty # steamsalty
SteamSalty notifies you on telegram about new comments on any steam profile. Get notifications about Steam Comments on Telegram
## 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,8 +13,6 @@ 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 {
@ -29,8 +27,9 @@ func bot() {
continue 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() { 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) commands.Commands(update, bot)
} }
} }

View file

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

View file

@ -1,54 +0,0 @@
#!/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,10 +2,7 @@ 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"
@ -14,41 +11,26 @@ 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":
startGithub(update, bot) start(update, bot)
case "github":
startGithub(update, bot)
case "info": case "info":
info(update, bot) info(update, bot)
} }
} }
func startGithub(update tgbotapi.Update, bot *tgbotapi.BotAPI) { func start(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: "Check out: https://github.com/watn3y/steamsalty", Text: "https://github.com/watn3y/steamsalty",
} }
botIO.SendMessage(message, bot) botIO.SendMessage(message, bot)
} }
@ -62,16 +44,9 @@ 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)
lastComment := "never :(" textInfo += fmt.Sprintf(`- <a href="%s">%s</a>`, profile.ProfileURL, profile.PersonaName) + "\n"
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,7 +2,6 @@ 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"
@ -12,9 +11,12 @@ 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 {
LogLevel int `env:"STEAMSALTY_LOGLEVEL, default=1"` TelegramAPIToken string `env:"TELEGRAMAPITOKEN, required"`
TelegramAPIToken string `env:"STEAMSALTY_TELEGRAMAPITOKEN, required"` SteamAPIKey string `env:"STEAMAPIKEY, required"`
SteamAPIKey string `env:"STEAMSALTY_STEAMAPIKEY, required"` DebugMode bool `env:"DEBUGMODE, default=false"`
ChatID int64 `env:"STEAMSALTY_CHATID, required"` ChatID int64 `env:"CHATID"`
Watchers []uint64 `env:"STEAMSALTY_WATCHERS, required"` Watchers []uint64 `env:"WATCHERS"`
SleepInterval int `env:"STEAMSALTY_SLEEPINTERVAL, default=60"` SleepInterval int `env:"SLEEPINTERVAL"`
} }

14
docker-compose.yaml Normal file
View file

@ -0,0 +1,14 @@
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,9 +1,8 @@
package main package main
import ( import (
"fmt"
"os" "os"
"strconv"
"strings"
"time" "time"
"github.com/rs/zerolog" "github.com/rs/zerolog"
@ -13,7 +12,7 @@ import (
) )
func main() { func main() {
println("Starting SteamSalty...") fmt.Println("Starting SteamSalty...")
configureLogger() configureLogger()
@ -24,20 +23,13 @@ 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("Got PlayerSummary from Steam API") log.Debug().Interface("Player", response[0]).Msg("Successfully 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.Trace().Interface("CommentPage", page).Uint64("ProfileID", steamID).Msg("Got Comment Page") log.Debug().Interface("CommentPage", page).Uint64("ProfileID", steamID).Msg("Successfully got Comment Page")
return page return page
} }
@ -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.Debug().Interface("Comments", comments).Msg("Successfully 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, time.Duration(config.BotConfig.SleepInterval)*time.Second) watcher(bot, steamID)
}(steamID) }(steamID)
} }
wg.Wait() wg.Wait()
} }
func watcher(bot *tgbotapi.BotAPI, steamID uint64, sleeptime time.Duration) { func watcher(bot *tgbotapi.BotAPI, steamID uint64) {
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)