Compare commits

...

46 Commits

Author SHA1 Message Date
a7ef859c43 Fix api attachment urls 2025-07-29 20:40:53 +03:30
ba06de2c11 Update admin dash 2025-07-29 19:29:07 +03:30
8dcdd27745 Minor improvements to admin panel API 2025-07-29 19:21:15 +03:30
a43bcc9c14 Update admin panel 2025-07-29 19:10:12 +03:30
71800440be Add imagekit fallback on no imagekit ID 2025-07-29 17:55:16 +03:30
051408fcdd Added imagekit optimization support 2025-07-29 17:52:39 +03:30
48b893a403 remove unused code 2025-07-26 22:43:06 +03:30
0a0af6b04b Minor updates 2025-07-25 22:19:29 +03:30
85fe309b05 Cleanup the dto models 2025-07-25 22:17:42 +03:30
e7b8338932 Add docker build cache to gitlab CI 2025-07-25 14:06:10 +03:30
ccb810ee69 Improve random post picker 2025-07-25 13:59:28 +03:30
cc2e2fe0a0 Set mysql connctions limit 2025-07-25 13:02:46 +03:30
714443156e Fix MariaDB RANDOM() eror 2025-07-25 12:28:07 +03:30
b818726b1b Fix mysql Foreign key constraint error 2025-07-25 12:15:44 +03:30
63b524277f Dep updates 2025-07-25 11:50:20 +03:30
e0aa177f07 Add mysql support 2025-07-25 11:50:08 +03:30
71ed94d943 Improve embed page 2025-07-25 11:49:43 +03:30
15485b03d4 Minor improvements 2025-07-11 17:59:38 +03:30
09772a423c Added retry 2025-07-11 17:59:28 +03:30
0c6160b756 Move GIN_MODE=release from build args to runtime environment variable 2025-06-25 16:33:08 +03:30
212637051f Update dependencies and organize go.mod file 2025-06-25 15:59:48 +03:30
d266c93b8a Add database indexes for better query performance and allow robots.txt access 2025-06-25 15:57:50 +03:30
95f5e03f65 Update admin UI assets and add robots.txt 2025-06-11 09:14:51 +03:30
593d32438a Set GIN_MODE to release during build for production environment 2025-06-05 18:47:01 +03:30
22adcee9d2 Use RemoteUrl instead of PreviewUrl and increase API timeout to 60 seconds 2025-06-05 18:15:16 +03:30
c4a192ac80 Add database indexes for PostID and approval status in MediaAttachment model 2025-05-24 11:49:21 +03:30
0f96344543 Modernize admin UI with React and update templates 2025-05-24 10:51:55 +03:30
d35a93e4bc Update login redirect path from root to /admin/ endpoint 2025-05-18 14:47:23 +03:30
e97b9bdb0c Update API endpoints and navigation paths in admin templates 2025-05-18 14:28:02 +03:30
5e8fd13ea4 Fix login API endpoint path by removing redundant 'admin/' prefix 2025-05-18 14:16:00 +03:30
501cdb0f15 Remove nonroot user from Dockerfile 2025-05-18 14:07:40 +03:30
98c56daf24 Switch from static to base Debian 12 distroless image for runtime support 2025-05-18 13:59:16 +03:30
d9fdd6919f Enable CGO in Dockerfile build step for Linux compilation 2025-05-18 13:46:56 +03:30
686eaec0ad Replace panic with slog.Error when .env file loading fails 2025-05-18 13:31:03 +03:30
60a55e3ad8 Fix Docker COPY paths to use absolute destination directories 2025-05-18 13:08:11 +03:30
36aa4589f3 Fix go build path by adding ./ prefix for proper module resolution 2025-05-18 13:04:41 +03:30
e668e795fb Fix build path to specify main.go entry point in Dockerfile 2025-05-18 13:02:24 +03:30
ad790b3d36 Add Docker build configuration and CI/CD pipeline for container deployment 2025-05-18 12:59:03 +03:30
0294418950 Create data dir and move DB file into it for better organization 2025-05-18 12:46:44 +03:30
7659cca37e Add web templates and handlers for home, embed card, and admin pages 2025-05-18 12:23:00 +03:30
0854387eb4 Fix string formatting in logging statements using strconv.Itoa 2025-05-17 21:40:12 +03:30
99e3debf7c Replace log package with slog for structured logging and improve error handling 2025-05-17 21:37:43 +03:30
3d7a3a043f Refactor config usage to access values through Config singleton 2025-05-17 21:08:12 +03:30
a1b3bfa18d Update import path from requestModels to dto package and update struct references 2025-05-17 20:49:56 +03:30
9565c06fba Add JWT auth and .env support for admin login 2025-05-17 20:47:04 +03:30
0d9cf11687 Remove unused imports and fix import alias naming 2025-05-17 20:29:07 +03:30
29 changed files with 980 additions and 898 deletions

6
.env.example Normal file
View File

@@ -0,0 +1,6 @@
CAOM_INSTANCE_URL=https://<CAOM-INSTANCE-URL>
CAOM_TAG=<CAOM-TAG>
CAOM_ADMIN_PASSWORD=<CAOM-ADMIN-PASSWORD>
CAOM_JWT_SECRET=<CAOM-JWT-SECRET> # Required
CAOM_JWT_ISSUER=<CAOM-JWT-ISSUER>
CAOM_JWT_AUDIENCE=<CAOM-JWT-AUDIENCE>

3
.gitignore vendored
View File

@@ -1,3 +1,4 @@
*.db
*.sqlite
build/
build/
.env

36
.gitlab-ci.yml Normal file
View File

@@ -0,0 +1,36 @@
# To contribute improvements to CI/CD templates, please follow the Development guide at:
# https://docs.gitlab.com/ee/development/cicd/templates.html
# This specific template is located at:
# https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Docker.gitlab-ci.yml
# Build a Docker image with CI/CD and push to the GitLab registry.
# Docker-in-Docker documentation: https://docs.gitlab.com/ee/ci/docker/using_docker_build.html
#
# This template uses one generic job with conditional builds
# for the default branch and all other (MR) branches.
docker-build:
# Use the official docker image.
image: docker:cli
stage: build
services:
- docker:dind
variables:
DOCKER_IMAGE_NAME: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG
before_script:
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
# All branches are tagged with $DOCKER_IMAGE_NAME (defaults to commit ref slug)
# Default branch is also tagged with `latest`
script:
- docker build --build-arg BUILDKIT_INLINE_CACHE=1 --cache-from $CI_REGISTRY_IMAGE:latest --pull -t "$DOCKER_IMAGE_NAME" .
- docker push "$DOCKER_IMAGE_NAME"
- |
if [[ "$CI_COMMIT_BRANCH" == "$CI_DEFAULT_BRANCH" ]]; then
docker tag "$DOCKER_IMAGE_NAME" "$CI_REGISTRY_IMAGE:latest"
docker push "$CI_REGISTRY_IMAGE:latest"
fi
# Run this job in a branch where a Dockerfile exists
rules:
- if: $CI_COMMIT_BRANCH
exists:
- Dockerfile

31
Dockerfile Normal file
View File

@@ -0,0 +1,31 @@
# https://docs.docker.com/guides/golang/build-images/
# syntax=docker/dockerfile:1
# Build the application from source
FROM golang:1.24.3 AS build-stage
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY cmd /app/cmd/
COPY internal /app/internal/
RUN CGO_ENABLED=1 GOOS=linux go build -o /CatsOfMastodonGo ./cmd/CatsOfMastodonBotGo/main.go
# Deploy the application binary into a lean image
FROM gcr.io/distroless/base-debian12 AS build-release-stage
WORKDIR /
COPY --from=build-stage /CatsOfMastodonGo /CatsOfMastodonGo
COPY --from=build-stage /app/internal/web/templates /internal/web/templates
EXPOSE 8080
ENV GIN_MODE=release
ENTRYPOINT ["/CatsOfMastodonGo"]

View File

