├── .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 | Update gist with Steam Playtime 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 | Update gist with Steam Playtime 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 | --------------------------------------------------------------------------------