├── .gitignore
├── README.md
├── docker-compose.yml
├── go.mod
├── go.sum
├── main.go
├── tmp
└── build-errors.log
├── utils
├── backup.go
├── badge.go
├── rate_limit.go
├── redis.go
├── strings.go
└── structs.go
└── v1
├── hits.go
└── leaderboard.go
/.gitignore:
--------------------------------------------------------------------------------
1 | .env
2 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # UPDATE #2:
2 | Bad news, I have archived this project due to time and money running it. _**Good news**_, AprilNEA has made an alternative that's a drop-in replacement to hits! If you would like to support and use their's, visit https://github.com/AprilNEA/hits.
3 |
4 | ```diff
5 | - https://hits-app.vercel.app/hits?url=https://yoururl.com/
6 | + https://hits.aprilnea.com/hits?url=https://yoururl.com/
7 | ```
8 |
9 | > [!NOTE]
10 | > I do not own nor have control over AprilNEA's websites, domains, or repositories!!
11 |
12 | # UPDATE:
13 | The renewal price for hits.link was very expensive, so we decided to drop the domain. If you'd like to continue using hits please update your URLs to `https://hits-app.vercel.app/hits?url=https://yoururl.com/`
14 |
15 | ```diff
16 | - https://hits.link/hits?url=https://yoururl.com/
17 | + https://hits-app.vercel.app/hits?url=https://yoururl.com/
18 | ```
19 |
20 | Frontend: `https://hits-app.vercel.app`
21 |
22 | API: `https://hits.hop.sh/`
23 |
24 | # [hits](https://hits-app.vercel.app) - the better hits counter
25 | 
26 |
27 |
28 | # Sponsored By
29 | Hits is brought to you by [Hop](https://hop.io/) - No more configs. No more fuss. Just push your code.
30 |
31 |
32 | ## API Docs
33 |
34 | **Create a new Hit:**
35 | `GET https://hits-app.vercel.app/hits?url=https://yoururl.com (20reqs/minute)`
36 |
37 | **Query Params:**
38 | > Valid hex color codes are accepted (remove "#" in the queries that apply)
39 | > Default border type is round
40 |
41 | ```bash
42 | ?json - Send JSON with data instead of SVG
43 | ?label - Set label text
44 | ?bgLeft - Set the background colour of the label
45 | ?bgRight - Set the background colour of the hits number
46 | ?color - Set the text color
47 | ?border - Choose a border (square or round)
48 | ```
49 |
50 | **Get Top Hits:**
51 | `GET https://hits.hop.sh/v1/leaderboard (30s cache, 50reqs/minute)`
52 |
53 | ## Used on
54 | **Open a PR to add your website!**
55 | - https://github.com/looskie
56 | - https://github.com/alii
57 | - https://github.com/heybereket
58 | - https://github.com/cnrad
59 | - https://github.com/walkator
60 |
61 |
62 | ## Running Locally
63 | ```bash
64 | # Run Docker Compose
65 | $ docker-compose up -d
66 |
67 | # Install packages
68 | $ go get
69 |
70 | # Start API
71 | $ go run main.go
72 |
73 | ```
74 |
75 | ## Frontend
76 | The frontend has been moved to a separate repository:
77 | [https://github.com/hitsapp/web](https://github.com/hitsapp/web)
78 |
79 | ## Maintainers
80 | - @heybereket
81 | - @Looskie
82 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3.9"
2 |
3 | services:
4 | redis:
5 | image: redis
6 | ports:
7 | - "6379:6379"
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/hitsapp/api
2 |
3 | go 1.18
4 |
5 | require (
6 | github.com/gofiber/fiber/v2 v2.51.0
7 | github.com/imroc/req/v3 v3.42.2
8 | github.com/joho/godotenv v1.5.1
9 | github.com/redis/go-redis/v9 v9.3.1
10 | github.com/robfig/cron/v3 v3.0.1
11 | github.com/valyala/bytebufferpool v1.0.0
12 | github.com/valyala/fasttemplate v1.2.2
13 | )
14 |
15 | require (
16 | github.com/andybalholm/brotli v1.0.6 // indirect
17 | github.com/cespare/xxhash/v2 v2.2.0 // indirect
18 | github.com/cloudflare/circl v1.3.7 // indirect
19 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
20 | github.com/gaukas/godicttls v0.0.4 // indirect
21 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
22 | github.com/golang/mock v1.6.0 // indirect
23 | github.com/google/pprof v0.0.0-20231229205709-960ae82b1e42 // indirect
24 | github.com/google/uuid v1.5.0 // indirect
25 | github.com/hashicorp/errwrap v1.1.0 // indirect
26 | github.com/hashicorp/go-multierror v1.1.1 // indirect
27 | github.com/klauspost/compress v1.17.4 // indirect
28 | github.com/mattn/go-colorable v0.1.13 // indirect
29 | github.com/mattn/go-isatty v0.0.20 // indirect
30 | github.com/mattn/go-runewidth v0.0.15 // indirect
31 | github.com/onsi/ginkgo/v2 v2.13.2 // indirect
32 | github.com/philhofer/fwd v1.1.2 // indirect
33 | github.com/quic-go/qpack v0.4.0 // indirect
34 | github.com/quic-go/qtls-go1-20 v0.4.1 // indirect
35 | github.com/quic-go/quic-go v0.40.1 // indirect
36 | github.com/refraction-networking/utls v1.6.0 // indirect
37 | github.com/rivo/uniseg v0.4.4 // indirect
38 | github.com/tinylib/msgp v1.1.9 // indirect
39 | github.com/valyala/fasthttp v1.51.0 // indirect
40 | github.com/valyala/tcplisten v1.0.0 // indirect
41 | go.uber.org/mock v0.4.0 // indirect
42 | golang.org/x/crypto v0.17.0 // indirect
43 | golang.org/x/exp v0.0.0-20231226003508-02704c960a9b // indirect
44 | golang.org/x/mod v0.14.0 // indirect
45 | golang.org/x/net v0.19.0 // indirect
46 | golang.org/x/sys v0.15.0 // indirect
47 | golang.org/x/text v0.14.0 // indirect
48 | golang.org/x/tools v0.16.1 // indirect
49 | )
50 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
2 | github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
3 | github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
4 | github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
5 | github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
6 | github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
7 | github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs=
8 | github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
9 | github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
10 | github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
11 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
12 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
13 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
14 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
15 | github.com/gaukas/godicttls v0.0.4 h1:NlRaXb3J6hAnTmWdsEKb9bcSBD6BvcIjdGdeb0zfXbk=
16 | github.com/gaukas/godicttls v0.0.4/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI=
17 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
18 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
19 | github.com/gofiber/fiber/v2 v2.49.2 h1:ONEN3/Vc+dUCxxDgZZwpqvhISgHqb+bu+isBiEyKEQs=
20 | github.com/gofiber/fiber/v2 v2.49.2/go.mod h1:gNsKnyrmfEWFpJxQAV0qvW6l70K1dZGno12oLtukcts=
21 | github.com/gofiber/fiber/v2 v2.51.0 h1:JNACcZy5e2tGApWB2QrRpenTWn0fq0hkFm6k0C86gKQ=
22 | github.com/gofiber/fiber/v2 v2.51.0/go.mod h1:xaQRZQJGqnKOQnbQw+ltvku3/h8QxvNi8o6JiJ7Ll0U=
23 | github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
24 | github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
25 | github.com/google/pprof v0.0.0-20230901174712-0191c66da455 h1:YhRUmI1ttDC4sxKY2V62BTI8hCXnyZBV9h38eAanInE=
26 | github.com/google/pprof v0.0.0-20230901174712-0191c66da455/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
27 | github.com/google/pprof v0.0.0-20231229205709-960ae82b1e42 h1:dHLYa5D8/Ta0aLR2XcPsrkpAgGeFs6thhMcQK0oQ0n8=
28 | github.com/google/pprof v0.0.0-20231229205709-960ae82b1e42/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
29 | github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
30 | github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
31 | github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
32 | github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
33 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
34 | github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
35 | github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
36 | github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
37 | github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
38 | github.com/imroc/req/v3 v3.42.2 h1:/BwrKXGR7X1/ptccaQAiziDCeZ7T6ye55g3ZhiLy1fc=
39 | github.com/imroc/req/v3 v3.42.2/go.mod h1:W7dOrfQORA9nFoj+CafIZ6P5iyk+rWdbp2sffOAvABU=
40 | github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
41 | github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
42 | github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
43 | github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
44 | github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
45 | github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
46 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
47 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
48 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
49 | github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
50 | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
51 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
52 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
53 | github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
54 | github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
55 | github.com/onsi/ginkgo/v2 v2.12.0 h1:UIVDowFPwpg6yMUpPjGkYvf06K3RAiJXUhCxEwQVHRI=
56 | github.com/onsi/ginkgo/v2 v2.12.0/go.mod h1:ZNEzXISYlqpb8S36iN71ifqLi3vVD1rVJGvWRCJOUpQ=
57 | github.com/onsi/ginkgo/v2 v2.13.2 h1:Bi2gGVkfn6gQcjNjZJVO8Gf0FHzMPf2phUei9tejVMs=
58 | github.com/onsi/ginkgo/v2 v2.13.2/go.mod h1:XStQ8QcGwLyF4HdfcZB8SFOS/MWCgDuXMSBe6zrvLgM=
59 | github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw=
60 | github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0=
61 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
62 | github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
63 | github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
64 | github.com/quic-go/qtls-go1-20 v0.3.3 h1:17/glZSLI9P9fDAeyCHBFSWSqJcwx1byhLwP5eUIDCM=
65 | github.com/quic-go/qtls-go1-20 v0.3.3/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k=
66 | github.com/quic-go/qtls-go1-20 v0.4.1 h1:D33340mCNDAIKBqXuAvexTNMUByrYmFYVfKfDN5nfFs=
67 | github.com/quic-go/qtls-go1-20 v0.4.1/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k=
68 | github.com/quic-go/quic-go v0.38.1 h1:M36YWA5dEhEeT+slOu/SwMEucbYd0YFidxG3KlGPZaE=
69 | github.com/quic-go/quic-go v0.38.1/go.mod h1:ijnZM7JsFIkp4cRyjxJNIzdSfCLmUMg9wdyhGmg+SN4=
70 | github.com/quic-go/quic-go v0.40.1 h1:X3AGzUNFs0jVuO3esAGnTfvdgvL4fq655WaOi1snv1Q=
71 | github.com/quic-go/quic-go v0.40.1/go.mod h1:PeN7kuVJ4xZbxSv/4OX6S1USOX8MJvydwpTx31vx60c=
72 | github.com/redis/go-redis/v9 v9.2.1 h1:WlYJg71ODF0dVspZZCpYmoF1+U1Jjk9Rwd7pq6QmlCg=
73 | github.com/redis/go-redis/v9 v9.2.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
74 | github.com/redis/go-redis/v9 v9.3.1 h1:KqdY8U+3X6z+iACvumCNxnoluToB+9Me+TvyFa21Mds=
75 | github.com/redis/go-redis/v9 v9.3.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
76 | github.com/refraction-networking/utls v1.5.3 h1:Ds5Ocg1+MC1ahNx5iBEcHe0jHeLaA/fLey61EENm7ro=
77 | github.com/refraction-networking/utls v1.5.3/go.mod h1:SPuDbBmgLGp8s+HLNc83FuavwZCFoMmExj+ltUHiHUw=
78 | github.com/refraction-networking/utls v1.6.0 h1:X5vQMqVx7dY7ehxxqkFER/W6DSjy8TMqSItXm8hRDYQ=
79 | github.com/refraction-networking/utls v1.6.0/go.mod h1:kHJ6R9DFFA0WsRgBM35iiDku4O7AqPR6y79iuzW7b10=
80 | github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
81 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
82 | github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
83 | github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
84 | github.com/robfig/cron/v3 v3.0.0 h1:kQ6Cb7aHOHTSzNVNEhmp8EcWKLb4CbiMW9h9VyIhO4E=
85 | github.com/robfig/cron/v3 v3.0.0/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
86 | github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
87 | github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
88 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
89 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
90 | github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0=
91 | github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw=
92 | github.com/tinylib/msgp v1.1.9 h1:SHf3yoO2sGA0veCJeCBYLHuttAVFHGm2RHgNodW7wQU=
93 | github.com/tinylib/msgp v1.1.9/go.mod h1:BCXGB54lDD8qUEPmiG0cQQUANC4IUQyB2ItS2UDlO/k=
94 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
95 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
96 | github.com/valyala/fasthttp v1.49.0 h1:9FdvCpmxB74LH4dPb7IJ1cOSsluR07XG3I1txXWwJpE=
97 | github.com/valyala/fasthttp v1.49.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA=
98 | github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
99 | github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
100 | github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
101 | github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
102 | github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
103 | github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
104 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
105 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
106 | go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
107 | go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
108 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
109 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
110 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
111 | golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
112 | golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
113 | golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
114 | golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
115 | golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 h1:m64FZMko/V45gv0bNmrNYoDEq8U5YUhetc9cBWKS1TQ=
116 | golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63/go.mod h1:0v4NqG35kSWCMzLaMeX+IQrlSnVE/bqGSyC2cz/9Le8=
117 | golang.org/x/exp v0.0.0-20231226003508-02704c960a9b h1:kLiC65FbiHWFAOu+lxwNPujcsl8VYyTYYEZnsOO1WK4=
118 | golang.org/x/exp v0.0.0-20231226003508-02704c960a9b/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI=
119 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
120 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
121 | golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
122 | golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
123 | golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
124 | golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
125 | golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
126 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
127 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
128 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
129 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
130 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
131 | golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
132 | golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14=
133 | golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
134 | golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
135 | golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
136 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
137 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
138 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
139 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
140 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
141 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
142 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
143 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
144 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
145 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
146 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
147 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
148 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
149 | golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
150 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
151 | golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
152 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
153 | golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
154 | golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
155 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
156 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
157 | golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
158 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
159 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
160 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
161 | golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
162 | golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
163 | golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
164 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
165 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
166 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
167 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
168 | golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
169 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
170 | golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=
171 | golang.org/x/tools v0.12.1-0.20230815132531-74c255bcf846 h1:Vve/L0v7CXXuxUmaMGIEK/dEeq7uiqb5qBgQrZzIE7E=
172 | golang.org/x/tools v0.12.1-0.20230815132531-74c255bcf846/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM=
173 | golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA=
174 | golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0=
175 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
176 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
177 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
178 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
179 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
180 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "log"
5 | "os"
6 |
7 | "github.com/gofiber/fiber/v2"
8 | "github.com/gofiber/fiber/v2/middleware/cors"
9 | "github.com/gofiber/fiber/v2/middleware/logger"
10 | "github.com/gofiber/fiber/v2/middleware/recover"
11 | "github.com/hitsapp/api/utils"
12 | v1 "github.com/hitsapp/api/v1"
13 | _ "github.com/joho/godotenv/autoload"
14 | "github.com/robfig/cron/v3"
15 | )
16 |
17 | func main() {
18 | app := fiber.New(fiber.Config{
19 | CaseSensitive: false,
20 | StrictRouting: true,
21 | ServerHeader: "hits",
22 | BodyLimit: 1024 * 1024,
23 | GETOnly: true,
24 | ProxyHeader: os.Getenv("PROXY_HEADER"),
25 | })
26 |
27 | app.Use(logger.New())
28 | app.Use(recover.New())
29 | app.Use(cors.New(cors.Config{
30 | AllowOrigins: "*",
31 | }))
32 |
33 | utils.ConnectRedis()
34 |
35 | app.Get("/", func(c *fiber.Ctx) error {
36 | return c.JSON(fiber.Map{
37 | "success": true,
38 | "message": "sponsored by https://hop.io",
39 | })
40 | })
41 |
42 | v1Group := app.Group("/v1")
43 | v1Group.Get("/leaderboard", utils.RateLimit(50), v1.Leaderboard)
44 | v1Group.Get("/hits", utils.RateLimit(1200), v1.Hits)
45 |
46 | cron := cron.New(cron.WithLogger(cron.DefaultLogger))
47 | cron.AddFunc("@daily", func() {
48 | utils.Backup()
49 | })
50 | cron.Start()
51 |
52 | log.Fatal(app.Listen(":8080"))
53 | }
54 |
--------------------------------------------------------------------------------
/tmp/build-errors.log:
--------------------------------------------------------------------------------
1 | exit status 2exit status 2exit status 2exit status 2exit status 0xc000013aexit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 0xc000013aexit status 1exit status 1exit status 1exit status 0xc000013a
--------------------------------------------------------------------------------
/utils/backup.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "os"
5 |
6 | "github.com/imroc/req/v3"
7 | )
8 |
9 | func Backup() {
10 | client := req.C()
11 | items := GetKey("hits")
12 |
13 | if len(items) == 0 {
14 | return
15 | }
16 |
17 | println("Attempting to backup")
18 | resp, err := client.R().SetBody(map[string]interface{}{
19 | "content": items,
20 | }).SetHeader("Authorization", os.Getenv("IMPERIAL_API_TOKEN")).Post("https://api.impb.in/v1/documents")
21 |
22 | if err != nil {
23 | println("Error occurred whilst backing up", err)
24 | }
25 |
26 | s, err := resp.ToString()
27 | if resp.StatusCode != 200 {
28 | println("backup failed with ", resp.StatusCode, s)
29 | }
30 |
31 | println("Successfully backed up")
32 | }
33 |
--------------------------------------------------------------------------------
/utils/badge.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "io"
5 |
6 | "github.com/valyala/bytebufferpool"
7 | "github.com/valyala/fasttemplate"
8 | )
9 |
10 | var badgeSVG = Trim(`
11 |
33 | `)
34 |
35 | func GenerateBadge(label, count, color, leftBgColor, rightBgColor, border string) (badge []byte) {
36 | const rectWidth = "150"
37 | var tmpl = fasttemplate.New(badgeSVG, "{{", "}}")
38 | buf := bytebufferpool.Get()
39 |
40 | if border == "square" {
41 | border = "0"
42 | } else {
43 | border = "3"
44 | }
45 |
46 | if label == "" {
47 | label = "hits"
48 | }
49 |
50 | _, _ = tmpl.ExecuteFunc(buf, func(w io.Writer, tag string) (int, error) {
51 | switch tag {
52 | case "label":
53 | return buf.WriteString(label)
54 | case "count":
55 | return buf.WriteString(count)
56 | case "color":
57 | return buf.WriteString(color)
58 | case "leftBgColor":
59 | return buf.WriteString(leftBgColor)
60 | case "rightBgColor":
61 | return buf.WriteString(rightBgColor)
62 | case "rectWidth":
63 | return buf.WriteString(rectWidth)
64 | case "border":
65 | return buf.WriteString(border)
66 | }
67 | return 0, nil
68 | })
69 | badge = buf.Bytes()
70 | bytebufferpool.Put(buf)
71 | return badge
72 | }
73 |
--------------------------------------------------------------------------------
/utils/rate_limit.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/gofiber/fiber/v2"
7 | "github.com/gofiber/fiber/v2/middleware/limiter"
8 | )
9 |
10 | func RateLimit(amount int) fiber.Handler {
11 | return limiter.New(limiter.Config{
12 | Next: func(c *fiber.Ctx) bool {
13 | return c.IP() == "127.0.0.1" || c.IP() == "localhost"
14 | },
15 | Max: amount,
16 | Expiration: 1 * time.Minute,
17 | LimitReached: func(c *fiber.Ctx) error {
18 | return c.Status(429).JSON(Response[*any]{
19 | Success: false,
20 | Error: &Error{
21 | Code: "rate_limit_reached",
22 | Message: "You have reached the rate limit, please try again in one minute",
23 | },
24 | })
25 | },
26 | })
27 | }
28 |
--------------------------------------------------------------------------------
/utils/redis.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "context"
5 | "os"
6 | "strings"
7 | "time"
8 |
9 | "github.com/redis/go-redis/v9"
10 | )
11 |
12 | var RedisClient *redis.Client
13 |
14 | func ConnectRedis() {
15 | println("Attempting to connect to redis in 2 seconds")
16 | time.Sleep(2 * time.Second)
17 | println("Attempting to connect to redis")
18 | RedisClient = redis.NewClient(&redis.Options{
19 | Addr: os.Getenv("REDIS_URL"),
20 | Password: os.Getenv("REDIS_PASSWORD"),
21 | DB: 0,
22 | })
23 |
24 | _, err := RedisClient.Ping(context.Background()).Result()
25 | if err != nil {
26 | panic(err)
27 | }
28 | }
29 |
30 | func GetKey(key string) string {
31 | return RedisClient.Get(context.Background(), key).Val()
32 | }
33 |
34 | func AddKey(key string, value string, expiration int) {
35 | value = strings.ReplaceAll(value, "'", "")
36 | RedisClient.Set(context.Background(), key, value, 0).Val()
37 | RedisClient.Expire(context.Background(), key, time.Duration(time.Second*time.Duration(expiration))).Val()
38 | }
39 |
40 | func GetHit(url string) float64 {
41 | return RedisClient.ZScore(context.Background(), "hits", url).Val()
42 | }
43 |
44 | func GetHits(limit int64) []redis.Z {
45 | return RedisClient.ZRevRangeWithScores(context.Background(), "hits", 0, limit-1).Val()
46 | }
47 |
48 | func AddHit(url string) {
49 | RedisClient.ZAdd(context.Background(), "hits", redis.Z{
50 | Score: 1,
51 | Member: url,
52 | })
53 | }
54 |
55 | func IncrementHit(url string) {
56 | RedisClient.ZIncrBy(context.Background(), "hits", 1, url)
57 | }
58 |
--------------------------------------------------------------------------------
/utils/strings.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "fmt"
5 | "regexp"
6 | "strings"
7 | )
8 |
9 | func Trim(str string) string {
10 | trimmed := strings.TrimSpace(regexp.MustCompile(`\s+`).ReplaceAllString(str, " "))
11 | trimmed = strings.Replace(trimmed, " <", "<", -1)
12 | trimmed = strings.Replace(trimmed, "> ", ">", -1)
13 | return trimmed
14 | }
15 |
16 | func TransformToHex(text string) string {
17 | var output = fmt.Sprintf("#%s", strings.Trim(text, "\""))
18 | return output
19 | }
20 |
21 | func TrimQuotes(text string) string {
22 | return strings.Trim(text, "\"")
23 | }
24 |
--------------------------------------------------------------------------------
/utils/structs.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | type Response[T any] struct {
4 | Success bool `json:"success"`
5 | Message string `json:"message,omitempty"`
6 | Error *Error `json:"error,omitempty"`
7 | Data T `json:"data,omitempty"`
8 | }
9 |
10 | type Error struct {
11 | Code string `json:"code"`
12 | Message string `json:"message"`
13 | }
14 |
15 | type Leaderboard struct {
16 | URL string `json:"url"`
17 | Hits int `json:"hits"`
18 | }
19 |
--------------------------------------------------------------------------------
/v1/hits.go:
--------------------------------------------------------------------------------
1 | package v1
2 |
3 | import (
4 | netURL "net/url"
5 | "regexp"
6 | "strconv"
7 | "strings"
8 |
9 | "github.com/gofiber/fiber/v2"
10 | "github.com/hitsapp/api/utils"
11 | )
12 |
13 | const urlRegex = `https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)`
14 |
15 | func Hits(c *fiber.Ctx) error {
16 | var url = c.Query("url")
17 | var color = c.Query("color")
18 | var leftBgColor = c.Query("bgLeft")
19 | var rightBgColor = c.Query("bgRight")
20 | var border = c.Query("border")
21 | var label = c.Query("label")
22 |
23 | var wantsJSON = string(c.Request().Header.Peek("Accept")) == "application/json"
24 |
25 | if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") {
26 | url = "https://" + url
27 | }
28 |
29 | parsedURL, err := netURL.Parse(url)
30 | url = parsedURL.Scheme + "://" + parsedURL.Host + parsedURL.Path
31 | matched, matchErr := regexp.MatchString(urlRegex, url)
32 |
33 | if err != nil || url == "" || matchErr != nil || !matched {
34 | return c.Status(400).JSON(utils.Response[any]{
35 | Success: false,
36 | Error: &utils.Error{
37 | Code: "invalid_url",
38 | Message: "You have passed in an invalid URL.",
39 | },
40 | })
41 | }
42 |
43 | if color == "" {
44 | color = "fff"
45 | }
46 |
47 | if leftBgColor == "" {
48 | leftBgColor = "555"
49 | }
50 |
51 | if rightBgColor == "" {
52 | rightBgColor = "2f3136"
53 | }
54 |
55 | if border == "" {
56 | border = "000000"
57 | }
58 |
59 | if label == "" {
60 | label = "hits"
61 | }
62 |
63 | // Theres no way to check if a user has already visited the URL since the only IP returned to us is the website's URL, so unfort this can be abused :/, if you're abusing this though, get a life, how many followers or views you have doesn't matter and you're not cool for abusing it.
64 | utils.IncrementHit(url)
65 |
66 | urlHits := utils.GetHit(url)
67 | if urlHits == 0 {
68 | utils.AddHit(url)
69 | urlHits = 1
70 | }
71 |
72 | if wantsJSON {
73 | return c.JSON(utils.Response[int]{
74 | Success: true,
75 | Data: int(urlHits),
76 | })
77 | }
78 |
79 | svg := utils.GenerateBadge(utils.TrimQuotes(label), strconv.Itoa(int(urlHits)), utils.TransformToHex(color), utils.TransformToHex(leftBgColor), utils.TransformToHex(rightBgColor), utils.TrimQuotes(border))
80 | c.Set(fiber.HeaderContentType, "image/svg+xml")
81 | c.Set(fiber.HeaderCacheControl, "max-age=0, s-maxage=0, must-revalidate, no-cache, no-store")
82 |
83 | return c.SendString(string(svg))
84 | }
85 |
--------------------------------------------------------------------------------
/v1/leaderboard.go:
--------------------------------------------------------------------------------
1 | package v1
2 |
3 | import (
4 | "github.com/gofiber/fiber/v2"
5 | "github.com/hitsapp/api/utils"
6 | )
7 |
8 | func Leaderboard(c *fiber.Ctx) error {
9 | leaderboard := utils.GetHits(20)
10 |
11 | var parsedLeaderboard []utils.Leaderboard
12 | for _, v := range leaderboard {
13 | parsedLeaderboard = append(parsedLeaderboard, utils.Leaderboard{
14 | URL: v.Member,
15 | Hits: int(v.Score),
16 | })
17 | }
18 |
19 | return c.JSON(utils.Response[[]utils.Leaderboard]{
20 | Success: true,
21 | Data: parsedLeaderboard,
22 | })
23 | }
24 |
--------------------------------------------------------------------------------