Compare commits

...

3 Commits

15 changed files with 199 additions and 68 deletions

View File

@@ -49,6 +49,7 @@ func main() {
runFetchPosts()
}
}()
// Run initial fetch on startup
go func() {
runFetchPosts()

1
go.mod
View File

@@ -9,6 +9,7 @@ require (
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

2
go.sum
View File

@@ -13,6 +13,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
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=

View File

@@ -1,6 +1,7 @@
package internal
import (
"CatsOfMastodonBotGo/internal/auth"
"CatsOfMastodonBotGo/internal/services"
"gorm.io/gorm"
@@ -10,6 +11,7 @@ import (
type AppContext struct {
Db *gorm.DB
PostService *services.PostService
Jwt *auth.JwtTokenGenerator
AdminPassword string
Instance string
Tag string

View File

@@ -1,11 +0,0 @@
package internal
import (
// "github.com/gin-gonic/gin"
// "gorm.io/gorm"
)
// type AppWebContext struct {
// Db *gorm.DB
// GinEngine *gin.Engine
// }

View File

@@ -1,2 +1,69 @@
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()
}
}

View File

@@ -2,6 +2,7 @@ package helpers
import (
"CatsOfMastodonBotGo/internal"
"CatsOfMastodonBotGo/internal/auth"
"CatsOfMastodonBotGo/internal/services"
"log"
"os"
@@ -27,6 +28,22 @@ func SetupAppContext() *internal.AppContext {
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 {
@@ -37,10 +54,15 @@ func SetupAppContext() *internal.AppContext {
//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,
}

View File

@@ -7,7 +7,7 @@ import (
)
func AddMigrations(db *gorm.DB) {
db.AutoMigrate(&models.Post{}, &models.MediaAttachment{}, &models.Account{}, models.ComUser{})
db.AutoMigrate(&models.Post{}, &models.MediaAttachment{}, &models.Account{})
}

View File

@@ -1,10 +0,0 @@
package models
// Funny you are
type ComUser struct {
Id string
Username string
Password string
Email string
IsVerified bool
}

View File

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

View File

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

View File

@@ -4,31 +4,35 @@ import (
"CatsOfMastodonBotGo/internal"
handlers_admin "CatsOfMastodonBotGo/internal/web/handlers/admin"
handlers_api "CatsOfMastodonBotGo/internal/web/handlers/api"
handlers_home "CatsOfMastodonBotGo/internal/web/handlers/home"
"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)
homePageHandler := handlers_home.NewMainPageHandler(appContext)
apiHandler := handlers_api.NewApiEndpointHandler(appContext)
admin := r.Group("/admin")
admin.GET("/", adminDashboardHandler.AdminHomePage)
// My man, this is done way more efficient and fast in .NET, specially the authentication part
admin.POST("/login", adminDashboardHandler.Login)
admin.POST("/approve", adminDashboardHandler.ApproveMedia)
admin.POST("/reject", adminDashboardHandler.RejectMedia)
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)
r.GET("/", homePageHandler.HomePageHandler)
return r
}

View File

@@ -60,11 +60,22 @@ func (ps *PostService) GetExistingAccountIds() []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 {
newPosts = append(newPosts, post)
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
@@ -88,16 +99,45 @@ 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").
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 {

View File

@@ -19,37 +19,61 @@ func NewAdminDashboardHandler(appContext *internal.AppContext) *AdminDashboardHa
}
}
func (appContext *AdminDashboardHandler) AdminHomePage(c *gin.Context) {
c.JSON(200, gin.H{
"YouAreOn": "AdminDashboardHomePage",
})
}
func (appContext *AdminDashboardHandler) ApproveMedia(c *gin.Context) {
c.JSON(200, gin.H{
"YouAreOn": "ApproveMedia",
})
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) {
c.JSON(200, gin.H{
"YouAreOn": "RejectMedia",
})
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 {
c.JSON(200, gin.H{
"YouAreOn": "AdminDashboardHomePage",
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
}
}

View File

@@ -1,21 +0,0 @@
package handlers_home
import (
"CatsOfMastodonBotGo/internal"
"github.com/gin-gonic/gin"
)
type MainPageHandler struct {
AppContext *internal.AppContext
}
func NewMainPageHandler(appContext *internal.AppContext) *MainPageHandler {
return &MainPageHandler{
AppContext: appContext,
}
}
func (appContext *MainPageHandler) HomePageHandler(c *gin.Context) {
c.Data(200, "text/html; charset=utf-8", []byte(`<p>Welcome to CatsOfMastodonBotGo - <a href="https://git.mahdium.ir/mahdium/CatsOfMastodon">The main project</a>, writen in C# is available at that link - This is only my attempt to port it to Go for learning purposes</p>`))
}