Skip to content
This repository was archived by the owner on Jan 21, 2025. It is now read-only.

Commit 1bbe289

Browse files
committed
feat: complete rewrite
1 parent fa5a8c7 commit 1bbe289

File tree

12 files changed

+454
-0
lines changed

12 files changed

+454
-0
lines changed

docker-compose.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
version: "3.9"
2+
3+
services:
4+
redis:
5+
image: redis
6+
ports:
7+
- "6379:6379"

go.mod

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
module github.com/hitsapp/api
2+
3+
go 1.21.3
4+
5+
require (
6+
github.com/gofiber/fiber/v2 v2.49.2
7+
github.com/redis/go-redis/v9 v9.2.1
8+
)
9+
10+
require (
11+
github.com/andybalholm/brotli v1.0.5 // indirect
12+
github.com/cespare/xxhash/v2 v2.2.0 // indirect
13+
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
14+
github.com/google/uuid v1.3.1 // indirect
15+
github.com/klauspost/compress v1.16.7 // indirect
16+
github.com/mattn/go-colorable v0.1.13 // indirect
17+
github.com/mattn/go-isatty v0.0.19 // indirect
18+
github.com/mattn/go-runewidth v0.0.15 // indirect
19+
github.com/philhofer/fwd v1.1.2 // indirect
20+
github.com/rivo/uniseg v0.2.0 // indirect
21+
github.com/tinylib/msgp v1.1.8 // indirect
22+
github.com/valyala/bytebufferpool v1.0.0 // indirect
23+
github.com/valyala/fasthttp v1.49.0 // indirect
24+
github.com/valyala/fasttemplate v1.2.2 // indirect
25+
github.com/valyala/tcplisten v1.0.0 // indirect
26+
golang.org/x/sys v0.12.0 // indirect
27+
)

go.sum

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
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/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
4+
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
5+
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
6+
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
7+
github.com/gofiber/fiber/v2 v2.49.2 h1:ONEN3/Vc+dUCxxDgZZwpqvhISgHqb+bu+isBiEyKEQs=
8+
github.com/gofiber/fiber/v2 v2.49.2/go.mod h1:gNsKnyrmfEWFpJxQAV0qvW6l70K1dZGno12oLtukcts=
9+
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
10+
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
11+
github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
12+
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
13+
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
14+
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
15+
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
16+
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
17+
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
18+
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
19+
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
20+
github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw=
21+
github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0=
22+
github.com/redis/go-redis/v9 v9.2.1 h1:WlYJg71ODF0dVspZZCpYmoF1+U1Jjk9Rwd7pq6QmlCg=
23+
github.com/redis/go-redis/v9 v9.2.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
24+
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
25+
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
26+
github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0=
27+
github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw=
28+
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
29+
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
30+
github.com/valyala/fasthttp v1.49.0 h1:9FdvCpmxB74LH4dPb7IJ1cOSsluR07XG3I1txXWwJpE=
31+
github.com/valyala/fasthttp v1.49.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA=
32+
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
33+
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
34+
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
35+
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
36+
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
37+
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
38+
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
39+
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
40+
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
41+
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
42+
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
43+
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
44+
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
45+
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
46+
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
47+
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
48+
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
49+
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
50+
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
51+
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
52+
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
53+
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
54+
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
55+
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
56+
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
57+
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
58+
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
59+
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
60+
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
61+
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
62+
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
63+
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
64+
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
65+
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
66+
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
67+
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
68+
golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=
69+
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

main.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
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+
)
14+
15+
func main() {
16+
app := fiber.New(fiber.Config{
17+
CaseSensitive: false,
18+
StrictRouting: true,
19+
ServerHeader: "hits",
20+
BodyLimit: 1024 * 1024,
21+
GETOnly: true,
22+
ProxyHeader: os.Getenv("PROXY_HEADER"),
23+
})
24+
25+
app.Use(logger.New())
26+
app.Use(recover.New())
27+
app.Use(cors.New(cors.Config{
28+
AllowOrigins: "*",
29+
}))
30+
31+
utils.ConnectRedis()
32+
33+
app.Get("/", func(c *fiber.Ctx) error {
34+
return c.JSON(fiber.Map{
35+
"success": true,
36+
"message": "sponsored by https://hop.io",
37+
})
38+
})
39+
40+
v1Group := app.Group("/v1")
41+
v1Group.Get("/leaderboard", utils.RateLimit(50), v1.Leaderboard)
42+
v1Group.Get("/hits", utils.RateLimit(1200), v1.Hits)
43+
44+
log.Fatal(app.Listen(":8080"))
45+
}

