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

Commit

Permalink
feat: complete rewrite
Browse files Browse the repository at this point in the history
  • Loading branch information
Looskie committed Oct 14, 2023
1 parent fa5a8c7 commit 1bbe289
Show file tree
Hide file tree
Showing 12 changed files with 454 additions and 0 deletions.
7 changes: 7 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
version: "3.9"

services:
redis:
image: redis
ports:
- "6379:6379"
27 changes: 27 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
module github.com/hitsapp/api

go 1.21.3

require (
github.com/gofiber/fiber/v2 v2.49.2
github.com/redis/go-redis/v9 v9.2.1
)

require (
github.com/andybalholm/brotli v1.0.5 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/google/uuid v1.3.1 // indirect
github.com/klauspost/compress v1.16.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/philhofer/fwd v1.1.2 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/tinylib/msgp v1.1.8 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.49.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
golang.org/x/sys v0.12.0 // indirect
)
69 changes: 69 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/gofiber/fiber/v2 v2.49.2 h1:ONEN3/Vc+dUCxxDgZZwpqvhISgHqb+bu+isBiEyKEQs=
github.com/gofiber/fiber/v2 v2.49.2/go.mod h1:gNsKnyrmfEWFpJxQAV0qvW6l70K1dZGno12oLtukcts=
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw=
github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0=
github.com/redis/go-redis/v9 v9.2.1 h1:WlYJg71ODF0dVspZZCpYmoF1+U1Jjk9Rwd7pq6QmlCg=
github.com/redis/go-redis/v9 v9.2.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0=
github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.49.0 h1:9FdvCpmxB74LH4dPb7IJ1cOSsluR07XG3I1txXWwJpE=
github.com/valyala/fasthttp v1.49.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA=
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
45 changes: 45 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package main

import (
"log"
"os"

"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/cors"
"github.com/gofiber/fiber/v2/middleware/logger"
"github.com/gofiber/fiber/v2/middleware/recover"
"github.com/hitsapp/api/utils"
v1 "github.com/hitsapp/api/v1"
)

func main() {
app := fiber.New(fiber.Config{
CaseSensitive: false,
StrictRouting: true,
ServerHeader: "hits",
BodyLimit: 1024 * 1024,
GETOnly: true,
ProxyHeader: os.Getenv("PROXY_HEADER"),
})

app.Use(logger.New())
app.Use(recover.New())
app.Use(cors.New(cors.Config{
AllowOrigins: "*",
}))

utils.ConnectRedis()

app.Get("/", func(c *fiber.Ctx) error {
return c.JSON(fiber.Map{
"success": true,
"message": "sponsored by https://hop.io",
})
})

v1Group := app.Group("/v1")
v1Group.Get("/leaderboard", utils.RateLimit(50), v1.Leaderboard)
v1Group.Get("/hits", utils.RateLimit(1200), v1.Hits)

log.Fatal(app.Listen(":8080"))
}
1 change: 1 addition & 0 deletions tmp/build-errors.log
Original file line number Diff line number Diff line change
@@ -0,0 +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
72 changes: 72 additions & 0 deletions utils/badge.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package utils

import (
"io"

"github.com/valyala/bytebufferpool"
"github.com/valyala/fasttemplate"
)

var badgeSVG = Trim(`
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" width="{{rectWidth}}" height="20" role="img" aria-label="{{label}}: {{count}} hits">
<title>{{label}}: {{count}} hits</title>
<linearGradient id="s" x2="0" y2="100%">
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
<stop offset="1" stop-opacity=".1"/>
</linearGradient>
<clipPath id="r">
<rect width="100" height="20" rx="{{border}}" fill="#fff"/>
</clipPath>
<g clip-path="url(#r)">
<rect width="38" height="20" fill="{{leftBgColor}}"/>
<rect x="38" width="62" height="20" fill="{{rightBgColor}}"/>
<rect width="{{rectWidth}}" height="20" fill="url(#s)"/>
</g>
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" font-size="110">
<text x="195" y="140" transform="scale(.1)" fill="{{color}}">{{label}}</text>
<text x="695" y="140" transform="scale(.1)" fill="{{color}}">{{count}}</text>
</g>
</svg>
`)

