Compare commits

...

14 Commits

18 changed files with 107 additions and 34 deletions

View File

@@ -22,7 +22,7 @@ docker-build:
# All branches are tagged with $DOCKER_IMAGE_NAME (defaults to commit ref slug)
# Default branch is also tagged with `latest`
script:
- docker build --pull -t "$DOCKER_IMAGE_NAME" .
- 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

View File

@@ -21,6 +21,9 @@ func main() {
services.InitPostService()
// Not needed but anyways
services.InitImgKitHelper()
ticker := time.NewTicker(10 * time.Minute)
runFetchPosts := func() {

View File

@@ -22,6 +22,8 @@ type config struct {
DBUser string
DBPassword string
DBName string
ImageKitId string
}
var Config *config
@@ -29,7 +31,7 @@ var Config *config
func Load() *config {
err := godotenv.Load()
if err != nil {
slog.Error("Error loading .env file - Using environment variables instead")
slog.Warn("Error loading .env file - Using environment variables instead")
}
// Get mastodon instance
@@ -65,7 +67,6 @@ func Load() *config {
audience = "CatsOfMastodonBotGo"
}
dbEngine := os.Getenv("CAOM_DB_ENGINE")
dbHost := os.Getenv("CAOM_DB_HOST")
dbPort := os.Getenv("CAOM_DB_PORT")
@@ -82,6 +83,11 @@ func Load() *config {
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,
@@ -98,6 +104,8 @@ func Load() *config {
DBUser: dbUser,
DBPassword: dbPassword,
DBName: dbName,
ImageKitId: imageKitId,
}
return appContext

View File

@@ -40,6 +40,13 @@ func Connect() (*gorm.DB, error) {
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

View File

@@ -5,11 +5,11 @@ type Post struct {
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"`
Attachments []MediaAttachment `json:"media_attachments" gorm:"foreignKey:PostID;references:ID"`
}
type Account struct {
AccId string `json:"id" gorm:"primaryKey;index"`
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"`
@@ -19,12 +19,12 @@ type Account struct {
}
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 `gorm:"index:idx_post_approved,post_id;index:idx_post_id"`
Approved bool `json:"approved" gorm:"index:idx_post_approved"`
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

@@ -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"
@@ -105,15 +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).
Where("exists (select 1 from media_attachments where post_id = posts.id and approved = ?)", true).
Order("RANDOM()").
First(&post)
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,10 +152,14 @@ 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
}

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

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

View File

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

View File

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

View File

@@ -53,6 +53,8 @@ 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)
}

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

@@ -22,6 +22,6 @@ func (ps *EmbedCardHandler) GetEmbedCard(c *gin.Context) {
post := ps.PostService.GetRandomPost()
c.HTML(200, "home/embed.html", gin.H{
"postUrl": post.Url,
"imageUrl": post.Attachments[0].RemoteUrl,
"imageUrl": services.GetRemoteUrl(post.Attachments[0].RemoteUrl),
})
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

View File

@@ -13,7 +13,7 @@
<meta property="og:type" content="website" />
<meta name="twitter:card" content="summary_large_image" />
<script type="module" crossorigin src="/admin/assets/index-Do3iuUd_.js"></script>
<script type="module" crossorigin src="/admin/assets/index-JxecVd-K.js"></script>
<link rel="stylesheet" crossorigin href="/admin/assets/index-DShmOgsI.css">
</head>

View File

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