@@ -7,31 +7,33 @@ import (
"CatsOfMastodonBotGo/internal/server"
"CatsOfMastodonBotGo/internal/services"
"context"
"log"
"log/slog"
"strconv"
"time"
)
func main() {
// Setup config
config.Init()
var config = config.Load()
// Initialize database
database.Init()
services.InitPostService()
// Not needed but anyways
services.InitImgKitHelper()
ticker := time.NewTicker(10 * time.Minute)
runFetchPosts := func() {
// Get posts
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
var posts []domain.Post = nil
err, posts := services.PostServiceInstance.GetPostsFromApi(ctx, config.Tag, config.Instance)
err, posts := services.PostServiceInstance.GetPostsFromApi(ctx, config.Config.Tag, config.Config.Instance)
if err != nil {
log.Println(err)
slog.Error(err.Error())
return
}
@@ -41,14 +43,13 @@ func main() {
var newAccounts = services.PostServiceInstance.GetNewAccounts(existingAccountIds, newPosts)
// Save to database
log.Printf("Fetched %d posts; %d existing posts; %d new posts and %d new accounts\n", len(posts), len(existingPostIds), len(newPosts), len(newAccounts))
slog.Info("Fetched " + strconv.Itoa(len(posts)) + " posts; " + strconv.Itoa(len(existingPostIds)) + " existing posts; " + strconv.Itoa(len(newPosts)) + " new posts and " + strconv.Itoa(len(newAccounts)) + " new accounts\n")
// Additional logging
if newAccounts != nil {
log.Printf("Inserted %d accounts\n", services.PostServiceInstance.InsertNewAccounts(newAccounts))
slog.Info("Inserted " + strconv.Itoa(services.PostServiceInstance.InsertNewAccounts(newAccounts)) + " accounts\n")
}
if newPosts != nil {
log.Printf("Inserted %d posts\n", services.PostServiceInstance.InsertNewPosts(newPosts))
slog.Info("Inserted " + strconv.Itoa(services.PostServiceInstance.InsertNewPosts(newPosts)) + " posts\n")
}
}
@@ -57,18 +58,18 @@ func main() {
runFetchPosts()
}
}()
// Run initial fetch on startup
go func() {
runFetchPosts()
}()
// https://seefnasrul.medium.com/create-your-first-go-rest-api-with-jwt-authentication-in-gin-framework-dbe5bda72817
r := server.SetupRouter()
err := r.Run(":8080")
if err != nil {
log.Fatal(err)
slog.Error(err.Error())
}
}

45
go.mod
View File

@@ -3,41 +3,46 @@ module CatsOfMastodonBotGo
go 1.24.2
require (
github.com/appleboy/gin-jwt/v2 v2.10.3 // indirect
github.com/bytedance/sonic v1.13.2 // indirect
github.com/bytedance/sonic/loader v0.2.4 // indirect
github.com/gin-contrib/cors v1.7.6
github.com/gin-contrib/static v1.1.5
github.com/gin-gonic/gin v1.10.1
github.com/golang-jwt/jwt/v5 v5.2.3
github.com/joho/godotenv v1.5.1
gorm.io/driver/mysql v1.6.0
gorm.io/driver/sqlite v1.6.0
gorm.io/gorm v1.30.1
)
require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/bytedance/sonic v1.14.0 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
github.com/gin-contrib/cors v1.7.5 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/gin-gonic/gin v1.10.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.26.0 // indirect
github.com/go-playground/validator/v10 v10.27.0 // indirect
github.com/go-sql-driver/mysql v1.9.3 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v1.14.22 // indirect
github.com/mattn/go-sqlite3 v1.14.29 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
golang.org/x/arch v0.17.0 // indirect
golang.org/x/crypto v0.38.0 // indirect
golang.org/x/net v0.40.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.25.0 // indirect
github.com/ugorji/go/codec v1.3.0 // indirect
golang.org/x/arch v0.19.0 // indirect
golang.org/x/crypto v0.40.0 // indirect
golang.org/x/net v0.42.0 // indirect
golang.org/x/sys v0.34.0 // indirect
golang.org/x/text v0.27.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gorm.io/driver/sqlite v1.5.7 // indirect
gorm.io/gorm v1.26.1 // indirect
)

80
go.sum
View File

@@ -1,53 +1,85 @@
github.com/appleboy/gin-jwt/v2 v2.10.3 h1:KNcPC+XPRNpuoBh+j+rgs5bQxN+SwG/0tHbIqpRoBGc=
github.com/appleboy/gin-jwt/v2 v2.10.3/go.mod h1:LDUaQ8mF2W6LyXIbd5wqlV2SFebuyYs4RDwqMNgpsp8=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ=
github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
github.com/gin-contrib/cors v1.7.5 h1:cXC9SmofOrRg0w9PigwGlHG3ztswH6bqq4vJVXnvYMk=
github.com/gin-contrib/cors v1.7.5/go.mod h1:4q3yi7xBEDDWKapjT2o1V7mScKDDr8k+jZ0fSquGoy0=
github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY=
github.com/gin-contrib/cors v1.7.6/go.mod h1:Ulcl+xN4jel9t1Ry8vqph23a60FwH9xVLd+3ykmTjOk=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-contrib/static v1.1.5 h1:bAPqT4KTZN+4uDY1b90eSrD1t8iNzod7Jj8njwmnzz4=
github.com/gin-contrib/static v1.1.5/go.mod h1:8JSEXwZHcQ0uCrLPcsvnAJ4g+ODxeupP8Zetl9fd8wM=
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-jwt/jwt/v5 v5.2.3 h1:kkGXqQOBSDDWRhWNXTFpqGSCMyh/PLnqUvMGJPDJDs0=
github.com/golang-jwt/jwt/v5 v5.2.3/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mattn/go-sqlite3 v1.14.29 h1:1O6nRLJKvsi1H2Sj0Hzdfojwt8GiGKm+LOfLaBFaouQ=
github.com/mattn/go-sqlite3 v1.14.29/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -55,7 +87,10 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
@@ -64,36 +99,53 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
golang.org/x/arch v0.17.0 h1:4O3dfLzd+lQewptAHqjewQZQDyEdejz3VwgeYwkZneU=
golang.org/x/arch v0.17.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/arch v0.19.0 h1:LmbDQUodHThXE+htjrnmVD73M//D9GTH6wFZjyDkjyU=
golang.org/x/arch v0.19.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg=
gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo=
gorm.io/driver/sqlite v1.5.7 h1:8NvsrhP0ifM7LX9G4zPB97NwovUakUxc+2V2uuf3Z1I=
gorm.io/driver/sqlite v1.5.7/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4=
gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde h1:9DShaph9qhkIYw7QF91I/ynrr4cOO2PZra2PFD7Mfeg=
gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
gorm.io/gorm v1.26.1 h1:ghB2gUI9FkS46luZtn6DLZ0f6ooBJ5IbVej2ENFDjRw=
gorm.io/gorm v1.26.1/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs=
gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
gorm.io/gorm v1.30.1 h1:lSHg33jJTBxs2mgJRfRZeLDG+WZaHYCk3Wtfl6Ngzo4=
gorm.io/gorm v1.30.1/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

View File

@@ -10,8 +10,8 @@ import (
)
type JwtTokenGenerator struct {
Key string
Issuer string
Key string
Issuer string
Audience string
}
@@ -19,15 +19,15 @@ var JwtTokenGeneratorInstance *JwtTokenGenerator
func InitJwtTokenGenerator() {
JwtTokenGeneratorInstance = &JwtTokenGenerator{
Key: config.Config.JwtSecret,
Issuer: config.Config.JwtIssuer,
Key: config.Config.JwtSecret,
Issuer: config.Config.JwtIssuer,
Audience: config.Config.JwtAudience,
}
}
func (j *JwtTokenGenerator) GenerateToken(claims map[string]interface{}) (string, error) {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"exp": time.Now().AddDate(0, 0, 3).Unix(),
"exp": time.Now().AddDate(0, 0, 1).Unix(),
"iat": time.Now().Unix(),
"iss": j.Issuer,
"aud": j.Audience,
@@ -69,4 +69,4 @@ func (j *JwtTokenGenerator) GinMiddleware() gin.HandlerFunc {
c.Next()
}
}
}