func GenerateBadge(label, count, color, leftBgColor, rightBgColor, border string) (badge []byte) {
const rectWidth = "150"
var tmpl = fasttemplate.New(badgeSVG, "{{", "}}")
buf := bytebufferpool.Get()

if border == "square" {
border = "0"
} else {
border = "3"
}

if label == "" {
label = "hits"
}

_, _ = tmpl.ExecuteFunc(buf, func(w io.Writer, tag string) (int, error) {
switch tag {
case "label":
return buf.WriteString(label)
case "count":
return buf.WriteString(count)
case "color":
return buf.WriteString(color)
case "leftBgColor":
return buf.WriteString(leftBgColor)
case "rightBgColor":
return buf.WriteString(rightBgColor)
case "rectWidth":
return buf.WriteString(rectWidth)
case "border":
return buf.WriteString(border)
}
return 0, nil
})
badge = buf.Bytes()
bytebufferpool.Put(buf)
return badge
}
30 changes: 30 additions & 0 deletions utils/rate_limit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package utils

import (
"time"

"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/limiter"
)

func RateLimit(amount int) fiber.Handler {
return limiter.New(limiter.Config{
Next: func(c *fiber.Ctx) bool {
return c.IP() == "127.0.0.1" || c.IP() == "localhost"
},
Max: amount,
Expiration: 1 * time.Minute,
KeyGenerator: func(c *fiber.Ctx) string {
return c.IP()
},
LimitReached: func(c *fiber.Ctx) error {
return c.Status(429).JSON(Response[*any]{
Success: false,
Error: &Error{
Code: "rate_limit_reached",
Message: "You have reached the rate limit, please try again in one minute",
},
})
},
})
}
52 changes: 52 additions & 0 deletions utils/redis.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package utils

import (
"context"
"os"
"time"

"github.com/redis/go-redis/v9"
)

var RedisClient *redis.Client

func ConnectRedis() {
RedisClient = redis.NewClient(&redis.Options{
Addr: os.Getenv("REDIS_URL"),
Password: os.Getenv("REDIS_PASSWORD"),
DB: 0,
})

_, err := RedisClient.Ping(context.Background()).Result()
if err != nil {
panic(err)
}
}

func GetKey(key string) string {
return RedisClient.Get(context.Background(), key).Val()
}

func AddKey(key string, value string, expiration int) {
RedisClient.Set(context.Background(), key, value, 0).Val()
RedisClient.Expire(context.Background(), key, time.Duration(time.Second*time.Duration(expiration))).Val()
}

func GetHit(url string) float64 {
return RedisClient.ZScore(context.Background(), "hits", url).Val()
}

func GetHits(limit int64) []redis.Z {
return RedisClient.ZRevRangeWithScores(context.Background(), "hits", 0, limit).Val()
}

func AddHit(url string) {
RedisClient.ZAdd(context.Background(), "hits", redis.Z{
Score: 1,
Member: url,
})
}

func IncrementHit(url string) {
RedisClient.ZIncrBy(context.Background(), "hits", 1, url)
}
23 changes: 23 additions & 0 deletions utils/strings.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package utils

import (
"fmt"
"regexp"
"strings"
)

func Trim(str string) string {
trimmed := strings.TrimSpace(regexp.MustCompile(`\s+`).ReplaceAllString(str, " "))
trimmed = strings.Replace(trimmed, " <", "<", -1)
trimmed = strings.Replace(trimmed, "> ", ">", -1)
return trimmed
}

func TransformToHex(text string) string {
var output = fmt.Sprintf("#%s", strings.Trim(text, "\""))
return output
}

func TrimQuotes(text string) string {
return strings.Trim(text, "\"")
}
18 changes: 18 additions & 0 deletions utils/structs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package utils

type Response[T any] struct {
Success bool `json:"success"`
Message string `json:"message,omitempty"`
Error *Error `json:"error,omitempty"`
Data T `json:"data,omitempty"`
}

type Error struct {
Code string `json:"code"`
Message string `json:"message"`
}

type Leaderboard struct {
URL string `json:"url"`
Hits int `json:"hits"`
}
Loading

0 comments on commit 1bbe289

Please sign in to comment.