mirror of
https://github.com/Alexander-D-Karpov/about.git
synced 2026-03-16 22:06:08 +03:00
457 lines
13 KiB
Go
457 lines
13 KiB
Go
package plugins
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"html/template"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/Alexander-D-Karpov/about/internal/storage"
|
|
"github.com/Alexander-D-Karpov/about/internal/stream"
|
|
)
|
|
|
|
type SteamPlugin struct {
|
|
storage *storage.Storage
|
|
hub *stream.Hub
|
|
apiKey string
|
|
recentGames []SteamGame
|
|
playerSummary *SteamPlayerSummary
|
|
lastUpdate time.Time
|
|
}
|
|
|
|
type SteamGame struct {
|
|
Name string `json:"name"`
|
|
Playtime2w int `json:"playtime_2weeks"`
|
|
PlaytimeAll int `json:"playtime_forever"`
|
|
AppID int `json:"appid"`
|
|
ImgIconURL string `json:"img_icon_url"`
|
|
}
|
|
|
|
type SteamCurrentGame struct {
|
|
GameID string `json:"gameid"`
|
|
GameExtraInfo string `json:"gameextrainfo"`
|
|
GameServerIP string `json:"gameserverip"`
|
|
GameServerSteamID string `json:"gameserversteamid"`
|
|
}
|
|
|
|
type SteamPlayerSummary struct {
|
|
SteamID string `json:"steamid"`
|
|
CommunityVisibilityState int `json:"communityvisibilitystate"`
|
|
ProfileState int `json:"profilestate"`
|
|
PersonaName string `json:"personaname"`
|
|
ProfileURL string `json:"profileurl"`
|
|
Avatar string `json:"avatar"`
|
|
AvatarMedium string `json:"avatarmedium"`
|
|
AvatarFull string `json:"avatarfull"`
|
|
AvatarHash string `json:"avatarhash"`
|
|
LastLogoff int64 `json:"lastlogoff"`
|
|
PersonaState int `json:"personastate"`
|
|
RealName string `json:"realname"`
|
|
PrimaryClanID string `json:"primaryclanid"`
|
|
TimeCreated int64 `json:"timecreated"`
|
|
PersonaStateFlags int `json:"personastateflags"`
|
|
GameID string `json:"gameid,omitempty"`
|
|
GameExtraInfo string `json:"gameextrainfo,omitempty"`
|
|
GameServerIP string `json:"gameserverip,omitempty"`
|
|
GameServerSteamID string `json:"gameserversteamid,omitempty"`
|
|
}
|
|
|
|
type SteamResponse struct {
|
|
Response struct {
|
|
TotalCount int `json:"total_count"`
|
|
Games []SteamGame `json:"games"`
|
|
} `json:"response"`
|
|
}
|
|
|
|
type SteamPlayerSummaryResponse struct {
|
|
Response struct {
|
|
Players []SteamPlayerSummary `json:"players"`
|
|
} `json:"response"`
|
|
}
|
|
|
|
func NewSteamPlugin(storage *storage.Storage, hub *stream.Hub, apiKey string) *SteamPlugin {
|
|
return &SteamPlugin{
|
|
storage: storage,
|
|
hub: hub,
|
|
apiKey: apiKey,
|
|
}
|
|
}
|
|
|
|
func (p *SteamPlugin) Name() string {
|
|
return "steam"
|
|
}
|
|
|
|
func (p *SteamPlugin) Render(ctx context.Context) (string, error) {
|
|
if p.apiKey == "" {
|
|
return p.renderNoAPI(), nil
|
|
}
|
|
|
|
if p.playerSummary == nil && len(p.recentGames) == 0 {
|
|
return p.renderLoading(), nil
|
|
}
|
|
|
|
tmpl := `
|
|
<div class="steam-section" id="steam-section">
|
|
<h3>Gaming Activity</h3>
|
|
|
|
{{if and .PlayerSummary .IsPlayingNow}}
|
|
<div class="current-game">
|
|
<div class="current-game-header">
|
|
<span class="status-indicator status-online"></span>
|
|
<span class="current-game-status">Currently Playing</span>
|
|
</div>
|
|
<div class="current-game-info">
|
|
<div class="current-game-name">{{.CurrentGameName}}</div>
|
|
<div class="current-game-actions">
|
|
<a href="https://store.steampowered.com/search/?term={{.CurrentGameNameEncoded}}" target="_blank" rel="noopener" class="btn btn-sm">
|
|
<svg viewBox="0 0 24 24" width="14" height="14">
|
|
<path fill="currentColor" d="M14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3m-2 16H5V5h7V3H5c-1.11 0-2 .89-2 2v14c0 1.11.89 2 2 2h14c1.11 0 2-.89 2-2v-7h-2v7Z"/>
|
|
</svg>
|
|
View on Steam
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{{else if .PlayerSummary}}
|
|
<div class="player-status">
|
|
<div class="status-info">
|
|
<span class="status-indicator {{.PlayerStatusClass}}"></span>
|
|
<span class="status-text">{{.PlayerStatusText}}</span>
|
|
</div>
|
|
</div>
|
|
{{end}}
|
|
|
|
{{if .RecentGames}}
|
|
<div class="recent-games">
|
|
<h4>Recent Games</h4>
|
|
<div class="games-list">
|
|
{{range .RecentGames}}
|
|
<div class="game-item" data-app-id="{{.AppID}}">
|
|
{{if .Icon}}
|
|
<img src="{{.Icon}}" alt="{{.Name}}" class="game-icon" loading="lazy">
|
|
{{end}}
|
|
<div class="game-info">
|
|
<div class="game-name">{{.Name}}</div>
|
|
<div class="game-stats">
|
|
<span class="game-playtime">{{.RecentHours}}h last 2 weeks</span>
|
|
<span class="game-total">{{.TotalHours}}h total</span>
|
|
</div>
|
|
</div>
|
|
<div class="game-actions">
|
|
<button class="btn btn-sm" onclick="window.open('https://store.steampowered.com/app/{{.AppID}}', '_blank')">
|
|
View
|
|
</button>
|
|
</div>
|
|
</div>
|
|
{{end}}
|
|
</div>
|
|
</div>
|
|
{{end}}
|
|
|
|
{{if .PlayerSummary}}
|
|
<div class="steam-profile-link">
|
|
<a href="{{.PlayerSummary.ProfileURL}}" target="_blank" rel="noopener" class="view-profile-btn">
|
|
View Steam Profile
|
|
</a>
|
|
</div>
|
|
{{end}}
|
|
</div>`
|
|
|
|
type gameData struct {
|
|
Name string
|
|
Icon string
|
|
RecentHours string
|
|
TotalHours string
|
|
AppID int
|
|
}
|
|
|
|
var games []gameData
|
|
for i, game := range p.recentGames {
|
|
if i >= 3 {
|
|
break
|
|
}
|
|
|
|
var icon string
|
|
if game.ImgIconURL != "" {
|
|
icon = fmt.Sprintf("https://media.steampowered.com/steamcommunity/public/images/apps/%d/%s.jpg",
|
|
game.AppID, game.ImgIconURL)
|
|
}
|
|
|
|
recentHours := fmt.Sprintf("%.1f", float64(game.Playtime2w)/60.0)
|
|
totalHours := fmt.Sprintf("%.1f", float64(game.PlaytimeAll)/60.0)
|
|
|
|
games = append(games, gameData{
|
|
Name: game.Name,
|
|
Icon: icon,
|
|
RecentHours: recentHours,
|
|
TotalHours: totalHours,
|
|
AppID: game.AppID,
|
|
})
|
|
}
|
|
|
|
isPlayingNow := p.playerSummary != nil && p.playerSummary.GameExtraInfo != ""
|
|
currentGameName := ""
|
|
currentGameNameEncoded := ""
|
|
playerStatusClass := "status-offline"
|
|
playerStatusText := "Offline"
|
|
|
|
if p.playerSummary != nil {
|
|
if isPlayingNow {
|
|
currentGameName = p.playerSummary.GameExtraInfo
|
|
currentGameNameEncoded = strings.ReplaceAll(currentGameName, " ", "%20")
|
|
}
|
|
|
|
switch p.playerSummary.PersonaState {
|
|
case 0:
|
|
playerStatusClass = "status-offline"
|
|
playerStatusText = "Offline"
|
|
case 1:
|
|
playerStatusClass = "status-online"
|
|
playerStatusText = "Online"
|
|
case 2:
|
|
playerStatusClass = "status-loading"
|
|
playerStatusText = "Busy"
|
|
case 3:
|
|
playerStatusClass = "status-loading"
|
|
playerStatusText = "Away"
|
|
case 4:
|
|
playerStatusClass = "status-loading"
|
|
playerStatusText = "Snooze"
|
|
case 5:
|
|
playerStatusClass = "status-loading"
|
|
playerStatusText = "Looking to trade"
|
|
case 6:
|
|
playerStatusClass = "status-loading"
|
|
playerStatusText = "Looking to play"
|
|
}
|
|
}
|
|
|
|
data := struct {
|
|
RecentGames []gameData
|
|
PlayerSummary *SteamPlayerSummary
|
|
IsPlayingNow bool
|
|
CurrentGameName string
|
|
CurrentGameNameEncoded string
|
|
PlayerStatusClass string
|
|
PlayerStatusText string
|
|
}{
|
|
RecentGames: games,
|
|
PlayerSummary: p.playerSummary,
|
|
IsPlayingNow: isPlayingNow,
|
|
CurrentGameName: currentGameName,
|
|
CurrentGameNameEncoded: currentGameNameEncoded,
|
|
PlayerStatusClass: playerStatusClass,
|
|
PlayerStatusText: playerStatusText,
|
|
}
|
|
|
|
tmplParsed, err := template.New("steam").Parse(tmpl)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
var buf strings.Builder
|
|
err = tmplParsed.Execute(&buf, data)
|
|
return buf.String(), err
|
|
}
|
|
|
|
func (p *SteamPlugin) renderNoAPI() string {
|
|
return `<div class="steam-section">
|
|
<h3>Recent Gaming Activity</h3>
|
|
<p class="text-muted">Steam API key not configured</p>
|
|
</div>`
|
|
}
|
|
|
|
func (p *SteamPlugin) renderLoading() string {
|
|
return `<div class="steam-section">
|
|
<h3>Recent Gaming Activity</h3>
|
|
<div class="loading-indicator">
|
|
<div class="loading"></div>
|
|
<p class="text-muted">Loading Steam data...</p>
|
|
</div>
|
|
</div>`
|
|
}
|
|
|
|
func (p *SteamPlugin) UpdateData(ctx context.Context) error {
|
|
if p.apiKey == "" || time.Since(p.lastUpdate) < 4*time.Minute {
|
|
return nil
|
|
}
|
|
|
|
config := p.storage.GetPluginConfig(p.Name())
|
|
steamID, ok := config.Settings["steamid"].(string)
|
|
if !ok {
|
|
return fmt.Errorf("steamid not configured")
|
|
}
|
|
|
|
if err := p.updatePlayerSummary(steamID); err != nil {
|
|
fmt.Printf("Warning: Failed to update Steam player summary: %v\n", err)
|
|
}
|
|
|
|
if err := p.updateRecentGames(steamID); err != nil {
|
|
fmt.Printf("Warning: Failed to update Steam recent games: %v\n", err)
|
|
}
|
|
|
|
p.lastUpdate = time.Now()
|
|
return nil
|
|
}
|
|
|
|
func (p *SteamPlugin) updatePlayerSummary(steamID string) error {
|
|
url := fmt.Sprintf("http://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002/?key=%s&steamids=%s",
|
|
p.apiKey, steamID)
|
|
|
|
client := &http.Client{Timeout: 15 * time.Second}
|
|
req, err := http.NewRequest("GET", url, nil)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create request: %v", err)
|
|
}
|
|
|
|
req.Header.Set("User-Agent", "AboutPage/1.0 (aboutpage.akarpov.ru)")
|
|
req.Header.Set("Accept", "application/json")
|
|
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to fetch Steam player summary: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return fmt.Errorf("Steam API returned status %d", resp.StatusCode)
|
|
}
|
|
|
|
var response SteamPlayerSummaryResponse
|
|
decoder := json.NewDecoder(resp.Body)
|
|
if err := decoder.Decode(&response); err != nil {
|
|
return fmt.Errorf("failed to decode Steam player summary: %v", err)
|
|
}
|
|
|
|
if len(response.Response.Players) > 0 {
|
|
oldGameStatus := ""
|
|
oldPersonaState := 0
|
|
if p.playerSummary != nil {
|
|
oldGameStatus = p.playerSummary.GameExtraInfo
|
|
oldPersonaState = p.playerSummary.PersonaState
|
|
}
|
|
|
|
p.playerSummary = &response.Response.Players[0]
|
|
|
|
newGameStatus := p.playerSummary.GameExtraInfo
|
|
newPersonaState := p.playerSummary.PersonaState
|
|
|
|
if oldGameStatus != newGameStatus || oldPersonaState != newPersonaState {
|
|
gameImage := ""
|
|
if p.playerSummary.GameID != "" {
|
|
gameImage = fmt.Sprintf("https://cdn.cloudflare.steamstatic.com/steam/apps/%s/header.jpg",
|
|
p.playerSummary.GameID)
|
|
}
|
|
|
|
p.hub.Broadcast("steam_status_update", map[string]interface{}{
|
|
"isPlaying": newGameStatus != "",
|
|
"currentGame": newGameStatus,
|
|
"gameImage": gameImage,
|
|
"gameId": p.playerSummary.GameID,
|
|
"personaState": newPersonaState,
|
|
"personaName": p.playerSummary.PersonaName,
|
|
"timestamp": time.Now().Unix(),
|
|
})
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *SteamPlugin) updateRecentGames(steamID string) error {
|
|
url := fmt.Sprintf("http://api.steampowered.com/IPlayerService/GetRecentlyPlayedGames/v0001/?key=%s&steamid=%s&format=json&count=3",
|
|
p.apiKey, steamID)
|
|
|
|
client := &http.Client{Timeout: 15 * time.Second}
|
|
req, err := http.NewRequest("GET", url, nil)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create request: %v", err)
|
|
}
|
|
|
|
req.Header.Set("User-Agent", "AboutPage/1.0 (aboutpage.akarpov.ru)")
|
|
req.Header.Set("Accept", "application/json")
|
|
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to fetch Steam data: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return fmt.Errorf("steam API returned status %d", resp.StatusCode)
|
|
}
|
|
|
|
contentType := resp.Header.Get("Content-Type")
|
|
if !strings.Contains(contentType, "application/json") {
|
|
return fmt.Errorf("steam API returned non-JSON content: %s", contentType)
|
|
}
|
|
|
|
var response SteamResponse
|
|
decoder := json.NewDecoder(resp.Body)
|
|
if err := decoder.Decode(&response); err != nil {
|
|
return fmt.Errorf("failed to decode Steam data: %v", err)
|
|
}
|
|
|
|
if len(response.Response.Games) > 0 {
|
|
oldCount := len(p.recentGames)
|
|
p.recentGames = response.Response.Games
|
|
|
|
if oldCount != len(p.recentGames) {
|
|
p.hub.Broadcast("steam_games_update", map[string]interface{}{
|
|
"games": len(p.recentGames),
|
|
})
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *SteamPlugin) GetSettings() map[string]interface{} {
|
|
config := p.storage.GetPluginConfig(p.Name())
|
|
return config.Settings
|
|
}
|
|
|
|
func (p *SteamPlugin) SetSettings(settings map[string]interface{}) error {
|
|
config := p.storage.GetPluginConfig(p.Name())
|
|
config.Settings = settings
|
|
return p.storage.SetPluginConfig(p.Name(), config)
|
|
}
|
|
|
|
func (p *SteamPlugin) RenderText(ctx context.Context) (string, error) {
|
|
if p.apiKey == "" {
|
|
return "Gaming: Steam API key not configured", nil
|
|
}
|
|
|
|
if p.playerSummary == nil {
|
|
return "Gaming: No Steam data available", nil
|
|
}
|
|
|
|
status := "Offline"
|
|
currentGame := ""
|
|
|
|
if p.playerSummary.GameExtraInfo != "" {
|
|
status = "Playing"
|
|
currentGame = fmt.Sprintf(" - %s", p.playerSummary.GameExtraInfo)
|
|
} else {
|
|
switch p.playerSummary.PersonaState {
|
|
case 1:
|
|
status = "Online"
|
|
case 2:
|
|
status = "Busy"
|
|
case 3:
|
|
status = "Away"
|
|
}
|
|
}
|
|
|
|
recentGamesCount := len(p.recentGames)
|
|
gamesInfo := ""
|
|
if recentGamesCount > 0 {
|
|
gamesInfo = fmt.Sprintf(", %d recent games", recentGamesCount)
|
|
}
|
|
|
|
return fmt.Sprintf("Gaming: %s%s%s", status, currentGame, gamesInfo), nil
|
|
}
|