View File

@@ -1,25 +1,39 @@
package config
import (
//"CatsOfMastodonBotGo/internal/auth"
//"CatsOfMastodonBotGo/internal/services"
"log"
"log/slog"
"os"
"github.com/joho/godotenv"
)
type config struct {
AdminPassword string
Instance string
Tag string
Instance string
Tag string
JwtSecret string
JwtIssuer string
JwtSecret string
JwtIssuer string
JwtAudience string
DBEngine string
DBHost string
DBPort string
DBUser string
DBPassword string
DBName string
ImageKitId string
}
var Config *config
func Load() *config {
err := godotenv.Load()
if err != nil {
slog.Warn("Error loading .env file - Using environment variables instead")
}
// Get mastodon instance
instance := os.Getenv("CAOM_INSTANCE")
if instance == "" {
@@ -33,36 +47,65 @@ func Load() *config {
// Get admin password (Its a single user/admin app so its just fine)
adminPassword := os.Getenv("CAOM_ADMIN_PASSWORD")
if adminPassword == "" {
log.Println("No admin password provided, using default password 'catsaregood'")
slog.Warn("No admin password provided, using default password 'catsaregood'")
adminPassword = "catsaregood"
}
// Jwt params
secret := os.Getenv("CAOM_JWT_SECRET")
if secret == "" {
log.Fatal("No jwt secret provided")
panic("No jwt secret provided")
}
issuer := os.Getenv("CAOM_JWT_ISSUER")
if issuer == "" {
log.Println("No jwt issuer provided, using default issuer 'CatsOfMastodonBotGo'")
slog.Info("No jwt issuer provided, using default issuer 'CatsOfMastodonBotGo'")
issuer = "CatsOfMastodonBotGo"
}
audience := os.Getenv("CAOM_JWT_AUDIENCE")
if audience == "" {
log.Println("No jwt audience provided, using default audience 'CatsOfMastodonBotGo'")
slog.Info("No jwt audience provided, using default audience 'CatsOfMastodonBotGo'")
audience = "CatsOfMastodonBotGo"
}
dbEngine := os.Getenv("CAOM_DB_ENGINE")
dbHost := os.Getenv("CAOM_DB_HOST")
dbPort := os.Getenv("CAOM_DB_PORT")
dbUser := os.Getenv("CAOM_DB_USER")
dbPassword := os.Getenv("CAOM_DB_PASSWORD")
dbName := os.Getenv("CAOM_DB_NAME")
if dbEngine == "" || dbHost == "" || dbPort == "" || dbUser == "" || dbPassword == "" || dbName == "" {
slog.Info("No database connection provided, using sqlite")
dbEngine = "sqlite"
dbHost = ""
dbPort = ""
dbUser = ""
dbPassword = ""
dbName = "caom.db"
}
imageKitId := os.Getenv("CAOM_IMAGEKIT_ID")
if imageKitId == "" {
slog.Info("No imagekit id provided, not using imagekit.io")
}
// Inititlize AppContext
var appContext = &config{
AdminPassword: adminPassword,
Instance: instance,
Tag: tag,
Instance: instance,
Tag: tag,
JwtSecret: secret,
JwtIssuer: issuer,
JwtSecret: secret,
JwtIssuer: issuer,
JwtAudience: audience,
DBEngine: dbEngine,
DBHost: dbHost,
DBPort: dbPort,
DBUser: dbUser,
DBPassword: dbPassword,
DBName: dbName,
ImageKitId: imageKitId,
}
return appContext

View File

@@ -1,8 +1,12 @@
package database
import (
"CatsOfMastodonBotGo/internal/config"
"CatsOfMastodonBotGo/internal/domain"
"fmt"
"os"
"gorm.io/driver/mysql"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
@@ -10,10 +14,41 @@ import (
var Gorm *gorm.DB
func Connect() (*gorm.DB, error) {
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
if err != nil {
return nil, err
var db *gorm.DB
var err error = nil
if config.Config.DBEngine == "sqlite" {
_, err = os.ReadDir("data")
if err != nil {
err = os.Mkdir("data", 0755)
if err != nil {
return nil, err
}
}
db, err = gorm.Open(sqlite.Open("data/caom.db"), &gorm.Config{})
if err != nil {
return nil, err
}
} else {
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
config.Config.DBUser,
config.Config.DBPassword,
config.Config.DBHost,
config.Config.DBPort,
config.Config.DBName)
db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
return nil, err
}
sqlDB, err := db.DB()
if err != nil {
return nil, err
}
sqlDB.SetMaxOpenConns(100)
sqlDB.SetMaxIdleConns(10)
}
// Migrate the schema
if err := db.AutoMigrate(&domain.Post{}, &domain.MediaAttachment{}, &domain.Account{}); err != nil {
return nil, err
@@ -22,10 +57,10 @@ func Connect() (*gorm.DB, error) {
}
// IDK if this is how it works or not, leave it as is for now
func Init () {
func Init() {
var err error
Gorm, err = Connect()
if err != nil {
panic(err)
}
}
}

View File

@@ -9,22 +9,22 @@ type Post struct {
}
type Account struct {
AccId string `json:"id" gorm:"primaryKey"`
Username string `json:"username"`
Acct string `json:"acct"`
DisplayName string `json:"display_name"`
IsBot bool `json:"bot"`
Url string `json:"url"`
AvatarStatic string `json:"avatar_static"`
AccId string `json:"id" gorm:"primaryKey;column:acc_id;type:varchar(19);index"`
Username string `json:"username"`
Acct string `json:"acct"`
DisplayName string `json:"display_name"`
IsBot bool `json:"bot"`
Url string `json:"url"`
AvatarStatic string `json:"avatar_static"`
}
type MediaAttachment struct {
ID string `json:"id" gorm:"primaryKey"`
ID string `json:"id" gorm:"primaryKey;type:varchar(19)"`
Type string `json:"type"`
Url string `json:"url"`
PreviewUrl string `json:"preview_url"`
RemoteUrl string `json:"remote_url"`
PostID string // Foreign key to Post
Approved bool `json:"approved"`
Rejected bool `json:"rejected"`
}
PostID string `gorm:"index:idx_post_approved,post_id;index:idx_post_id;type:varchar(19)"`
Approved bool `json:"approved" gorm:"index:idx_post_approved"`
Rejected bool `json:"rejected" gorm:"index:idx_post_rejected"`
}

View File

@@ -1,5 +0,0 @@
package requestmodels
type ApproveMediaInput struct {
MediaId string `json:"mediaId" binding:"required"`
}

View File

@@ -1,5 +0,0 @@
package requestmodels
type LoginInput struct {
Password string `json:"password" binding:"required"`
}

View File

@@ -1,5 +0,0 @@
package requestmodels
type RejectMediaInput struct {
MediaId string `json:"mediaId" binding:"required"`
}

View File

@@ -3,8 +3,10 @@ package server
import (
"CatsOfMastodonBotGo/internal/auth"
"CatsOfMastodonBotGo/internal/web/handlers"
"net/http"
"github.com/gin-contrib/cors"
"github.com/gin-contrib/static"
"github.com/gin-gonic/gin"
)
@@ -12,23 +14,42 @@ func SetupRouter() *gin.Engine {
r := gin.Default()
r.Use(cors.New(cors.Config{
AllowOrigins: []string{"https://extra-mama-chiz.surge.sh"}, // Just for test
AllowAllOrigins: true,
AllowMethods: []string{"POST", "GET", "OPTIONS"},
AllowHeaders: []string{"Origin", "Content-Type", "Authorization"},
AllowCredentials: true,
}))
r.LoadHTMLGlob("internal/web/templates/home/*")
auth.InitJwtTokenGenerator() // Must be befor initializing admin handler, otherwise 'panic: runtime error: invalid memory address or nil pointer dereference'
handlers.InitAdminDashboardHandler()
handlers.InitApiEndpointHandler()
auth.InitJwtTokenGenerator()
handlers.InitEmbedCardHandler()
// Main page
r.GET("/", func(c *gin.Context) {
c.HTML(http.StatusOK, "home/index.html", nil)
})
// Embed card
r.GET("/embed", handlers.EmbedCardHandlerInstance.GetEmbedCard)
admin := r.Group("/admin")
// My man, this is done way more efficient and fast in .NET, specially the authentication part
admin.POST("/login", handlers.AdminDashboardHandlerInstance.Login)
admin.GET("/getmedia", auth.JwtTokenGeneratorInstance.GinMiddleware(), handlers.AdminDashboardHandlerInstance.GetMedia)
admin.POST("/approve", auth.JwtTokenGeneratorInstance.GinMiddleware(), handlers.AdminDashboardHandlerInstance.ApproveMedia)
admin.POST("/reject", auth.JwtTokenGeneratorInstance.GinMiddleware(), handlers.AdminDashboardHandlerInstance.RejectMedia)
// admin.GET("/", func(c *gin.Context) {
// c.HTML(http.StatusOK, "admin/index.html", nil)
// })
// admin.GET("/login", func(c *gin.Context) {
// c.HTML(http.StatusOK, "admin/index.html", nil)
// })
r.Use(static.Serve("/admin", static.LocalFile("internal/web/templates/admin", true)))
adminApi := admin.Group("/api")
adminApi.POST("/login", handlers.AdminDashboardHandlerInstance.Login)
adminApi.GET("/getmedia", auth.JwtTokenGeneratorInstance.GinMiddleware(), handlers.AdminDashboardHandlerInstance.GetMedia)
adminApi.POST("/approve", auth.JwtTokenGeneratorInstance.GinMiddleware(), handlers.AdminDashboardHandlerInstance.ApproveMedia)
adminApi.POST("/reject", auth.JwtTokenGeneratorInstance.GinMiddleware(), handlers.AdminDashboardHandlerInstance.RejectMedia)
api := r.Group("/api")

View File

@@ -0,0 +1,26 @@
package services
import "CatsOfMastodonBotGo/internal/config"
type ImgKitHelper struct {
}
var ImgKitHelperInstance *ImgKitHelper
func InitImgKitHelper() {
ImgKitHelperInstance = &ImgKitHelper{}
}
func GetPreviewUrl(url string) string {
if config.Config.ImageKitId == "" {
return url
}
return "https://ik.imagekit.io/" + config.Config.ImageKitId + "/tr:w-500,h-500,c-at_max,f-webp,q-50/" + url
}
func GetRemoteUrl(url string) string {
if config.Config.ImageKitId == "" {
return url
}
return "https://ik.imagekit.io/" + config.Config.ImageKitId + "/tr:q-70,dpr-auto,f-webp/" + url
}

View File

@@ -1,11 +1,13 @@
package services
import (
"CatsOfMastodonBotGo/internal/config"
"CatsOfMastodonBotGo/internal/database"
"CatsOfMastodonBotGo/internal/domain"
"context"
"encoding/json"
"fmt"
"math/rand"
"net/http"
"strings"
@@ -64,7 +66,6 @@ func (ps *PostService) GetExistingAccountIds() []string {
return existingAccountIds
}
func (*PostService) GetNewPosts(existingPostIds []string, posts []domain.Post) []domain.Post {
var newPosts []domain.Post = nil
for _, post := range posts {
@@ -106,14 +107,33 @@ func (ps *PostService) InsertNewAccounts(newAccounts []domain.Account) int {
func (ps *PostService) GetRandomPost() domain.Post {
var post domain.Post
var postIDs []uint
// AI Enhanced
// Step 1: Fetch eligible post IDs (with at least one approved attachment)
ps.db.
Model(&domain.Post{}).
Where("exists (select 1 from media_attachments where post_id = posts.id and approved = ?)", true).
Pluck("id", &postIDs)
if len(postIDs) == 0 {
return domain.Post{} // No eligible posts
}
// Step 2: Pick a random ID in Go
randomID := postIDs[rand.Intn(len(postIDs))]
// Step 3: Load the full post with related data
ps.db.
Preload("Account").
Preload("Attachments", "approved = ?", true).
Order("RANDOM()").
First(&post)
Preload("Attachments", "Approved = ?", true).
First(&post, randomID)
// Step 4: Keep only the first attachment if any
if len(post.Attachments) > 0 {
post.Attachments = []domain.MediaAttachment{post.Attachments[0]}
}
return post
}
@@ -132,15 +152,18 @@ func (ps *PostService) RejectMedia(mediaId string) bool {
// Get a post which approve and rejet are false (For admin panel)
func (ps *PostService) GetMedia() domain.MediaAttachment {
var media domain.MediaAttachment
orderExpr := "RANDOM()" // sqlite
if config.Config.DBEngine != "sqlite" {
orderExpr = "RAND()" // mariadb/mysql
}
ps.db.Model(&domain.MediaAttachment{}).
Where("approved = ?", false).
Where("rejected = ?", false).
Order("RANDOM()").
Order(orderExpr).
First(&media)
return media
}
func arrayContains(arr []string, str string) bool {
for _, a := range arr {
if a == str {

View File

@@ -0,0 +1,13 @@
package dto
type ApproveMediaInput struct {
MediaId string `json:"mediaId" binding:"required"`
}
type LoginInput struct {
Password string `json:"password" binding:"required"`
}
type RejectMediaInput struct {
MediaId string `json:"mediaId" binding:"required"`
}

View File

@@ -3,8 +3,9 @@ package handlers
import (
"net/http"
"CatsOfMastodonBotGo/internal/auth"
"CatsOfMastodonBotGo/internal/config"
requestmodels "CatsOfMastodonBotGo/internal/domain/requestModels"
"CatsOfMastodonBotGo/internal/web/dto"
"CatsOfMastodonBotGo/internal/services"
"github.com/gin-gonic/gin"
@@ -12,6 +13,7 @@ import (
type AdminDashboardHandler struct {
PostService services.PostService
Jwt auth.JwtTokenGenerator
}
var AdminDashboardHandlerInstance *AdminDashboardHandler
@@ -19,11 +21,12 @@ var AdminDashboardHandlerInstance *AdminDashboardHandler
func InitAdminDashboardHandler() {
AdminDashboardHandlerInstance = &AdminDashboardHandler{
PostService: *services.PostServiceInstance,
Jwt: *auth.JwtTokenGeneratorInstance,
}
}
func (ps *AdminDashboardHandler) ApproveMedia(c *gin.Context) {
var input requestmodels.ApproveMediaInput
var input dto.ApproveMediaInput
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
@@ -36,7 +39,7 @@ func (ps *AdminDashboardHandler) ApproveMedia(c *gin.Context) {
}
func (ps *AdminDashboardHandler) RejectMedia(c *gin.Context) {
var input requestmodels.RejectMediaInput
var input dto.RejectMediaInput
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
@@ -50,12 +53,14 @@ func (ps *AdminDashboardHandler) RejectMedia(c *gin.Context) {
func (ps *AdminDashboardHandler) GetMedia(c *gin.Context) {
media := ps.PostService.GetMedia()
media.PreviewUrl = services.GetPreviewUrl(media.RemoteUrl)
media.RemoteUrl = services.GetPreviewUrl(media.RemoteUrl)
c.JSON(http.StatusOK, media)
}
func (ps *AdminDashboardHandler) Login(c *gin.Context) {
var input requestmodels.LoginInput
var input dto.LoginInput
// Validate data
if err := c.ShouldBindJSON(&input); err != nil {
@@ -64,13 +69,13 @@ func (ps *AdminDashboardHandler) Login(c *gin.Context) {
}
if input.Password == config.Config.AdminPassword { // Its more than enough for this project
// token, err := ps.ps.Jwt.GenerateToken(map[string]interface{}{"role": "admin"})
// if err != nil {
// c.JSON(http.StatusInternalServerError, gin.H{"error": "Token generation failed"})
// return
// }
token, err := ps.Jwt.GenerateToken(map[string]interface{}{"role": "admin"})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Token generation failed"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Login successful", "token": "yoy"}) // TODO: Add token
c.JSON(http.StatusOK, gin.H{"message": "Login successful", "token": token})
} else {
c.JSON(401, gin.H{

View File

@@ -16,9 +16,14 @@ func InitApiEndpointHandler() {
ApiEndpointHandlerInstance = &ApiEndpointHandler{
PostService: *services.PostServiceInstance,
}
}
func (ps *ApiEndpointHandler) GetRandomPost(c *gin.Context) {
c.JSON(200,ps.PostService.GetRandomPost())
}
post := ps.PostService.GetRandomPost()
for i := range post.Attachments {
post.Attachments[i].RemoteUrl = services.GetRemoteUrl(post.Attachments[i].RemoteUrl)
post.Attachments[i].PreviewUrl = services.GetPreviewUrl(post.Attachments[i].RemoteUrl)
}
c.JSON(200, post)
}

View File

@@ -0,0 +1,27 @@
package handlers
import (
"CatsOfMastodonBotGo/internal/services"
"github.com/gin-gonic/gin"
)
type EmbedCardHandler struct {
PostService services.PostService
}
var EmbedCardHandlerInstance *EmbedCardHandler
func InitEmbedCardHandler() {
EmbedCardHandlerInstance = &EmbedCardHandler{
PostService: *services.PostServiceInstance,
}
}
func (ps *EmbedCardHandler) GetEmbedCard(c *gin.Context) {
post := ps.PostService.GetRandomPost()
c.HTML(200, "home/embed.html", gin.H{
"postUrl": post.Url,
"imageUrl": services.GetRemoteUrl(post.Attachments[0].RemoteUrl),
})
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,25 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Admin Dashboard</title>
<meta name="description" content="Admin Dashboard for Media Review" />
<meta name="author" content="" />
<meta property="og:title" content="Admin Dashboard" />
<meta property="og:description" content="Admin Dashboard for Media Review" />
<meta property="og:type" content="website" />
<meta name="twitter:card" content="summary_large_image" />
<script type="module" crossorigin src="/admin/assets/index-JxecVd-K.js"></script>
<link rel="stylesheet" crossorigin href="/admin/assets/index-DShmOgsI.css">
</head>
<body>
<div id="root"></div>
<!-- IMPORTANT: DO NOT REMOVE THIS SCRIPT TAG OR THIS VERY COMMENT! -->
<script src="https://cdn.gpteng.co/gptengineer.js" type="module"></script>
</body>
</html>

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="1200" fill="none"><rect width="1200" height="1200" fill="#EAEAEA" rx="3"/><g opacity=".5"><g opacity=".5"><path fill="#FAFAFA" d="M600.709 736.5c-75.454 0-136.621-61.167-136.621-136.62 0-75.454 61.167-136.621 136.621-136.621 75.453 0 136.62 61.167 136.62 136.621 0 75.453-61.167 136.62-136.62 136.62Z"/><path stroke="#C9C9C9" stroke-width="2.418" d="M600.709 736.5c-75.454 0-136.621-61.167-136.621-136.62 0-75.454 61.167-136.621 136.621-136.621 75.453 0 136.62 61.167 136.62 136.621 0 75.453-61.167 136.62-136.62 136.62Z"/></g><path stroke="url(#a)" stroke-width="2.418" d="M0-1.209h553.581" transform="scale(1 -1) rotate(45 1163.11 91.165)"/><path stroke="url(#b)" stroke-width="2.418" d="M404.846 598.671h391.726"/><path stroke="url(#c)" stroke-width="2.418" d="M599.5 795.742V404.017"/><path stroke="url(#d)" stroke-width="2.418" d="m795.717 796.597-391.441-391.44"/><path fill="#fff" d="M600.709 656.704c-31.384 0-56.825-25.441-56.825-56.824 0-31.384 25.441-56.825 56.825-56.825 31.383 0 56.824 25.441 56.824 56.825 0 31.383-25.441 56.824-56.824 56.824Z"/><g clip-path="url(#e)"><path fill="#666" fill-rule="evenodd" d="M616.426 586.58h-31.434v16.176l3.553-3.554.531-.531h9.068l.074-.074 8.463-8.463h2.565l7.18 7.181V586.58Zm-15.715 14.654 3.698 3.699 1.283 1.282-2.565 2.565-1.282-1.283-5.2-5.199h-6.066l-5.514 5.514-.073.073v2.876a2.418 2.418 0 0 0 2.418 2.418h26.598a2.418 2.418 0 0 0 2.418-2.418v-8.317l-8.463-8.463-7.181 7.181-.071.072Zm-19.347 5.442v4.085a6.045 6.045 0 0 0 6.046 6.045h26.598a6.044 6.044 0 0 0 6.045-6.045v-7.108l1.356-1.355-1.282-1.283-.074-.073v-17.989h-38.689v23.43l-.146.146.146.147Z" clip-rule="evenodd"/></g><path stroke="#C9C9C9" stroke-width="2.418" d="M600.709 656.704c-31.384 0-56.825-25.441-56.825-56.824 0-31.384 25.441-56.825 56.825-56.825 31.383 0 56.824 25.441 56.824 56.825 0 31.383-25.441 56.824-56.824 56.824Z"/></g><defs><linearGradient id="a" x1="554.061" x2="-.48" y1=".083" y2=".087" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><linearGradient id="b" x1="796.912" x2="404.507" y1="599.963" y2="599.965" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><linearGradient id="c" x1="600.792" x2="600.794" y1="403.677" y2="796.082" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><linearGradient id="d" x1="404.85" x2="796.972" y1="403.903" y2="796.02" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><clipPath id="e"><path fill="#fff" d="M581.364 580.535h38.689v38.689h-38.689z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@@ -0,0 +1,2 @@
User-agent: *
Disallow: /

View File

@@ -0,0 +1,99 @@
{{ define "home/embed.html" }}
<html lang='en'>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cat of the Day</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Atma:wght@300;400;500;600;700&display=swap');
.card {
background-color: #161616;
border-radius: 15px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
padding: 20px;
width: 90%;
max-width: 400px;
text-align: center;
transition: transform 0.2s ease-in-out;
font-family: "Atma", system-ui;
}
.cat-image {
width: 100%;
border-radius: 5px;
margin-bottom: 15px;
object-fit: cover;
max-height: auto;
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.2);
transition: opacity 0.3s ease;
}
.button {
background-color: #9e44ae;
color: #e0e0e0;
border: none;
padding: 2px 15px;
border-radius: 5px;
cursor: pointer;
text-decoration: none;
box-sizing: border-box;
transition: background-color 0.3s ease, transform 0.2s ease-in-out;
font-weight: 500;
}
.button:hover {
background-color: #6d0e7e;
transform: scale(1.01);
}
</style>
</head>
<body>
<div class="card" style="margin: 0 auto;">
<img class="cat-image" id="catImage" src="{{ .imageUrl }}" alt="Cat Photo" onload="this.removeAttribute('onload')">
<a class="button" id="mastodonLink" href="{{ .postUrl }}" target="_blank" style="display: block;">View on Mastodon</a>
</div>
</body>
<script>
const img = document.querySelector('.cat-image');
const link = document.getElementById('mastodonLink');
img.onerror = () => {
console.warn("failed to load the image - retrying...");
fetch('/api/post/random')
.then(response => {
if (!response.ok) throw new Error("Network error");
return response.json();
})
.then(data => {
if (!data || !data.account || !data.media_attachments || data.media_attachments.length === 0) {
throw new Error("Invalid data format");
}
let imageUrl = data.media_attachments[0].remote_url;
if (imageUrl) {
imageUrl = imageUrl.replace('/original/', '/small/');
} else if (data.media_attachments[0].preview_url) {
imageUrl = data.media_attachments[0].preview_url;
} else {
throw new Error("No image URL found");
}
img.src = imageUrl;
link.href = data.url;
})
.catch(error => {
console.error("Failed to load fallback image:", error);
img.src = "https://s6.uupload.ir/files/im_sad_0zg9.jpg";
});
};
</script>
</html>
{{ end }}

View File

@@ -0,0 +1,298 @@
{{ define "home/index.html" }}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cats of Mastodon! - Mahdium</title><style>
@import url('https://fonts.googleapis.com/css2?family=Atma:wght@300;400;500;600;700&display=swap');
body {
font-family: "Atma", system-ui;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
min-height: 100vh;
background-color: #1e1e1e;
color: #e0e0e0;
margin: 0;
overflow-x: hidden;
}
.mastodon-title {
font-size: 2.5em;
margin-bottom: 10px;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
text-align: center;
}
.welcome-message {
margin-bottom: 20px;
font-size: 1.2em;
line-height: 1.5;
text-align: center;
}
.welcome-message span {
font-size: 1.5em;
}
.post-container {
background-color: #282828;
border-radius: 15px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
padding: 20px;
width: 90%;
max-width: 500px;
text-align: center;
transition: transform 0.2s ease-in-out;
margin-bottom: 20px;
}
.post-container:hover {
transform: scale(1.02);
}
.post-title-inner {
margin-bottom: 10px;
font-size: 1.2em;
}
.user-name {
color: #ffb347;
font-weight: bold;
font-size: 1.2em;
}
.cat-text {
color: #bbdefb;
font-style: italic;
}
.post-image {
width: 100%;
border-radius: 10px;
margin-bottom: 15px;
object-fit: cover;
max-height: 500px;
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.2);
transition: opacity 0.3s ease;
}
.post-image.loading {
opacity: 0;
}
.image-container {
position: relative;
display: inline-block;
}
.image-loading-spinner {
display: none;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
border: 4px solid #f3f3f3;
border-top: 4px solid #6364ff;
border-radius: 50%;
width: 30px;
height: 30px;
animation: spin 1s linear infinite;
}
.post-image.loading + .image-loading-spinner {
display: block;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.button-container {
display: flex;
flex-direction: column;
gap: 10px;
width: 100%;
}
.mastodon-button,
.neutral-button {
background-color: #404040;
color: #e0e0e0;
border: none;
padding: 12px 18px;
border-radius: 8px;
cursor: pointer;
text-decoration: none;
width: 100%;
box-sizing: border-box;
transition: background-color 0.3s ease, transform 0.2s ease-in-out;
font-weight: 500;
}
.mastodon-button {
background-color: #6364ff;
}
.mastodon-button:hover {
background-color: #5253e0;
transform: scale(1.03);
}
.neutral-button:hover {
background-color: #505050;
transform: scale(1.03);
}
.loading-spinner {
display: none;
border: 6px solid #f3f3f3;
border-top: 6px solid #6364ff;
border-radius: 50%;
width: 60px;
height: 60px;
animation: spin 1s linear infinite;
margin: 20px auto;
}
.post-container.loading .loading-spinner {
display: block;
}
.post-container.loading .post-image,
.post-container.loading .button-container,
.post-container.loading .post-title-inner {
display: none;
}
.footer {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
font-size: 0.8em;
color: #909090;
width: 90%;
max-width: 500px;
margin: 0 auto;
}
.footer a {
color: inherit;
text-decoration: none;
}
.footer a:hover {
text-decoration: underline;
}
.footer-button {
background: none;
border: none;
padding: 0;
cursor: pointer;
}
</style>
</head>
<body>
<h2 class="mastodon-title">Cats of Mastodon!</h2>
<p class="welcome-message">Welcome to Daily Catventures! </br> Get your daily dose of purr-fectly adorable feline fun! <span></span><br>Posts gathered across Mastodon 🤝</p>
<div class="post-container">
<div class="post-content" style="display: none;">
<div class="post-title-inner">
<span class="user-name"></span><span class="cat-text">'s cat!</span>
</div>
<div class="image-container">
<img class="post-image" src="" alt="Cat Photo">
<div class="image-loading-spinner"></div>
</div>
<div class="button-container">
<a class="mastodon-button" href="" target="_blank">View on Mastodon</a>
<button class="neutral-button" onclick="loadNewPost()">Show me another cat!</button>
</div>
</div>
<div class="loading-spinner"></div>
</div>
<div class="footer">
<span>© 2024 Mahdium</span>
<a href="https://mahdium.ir" class="footer-button">mahdium.ir</a>
<a href="https://gitlab.com/mahdium/cats-of-mastodon-telegram-bot" class="footer-button">Source Code</a>
</div>
<script>
const postContainer = document.querySelector('.post-container');
const postContent = document.querySelector('.post-content');
const userNameSpan = document.querySelector('.post-content .user-name');
const postImage = document.querySelector('.post-content .post-image');
const mastodonLink = document.querySelector('.post-content .mastodon-button');
const imageLoadingSpinner = document.querySelector('.image-container .image-loading-spinner');
function loadNewPost() {
postContainer.classList.add('loading');
postContent.style.display = 'none';
postImage.classList.add('loading');
fetch('/api/post/random')
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => {
postContainer.classList.remove('loading');
postContent.style.display = 'block';
if (!data || !data.account || !data.media_attachments || data.media_attachments.length === 0) {
console.error("Invalid API response format:", data);
alert("Invalid data received from the server.");
postImage.classList.remove('loading');
return;
}
userNameSpan.textContent = data.account.display_name;
let imageUrl = data.media_attachments[0].remote_url;
if (imageUrl) {
imageUrl = imageUrl.replace('/original/', '/small/');
} else if (data.media_attachments[0].PreviewUrl) {
imageUrl = data.media_attachments[0].PreviewUrl;
} else {
console.warn("No RemoteUrl or PreviewUrl found, using placeholder");
postImage.src = "https://s6.uupload.ir/files/a69d5fc9e900cc51_1920_kmnr.png";
postImage.classList.remove('loading');
return;
}
postImage.onload = () => {
postImage.classList.remove('loading');
window.scrollTo(0, document.body.scrollHeight);
};
postImage.onerror = () => {
console.error("Error loading image:", imageUrl);
postImage.src = "https://s6.uupload.ir/files/a69d5fc9e900cc51_1920_kmnr.png";
loadNewPost();
postImage.classList.remove('loading');
};
postImage.src = imageUrl;
mastodonLink.href = data.url;
})
.catch(error => {
postContainer.classList.remove('loading');
postImage.classList.remove('loading');
console.error("Error fetching data:", error);
alert("Failed to load a new post. Please try again later.");
});
}
loadNewPost();
</script>
</body>
</html>
{{ end }}

View File

@@ -1,772 +0,0 @@
internal/AppContext.go
package internal
import (
"CatsOfMastodonBotGo/internal/auth"
"CatsOfMastodonBotGo/internal/services"
"gorm.io/gorm"
)
type AppContext struct {
Db *gorm.DB
PostService *services.PostService
Jwt *auth.JwtTokenGenerator
AdminPassword string
Instance string
Tag string
}
internal/auth/jwt.go
package auth
import (
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
)
type JwtTokenGenerator struct {
Key string
Issuer string
Audience string
}
func NewJwtTokenGenerator(key string, issuer string, audience string) *JwtTokenGenerator {
return &JwtTokenGenerator{
Key: key,
Issuer: issuer,
Audience: audience,
}
}
func (j *JwtTokenGenerator) GenerateToken(claims map[string]interface{}) (string, error) {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"exp": time.Now().AddDate(0, 0, 3).Unix(),
"iat": time.Now().Unix(),
"iss": j.Issuer,
"aud": j.Audience,
})
for k, v := range claims {
token.Claims.(jwt.MapClaims)[k] = v
}
return token.SignedString([]byte(j.Key))
}
// Gin middleware
func (j *JwtTokenGenerator) GinMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.AbortWithStatusJSON(401, gin.H{"error": "Unauthorized"})
return
}
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
t, err := jwt.Parse(tokenString, func(t *jwt.Token) (interface{}, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, jwt.ErrSignatureInvalid
}
return []byte(j.Key), nil
})
if err != nil {
c.AbortWithStatusJSON(401, gin.H{"error": "Unauthorized"})
return
}
claims, ok := t.Claims.(jwt.MapClaims)
if !ok || claims["role"] != "admin" {
c.AbortWithStatusJSON(401, gin.H{"error": "Unauthorized"})
return
}
c.Next()
}
}
internal/helpers/dbHelpers.go
package helpers
import (
"CatsOfMastodonBotGo/internal/models"
"gorm.io/gorm"
)
func AddMigrations(db *gorm.DB) {
db.AutoMigrate(&models.Post{}, &models.MediaAttachment{}, &models.Account{})
}
internal/helpers/SetupAppContext.go
package helpers
import (
"CatsOfMastodonBotGo/internal"
"CatsOfMastodonBotGo/internal/auth"
"CatsOfMastodonBotGo/internal/services"
"log"
"os"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
func SetupAppContext() *internal.AppContext {
// Setup AppContext
instance := os.Getenv("CAOM_INSTANCE")
if instance == "" {
instance = "https://mstdn.party"
}
tag := os.Getenv("CAOM_TAG")
if tag == "" {
tag = "catsofmastodon"
}
adminPassword := os.Getenv("CAOM_ADMIN_PASSWORD")
if adminPassword == "" {
log.Println("No admin password provided, using default password 'catsaregood'")
adminPassword = "catsaregood"
}
// Jwt params
secret := os.Getenv("CAOM_JWT_SECRET")
if secret == "" {
log.Fatal("No jwt secret provided, using default secret 'secret'")
}
issuer := os.Getenv("CAOM_JWT_ISSUER")
if issuer == "" {
log.Println("No jwt issuer provided, using default issuer 'CatsOfMastodonBotGo'")
issuer = "CatsOfMastodonBotGo"
}
audience := os.Getenv("CAOM_JWT_AUDIENCE")
if audience == "" {
log.Println("No jwt audience provided, using default audience 'CatsOfMastodonBotGo'")
audience = "CatsOfMastodonBotGo"
}
// Setup database
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{Logger: logger.Default.LogMode(logger.Warn)})
if err != nil {
panic("failed to connect database")
}
AddMigrations(db)
//Setup PostService
var postService = services.NewPostService(db)
// Setup Jwt
var jwt = auth.NewJwtTokenGenerator(secret, issuer, audience)
// Inititlize AppContext
var appContext = &internal.AppContext{
Db: db,
PostService: postService,
Jwt: jwt,
AdminPassword: adminPassword,
Instance: instance,
Tag: tag,
}
return appContext
}
internal/models/account.go
package models
type Account struct {
AccId string `json:"id" gorm:"primaryKey"`
Username string `json:"username"`
Acct string `json:"acct"`
DisplayName string `json:"display_name"`
IsBot bool `json:"bot"`
Url string `json:"url"`
AvatarStatic string `json:"avatar_static"`
}
internal/models/mediaAttachment.go
package models
type MediaAttachment struct {
ID string `json:"id" gorm:"primaryKey"`
Type string `json:"type"`
Url string `json:"url"`
PreviewUrl string `json:"preview_url"`
RemoteUrl string `json:"remote_url"`
PostID string // Foreign key to Post
Approved bool `json:"approved"`
Rejected bool `json:"rejected"`
}
internal/models/post.go
package models
type Post struct {
ID string `json:"id" gorm:"primaryKey"`
Url string `json:"url"`
AccountID string // Foreign key field (must match Account.AccId)
Account Account `json:"account" gorm:"foreignKey:AccountID;references:AccId"`
Attachments []MediaAttachment `json:"media_attachments" gorm:"foreignKey:PostID;references:ID"`
}
internal/models/requestModels/approveMedia.go
package requestmodels
type ApproveMediaInput struct {
MediaId string `json:"mediaId" binding:"required"`
}
internal/models/requestModels/login.go
package requestmodels
type LoginInput struct {
Password string `json:"password" binding:"required"`
}
internal/models/requestModels/rejectMedia.go
package requestmodels
type RejectMediaInput struct {
MediaId string `json:"mediaId" binding:"required"`
}
internal/server/router.go
package server
import (
"CatsOfMastodonBotGo/internal"
handlers_admin "CatsOfMastodonBotGo/internal/web/handlers/admin"
handlers_api "CatsOfMastodonBotGo/internal/web/handlers/api"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
)
func SetupRouter(appContext *internal.AppContext) *gin.Engine {
r := gin.Default()
r.Use(cors.New(cors.Config{
AllowOrigins: []string{"https://extra-mama-chiz.surge.sh"}, // Just for test
AllowMethods: []string{"POST", "GET", "OPTIONS"},
AllowHeaders: []string{"Origin", "Content-Type", "Authorization"},
AllowCredentials: true,
}))
adminDashboardHandler := handlers_admin.NewAdminDashboardHandler(appContext)
apiHandler := handlers_api.NewApiEndpointHandler(appContext)
admin := r.Group("/admin")
// My man, this is done way more efficient and fast in .NET, specially the authentication part
admin.POST("/login", adminDashboardHandler.Login)
admin.GET("/getmedia", appContext.Jwt.GinMiddleware(), adminDashboardHandler.GetMedia)
admin.POST("/approve", appContext.Jwt.GinMiddleware(), adminDashboardHandler.ApproveMedia)
admin.POST("/reject", appContext.Jwt.GinMiddleware(), adminDashboardHandler.RejectMedia)
api := r.Group("/api")
api.GET("/post/random", apiHandler.GetRandomPost)
return r
}
internal/services/postService.go
package services
import (
"CatsOfMastodonBotGo/internal/models"
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
type PostService struct {
db *gorm.DB
}
// Constructor
func NewPostService(db *gorm.DB) *PostService {
return &PostService{db: db}
}
func (*PostService) GetPostsFromApi(ctx context.Context, tag string, instance string) (error, []models.Post) {
var requestUrl = instance + "/api/v1/timelines/tag/" + tag + "?limit=40"
req, err := http.NewRequestWithContext(ctx, "GET", requestUrl, nil)
if err != nil {
return err, nil
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err, nil
}
if resp.StatusCode != 200 || strings.Split(strings.ToLower(resp.Header.Get("Content-Type")), ";")[0] != "application/json" {
return fmt.Errorf("Status code:", resp.StatusCode, " Content-Type:", resp.Header.Get("Content-Type")), nil
}
var posts []models.Post = nil
err = json.NewDecoder(resp.Body).Decode(&posts)
if err != nil {
return err, nil
}
// defer: it basically means "do this later when the function returns"
defer resp.Body.Close()
if posts == nil {
return fmt.Errorf("no posts found for tag %s on instance %s", tag, instance), nil
}
return nil, posts
}
func (ps *PostService) GetExistingPostIds() []string {
var existingPostIds []string
ps.db.Model(&models.Post{}).Pluck("id", &existingPostIds)
return existingPostIds
}
func (ps *PostService) GetExistingAccountIds() []string {
var existingAccountIds []string
ps.db.Model(&models.Account{}).Pluck("acc_id", &existingAccountIds)
return existingAccountIds
}
func (*PostService) GetNewPosts(existingPostIds []string, posts []models.Post) []models.Post {
var newPosts []models.Post = nil
for _, post := range posts {
if !arrayContains(existingPostIds, post.ID) && len(post.Attachments) > 0 && !post.Account.IsBot {
var allImageMedia = true
for _, attachment := range post.Attachments {
if attachment.Type != "image" {
allImageMedia = false
break
}
} // Inefficient but anyways
if allImageMedia {
newPosts = append(newPosts, post)
}
}
}
return newPosts
}
func (*PostService) GetNewAccounts(existingAccountIds []string, posts []models.Post) []models.Account {
var newAccounts []models.Account = nil
for _, post := range posts {
if !arrayContains(existingAccountIds, post.Account.AccId) {
newAccounts = append(newAccounts, post.Account)
}
}
return newAccounts
}
func (ps *PostService) InsertNewPosts(newPosts []models.Post) int {
return int(ps.db.Create(&newPosts).RowsAffected)
}
func (ps *PostService) InsertNewAccounts(newAccounts []models.Account) int {
return int(ps.db.Clauses(clause.OnConflict{UpdateAll: true}).Create(&newAccounts).RowsAffected)
}
// From this point on, its for the api endpoints
func (ps *PostService) GetRandomPost() models.Post {
var post models.Post
ps.db.
Preload("Account").
Preload("Attachments", "approved = ?", true).
Order("RANDOM()").
First(&post)
if len(post.Attachments) > 0 {
post.Attachments = []models.MediaAttachment{post.Attachments[0]}
}
return post
}
func (ps *PostService) ApproveMedia(mediaId string) bool {
return ps.db.Model(&models.MediaAttachment{}).
Where("id = ?", mediaId).
Update("approved", true).RowsAffected > 0
}
func (ps *PostService) RejectMedia(mediaId string) bool {
return ps.db.Model(&models.MediaAttachment{}).
Where("id = ?", mediaId).
Update("rejected", true).RowsAffected > 0
}
// Get a post which approve and rejet are false (For admin panel)
func (ps *PostService) GetMedia() models.MediaAttachment {
var media models.MediaAttachment
ps.db.Model(&models.MediaAttachment{}).
Where("approved = ?", false).
Where("rejected = ?", false).
Order("RANDOM()").
First(&media)
return media
}
func arrayContains(arr []string, str string) bool {
for _, a := range arr {
if a == str {
return true
}
}
return false
}
internal/web/handlers/admin/adminDash.go
package handlers_admin
import (
"CatsOfMastodonBotGo/internal"
"net/http"
requestmodels "CatsOfMastodonBotGo/internal/models/requestModels"
"github.com/gin-gonic/gin"
)
type AdminDashboardHandler struct {
AppContext *internal.AppContext
}
func NewAdminDashboardHandler(appContext *internal.AppContext) *AdminDashboardHandler {
return &AdminDashboardHandler{
AppContext: appContext,
}
}
func (appContext *AdminDashboardHandler) ApproveMedia(c *gin.Context) {
var input requestmodels.ApproveMediaInput
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if appContext.AppContext.PostService.ApproveMedia(input.MediaId) {
c.JSON(http.StatusOK, gin.H{"message": "Media approved successfully"})
} else {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to approve media"})
}
}
func (appContext *AdminDashboardHandler) RejectMedia(c *gin.Context) {
var input requestmodels.RejectMediaInput
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if appContext.AppContext.PostService.RejectMedia(input.MediaId) {
c.JSON(http.StatusOK, gin.H{"message": "Media rejected successfully"})
} else {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to reject media"})
}
}
func (appContext *AdminDashboardHandler) GetMedia(c *gin.Context) {
media := appContext.AppContext.PostService.GetMedia()
c.JSON(http.StatusOK, media)
}
func (appContext *AdminDashboardHandler) Login(c *gin.Context) {
var input requestmodels.LoginInput
// Validate data
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if input.Password == appContext.AppContext.AdminPassword { // Its more than enough for this project
token, err := appContext.AppContext.Jwt.GenerateToken(map[string]interface{}{"role": "admin"})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Token generation failed"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Login successful", "token": token})
} else {
c.JSON(401, gin.H{
"error": "wrong password",
})
return
}
}
internal/web/handlers/api/apiEndpoint.go
package handlers_api
import (
"CatsOfMastodonBotGo/internal"
"github.com/gin-gonic/gin"
)
type ApiEndpointHandler struct {
AppContext *internal.AppContext
}
func NewApiEndpointHandler(appContext *internal.AppContext) *ApiEndpointHandler {
return &ApiEndpointHandler{
AppContext: appContext,
}
}
func (appContext *ApiEndpointHandler) GetRandomPost(c *gin.Context) {
c.JSON(200,appContext.AppContext.PostService.GetRandomPost())
}
cmd/main.go
package main
import (
"CatsOfMastodonBotGo/internal/helpers"
"CatsOfMastodonBotGo/internal/models"
"CatsOfMastodonBotGo/internal/server"
"context"
"log"
"time"
)
func main() {
// Setup AppContext
var appContext = helpers.SetupAppContext()
ticker := time.NewTicker(10 * time.Minute)
runFetchPosts := func() {
// Get posts
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
var posts []models.Post = nil
err, posts := appContext.PostService.GetPostsFromApi(ctx, appContext.Tag, appContext.Instance)
if err != nil {
log.Println(err)
return
}
var existingPostIds = appContext.PostService.GetExistingPostIds()
var existingAccountIds = appContext.PostService.GetExistingAccountIds()
var newPosts = appContext.PostService.GetNewPosts(existingPostIds, posts)
var newAccounts = appContext.PostService.GetNewAccounts(existingAccountIds, newPosts)
// Save to database
log.Printf("Fetched %d posts; %d existing posts; %d new posts and %d new accounts\n", len(posts), len(existingPostIds), len(newPosts), len(newAccounts))
// Additional logging
if newAccounts != nil {
log.Printf("Inserted %d accounts\n", appContext.PostService.InsertNewAccounts(newAccounts))
}
if newPosts != nil {
log.Printf("Inserted %d posts\n", appContext.PostService.InsertNewPosts(newPosts))
}
}
go func() {
for range ticker.C {
runFetchPosts()
}
}()
// Run initial fetch on startup
go func() {
runFetchPosts()
}()
// https://seefnasrul.medium.com/create-your-first-go-rest-api-with-jwt-authentication-in-gin-framework-dbe5bda72817
r := server.SetupRouter(appContext)
err := r.Run(":8080")
if err != nil {
log.Fatal(err)
}
}
go.mod
module CatsOfMastodonBotGo
go 1.24.2
require (
github.com/appleboy/gin-jwt/v2 v2.10.3 // indirect
github.com/bytedance/sonic v1.13.2 // indirect
github.com/bytedance/sonic/loader v0.2.4 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
github.com/gin-contrib/cors v1.7.5 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/gin-gonic/gin v1.10.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.26.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v1.14.22 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
golang.org/x/arch v0.17.0 // indirect
golang.org/x/crypto v0.38.0 // indirect
golang.org/x/net v0.40.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.25.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gorm.io/driver/sqlite v1.5.7 // indirect
gorm.io/gorm v1.26.1 // indirect
)
go.sum
github.com/appleboy/gin-jwt/v2 v2.10.3 h1:KNcPC+XPRNpuoBh+j+rgs5bQxN+SwG/0tHbIqpRoBGc=
github.com/appleboy/gin-jwt/v2 v2.10.3/go.mod h1:LDUaQ8mF2W6LyXIbd5wqlV2SFebuyYs4RDwqMNgpsp8=
github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ=
github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
github.com/gin-contrib/cors v1.7.5 h1:cXC9SmofOrRg0w9PigwGlHG3ztswH6bqq4vJVXnvYMk=
github.com/gin-contrib/cors v1.7.5/go.mod h1:4q3yi7xBEDDWKapjT2o1V7mScKDDr8k+jZ0fSquGoy0=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
golang.org/x/arch v0.17.0 h1:4O3dfLzd+lQewptAHqjewQZQDyEdejz3VwgeYwkZneU=
golang.org/x/arch v0.17.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/sqlite v1.5.7 h1:8NvsrhP0ifM7LX9G4zPB97NwovUakUxc+2V2uuf3Z1I=
gorm.io/driver/sqlite v1.5.7/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4=
gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde h1:9DShaph9qhkIYw7QF91I/ynrr4cOO2PZra2PFD7Mfeg=
gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
gorm.io/gorm v1.26.1 h1:ghB2gUI9FkS46luZtn6DLZ0f6ooBJ5IbVej2ENFDjRw=
gorm.io/gorm v1.26.1/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=