tmp/build-errors.log

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
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/badge.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
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+
<svg
12+
xmlns="http://www.w3.org/2000/svg"
13+
xmlns:xlink="http://www.w3.org/1999/xlink" width="{{rectWidth}}" height="20" role="img" aria-label="{{label}}: {{count}} hits">
14+
15+
<title>{{label}}: {{count}} hits</title>
16+
<linearGradient id="s" x2="0" y2="100%">
17+
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
18+
<stop offset="1" stop-opacity=".1"/>
19+
</linearGradient>
20+
<clipPath id="r">
21+
<rect width="100" height="20" rx="{{border}}" fill="#fff"/>
22+
</clipPath>
23+
<g clip-path="url(#r)">
24+
<rect width="38" height="20" fill="{{leftBgColor}}"/>
25+
<rect x="38" width="62" height="20" fill="{{rightBgColor}}"/>
26+
<rect width="{{rectWidth}}" height="20" fill="url(#s)"/>
27+
</g>
28+
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" font-size="110">
29+
<text x="195" y="140" transform="scale(.1)" fill="{{color}}">{{label}}</text>
30+
<text x="695" y="140" transform="scale(.1)" fill="{{color}}">{{count}}</text>
31+
</g>
32+
</svg>
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+
}

utils/rate_limit.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
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+
KeyGenerator: func(c *fiber.Ctx) string {
18+
return c.IP()
19+
},
20+
LimitReached: func(c *fiber.Ctx) error {
21+
return c.Status(429).JSON(Response[*any]{
22+
Success: false,
23+
Error: &Error{
24+
Code: "rate_limit_reached",
25+
Message: "You have reached the rate limit, please try again in one minute",
26+
},
27+
})
28+
},
29+
})
30+
}

utils/redis.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package utils
2+
3+
import (
4+
"context"
5+
"os"
6+
"time"
7+
8+
"github.com/redis/go-redis/v9"
9+
)
10+
11+
var RedisClient *redis.Client
12+
13+
func ConnectRedis() {
14+
RedisClient = redis.NewClient(&redis.Options{
15+
Addr: os.Getenv("REDIS_URL"),
16+
Password: os.Getenv("REDIS_PASSWORD"),
17+
DB: 0,
18+
})
19+
20+
_, err := RedisClient.Ping(context.Background()).Result()
21+
if err != nil {
22+
panic(err)
23+
}
24+
}
25+
26+
func GetKey(key string) string {
27+
return RedisClient.Get(context.Background(), key).Val()
28+
}
29+
30+
func AddKey(key string, value string, expiration int) {
31+
RedisClient.Set(context.Background(), key, value, 0).Val()
32+
RedisClient.Expire(context.Background(), key, time.Duration(time.Second*time.Duration(expiration))).Val()
33+
}
34+
35+
func GetHit(url string) float64 {
36+
return RedisClient.ZScore(context.Background(), "hits", url).Val()
37+
}
38+
39+
func GetHits(limit int64) []redis.Z {
40+
return RedisClient.ZRevRangeWithScores(context.Background(), "hits", 0, limit).Val()
41+
}
42+
43+
func AddHit(url string) {
44+
RedisClient.ZAdd(context.Background(), "hits", redis.Z{
45+
Score: 1,
46+
Member: url,
47+
})
48+
}
49+
50+
func IncrementHit(url string) {
51+
RedisClient.ZIncrBy(context.Background(), "hits", 1, url)
52+
}

utils/strings.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
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+
}

utils/structs.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
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+
}

0 commit comments

Comments
 (0)