├── .github
└── workflows
│ └── schedule.yml
├── LICENSE
├── README.md
├── README_zh.md
├── cmd
└── box
│ └── main.go
├── go.mod
├── go.sum
└── pkg
└── steambox
├── box.go
├── box_test.go
└── test.md
/.github/workflows/schedule.yml:
--------------------------------------------------------------------------------
1 | name: Update gist with Steam Playtime
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | pull_request:
8 | branches:
9 | - master
10 | schedule:
11 | - cron: "0 * * * *"
12 |
13 |
14 | jobs:
15 |
16 | build:
17 | name: Upddate-gist
18 | runs-on: ubuntu-latest
19 | env:
20 | GH_TOKEN: ${{ secrets.GH_TOKEN }}
21 | GIST_ID: 8bf56353bcb3a8e798b55b546b9619cf
22 | STEAM_API_KEY: ${{ secrets.STEAM_API_KEY }}
23 | STEAM_ID: ${{ secrets.STEAM_ID }}
24 | steps:
25 |
26 | - name: Set up Go 1.x
27 | uses: actions/setup-go@v2
28 | with:
29 | go-version: ^1.14
30 | id: go
31 |
32 | - name: Check out code into the Go module directory
33 | uses: actions/checkout@v2
34 |
35 | - name: Update-gist
36 | run: go run ./cmd/box/main.go
37 |
38 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction, and
10 | distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by the copyright
13 | owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all other entities
16 | that control, are controlled by, or are under common control with that entity.
17 | For the purposes of this definition, "control" means (i) the power, direct or
18 | indirect, to cause the direction or management of such entity, whether by
19 | contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the
20 | outstanding shares, or (iii) beneficial ownership of such entity.
21 |
22 | "You" (or "Your") shall mean an individual or Legal Entity exercising
23 | permissions granted by this License.
24 |
25 | "Source" form shall mean the preferred form for making modifications, including
26 | but not limited to software source code, documentation source, and configuration
27 | files.
28 |
29 | "Object" form shall mean any form resulting from mechanical transformation or
30 | translation of a Source form, including but not limited to compiled object code,
31 | generated documentation, and conversions to other media types.
32 |
33 | "Work" shall mean the work of authorship, whether in Source or Object form, made
34 | available under the License, as indicated by a copyright notice that is included
35 | in or attached to the work (an example is provided in the Appendix below).
36 |
37 | "Derivative Works" shall mean any work, whether in Source or Object form, that
38 | is based on (or derived from) the Work and for which the editorial revisions,
39 | annotations, elaborations, or other modifications represent, as a whole, an
40 | original work of authorship. For the purposes of this License, Derivative Works
41 | shall not include works that remain separable from, or merely link (or bind by
42 | name) to the interfaces of, the Work and Derivative Works thereof.
43 |
44 | "Contribution" shall mean any work of authorship, including the original version
45 | of the Work and any modifications or additions to that Work or Derivative Works
46 | thereof, that is intentionally submitted to Licensor for inclusion in the Work
47 | by the copyright owner or by an individual or Legal Entity authorized to submit
48 | on behalf of the copyright owner. For the purposes of this definition,
49 | "submitted" means any form of electronic, verbal, or written communication sent
50 | to the Licensor or its representatives, including but not limited to
51 | communication on electronic mailing lists, source code control systems, and
52 | issue tracking systems that are managed by, or on behalf of, the Licensor for
53 | the purpose of discussing and improving the Work, but excluding communication
54 | that is conspicuously marked or otherwise designated in writing by the copyright
55 | owner as "Not a Contribution."
56 |
57 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf
58 | of whom a Contribution has been received by Licensor and subsequently
59 | incorporated within the Work.
60 |
61 | 2. Grant of Copyright License.
62 |
63 | Subject to the terms and conditions of this License, each Contributor hereby
64 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
65 | irrevocable copyright license to reproduce, prepare Derivative Works of,
66 | publicly display, publicly perform, sublicense, and distribute the Work and such
67 | Derivative Works in Source or Object form.
68 |
69 | 3. Grant of Patent License.
70 |
71 | Subject to the terms and conditions of this License, each Contributor hereby
72 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
73 | irrevocable (except as stated in this section) patent license to make, have
74 | made, use, offer to sell, sell, import, and otherwise transfer the Work, where
75 | such license applies only to those patent claims licensable by such Contributor
76 | that are necessarily infringed by their Contribution(s) alone or by combination
77 | of their Contribution(s) with the Work to which such Contribution(s) was
78 | submitted. If You institute patent litigation against any entity (including a
79 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a
80 | Contribution incorporated within the Work constitutes direct or contributory
81 | patent infringement, then any patent licenses granted to You under this License
82 | for that Work shall terminate as of the date such litigation is filed.
83 |
84 | 4. Redistribution.
85 |
86 | You may reproduce and distribute copies of the Work or Derivative Works thereof
87 | in any medium, with or without modifications, and in Source or Object form,
88 | provided that You meet the following conditions:
89 |
90 | You must give any other recipients of the Work or Derivative Works a copy of
91 | this License; and
92 | You must cause any modified files to carry prominent notices stating that You
93 | changed the files; and
94 | You must retain, in the Source form of any Derivative Works that You distribute,
95 | all copyright, patent, trademark, and attribution notices from the Source form
96 | of the Work, excluding those notices that do not pertain to any part of the
97 | Derivative Works; and
98 | If the Work includes a "NOTICE" text file as part of its distribution, then any
99 | Derivative Works that You distribute must include a readable copy of the
100 | attribution notices contained within such NOTICE file, excluding those notices
101 | that do not pertain to any part of the Derivative Works, in at least one of the
102 | following places: within a NOTICE text file distributed as part of the
103 | Derivative Works; within the Source form or documentation, if provided along
104 | with the Derivative Works; or, within a display generated by the Derivative
105 | Works, if and wherever such third-party notices normally appear. The contents of
106 | the NOTICE file are for informational purposes only and do not modify the
107 | License. You may add Your own attribution notices within Derivative Works that
108 | You distribute, alongside or as an addendum to the NOTICE text from the Work,
109 | provided that such additional attribution notices cannot be construed as
110 | modifying the License.
111 | You may add Your own copyright statement to Your modifications and may provide
112 | additional or different license terms and conditions for use, reproduction, or
113 | distribution of Your modifications, or for any such Derivative Works as a whole,
114 | provided Your use, reproduction, and distribution of the Work otherwise complies
115 | with the conditions stated in this License.
116 |
117 | 5. Submission of Contributions.
118 |
119 | Unless You explicitly state otherwise, any Contribution intentionally submitted
120 | for inclusion in the Work by You to the Licensor shall be under the terms and
121 | conditions of this License, without any additional terms or conditions.
122 | Notwithstanding the above, nothing herein shall supersede or modify the terms of
123 | any separate license agreement you may have executed with Licensor regarding
124 | such Contributions.
125 |
126 | 6. Trademarks.
127 |
128 | This License does not grant permission to use the trade names, trademarks,
129 | service marks, or product names of the Licensor, except as required for
130 | reasonable and customary use in describing the origin of the Work and
131 | reproducing the content of the NOTICE file.
132 |
133 | 7. Disclaimer of Warranty.
134 |
135 | Unless required by applicable law or agreed to in writing, Licensor provides the
136 | Work (and each Contributor provides its Contributions) on an "AS IS" BASIS,
137 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
138 | including, without limitation, any warranties or conditions of TITLE,
139 | NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
140 | solely responsible for determining the appropriateness of using or
141 | redistributing the Work and assume any risks associated with Your exercise of
142 | permissions under this License.
143 |
144 | 8. Limitation of Liability.
145 |
146 | In no event and under no legal theory, whether in tort (including negligence),
147 | contract, or otherwise, unless required by applicable law (such as deliberate
148 | and grossly negligent acts) or agreed to in writing, shall any Contributor be
149 | liable to You for damages, including any direct, indirect, special, incidental,
150 | or consequential damages of any character arising as a result of this License or
151 | out of the use or inability to use the Work (including but not limited to
152 | damages for loss of goodwill, work stoppage, computer failure or malfunction, or
153 | any and all other commercial damages or losses), even if such Contributor has
154 | been advised of the possibility of such damages.
155 |
156 | 9. Accepting Warranty or Additional Liability.
157 |
158 | While redistributing the Work or Derivative Works thereof, You may choose to
159 | offer, and charge a fee for, acceptance of support, warranty, indemnity, or
160 | other liability obligations and/or rights consistent with this License. However,
161 | in accepting such obligations, You may act only on Your own behalf and on Your
162 | sole responsibility, not on behalf of any other Contributor, and only if You
163 | agree to indemnify, defend, and hold each Contributor harmless for any liability
164 | incurred by, or claims asserted against, such Contributor by reason of your
165 | accepting any such warranty or additional liability.
166 |
167 | END OF TERMS AND CONDITIONS
168 |
169 | APPENDIX: How to apply the Apache License to your work
170 |
171 | To apply the Apache License to your work, attach the following boilerplate
172 | notice, with the fields enclosed by brackets "[]" replaced with your own
173 | identifying information. (Don't include the brackets!) The text should be
174 | enclosed in the appropriate comment syntax for the file format. We also
175 | recommend that a file or class name and description of purpose be included on
176 | the same "printed page" as the copyright notice for easier identification within
177 | third-party archives.
178 |
179 | Copyright [2020] [chuiyouwu@gmail.com]
180 |
181 | Licensed under the Apache License, Version 2.0 (the "License");
182 | you may not use this file except in compliance with the License.
183 | You may obtain a copy of the License at
184 |
185 | http://www.apache.org/licenses/LICENSE-2.0
186 |
187 | Unless required by applicable law or agreed to in writing, software
188 | distributed under the License is distributed on an "AS IS" BASIS,
189 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
190 | See the License for the specific language governing permissions and
191 | limitations under the License.
192 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | #
2 |
3 |
4 |
5 |
6 |
steam-box
7 |
Update pinned gist / profile README to contain your Steam playtime leaderboard.
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | ---
16 | English | [简体中文](./README_zh.md)
17 |
18 | > 📌✨ For more pinned-gist projects like this one, check out: https://github.com/matchai/awesome-pinned-gists
19 |
20 |
21 | ## 💻 Setup
22 |
23 | ### 🎒 Prep work
24 | > if only want's to update a markdown,like profile README,skip step 1 and step 2.
25 | 1. Create a new public GitHub Gist (https://gist.github.com/)
26 | 1. Create a token with the `gist` scope and copy it. (https://github.com/settings/tokens/new)
27 | 1. Create a Steam API key. (https://steamcommunity.com/dev/apikey)
28 | 1. Find the steam ID (steamID64) of your account. (https://steamid.io)
29 | 1. For updating a markdown file,add comments to the place where you want to update in the markdown file.
30 | ```markdown
31 |
32 |
33 |
34 | ```
35 |
36 |
37 | ### 🚀 Project setup
38 | 1. Fork this repo
39 | 1. Edit the [environment variable](https://github.com/YouEclipse/steam-box/actions/runs/126970182/workflow#L17-L19) in `.github/workflows/schedule.yml`:
40 |
41 | > For updating github profile README,you can follow [steam-box.yml](https://github.com/YouEclipse/YouEclipse/blob/master/.github/workflows/steam-box.yml) in [YouEclipse](https://github.com/YouEclipse/YouEclipse) to create a Action in your README repo.Remember it's unsafe to use token with **`repo`** scope for updating the repo, steam-box update the profile repo using git command in Github Action instead of using github API.
42 |
43 | - **GIST_ID:** The ID portion from your gist url: `https://gist.github.com/YouEclipse/`**`9bc7025496e478f439b9cd43eba989a4`**.
44 |
45 | 1. Go to the repo **Settings > Secrets**
46 | 1. Add the following environment variables:
47 | - **GH_TOKEN:** The GitHub token generated above.
48 | - **STEAM_API_KEY:** The steam API key you created above.
49 | - **STEAM_ID:** The steam ID of your account.
50 | 1. If you want to show specific games,put the ids in environmet variable **APP_ID**:
51 | - like `APP_ID=431960,730`
52 | - you can get the id of a game from the store url: `https://store.steampowered.com/app/`**730**`/CounterStrike_Global_Offensive/`
53 |
54 | ## 🕵️ How it works
55 | - Get your games playtime from [Steamwork Web API](https://partner.steamgames.com/doc/webapi)
56 | - Update Gist with Github API
57 | - Use Github Actions for updating Gist
58 |
59 | ## 📄 License
60 | This project is licensed under [Apache-2.0](./LICENSE)
61 |
--------------------------------------------------------------------------------
/README_zh.md:
--------------------------------------------------------------------------------
1 | #
2 |
3 |
4 |
5 |
6 |
steam-box
7 |
将你的 steam 游玩时间显示在 profile README/pinned gist.
8 |
9 |
10 |
11 |
12 |
13 |
14 | ---
15 | [English](./README.md) | 简体中文
16 |
17 |
18 |
19 | > 📌✨ 查看更多像这样的 Pinned Gist 项目,传送门: https://github.com/matchai/awesome-pinned-gists
20 |
21 |
22 |
23 | ## 💻 安装
24 |
25 | ### 🎒 前置工作
26 |
27 | 1. 创建一个公开的 GitHub Gist (https://gist.github.com/)
28 | 1. 创建一个拥有 `gist` 权限的 token 并复制. (https://github.com/settings/tokens/new)
29 | 1. 创建你的 Steam API key. (https://steamcommunity.com/dev/apikey)
30 | 1. 找到你的账号的 64 位 ID. (https://steamid.io)
31 | 1. 如果需要更新到某个 markdown 文件(比如profile README),请在对应文件需要更新的地方添加以下注释
32 |
33 | ```markdown
34 |
35 |
36 | ```
37 |
38 | ### 🚀 开始安装
39 |
40 | 1. Fork 这个仓库
41 | 1. 编辑 `.github/workflows/schedule.yml` 中的[环境变量](https://github.com/YouEclipse/steam-box/actions/runs/126970182/workflow#L17-L19) :
42 |
43 | > 如果是需要更新 github profile README,可以在 profile README 的仓库中创建 Action,具体配置参考 我的 [YouEclipse](https://github.com/YouEclipse/YouEclipse) 中的 [steam-box.yml](https://github.com/YouEclipse/YouEclipse/blob/master/.github/workflows/steam-box.yml).因为使用 **`repo`** 权限的token 来通过 API 更新仓库,可能会不安全,所以我的示例中使用 git 命令来更新,这样更加安全。
44 |
45 | - **GIST_ID:** ID 是 gist url 的后缀 : `https://gist.github.com/YouEclipse/`**`9bc7025496e478f439b9cd43eba989a4`**.
46 |
47 | 3. 前往 fork 后的仓库的 **Settings > Secrets**
48 | 4. 添加以下环境变量:
49 |
50 | - **GH_TOKEN:** 前置工作中生成的 github token.
51 | - **STEAM_API_KEY:** 前置工作中创建的 steam API key.
52 | - **STEAM_ID:** 你的 steam 64 位 ID.
53 | 5. 如果你想展示某几个指定的游戏,可以把他们的 ID 设置在环境变量 **APP_ID**:
54 | - 如 `APP_ID=431960,730`
55 | - 你可以在对应游戏的 steam 商店的 url 获取到游戏 id: `https://store.steampowered.com/app/`**730**`/CounterStrike_Global_Offensive/`
56 |
57 | ## 🕵️ 工作原理
58 | - 基于 [Steam API](https://partner.steamgames.com/doc/webapi) 获取游戏的游玩时间
59 | - 基于 Github API 更新 Gist
60 | - 使用 Github Actions 自动更新 Gist
61 |
62 | ## 📄 开源协议
63 | 本项目使用 [Apache-2.0](./LICENSE) 协议
--------------------------------------------------------------------------------
/cmd/box/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "fmt"
7 | "os"
8 | "strconv"
9 | "strings"
10 |
11 | "github.com/YouEclipse/steam-box/pkg/steambox"
12 | "github.com/google/go-github/github"
13 | )
14 |
15 | func main() {
16 | var err error
17 | steamAPIKey := os.Getenv("STEAM_API_KEY")
18 | steamID, _ := strconv.ParseUint(os.Getenv("STEAM_ID"), 10, 64)
19 | appIDs := os.Getenv("APP_ID")
20 | appIDList := make([]uint32, 0)
21 |
22 | for _, appID := range strings.Split(appIDs, ",") {
23 | appid, err := strconv.ParseUint(appID, 10, 32)
24 | if err != nil {
25 | continue
26 | }
27 | appIDList = append(appIDList, uint32(appid))
28 | }
29 |
30 | ghToken := os.Getenv("GH_TOKEN")
31 | ghUsername := os.Getenv("GH_USER")
32 | gistID := os.Getenv("GIST_ID")
33 |
34 | steamOption := "ALLTIME" // options for types of games to list: RECENT (recently played games), ALLTIME (playtime of games in descending order)
35 | if os.Getenv("STEAM_OPTION") != "" {
36 | steamOption = os.Getenv("STEAM_OPTION")
37 | }
38 |
39 | multiLined := false // boolean for whether hours should have their own line - YES = true, NO = false
40 | if os.Getenv("MULTILINE") != "" {
41 | lineOption := os.Getenv("MULTILINE")
42 | if lineOption == "YES" {
43 | multiLined = true
44 | }
45 | }
46 |
47 | updateOption := os.Getenv("UPDATE_OPTION") // options for update: GIST (Gist only), MARKDOWN (README only), GIST_AND_MARKDOWN (Gist and README)
48 | markdownFile := os.Getenv("MARKDOWN_FILE") // the markdown filename (e.g. MYFILE.md)
49 |
50 | var updateGist, updateMarkdown bool
51 | if updateOption == "MARKDOWN" {
52 | updateMarkdown = true
53 | } else if updateOption == "GIST_AND_MARKDOWN" {
54 | updateGist = true
55 | updateMarkdown = true
56 | } else {
57 | updateGist = true
58 | }
59 |
60 | box := steambox.NewBox(steamAPIKey, ghUsername, ghToken)
61 |
62 | ctx := context.Background()
63 |
64 | var (
65 | filename string
66 | lines []string
67 | )
68 |
69 | if steamOption == "ALLTIME" {
70 | filename = "🎮 Steam playtime leaderboard"
71 | lines, err = box.GetPlayTime(ctx, steamID, multiLined, appIDList...)
72 | if err != nil {
73 | panic("GetPlayTime err:" + err.Error())
74 | }
75 | } else if steamOption == "RECENT" {
76 | filename = "🎮 Recently played Steam games"
77 | lines, err = box.GetRecentGames(ctx, steamID, multiLined)
78 | if err != nil {
79 | panic("GetRecentGames err:" + err.Error())
80 | }
81 | }
82 |
83 | if updateGist {
84 | gist, err := box.GetGist(ctx, gistID)
85 | if err != nil {
86 | panic("GetGist err:" + err.Error())
87 | }
88 |
89 | f := gist.Files[github.GistFilename(filename)]
90 |
91 | f.Content = github.String(strings.Join(lines, "\n"))
92 | gist.Files[github.GistFilename(filename)] = f
93 |
94 | err = box.UpdateGist(ctx, gistID, gist)
95 | if err != nil {
96 | panic("UpdateGist err:" + err.Error())
97 | }
98 | }
99 |
100 | if updateMarkdown && markdownFile != "" {
101 | title := filename
102 | if updateGist {
103 | title = fmt.Sprintf(`#### %s`, gistID, title)
104 | }
105 |
106 | content := bytes.NewBuffer(nil)
107 | content.WriteString(strings.Join(lines, "\n"))
108 |
109 | err = box.UpdateMarkdown(ctx, title, markdownFile, content.Bytes())
110 | if err != nil {
111 | fmt.Println(err)
112 | }
113 | fmt.Println("updating markdown successfully on ", markdownFile)
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/YouEclipse/steam-box
2 |
3 | go 1.14
4 |
5 | require (
6 | github.com/YouEclipse/steam-go v0.0.0-20200711125252-eccd89412923
7 | github.com/google/go-github v17.0.0+incompatible
8 | github.com/google/go-querystring v1.0.0 // indirect
9 | github.com/mattn/go-runewidth v0.0.9
10 | )
11 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/YouEclipse/steam-go v0.0.0-20200607095305-d18221313a45 h1:kUsRRKxquRByz4Jd2e0eF8SYFCnvlipmXbuWSBpWzCI=
2 | github.com/YouEclipse/steam-go v0.0.0-20200607095305-d18221313a45/go.mod h1:jlP5azGfKH8ZkFb2h1pK2n2JIZlbHbStD841RkwcH7I=
3 | github.com/YouEclipse/steam-go v0.0.0-20200711125252-eccd89412923 h1:4maE6WS9ueQD1TzZ1pdHgW0Jm4Sb/UOJL1n8eZoGVmY=
4 | github.com/YouEclipse/steam-go v0.0.0-20200711125252-eccd89412923/go.mod h1:jlP5azGfKH8ZkFb2h1pK2n2JIZlbHbStD841RkwcH7I=
5 | github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY=
6 | github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
7 | github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
8 | github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
9 | github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
10 | github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
11 |
--------------------------------------------------------------------------------
/pkg/steambox/box.go:
--------------------------------------------------------------------------------
1 | package steambox
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "fmt"
7 | "io/ioutil"
8 | "math"
9 | "os"
10 | "sort"
11 | "strings"
12 |
13 | steam "github.com/YouEclipse/steam-go/pkg"
14 | "github.com/google/go-github/github"
15 | "github.com/mattn/go-runewidth"
16 | )
17 |
18 | // Box defines the steam box.
19 | type Box struct {
20 | steam *steam.Client
21 | github *github.Client
22 | }
23 |
24 | // NewBox creates a new Box with the given API key.
25 | func NewBox(apikey string, ghUsername, ghToken string) *Box {
26 | box := &Box{}
27 | box.steam = steam.NewClient(apikey, nil)
28 | tp := github.BasicAuthTransport{
29 | Username: strings.TrimSpace(ghUsername),
30 | Password: strings.TrimSpace(ghToken),
31 | }
32 |
33 | box.github = github.NewClient(tp.Client())
34 |
35 | return box
36 |
37 | }
38 |
39 | // GetGist gets the gist from github.com.
40 | func (b *Box) GetGist(ctx context.Context, id string) (*github.Gist, error) {
41 | gist, _, err := b.github.Gists.Get(ctx, id)
42 | if err != nil {
43 | return nil, err
44 | }
45 |
46 | return gist, nil
47 | }
48 |
49 | // UpdateGist updates the gist.
50 | func (b *Box) UpdateGist(ctx context.Context, id string, gist *github.Gist) error {
51 | _, _, err := b.github.Gists.Edit(ctx, id, gist)
52 | return err
53 | }
54 |
55 | // GetPlayTime gets the top 5 Steam games played in descending order from the Steam API.
56 | func (b *Box) GetPlayTime(ctx context.Context, steamID uint64, multiLined bool, appID ...uint32) ([]string, error) {
57 | params := &steam.GetOwnedGamesParams{
58 | SteamID: steamID,
59 | IncludeAppInfo: true,
60 | IncludePlayedFreeGames: true,
61 | }
62 | if len(appID) > 0 {
63 | params.AppIDsFilter = appID
64 | }
65 |
66 | gameRet, err := b.steam.IPlayerService.GetOwnedGames(ctx, params)
67 | if err != nil {
68 | return nil, err
69 | }
70 | var lines []string
71 | var max = 0
72 | sort.Slice(gameRet.Games, func(i, j int) bool {
73 | return gameRet.Games[i].PlaytimeForever > gameRet.Games[j].PlaytimeForever
74 | })
75 |
76 | for _, game := range gameRet.Games {
77 | if max >= 5 {
78 | break
79 | }
80 |
81 | hours := int(math.Floor(float64(game.PlaytimeForever / 60)))
82 | mins := int(math.Floor(float64(game.PlaytimeForever % 60)))
83 |
84 | if multiLined {
85 | gameLine := getNameEmoji(game.Appid, game.Name)
86 | lines = append(lines, gameLine)
87 | hoursLine := fmt.Sprintf(" 🕘 %d hrs %d mins", hours, mins)
88 | lines = append(lines, hoursLine)
89 | } else {
90 | line := pad(getNameEmoji(game.Appid, game.Name), " ", 35) + " " +
91 | pad(fmt.Sprintf("🕘 %d hrs %d mins", hours, mins), "", 16)
92 | lines = append(lines, line)
93 | }
94 | max++
95 | }
96 | return lines, nil
97 | }
98 |
99 | // GetRecentGames gets 5 recently played games from the Steam API.
100 | func (b *Box) GetRecentGames(ctx context.Context, steamID uint64, multiLined bool) ([]string, error) {
101 | params := &steam.GetRecentlyPlayedGamesParams{
102 | SteamID: steamID,
103 | Count: 5,
104 | }
105 |
106 | gameRet, err := b.steam.IPlayerService.GetRecentlyPlayedGames(ctx, params)
107 | if err != nil {
108 | return nil, err
109 | }
110 | var lines []string
111 | var max = 0
112 |
113 | for _, game := range gameRet.Games {
114 | if max >= 5 {
115 | break
116 | }
117 |
118 | if game.Name == "" {
119 | game.Name = "Unknown Game"
120 | }
121 |
122 | hours := int(math.Floor(float64(game.PlaytimeForever / 60)))
123 | mins := int(math.Floor(float64(game.PlaytimeForever % 60)))
124 |
125 | if multiLined {
126 | gameLine := getNameEmoji(game.Appid, game.Name)
127 | lines = append(lines, gameLine)
128 | hoursLine := fmt.Sprintf(" 🕘 %d hrs %d mins", hours, mins)
129 | lines = append(lines, hoursLine)
130 | } else {
131 | line := pad(getNameEmoji(game.Appid, game.Name), " ", 35) + " " +
132 | pad(fmt.Sprintf("🕘 %d hrs %d mins", hours, mins), "", 16)
133 | lines = append(lines, line)
134 | }
135 | max++
136 | }
137 | return lines, nil
138 | }
139 |
140 | // UpdateMarkdown updates the content to the markdown file.
141 | func (b *Box) UpdateMarkdown(ctx context.Context, title, filename string, content []byte) error {
142 | md, err := ioutil.ReadFile(filename)
143 | if err != nil {
144 | return fmt.Errorf("steambox.UpdateMarkdown: Error reade a file: %w", err)
145 | }
146 |
147 | start := []byte("")
148 | before := md[:bytes.Index(md, start)+len(start)]
149 | end := []byte("")
150 | after := md[bytes.Index(md, end):]
151 |
152 | newMd := bytes.NewBuffer(nil)
153 | newMd.Write(before)
154 | newMd.WriteString("\n" + title + "\n")
155 | newMd.WriteString("```text\n")
156 | newMd.Write(content)
157 | newMd.WriteString("\n")
158 | newMd.WriteString("```\n")
159 | newMd.WriteString("\n")
160 | newMd.Write(after)
161 |
162 | err = ioutil.WriteFile(filename, newMd.Bytes(), os.ModeAppend)
163 | if err != nil {
164 | return fmt.Errorf("steambox.UpdateMarkdown: Error writing a file: %w", err)
165 | }
166 |
167 | return nil
168 | }
169 |
170 | func pad(s, pad string, targetLength int) string {
171 | padding := targetLength - runewidth.StringWidth(s)
172 |
173 | if padding <= 0 {
174 | return s
175 | }
176 |
177 | return s + strings.Repeat(pad, padding)
178 | }
179 |
180 | func getNameEmoji(id int, name string) string {
181 | // hard code some game's emoji
182 | var nameEmojiMap = map[int]string{
183 | 70: "λ ", // Half-Life
184 | 220: "λ² ", // Half-Life 2
185 | 500: "🧟 ", // Left 4 Dead
186 | 550: "🧟 ", // Left 4 Dead 2
187 | 570: "⚔️ ", // Dota 2
188 | 730: "🔫 ", // CS:GO
189 | 8930: "🌏 ", // Sid Meier's Civilization V
190 | 252950: "🚀 ", // Rocket League
191 | 269950: "✈️ ", // X-Plane 11
192 | 271590: "🚓 ", // GTA 5
193 | 359550: "🔫 ", // Tom Clancy's Rainbow Six Siege
194 | 431960: "💻 ", // Wallpaper Engine
195 | 578080: "🍳 ", // PUBG
196 | 945360: "🕵️♂️ ", // Among Us
197 | 1250410: "🛩️ ", // Microsoft Flight Simulator
198 | 1091500: "🦾 ", // Cyberpunk 2077
199 | 594650: "🎯 ", // Hunt: Showdown
200 | 230410: "🐹 ", // Warframe
201 | 397540: "🤖 ", // Borderlands 3
202 | 49520: "🤖 ", // Borderlands 2
203 | }
204 |
205 | if emoji, ok := nameEmojiMap[id]; ok {
206 | return emoji + name
207 | }
208 |
209 | if name == "Unknown Game" {
210 | return "❓ " + name
211 | }
212 |
213 | return "🎮 " + name
214 | }
215 |
--------------------------------------------------------------------------------
/pkg/steambox/box_test.go:
--------------------------------------------------------------------------------
1 | package steambox
2 |
3 | import (
4 | "context"
5 | "io/ioutil"
6 | "os"
7 | "strconv"
8 | "strings"
9 | "testing"
10 | )
11 |
12 | func TestBox_GetPlayTime(t *testing.T) {
13 | var err error
14 | steamAPIKey := os.Getenv("STEAM_API_KEY")
15 | steamID, _ := strconv.ParseUint(os.Getenv("STEAM_ID"), 10, 64)
16 |
17 | multiLined := false // boolean for whether hours should have their own line
18 | if os.Getenv("MULTILINE") != "" {
19 | multiLined, err = strconv.ParseBool(os.Getenv("MULTILINE"))
20 | if err != nil {
21 | panic("multiLined option error: "+ err.Error())
22 | }
23 | }
24 |
25 | appIDs := os.Getenv("APP_ID")
26 | appIDList := make([]uint32, 0)
27 |
28 | for _, appID := range strings.Split(appIDs, ",") {
29 | appid, err := strconv.ParseUint(appID, 10, 32)
30 | if err != nil {
31 | continue
32 | }
33 | appIDList = append(appIDList, uint32(appid))
34 | }
35 |
36 | ghToken := os.Getenv("GH_TOKEN")
37 | ghUsername := os.Getenv("GH_USER")
38 |
39 | box := NewBox(steamAPIKey, ghUsername, ghToken)
40 | lines, err := box.GetPlayTime(context.Background(), steamID, multiLined, appIDList...)
41 | if err != nil {
42 | t.Error(err)
43 | }
44 | t.Log(strings.Join(lines, "\n"))
45 | }
46 |
47 | func TestBox_GetRecentGames(t *testing.T) {
48 | var err error
49 | steamAPIKey := os.Getenv("STEAM_API_KEY")
50 | steamID, _ := strconv.ParseUint(os.Getenv("STEAM_ID"), 10, 64)
51 |
52 | ghToken := os.Getenv("GH_TOKEN")
53 | ghUsername := os.Getenv("GH_USER")
54 |
55 | multiLined := false // boolean for whether hours should have their own line - YES = true, NO = false
56 | if os.Getenv("MULTILINE") != "" {
57 | lineOption := os.Getenv("MULTILINE")
58 | if lineOption == "YES" {
59 | multiLined = true
60 | }
61 | }
62 |
63 | box := NewBox(steamAPIKey, ghUsername, ghToken)
64 | lines, err := box.GetRecentGames(context.Background(), steamID, multiLined)
65 | if err != nil {
66 | t.Error(err)
67 | }
68 | t.Log(strings.Join(lines, "\n"))
69 | }
70 |
71 | func TestBox_Readme(t *testing.T) {
72 |
73 | ghToken := os.Getenv("GH_TOKEN")
74 | ghUsername := os.Getenv("GH_USER")
75 |
76 | box := NewBox("", ghUsername, ghToken)
77 |
78 | ctx := context.Background()
79 |
80 | filename := "test.md"
81 | title := `#### 🎮 Steam playtime leaderboard`
82 | content := []byte(`🔫 Counter-Strike: Global Offensive 🕘 1546 hrs 25 mins
83 | 🚓 Grand Theft Auto V 🕘 52 hrs 15 mins
84 | 💻 Wallpaper Engine 🕘 39 hrs 59 mins
85 | 🍳 PLAYERUNKNOWN'S BATTLEGROUNDS 🕘 34 hrs 40 mins
86 | 🌏 Sid Meier's Civilization V 🕘 11 hrs 9 mins`)
87 |
88 | err := box.UpdateMarkdown(ctx, title, filename, content)
89 | if err != nil {
90 | t.Error(err)
91 | }
92 | c, _ := ioutil.ReadFile(filename)
93 | if err != nil {
94 | t.Error(err)
95 | }
96 | t.Logf("%s", c)
97 | }
98 |
--------------------------------------------------------------------------------
/pkg/steambox/test.md:
--------------------------------------------------------------------------------
1 |
2 | #### 🎮 Steam playtime leaderboard
3 | ```text
4 | 🔫 Counter-Strike: Global Offensive 🕘 1546 hrs 25 mins
5 | 🚓 Grand Theft Auto V 🕘 52 hrs 15 mins
6 | 💻 Wallpaper Engine 🕘 39 hrs 59 mins
7 | 🍳 PLAYERUNKNOWN'S BATTLEGROUNDS 🕘 34 hrs 40 mins
8 | 🌏 Sid Meier's Civilization V 🕘 11 hrs 9 mins
9 | ```
10 |
11 |
12 |
--------------------------------------------------------------------------------