Skip to content

Commit f0faefc

Browse files
committed
Add basic implementation
1 parent 4392e32 commit f0faefc

13 files changed

Lines changed: 826 additions & 2 deletions

File tree

.github/workflows/build.yml

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
name: build
2+
3+
on:
4+
push:
5+
tags:
6+
- v*
7+
branches:
8+
- master
9+
- develop
10+
pull_request:
11+
branches: [ master ]
12+
13+
jobs:
14+
build:
15+
name: Build
16+
runs-on: ubuntu-latest
17+
steps:
18+
- name: Set up Go 1.x
19+
uses: actions/setup-go@v5
20+
with:
21+
go-version: 1.23.3
22+
23+
- name: Check out code into the Go module directory
24+
uses: actions/checkout@v4
25+
26+
- name: Lint
27+
uses: golangci/golangci-lint-action@v3
28+
with:
29+
# Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version.
30+
version: v1.62.0
31+
32+
# Optional: working directory, useful for monorepos
33+
# working-directory: somedir
34+
35+
# Optional: golangci-lint command line arguments.
36+
# args: --issues-exit-code=0
37+
args: --config=.golangci.yml
38+
39+
# Optional: show only new issues if it's a pull request. The default value is `false`.
40+
only-new-issues: false
41+
42+
- name: Get dependencies
43+
run: |
44+
go get -v -t -d ./...
45+
46+
- name: Test
47+
run: go test -v ./...
48+
49+
- name: Update coverage report
50+
uses: ncruces/go-coverage-report@main
51+
52+
- name: Build
53+
run: go build -v .

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
.idea/
2+
.vscode/
3+
vendor/
4+
devtools/
5+
.tmp/

