├── .github └── workflows │ └── update.yaml ├── LICENSE ├── README.en.md ├── README.md ├── assets └── box.png ├── go.mod ├── go.sum ├── lib ├── client.go ├── gist.go ├── helper.go ├── helper_test.go ├── render.go ├── render_test.go ├── solved.go └── solved_test.go └── main.go /.github/workflows/update.yaml: -------------------------------------------------------------------------------- 1 | name: Update Gist 2 | on: 3 | schedule: 4 | # 21 UTC is equivalent to 6 KST 5 | # when the solved.ac streak updates 6 | - cron: 0 21 * * * 7 | push: 8 | branches: 9 | - main 10 | # allow user to manually start the action 11 | workflow_dispatch: 12 | jobs: 13 | update: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | - name: Setup Go 18 | uses: actions/setup-go@v4 19 | - name: Build 20 | run: go build -v . 21 | - name: Update 22 | run: go run . 23 | env: 24 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 25 | GIST_ID: ${{ secrets.GIST_ID }} 26 | USERNAME: ${{ secrets.USERNAME }} 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2024 Abiria 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.en.md: -------------------------------------------------------------------------------- 1 |
6 |
7 |
8 |
9 | 📊 Show your Solved.ac profile information as a GitHub Gist 📊 10 |
11 | 12 | --- 13 | 14 | ## 🎒 Prep Work 15 | 16 | 1. [Create a new GitHub PAT token](https://github.com/settings/personal-access-tokens/new) with `Gists` to `Read and write` and copy it. 17 | 2. Create a new public [GitHub Gist](https://gist.github.com/) then copy the ID from the URL. 18 | 3. Lastly, prepare your BOJ(Baekjoon Online Judge) account ID. 19 | 20 | ## 🖥 Project Setup 21 | 22 | 1. [Fork this repository](https://github.com/abiriadev/solvedac-box/fork). 23 | 2. Go to `Settings` > `Secrets and variables` > `Actions` then add the below informations as a `New repository secret`s. 24 | 3. Go to `Actions` > `Update Gist` then push the `Run workflow` > `Run workflow` button! 25 | 4. After the Gist has beed updated, you can pin it to your GitHub profile! 26 | 27 | ## 🤫 Environments Secrets 28 | 29 | - **GH_TOKEN:** The GitHub PAT token generated above 30 | - **GIST_ID:** The ID of your GitHub Gist 31 | - **USERNAME:** Your BOJ account ID 32 | 33 | ## 📄 License 34 | 35 | [](./LICENSE) 36 | 37 | _Special thanks to [BOJ](https://www.acmicpc.net/) and [@solved-ac](https://github.com/solved-ac)_ 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
6 |
7 |
8 |
9 | 📊 Solved.ac의 프로필 정보를 GitHub Gist로 보여주는 GitHub Action 📊 10 |
11 | 12 | --- 13 | 14 | > [!TIP] 15 | > You can find the English README [here](./README.en.md). 16 | 17 | ## 🎒 사전 준비 18 | 19 | 1. [새 GitHub PAT 토큰 생성 페이지](https://github.com/settings/personal-access-tokens/new)에서 `Gists`를 `Read and write`로 변경하고 생성 후 생성된 토큰 저장 20 | 2. [GitHub Gist](https://gist.github.com/)에서 새 Public Gist를 생성한 후 ID 저장 21 | 3. 마지막으로, 자신의 백준 ID 준비! 22 | 23 | ## 🖥 셋업 방법 24 | 25 | 1. [본 저장소를 포크](https://github.com/abiriadev/solvedac-box/fork)합니다. 26 | 2. `Settings` > `Secrets and variables` > `Actions` 로 들어가 `New repository secret`을 눌러 다음 정보를 시크릿으로 추가합니다. 27 | 3. `Actions` > `Update Gist` 로 들어가 `Run workflow` > `Run workflow` 버튼을 클릭! 28 | 4. Gist가 업데이트 된 후 자신의 GitHub 프로필에 핀하면 완료! 29 | 30 | ## 🤫 환경변수 설정 31 | 32 | - **GH_TOKEN:** 앞서 생성한 GitHub 토큰 33 | - **GIST_ID:** 앞서 생성한 Gist의 ID 34 | - **USERNAME:** 백준 ID 35 | 36 | ## 📄 라이선스 37 | 38 | [](./LICENSE) 39 | 40 | _Special thanks to [BOJ](https://www.acmicpc.net/) and [@solved-ac](https://github.com/solved-ac)_ 41 | -------------------------------------------------------------------------------- /assets/box.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abiriadev/solvedac-box/5c51805e715506505d93b56b14478e3242cc2e4a/assets/box.png -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/abiriadev/solvedac-box 2 | 3 | go 1.22.1 4 | 5 | require ( 6 | github.com/dustin/go-humanize v1.0.1 7 | github.com/google/go-github/v60 v60.0.0 8 | github.com/imroc/req/v3 v3.43.0 9 | github.com/kelseyhightower/envconfig v1.4.0 10 | github.com/mattn/go-runewidth v0.0.15 11 | github.com/stretchr/testify v1.9.0 12 | ) 13 | 14 | require ( 15 | github.com/andybalholm/brotli v1.1.0 // indirect 16 | github.com/cloudflare/circl v1.3.7 // indirect 17 | github.com/davecgh/go-spew v1.1.1 // indirect 18 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect 19 | github.com/google/go-querystring v1.1.0 // indirect 20 | github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7 // indirect 21 | github.com/hashicorp/errwrap v1.1.0 // indirect 22 | github.com/hashicorp/go-multierror v1.1.1 // indirect 23 | github.com/klauspost/compress v1.17.7 // indirect 24 | github.com/kr/pretty v0.3.1 // indirect 25 | github.com/onsi/ginkgo/v2 v2.16.0 // indirect 26 | github.com/pmezard/go-difflib v1.0.0 // indirect 27 | github.com/quic-go/qpack v0.4.0 // indirect 28 | github.com/quic-go/quic-go v0.42.0 // indirect 29 | github.com/refraction-networking/utls v1.6.3 // indirect 30 | github.com/rivo/uniseg v0.2.0 // indirect 31 | go.uber.org/mock v0.4.0 // indirect 32 | golang.org/x/crypto v0.21.0 // indirect 33 | golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect 34 | golang.org/x/mod v0.16.0 // indirect 35 | golang.org/x/net v0.23.0 // indirect 36 | golang.org/x/sys v0.18.0 // indirect 37 | golang.org/x/text v0.14.0 // indirect 38 | golang.org/x/tools v0.19.0 // indirect 39 | gopkg.in/yaml.v3 v3.0.1 // indirect 40 | ) 41 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= 2 | github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= 3 | github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= 4 | github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= 5 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 6 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 8 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= 10 | github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 11 | github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= 12 | github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 13 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= 14 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= 15 | github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= 16 | github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 17 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 18 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 19 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 20 | github.com/google/go-github/v60 v60.0.0 h1:oLG98PsLauFvvu4D/YPxq374jhSxFYdzQGNCyONLfn8= 21 | github.com/google/go-github/v60 v60.0.0/go.mod h1:ByhX2dP9XT9o/ll2yXAu2VD8l5eNVg8hD4Cr0S/LmQk= 22 | github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= 23 | github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= 24 | github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7 h1:y3N7Bm7Y9/CtpiVkw/ZWj6lSlDF3F74SfKwfTCer72Q= 25 | github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= 26 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 27 | github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= 28 | github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 29 | github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= 30 | github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= 31 | github.com/imroc/req/v3 v3.43.0 h1:TZDLMcuXEUFOvoyjKI2vMeimhq/OWnxvRJDPfEVCEhk= 32 | github.com/imroc/req/v3 v3.43.0/go.mod h1:SQIz5iYop16MJxbo8ib+4LnostGCok8NQf8ToyQc2xA= 33 | github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= 34 | github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= 35 | github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg= 36 | github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= 37 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 38 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 39 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 40 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 41 | github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= 42 | github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 43 | github.com/onsi/ginkgo/v2 v2.16.0 h1:7q1w9frJDzninhXxjZd+Y/x54XNjG/UlRLIYPZafsPM= 44 | github.com/onsi/ginkgo/v2 v2.16.0/go.mod h1:llBI3WDLL9Z6taip6f33H76YcWtJv+7R3HigUjbIBOs= 45 | github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8= 46 | github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= 47 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 48 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 49 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 50 | github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= 51 | github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= 52 | github.com/quic-go/quic-go v0.42.0 h1:uSfdap0eveIl8KXnipv9K7nlwZ5IqLlYOpJ58u5utpM= 53 | github.com/quic-go/quic-go v0.42.0/go.mod h1:132kz4kL3F9vxhW3CtQJLDVwcFe5wdWeJXXijhsO57M= 54 | github.com/refraction-networking/utls v1.6.3 h1:MFOfRN35sSx6K5AZNIoESsBuBxS2LCgRilRIdHb6fDc= 55 | github.com/refraction-networking/utls v1.6.3/go.mod h1:yil9+7qSl+gBwJqztoQseO6Pr3h62pQoY1lXiNR/FPs= 56 | github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= 57 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 58 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= 59 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 60 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 61 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 62 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 63 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 64 | go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= 65 | go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= 66 | golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= 67 | golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= 68 | golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ= 69 | golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc= 70 | golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic= 71 | golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 72 | golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= 73 | golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= 74 | golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= 75 | golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 76 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 77 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 78 | golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= 79 | golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 80 | golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw= 81 | golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc= 82 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 83 | google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= 84 | google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 85 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 86 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 87 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 88 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 89 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 90 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 91 | -------------------------------------------------------------------------------- /lib/client.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | import ( 4 | "github.com/google/go-github/v60/github" 5 | "github.com/imroc/req/v3" 6 | ) 7 | 8 | type BoxClient struct { 9 | ghClient github.Client 10 | req req.Client 11 | } 12 | 13 | func NewBoxClient(token string) BoxClient { 14 | return BoxClient{ 15 | ghClient: *github.NewClient(nil).WithAuthToken(token), 16 | req: *req.C(), 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/gist.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/google/go-github/v60/github" 7 | ) 8 | 9 | func (client BoxClient) UpdateGist(id, filename, content string) error { 10 | ctx := context.Background() 11 | 12 | gist, _, err := client.ghClient.Gists.Get(ctx, id) 13 | if err != nil { 14 | return err 15 | } 16 | 17 | for k := range gist.Files { 18 | gist.Files[k] = github.GistFile{} 19 | } 20 | 21 | gist.Files[github.GistFilename(filename)] = github.GistFile{ 22 | Content: &content, 23 | } 24 | 25 | _, _, err = client.ghClient.Gists.Edit(ctx, id, gist) 26 | return err 27 | } 28 | -------------------------------------------------------------------------------- /lib/helper.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | ) 7 | 8 | var tierMap = []string{ 9 | "Unrated", 10 | "Bronze V", 11 | "Bronze IV", 12 | "Bronze III", 13 | "Bronze II", 14 | "Bronze I", 15 | "Silver V", 16 | "Silver IV", 17 | "Silver III", 18 | "Silver II", 19 | "Silver I", 20 | "Gold V", 21 | "Gold IV", 22 | "Gold III", 23 | "Gold II", 24 | "Gold I", 25 | "Platinum V", 26 | "Platinum IV", 27 | "Platinum III", 28 | "Platinum II", 29 | "Platinum I", 30 | "Diamond V", 31 | "Diamond IV", 32 | "Diamond III", 33 | "Diamond II", 34 | "Diamond I", 35 | "Ruby V", 36 | "Ruby IV", 37 | "Ruby III", 38 | "Ruby II", 39 | "Ruby I", 40 | "Master", 41 | } 42 | 43 | var tierPercentageMap = []int{ 44 | 0, // unrated 45 | 30, 60, 90, 120, 150, // bronze 46 | 200, 300, 400, 500, 650, // silver 47 | 800, 950, 1100, 1250, 1400, // gold 48 | 1600, 1750, 1900, 2000, 2100, // platinum 49 | 2200, 2300, 2400, 2500, 2600, // diamond 50 | 2700, 2800, 2850, 2900, 2950, // ruby 51 | 3000, // master 52 | } 53 | 54 | var tierEmojis = []rune("🟫⬜🟨🟩🟦🟥🟪") 55 | 56 | func tierToEmoji(tier int) (rune, error) { 57 | if 0 < tier && tier <= 5 { 58 | return tierEmojis[0], nil 59 | } else if 5 < tier && tier <= 10 { 60 | return tierEmojis[1], nil 61 | } else if 10 < tier && tier <= 15 { 62 | return tierEmojis[2], nil 63 | } else if 15 < tier && tier <= 20 { 64 | return tierEmojis[3], nil 65 | } else if 20 < tier && tier <= 25 { 66 | return tierEmojis[4], nil 67 | } else if 25 < tier && tier <= 30 { 68 | return tierEmojis[5], nil 69 | } else if tier == 31 { 70 | return tierEmojis[6], nil 71 | } else { 72 | return 0, errors.New("tier out of range") 73 | } 74 | } 75 | 76 | func ratingToPercentage(rating int, tier int) float64 { 77 | // master progress is always 100% 78 | if tier == 31 { 79 | return 1 80 | } 81 | 82 | base := tierPercentageMap[tier] 83 | next := tierPercentageMap[tier+1] 84 | 85 | delta, curr := next-base, rating-base 86 | 87 | return float64(curr) / float64(delta) 88 | } 89 | 90 | var progressBarChars = []rune("░▏▎▍▌▋▊▉█") 91 | var semi = 8 92 | 93 | func drawProgressBar(size int, frac float64) string { 94 | l := int(float64(semi) * frac * float64(size)) 95 | fl := l / semi 96 | 97 | var buf strings.Builder 98 | buf.WriteString(strings.Repeat(string(progressBarChars[semi]), min(fl, size))) 99 | 100 | if fl >= size { 101 | return buf.String() 102 | } 103 | 104 | buf.WriteRune(progressBarChars[l%semi]) 105 | buf.WriteString(strings.Repeat(string(progressBarChars[0]), size-fl-1)) 106 | 107 | return buf.String() 108 | } 109 | -------------------------------------------------------------------------------- /lib/helper_test.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestTierMap(t *testing.T) { 10 | assert := assert.New(t) 11 | 12 | assert.Equal("Ruby III", tierMap[28]) 13 | } 14 | 15 | func TestTierPercentageMap(t *testing.T) { 16 | assert := assert.New(t) 17 | 18 | assert.Equal(1250, tierPercentageMap[14]) 19 | } 20 | -------------------------------------------------------------------------------- /lib/render.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | humanize "github.com/dustin/go-humanize" 8 | "github.com/mattn/go-runewidth" 9 | ) 10 | 11 | var gistWidth = 53 12 | 13 | func renderField(field string, num int, width int) string { 14 | fl := runewidth.StringWidth(field) 15 | return fmt.Sprintf( 16 | "%s%*s", 17 | field, 18 | width-fl, 19 | humanize.Comma(int64(num)), 20 | ) 21 | } 22 | 23 | func (user User) Render() (string, error) { 24 | var buf strings.Builder 25 | 26 | emoji, err := tierToEmoji(user.Tier) 27 | if err != nil { 28 | return buf.String(), err 29 | } 30 | 31 | tier := tierMap[user.Tier] 32 | rating := humanize.Comma(int64(user.Rating)) 33 | rank := humanize.Comma(int64(user.Rank)) 34 | 35 | var hl = gistWidth - 36 | runewidth.StringWidth( 37 | string(emoji), 38 | ) - 39 | 4 - 40 | runewidth.StringWidth(rank) - 41 | runewidth.StringWidth(user.Handle) 42 | 43 | res := fmt.Sprintf("%c %-*s#%s @%s\n", emoji, hl, tier, rank, user.Handle) 44 | buf.WriteString(res) 45 | buf.WriteString(user.Bio + "\n") 46 | pbl := gistWidth - runewidth.StringWidth(rating) - 1 47 | percentage := ratingToPercentage(user.Rating, user.Tier) 48 | pb := drawProgressBar(pbl, percentage) 49 | buf.WriteString(fmt.Sprintf("%s %s\n", pb, rating)) 50 | 51 | half := (gistWidth-1)/2 - 2 52 | buf.WriteString( 53 | fmt.Sprintf("%s %s\n%s %s\n", 54 | renderField("✅ Solved: ", user.SolvedCount, half), 55 | renderField("💠 Class: ", user.Class, half), 56 | renderField("💡 Contributions: ", user.VoteCount, half), 57 | renderField("🔥 Rivals: ", user.ReverseRivalCount, half), 58 | ), 59 | ) 60 | 61 | return buf.String(), nil 62 | } 63 | -------------------------------------------------------------------------------- /lib/render_test.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/mattn/go-runewidth" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestUserRender(t *testing.T) { 12 | assert := assert.New(t) 13 | 14 | mockUser := User{ 15 | Handle: "abiriadev", 16 | Bio: "hello, world!", 17 | SolvedCount: 1234, 18 | VoteCount: 56, 19 | Tier: 14, 20 | Rating: 1351, 21 | Rank: 1000, 22 | } 23 | 24 | rendered, err := mockUser.Render() 25 | assert.Nil(err) 26 | 27 | lines := strings.Split(rendered, "\n") 28 | 29 | assert.Equal(53, runewidth.StringWidth(lines[0])) 30 | assert.Equal("hello, world!", lines[1]) 31 | 32 | t.Log(rendered) 33 | } 34 | -------------------------------------------------------------------------------- /lib/solved.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | type User struct { 8 | Handle string `json:"handle"` 9 | Bio string `json:"bio"` 10 | SolvedCount int `json:"solvedCount"` 11 | VoteCount int `json:"voteCount"` 12 | Tier int `json:"tier"` 13 | Rating int `json:"rating"` 14 | Class int `json:"class"` 15 | ClassDecoration string `json:"classDecoration"` 16 | RivalCount int `json:"rivalCount"` 17 | ReverseRivalCount int `json:"reverseRivalCount"` 18 | Rank int `json:"rank"` 19 | } 20 | 21 | var solvedacApiEndpoint = "https://solved.ac/api/v3" 22 | var solvedacUserShowApi = solvedacApiEndpoint + "/user/show" 23 | 24 | func (client BoxClient) FetchUserData(username string) (User, error) { 25 | var user User 26 | 27 | res, err := client.req.R().SetQueryParam("handle", username). 28 | SetSuccessResult(&user). 29 | Get(solvedacUserShowApi) 30 | if err != nil { 31 | return user, err 32 | } 33 | 34 | if res.IsSuccessState() { 35 | return user, nil 36 | } else { 37 | return user, errors.New("http response was not successful") 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/solved_test.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestSolvedacApi(t *testing.T) { 10 | res, err := NewBoxClient("").FetchUserData("abiriadev") 11 | assert := assert.New(t) 12 | 13 | assert.Nil(err) 14 | assert.Equal(res.Handle, "abiriadev") 15 | } 16 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/abiriadev/solvedac-box/lib" 5 | "github.com/kelseyhightower/envconfig" 6 | ) 7 | 8 | type Config struct { 9 | GhToken string `envconfig:"GH_TOKEN" required:"true"` 10 | Username string `envconfig:"USERNAME" required:"true"` 11 | GistId string `envconfig:"GIST_ID" required:"true"` 12 | } 13 | 14 | func main() { 15 | var config Config 16 | if err := envconfig.Process("", &config); err != nil { 17 | panic(err) 18 | } 19 | 20 | client := lib.NewBoxClient( 21 | config.GhToken, 22 | ) 23 | 24 | res, err := client.FetchUserData(config.Username) 25 | if err != nil { 26 | panic(err) 27 | } 28 | 29 | rendered, err := res.Render() 30 | if err != nil { 31 | panic(err) 32 | } 33 | 34 | err = client.UpdateGist(config.GistId, "💯 My Solved.ac Profile", rendered) 35 | if err != nil { 36 | panic(err) 37 | } 38 | } 39 | --------------------------------------------------------------------------------