├── .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 | ![Hits](https://hits-app.vercel.app/hits?url=https://github.com/heybereket/hits&bgRight=292B2F) 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 | 14 | 15 | {{label}}: {{count}} hits 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | {{label}} 30 | {{count}} 31 | 32 | 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 | --------------------------------------------------------------------------------