.golangci.yml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
output:
2+
formats:
3+
- format: colored-line-number
4+
print-issued-lines: true
5+
print-linter-name: true
6+
7+
# SEE: https://golangci-lint.run/usage/configuration/
8+
linters-settings:
9+
godox:
10+
keywords:
11+
- "BUG"
12+
- "FIXME"
13+
# - "TODO"
14+
15+
linters:
16+
enable-all: true
17+
disable:
18+
- exportloopref # is deprecated (since v1.60.2)
19+
- exhaustruct # mad linter
20+
- wrapcheck # mad linter
21+
- tagliatelle # no idea how to fix it - there are both camel and snake tags on Steam api
22+
23+
issues:
24+
exclude:
25+
exclude-rules:
26+
exclude-dirs:
27+
- vendor/
28+
exclude-files:
29+
- ".*_test.go$"

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Changelog
2+
All notable changes to this project will be documented in this file.
3+
4+
**ATTN**: This project uses [semantic versioning](http://semver.org/).
5+
6+
## [Unreleased]
7+
### Added
8+
- Initial implementation.
9+
10+
[Unreleased]: https://github.com/gorcon/steamweb/compare/4392e326b75394c3a866ceb06138f78e69cbba82...HEAD

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
MIT License
22

3-
Copyright (c) 2025 gorcon
3+
Copyright (c) 2025 Pavel Korotkiy (outdead)
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

README.md

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,58 @@
11
# steamweb
2-
Steam Web API Implementation in Golang
2+
[![GitHub Build](https://github.com/gorcon/steamweb/workflows/build/badge.svg)](https://github.com/gorcon/steamweb/actions)
3+
[![Go Coverage](https://github.com/gorcon/steamweb/wiki/coverage.svg)](https://raw.githack.com/wiki/gorcon/steamweb/coverage.html)
4+
[![Go Report Card](https://goreportcard.com/badge/github.com/gorcon/steamweb)](https://goreportcard.com/report/github.com/gorcon/steamweb)
5+
[![GoDoc](https://img.shields.io/badge/godoc-reference-blue.svg)](https://godoc.org/github.com/gorcon/steamweb)
6+
7+
Steam Web API Implementation in Golang.
8+
9+
## API Specifications
10+
Steam API described in the [valve documentation](https://developer.valvesoftware.com/wiki/Steam_Web_API).
11+
12+
## Install
13+
```text
14+
go get github.com/gorcon/steamweb
15+
```
16+
17+
See [Changelog](CHANGELOG.md) for release details.
18+
19+
## Usage
20+
21+
```go
22+
package main
23+
24+
import (
25+
"encoding/json"
26+
"fmt"
27+
"log"
28+
29+
steamweb "github.com/gorcon/steamweb/steamwebdraft"
30+
)
31+
32+
func main() {
33+
client := steamweb.NewClient(&steamweb.Config{Key: "{Steam API Key}"})
34+
35+
servers, err := client.GetServerList(&steamweb.GetServerListFilter{}) // Set filters here
36+
if err != nil {
37+
log.Fatal(err)
38+
}
39+
40+
js, _ := json.Marshal(servers)
41+
42+
fmt.Println(string(js))
43+
}
44+
```
45+
46+
## Requirements
47+
Go 1.23 or higher
48+
49+
## Contribute
50+
Contributions are more than welcome!
51+
52+
If you think that you have found a bug, create an issue and publish the minimum amount of code triggering the bug, so
53+
it can be reproduced.
54+
55+
If you want to fix the bug then you can create a pull request. If possible, write a test that will cover this bug.
56+
57+
## License
58+
MIT License, see [LICENSE](LICENSE)

go.mod

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
module github.com/gorcon/steamweb
2+
3+
go 1.23.3
4+
5+
require github.com/stretchr/testify v1.10.0
6+
7+
require (
8+
github.com/davecgh/go-spew v1.1.1 // indirect
9+
github.com/pmezard/go-difflib v1.0.0 // indirect
10+
gopkg.in/yaml.v3 v3.0.1 // indirect
11+
)

go.sum

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
2+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
4+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
5+
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
6+
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
7+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
8+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
9+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
10+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

steamwebdraft/client.go

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
package steamweb
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"errors"
7+
"fmt"
8+
"io"
9+
"net"
10+
"net/http"
11+
"sort"
12+
"strings"
13+
)
14+
15+
const (
16+
GetPlayerBansURL = "/ISteamUser/GetPlayerBans/v1?key=%s&steamids=%s"
17+
GetServerListURL = "/IGameServersService/GetServerList/v1?key=%s&limit=%d&filter=%s"
18+
)
19+
20+
var (
21+
ErrWrongStatusCode = errors.New("wrong status code")
22+
ErrEmptyResponse = errors.New("empty response")
23+
)
24+
25+
// Client is http client for getting requests to ISteamUser api.
26+
type Client struct {
27+
config *Config
28+
http *http.Client
29+
}
30+
31+
// NewClient creates and returns a new Client instance initialized with the provided configuration.
32+
func NewClient(cfg *Config) *Client {
33+
cfg.SetDefaults()
34+
35+
return &Client{
36+
config: cfg,
37+
http: &http.Client{
38+
Timeout: cfg.Timeout,
39+
Transport: &http.Transport{
40+
DialContext: (&net.Dialer{
41+
Timeout: cfg.Transport.Dialer.Timeout,
42+
Deadline: cfg.Transport.Dialer.Deadline,
43+
FallbackDelay: cfg.Transport.Dialer.FallbackDelay,
44+
KeepAlive: cfg.Transport.Dialer.KeepAlive,
45+
}).DialContext,
46+
TLSHandshakeTimeout: cfg.Transport.TLSHandshakeTimeout,
47+
},
48+
},
49+
}
50+
}
51+
52+
// GetPlayerBans returns Community, VAC, and Economy ban statuses for given players.
53+
// Example URL: http://api.steampowered.com/ISteamUser/GetPlayerBans/v1/?key=XXXXXXXXXXXXXXXXX&steamids=XXXXXXXX,YYYYY
54+
func (c *Client) GetPlayerBans(steamIDs ...string) ([]PlayerBans, error) {
55+
response := GetPlayerBansResponse{}
56+
57+
// Return empty ban history with disabled client.
58+
if c.config.Disabled {
59+
return response.Players, nil
60+
}
61+
62+
uri := c.config.URL + fmt.Sprintf(GetPlayerBansURL, c.config.Key, strings.Join(steamIDs, ","))
63+
64+
body, err := c.sendRequest(context.Background(), http.MethodGet, uri, http.NoBody)
65+
if err != nil {
66+
return nil, err
67+
}
68+
69+
if err := json.Unmarshal(body, &response); err != nil {
70+
return nil, err
71+
}
72+
73+
return response.Players, nil
74+
}
75+
76+
// GetServerList returns Steam servers from filter query.
77+
// Example URL: http://api.steampowered.com/IGameServersService/GetServerList/v1/?key=XXXXXXXXXXXXXXXXX&limit=X&filter=F
78+
func (c *Client) GetServerList(filter *GetServerListFilter) ([]Server, error) {
79+
response := GetServerListResponse{}
80+
81+
// Return empty servers list with disabled client.
82+
if c.config.Disabled {
83+
return response.Response.Servers, nil
84+
}
85+
86+
limit := filter.Limit
87+
if limit == 0 {
88+
limit = DefaultLimit
89+
}
90+
91+
uri := c.config.URL + fmt.Sprintf(GetServerListURL, c.config.Key, limit, filter.String())
92+
93+
body, err := c.sendRequest(context.Background(), http.MethodGet, uri, http.NoBody)
94+
if err != nil {
95+
return nil, err
96+
}
97+
98+
if err := json.Unmarshal(body, &response); err != nil {
99+
return nil, err
100+
}
101+
102+
return c.filterServers(response.Response.Servers, filter), nil
103+
}
104+
105+
func (c *Client) sendRequest(ctx context.Context, method, uri string, body io.Reader) ([]byte, error) {
106+
req, err := http.NewRequestWithContext(ctx, method, uri, body)
107+
if err != nil {
108+
return nil, err
109+
}
110+
111+
res, err := c.http.Do(req)
112+
if err != nil {
113+
return nil, err
114+
}
115+
116+
if res != nil {
117+
defer res.Body.Close()
118+
} else {
119+
return nil, ErrEmptyResponse
120+
}
121+
122+
if res.StatusCode != http.StatusOK {
123+
return nil, fmt.Errorf("%w: %d %s", ErrWrongStatusCode, res.StatusCode, res.Status)
124+
}
125+
126+
resBody, err := io.ReadAll(res.Body)
127+
if err != nil {
128+
return nil, err
129+
}
130+
131+
return resBody, nil
132+
}
133+
134+
func (c *Client) filterServers(servers []Server, filter *GetServerListFilter) []Server {
135+
if filter.NoHidden || filter.NoDefaultServers {
136+
removeAddrs := make(map[string]bool)
137+
138+
for i := range servers {
139+
server := &servers[i]
140+
141+
if filter.NoHidden && strings.Contains(server.GameType, "hidden") {
142+
removeAddrs[server.Addr] = true
143+
144+
continue
145+
}
146+
147+
if filter.NoDefaultServers {
148+
for _, name := range c.config.DefaultServerNames {
149+
if server.Name == name {
150+
removeAddrs[server.Addr] = true
151+
152+
continue
153+
}
154+
}
155+
}
156+
}
157+
158+
servers = c.removeFilteredServers(servers, removeAddrs)
159+
}
160+
161+
sort.Slice(servers, func(i, j int) bool {
162+
return servers[i].Players > servers[j].Players
163+
})
164+
165+
return servers
166+
}
167+
168+
func (c *Client) removeFilteredServers(servers []Server, addrs map[string]bool) []Server {
169+
result := make([]Server, 0, len(servers))
170+
171+
for i := range servers {
172+
if _, ok := addrs[servers[i].Addr]; !ok {
173+
result = append(result, servers[i])
174+
}
175+
}
176+
177+
return result
178+
}

0 commit comments

Comments
 (0)