⬆️ Upgrade to support the latest version Hydrogen Project standard

This commit is contained in:
2024-06-22 17:29:53 +08:00
parent 52c864b11f
commit c79c8d3618
45 changed files with 538 additions and 279 deletions

View File

@ -0,0 +1,25 @@
package database
import (
"git.solsynth.dev/hydrogen/interactive/pkg/internal/models"
"gorm.io/gorm"
)
var AutoMaintainRange = []any{
&models.Account{},
&models.Realm{},
&models.Category{},
&models.Tag{},
&models.Post{},
&models.Reaction{},
}
func RunMigration(source *gorm.DB) error {
if err := source.AutoMigrate(
AutoMaintainRange...,
); err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,28 @@
package database
import (
"github.com/rs/zerolog/log"
"github.com/samber/lo"
"github.com/spf13/viper"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"gorm.io/gorm/schema"
)
var C *gorm.DB
func NewSource() error {
var err error
dialector := postgres.Open(viper.GetString("database.dsn"))
C, err = gorm.Open(dialector, &gorm.Config{NamingStrategy: schema.NamingStrategy{
TablePrefix: viper.GetString("database.prefix"),
}, Logger: logger.New(&log.Logger, logger.Config{
Colorful: true,
IgnoreRecordNotFoundError: true,
LogLevel: lo.Ternary(viper.GetBool("debug.database"), logger.Info, logger.Silent),
})})
return err
}

6
pkg/internal/embed.go Normal file
View File

@ -0,0 +1,6 @@
package pkg
import "embed"
//go:embed views/*
var FS embed.FS

View File

@ -0,0 +1,12 @@
package gap
import (
"git.solsynth.dev/hydrogen/passport/pkg/hyper"
"github.com/spf13/viper"
)
var H *hyper.HyperConn
func NewHyperClient() {
H = hyper.NewHyperConn(viper.GetString("consul.addr"))
}

15
pkg/internal/gap/net.go Normal file
View File

@ -0,0 +1,15 @@
package gap
import "net"
func GetOutboundIP() (net.IP, error) {
conn, err := net.Dial("udp", "1.1.1.1:80")
if err != nil {
return nil, err
} else {
defer conn.Close()
}
localAddr := conn.LocalAddr().(*net.UDPAddr)
return localAddr.IP, nil
}

View File

@ -0,0 +1,40 @@
package gap
import (
"fmt"
"strconv"
"strings"
"github.com/hashicorp/consul/api"
"github.com/spf13/viper"
)
func Register() error {
cfg := api.DefaultConfig()
cfg.Address = viper.GetString("consul.addr")
client, err := api.NewClient(cfg)
if err != nil {
return err
}
httpBind := strings.SplitN(viper.GetString("bind"), ":", 2)
grpcBind := strings.SplitN(viper.GetString("grpc_bind"), ":", 2)
outboundIp, _ := GetOutboundIP()
port, _ := strconv.Atoi(httpBind[1])
registration := new(api.AgentServiceRegistration)
registration.ID = viper.GetString("id")
registration.Name = "Hydrogen.Interactive"
registration.Address = outboundIp.String()
registration.Port = port
registration.Check = &api.AgentServiceCheck{
GRPC: fmt.Sprintf("%s:%s", outboundIp, grpcBind[1]),
Timeout: "5s",
Interval: "1m",
DeregisterCriticalServiceAfter: "3m",
}
return client.Agent().ServiceRegister(registration)
}

View File

@ -0,0 +1,26 @@
package grpc
import (
"context"
health "google.golang.org/grpc/health/grpc_health_v1"
"time"
)
func (v *Server) Check(ctx context.Context, request *health.HealthCheckRequest) (*health.HealthCheckResponse, error) {
return &health.HealthCheckResponse{
Status: health.HealthCheckResponse_SERVING,
}, nil
}
func (v *Server) Watch(request *health.HealthCheckRequest, server health.Health_WatchServer) error {
for {
if server.Send(&health.HealthCheckResponse{
Status: health.HealthCheckResponse_SERVING,
}) != nil {
break
}
time.Sleep(1000 * time.Millisecond)
}
return nil
}

View File

@ -0,0 +1,31 @@
package grpc
import (
"github.com/spf13/viper"
"google.golang.org/grpc"
health "google.golang.org/grpc/health/grpc_health_v1"
"google.golang.org/grpc/reflection"
"net"
)
type Server struct {
}
var S *grpc.Server
func NewGRPC() {
S = grpc.NewServer()
health.RegisterHealthServer(S, &Server{})
reflection.Register(S)
}
func ListenGRPC() error {
listener, err := net.Listen("tcp", viper.GetString("grpc_bind"))
if err != nil {
return err
}
return S.Serve(listener)
}

5
pkg/internal/meta.go Normal file
View File

@ -0,0 +1,5 @@
package pkg
const (
AppVersion = "1.0.0"
)

View File

@ -0,0 +1,19 @@
package models
// Account profiles basically fetched from Hydrogen.Passport
// But cache at here for better usage
// At the same time this model can make relations between local models
type Account struct {
BaseModel
Name string `json:"name"`
Nick string `json:"nick"`
Avatar string `json:"avatar"`
Banner string `json:"banner"`
Description string `json:"description"`
EmailAddress string `json:"email_address"`
PowerLevel int `json:"power_level"`
Posts []Post `json:"posts" gorm:"foreignKey:AuthorID"`
Reactions []Reaction `json:"reactions"`
ExternalID uint `json:"external_id"`
}

View File

@ -0,0 +1,17 @@
package models
import (
"time"
"gorm.io/datatypes"
"gorm.io/gorm"
)
type JSONMap = datatypes.JSONType[map[string]any]
type BaseModel struct {
ID uint `json:"id" gorm:"primaryKey"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `json:"deleted_at" gorm:"index"`
}

View File

@ -0,0 +1,19 @@
package models
type Tag struct {
BaseModel
Alias string `json:"alias" gorm:"uniqueIndex" validate:"lowercase,alphanum,min=4,max=24"`
Name string `json:"name"`
Description string `json:"description"`
Posts []Post `json:"posts" gorm:"many2many:post_tags"`
}
type Category struct {
BaseModel
Alias string `json:"alias" gorm:"uniqueIndex" validate:"lowercase,alphanum,min=4,max=24"`
Name string `json:"name"`
Description string `json:"description"`
Posts []Post `json:"posts" gorm:"many2many:post_categories"`
}

View File

@ -0,0 +1,42 @@
package models
import (
"gorm.io/datatypes"
"time"
)
type PostReactInfo struct {
PostID uint `json:"post_id"`
LikeCount int64 `json:"like_count"`
DislikeCount int64 `json:"dislike_count"`
ReplyCount int64 `json:"reply_count"`
RepostCount int64 `json:"repost_count"`
}
type Post struct {
BaseModel
Alias string `json:"alias" gorm:"uniqueIndex"`
Content string `json:"content"`
Tags []Tag `json:"tags" gorm:"many2many:post_tags"`
Categories []Category `json:"categories" gorm:"many2many:post_categories"`
Reactions []Reaction `json:"reactions"`
Replies []Post `json:"replies" gorm:"foreignKey:ReplyID"`
Attachments datatypes.JSONSlice[uint] `json:"attachments"`
ReplyID *uint `json:"reply_id"`
RepostID *uint `json:"repost_id"`
RealmID *uint `json:"realm_id"`
ReplyTo *Post `json:"reply_to" gorm:"foreignKey:ReplyID"`
RepostTo *Post `json:"repost_to" gorm:"foreignKey:RepostID"`
Realm *Realm `json:"realm"`
PublishedAt *time.Time `json:"published_at"`
AuthorID uint `json:"author_id"`
Author Account `json:"author"`
// Dynamic Calculated Values
ReplyCount int64 `json:"reply_count"`
ReactionCount int64 `json:"reaction_count"`
ReactionList map[string]int64 `json:"reaction_list" gorm:"-"`
}

View File

@ -0,0 +1,25 @@
package models
import (
"time"
)
type ReactionAttitude = uint8
const (
AttitudeNeutral = ReactionAttitude(iota)
AttitudePositive
AttitudeNegative
)
type Reaction struct {
ID uint `json:"id" gorm:"primaryKey"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Symbol string `json:"symbol"`
Attitude ReactionAttitude `json:"attitude"`
PostID *uint `json:"post_id"`
AccountID uint `json:"account_id"`
}

View File

@ -0,0 +1,15 @@
package models
// Realm profiles basically fetched from Hydrogen.Passport
// But cache at here for better usage and database relations
type Realm struct {
BaseModel
Alias string `json:"alias"`
Name string `json:"name"`
Description string `json:"description"`
Posts []Post `json:"posts"`
IsPublic bool `json:"is_public"`
IsCommunity bool `json:"is_community"`
ExternalID uint `json:"external_id"`
}

View File

@ -0,0 +1,100 @@
package api
import (
"git.solsynth.dev/hydrogen/interactive/pkg/internal/gap"
"git.solsynth.dev/hydrogen/interactive/pkg/internal/models"
"git.solsynth.dev/hydrogen/interactive/pkg/internal/server/exts"
"git.solsynth.dev/hydrogen/interactive/pkg/internal/services"
"github.com/gofiber/fiber/v2"
)
func listCategories(c *fiber.Ctx) error {
categories, err := services.ListCategory()
if err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
}
return c.JSON(categories)
}
func newCategory(c *fiber.Ctx) error {
if err := gap.H.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("user").(models.Account)
if user.PowerLevel <= 55 {
return fiber.NewError(fiber.StatusForbidden, "require power level 55 to create categories")
}
var data struct {
Alias string `json:"alias" validate:"required"`
Name string `json:"name" validate:"required"`
Description string `json:"description"`
}
if err := exts.BindAndValidate(c, &data); err != nil {
return err
}
category, err := services.NewCategory(data.Alias, data.Name, data.Description)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
return c.JSON(category)
}
func editCategory(c *fiber.Ctx) error {
if err := gap.H.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("user").(models.Account)
if user.PowerLevel <= 55 {
return fiber.NewError(fiber.StatusForbidden, "require power level 55 to edit categories")
}
id, _ := c.ParamsInt("categoryId", 0)
category, err := services.GetCategoryWithID(uint(id))
if err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
}
var data struct {
Alias string `json:"alias" validate:"required"`
Name string `json:"name" validate:"required"`
Description string `json:"description"`
}
if err := exts.BindAndValidate(c, &data); err != nil {
return err
}
category, err = services.EditCategory(category, data.Alias, data.Name, data.Description)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
return c.JSON(category)
}
func deleteCategory(c *fiber.Ctx) error {
if err := gap.H.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("user").(models.Account)
if user.PowerLevel <= 55 {
return fiber.NewError(fiber.StatusForbidden, "require power level 55 to delete categories")
}
id, _ := c.ParamsInt("categoryId", 0)
category, err := services.GetCategoryWithID(uint(id))
if err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
}
if err := services.DeleteCategory(category); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
return c.JSON(category)
}

View File

@ -0,0 +1,49 @@
package api
import (
"git.solsynth.dev/hydrogen/interactive/pkg/internal/database"
"git.solsynth.dev/hydrogen/interactive/pkg/internal/models"
"git.solsynth.dev/hydrogen/interactive/pkg/internal/services"
"github.com/gofiber/fiber/v2"
)
func listFeed(c *fiber.Ctx) error {
take := c.QueryInt("take", 0)
offset := c.QueryInt("offset", 0)
realmId := c.QueryInt("realmId", 0)
tx := database.C
if realmId > 0 {
tx = services.FilterWithRealm(tx, uint(realmId))
}
if len(c.Query("authorId")) > 0 {
var author models.Account
if err := database.C.Where(&models.Account{Name: c.Query("authorId")}).First(&author).Error; err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
}
tx = tx.Where("author_id = ?", author.ID)
}
if len(c.Query("category")) > 0 {
tx = services.FilterPostWithCategory(tx, c.Query("category"))
}
if len(c.Query("tag")) > 0 {
tx = services.FilterPostWithTag(tx, c.Query("tag"))
}
count, err := services.CountPost(tx)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
items, err := services.ListPost(tx, take, offset)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
return c.JSON(fiber.Map{
"count": count,
"data": items,
})
}

View File

@ -0,0 +1,32 @@
package api
import (
"github.com/gofiber/fiber/v2"
)
func MapAPIs(app *fiber.App) {
api := app.Group("/api").Name("API")
{
api.Get("/users/me", getUserinfo)
api.Get("/users/:accountId", getOthersInfo)
api.Get("/feed", listFeed)
posts := api.Group("/posts").Name("Posts API")
{
posts.Get("/", listPost)
posts.Get("/:post", getPost)
posts.Post("/", createPost)
posts.Post("/:post/react", reactPost)
posts.Put("/:postId", editPost)
posts.Delete("/:postId", deletePost)
posts.Get("/:post/replies", listReplies)
}
api.Get("/categories", listCategories)
api.Post("/categories", newCategory)
api.Put("/categories/:categoryId", editCategory)
api.Delete("/categories/:categoryId", deleteCategory)
}
}

View File

@ -0,0 +1,256 @@
package api
import (
"fmt"
"git.solsynth.dev/hydrogen/interactive/pkg/internal/database"
"git.solsynth.dev/hydrogen/interactive/pkg/internal/gap"
"git.solsynth.dev/hydrogen/interactive/pkg/internal/models"
"git.solsynth.dev/hydrogen/interactive/pkg/internal/server/exts"
"git.solsynth.dev/hydrogen/interactive/pkg/internal/services"
"github.com/gofiber/fiber/v2"
"github.com/google/uuid"
"github.com/samber/lo"
"strings"
"time"
)
func getPost(c *fiber.Ctx) error {
alias := c.Params("post")
item, err := services.GetPostWithAlias(alias)
if err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
}
item.ReplyCount = services.CountPostReply(item.ID)
item.ReactionCount = services.CountPostReactions(item.ID)
item.ReactionList, err = services.ListPostReactions(item.ID)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
return c.JSON(item)
}
func listPost(c *fiber.Ctx) error {
take := c.QueryInt("take", 0)
offset := c.QueryInt("offset", 0)
realmId := c.QueryInt("realmId", 0)
tx := database.C
if realmId > 0 {
tx = services.FilterWithRealm(tx, uint(realmId))
}
if len(c.Query("authorId")) > 0 {
var author models.Account
if err := database.C.Where(&models.Account{Name: c.Query("authorId")}).First(&author).Error; err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
}
tx = tx.Where("author_id = ?", author.ID)
}
if len(c.Query("category")) > 0 {
tx = services.FilterPostWithCategory(tx, c.Query("category"))
}
if len(c.Query("tag")) > 0 {
tx = services.FilterPostWithTag(tx, c.Query("tag"))
}
count, err := services.CountPost(tx)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
items, err := services.ListPost(tx, take, offset)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
return c.JSON(fiber.Map{
"count": count,
"data": items,
})
}
func createPost(c *fiber.Ctx) error {
if err := gap.H.EnsureGrantedPerm(c, "CreateInteractivePost", true); err != nil {
return err
}
user := c.Locals("user").(models.Account)
var data struct {
Alias string `json:"alias" form:"alias"`
Content string `json:"content" form:"content" validate:"required,max=4096"`
Tags []models.Tag `json:"tags" form:"tags"`
Categories []models.Category `json:"categories" form:"categories"`
Attachments []uint `json:"attachments" form:"attachments"`
PublishedAt *time.Time `json:"published_at" form:"published_at"`
RealmAlias string `json:"realm" form:"realm"`
ReplyTo *uint `json:"reply_to" form:"reply_to"`
RepostTo *uint `json:"repost_to" form:"repost_to"`
}
if err := exts.BindAndValidate(c, &data); err != nil {
return err
} else if len(data.Alias) == 0 {
data.Alias = strings.ReplaceAll(uuid.NewString(), "-", "")
}
for _, attachment := range data.Attachments {
if !services.CheckAttachmentByIDExists(attachment, "i.attachment") {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("attachment %d not found", attachment))
}
}
item := models.Post{
Alias: data.Alias,
PublishedAt: data.PublishedAt,
AuthorID: user.ID,
Tags: data.Tags,
Categories: data.Categories,
Attachments: data.Attachments,
Content: data.Content,
}
if data.ReplyTo != nil {
var replyTo models.Post
if err := database.C.Where("id = ?", data.ReplyTo).First(&replyTo).Error; err != nil {
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("related post was not found: %v", err))
} else {
item.ReplyID = &replyTo.ID
}
}
if data.RepostTo != nil {
var repostTo models.Post
if err := database.C.Where("id = ?", data.RepostTo).First(&repostTo).Error; err != nil {
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("related post was not found: %v", err))
} else {
item.RepostID = &repostTo.ID
}
}
if len(data.RealmAlias) > 0 {
if realm, err := services.GetRealmWithAlias(data.RealmAlias); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
} else if _, err := services.GetRealmMember(realm.ExternalID, user.ExternalID); err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("you aren't a part of related realm: %v", err))
} else {
item.RealmID = &realm.ID
}
}
item, err := services.NewPost(user, item)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
return c.JSON(item)
}
func editPost(c *fiber.Ctx) error {
id, _ := c.ParamsInt("postId", 0)
if err := gap.H.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("user").(models.Account)
var data struct {
Alias string `json:"alias" form:"alias" validate:"required"`
Content string `json:"content" form:"content" validate:"required,max=1024"`
PublishedAt *time.Time `json:"published_at" form:"published_at"`
Tags []models.Tag `json:"tags" form:"tags"`
Categories []models.Category `json:"categories" form:"categories"`
Attachments []uint `json:"attachments" form:"attachments"`
}
if err := exts.BindAndValidate(c, &data); err != nil {
return err
}
var item models.Post
if err := database.C.Where(models.Post{
BaseModel: models.BaseModel{ID: uint(id)},
AuthorID: user.ID,
}).First(&item).Error; err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
}
for _, attachment := range data.Attachments {
if !services.CheckAttachmentByIDExists(attachment, "i.attachment") {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("attachment %d not found", attachment))
}
}
item.Alias = data.Alias
item.Content = data.Content
item.PublishedAt = data.PublishedAt
item.Tags = data.Tags
item.Categories = data.Categories
item.Attachments = data.Attachments
if item, err := services.EditPost(item); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
} else {
return c.JSON(item)
}
}
func deletePost(c *fiber.Ctx) error {
if err := gap.H.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("user").(models.Account)
id, _ := c.ParamsInt("postId", 0)
var item models.Post
if err := database.C.Where(models.Post{
BaseModel: models.BaseModel{ID: uint(id)},
AuthorID: user.ID,
}).First(&item).Error; err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
}
if err := services.DeletePost(item); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
return c.SendStatus(fiber.StatusOK)
}
func reactPost(c *fiber.Ctx) error {
if err := gap.H.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("user").(models.Account)
var data struct {
Symbol string `json:"symbol" form:"symbol" validate:"required"`
Attitude models.ReactionAttitude `json:"attitude" form:"attitude"`
}
if err := exts.BindAndValidate(c, &data); err != nil {
return err
}
reaction := models.Reaction{
Symbol: data.Symbol,
Attitude: data.Attitude,
AccountID: user.ID,
}
alias := c.Params("post")
var res models.Post
if err := database.C.Where("alias = ?", alias).Select("id").First(&res).Error; err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("unable to find post to react: %v", err))
} else {
reaction.PostID = &res.ID
}
if positive, reaction, err := services.ReactPost(user, reaction); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
} else {
return c.Status(lo.Ternary(positive, fiber.StatusCreated, fiber.StatusNoContent)).JSON(reaction)
}
}

View File

@ -0,0 +1,52 @@
package api
import (
"fmt"
"git.solsynth.dev/hydrogen/interactive/pkg/internal/database"
"git.solsynth.dev/hydrogen/interactive/pkg/internal/models"
"git.solsynth.dev/hydrogen/interactive/pkg/internal/services"
"github.com/gofiber/fiber/v2"
)
func listReplies(c *fiber.Ctx) error {
take := c.QueryInt("take", 0)
offset := c.QueryInt("offset", 0)
tx := database.C
var post models.Post
if err := database.C.Where("alias = ?", c.Params("post")).First(&post).Error; err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("unable to find post: %v", err))
} else {
tx = services.FilterPostReply(tx, post.ID)
}
if len(c.Query("authorId")) > 0 {
var author models.Account
if err := database.C.Where(&models.Account{Name: c.Query("authorId")}).First(&author).Error; err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
}
tx = tx.Where("author_id = ?", author.ID)
}
if len(c.Query("category")) > 0 {
tx = services.FilterPostWithCategory(tx, c.Query("category"))
}
if len(c.Query("tag")) > 0 {
tx = services.FilterPostWithTag(tx, c.Query("tag"))
}
count, err := services.CountPost(tx)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
items, err := services.ListPost(tx, take, offset)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
return c.JSON(fiber.Map{
"count": count,
"data": items,
})
}

View File

@ -0,0 +1,37 @@
package api
import (
"git.solsynth.dev/hydrogen/interactive/pkg/internal/database"
"git.solsynth.dev/hydrogen/interactive/pkg/internal/gap"
"git.solsynth.dev/hydrogen/interactive/pkg/internal/models"
"github.com/gofiber/fiber/v2"
)
func getUserinfo(c *fiber.Ctx) error {
if err := gap.H.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("user").(models.Account)
var data models.Account
if err := database.C.
Where(&models.Account{BaseModel: models.BaseModel{ID: user.ID}}).
First(&data).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
return c.JSON(data)
}
func getOthersInfo(c *fiber.Ctx) error {
accountId := c.Params("accountId")
var data models.Account
if err := database.C.
Where(&models.Account{Name: accountId}).
First(&data).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
return c.JSON(data)
}

View File

@ -0,0 +1,19 @@
package exts
import (
"git.solsynth.dev/hydrogen/interactive/pkg/internal/services"
"git.solsynth.dev/hydrogen/passport/pkg/proto"
"github.com/gofiber/fiber/v2"
)
func LinkAccountMiddleware(c *fiber.Ctx) error {
if val, ok := c.Locals("p_user").(*proto.Userinfo); ok {
if account, err := services.LinkAccount(val); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
} else {
c.Locals("user", account)
}
}
return c.Next()
}

View File

@ -0,0 +1,18 @@
package exts
import (
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
)
var validation = validator.New(validator.WithRequiredStructEnabled())
func BindAndValidate(c *fiber.Ctx, out any) error {
if err := c.BodyParser(out); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
} else if err := validation.Struct(out); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
return nil
}

View File

@ -0,0 +1,84 @@
package server
import (
pkg "git.solsynth.dev/hydrogen/interactive/pkg/internal"
"git.solsynth.dev/hydrogen/interactive/pkg/internal/gap"
"git.solsynth.dev/hydrogen/interactive/pkg/internal/server/api"
"git.solsynth.dev/hydrogen/interactive/pkg/internal/server/exts"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/cors"
"github.com/gofiber/fiber/v2/middleware/favicon"
"github.com/gofiber/fiber/v2/middleware/idempotency"
"github.com/gofiber/fiber/v2/middleware/logger"
"github.com/gofiber/template/html/v2"
jsoniter "github.com/json-iterator/go"
"github.com/rs/zerolog/log"
"github.com/spf13/viper"
"net/http"
"strings"
)
var A *fiber.App
func NewServer() {
templates := html.NewFileSystem(http.FS(pkg.FS), ".gohtml")
A = fiber.New(fiber.Config{
DisableStartupMessage: true,
EnableIPValidation: true,
ServerHeader: "Hydrogen.Interactive",
AppName: "Hydrogen.Interactive",
ProxyHeader: fiber.HeaderXForwardedFor,
JSONEncoder: jsoniter.ConfigCompatibleWithStandardLibrary.Marshal,
JSONDecoder: jsoniter.ConfigCompatibleWithStandardLibrary.Unmarshal,
BodyLimit: 50 * 1024 * 1024,
EnablePrintRoutes: viper.GetBool("debug.print_routes"),
Views: templates,
ViewsLayout: "views/index",
})
A.Use(idempotency.New())
A.Use(cors.New(cors.Config{
AllowCredentials: true,
AllowMethods: strings.Join([]string{
fiber.MethodGet,
fiber.MethodPost,
fiber.MethodHead,
fiber.MethodOptions,
fiber.MethodPut,
fiber.MethodDelete,
fiber.MethodPatch,
}, ","),
AllowOriginsFunc: func(origin string) bool {
return true
},
}))
A.Use(logger.New(logger.Config{
Format: "${status} | ${latency} | ${method} ${path}\n",
Output: log.Logger,
}))
A.Use(gap.H.AuthMiddleware)
A.Use(exts.LinkAccountMiddleware)
A.Use(favicon.New(favicon.Config{
FileSystem: http.FS(pkg.FS),
File: "views/favicon.png",
URL: "/favicon.png",
}))
api.MapAPIs(A)
A.Get("/", func(c *fiber.Ctx) error {
return c.Render("views/open", fiber.Map{
"frontend": viper.GetString("frontend"),
})
})
}
func Listen() {
if err := A.Listen(viper.GetString("bind")); err != nil {
log.Fatal().Err(err).Msg("An error occurred when starting server...")
}
}

View File

@ -0,0 +1,64 @@
package services
import (
"context"
"git.solsynth.dev/hydrogen/interactive/pkg/internal/database"
"git.solsynth.dev/hydrogen/interactive/pkg/internal/gap"
"git.solsynth.dev/hydrogen/interactive/pkg/internal/models"
"git.solsynth.dev/hydrogen/passport/pkg/proto"
"github.com/rs/zerolog/log"
"github.com/spf13/viper"
"time"
)
func GetAccountFriend(userId, relatedId uint, status int) (*proto.FriendshipResponse, error) {
var user models.Account
if err := database.C.Where("id = ?", userId).First(&user).Error; err != nil {
return nil, err
}
var related models.Account
if err := database.C.Where("id = ?", relatedId).First(&related).Error; err != nil {
return nil, err
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
pc, err := gap.H.DiscoverServiceGRPC("Hydrogen.Passport")
if err != nil {
return nil, err
}
return proto.NewFriendshipsClient(pc).GetFriendship(ctx, &proto.FriendshipTwoSideLookupRequest{
AccountId: uint64(user.ExternalID),
RelatedId: uint64(related.ExternalID),
Status: uint32(status),
})
}
func NotifyPosterAccount(user models.Account, subject, content string, links ...*proto.NotifyLink) error {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
pc, err := gap.H.DiscoverServiceGRPC("Hydrogen.Passport")
if err != nil {
return err
}
_, err = proto.NewNotifyClient(pc).NotifyUser(ctx, &proto.NotifyRequest{
ClientId: viper.GetString("passport.client_id"),
ClientSecret: viper.GetString("passport.client_secret"),
Type: "interactive.feedback",
Subject: subject,
Content: content,
Links: links,
RecipientId: uint64(user.ExternalID),
IsRealtime: false,
IsForcePush: true,
})
if err != nil {
log.Warn().Err(err).Msg("An error occurred when notify account...")
} else {
log.Debug().Uint("eid", user.ExternalID).Msg("Notified account.")
}
return err
}

View File

@ -0,0 +1,20 @@
package services
import (
"context"
"git.solsynth.dev/hydrogen/interactive/pkg/internal/gap"
"git.solsynth.dev/hydrogen/paperclip/pkg/proto"
"github.com/samber/lo"
)
func CheckAttachmentByIDExists(id uint, usage string) bool {
pc, err := gap.H.DiscoverServiceGRPC("Hydrogen.Paperclip")
if err != nil {
return false
}
_, err = proto.NewAttachmentsClient(pc).CheckAttachmentExists(context.Background(), &proto.AttachmentLookupRequest{
Id: lo.ToPtr(uint64(id)),
Usage: &usage,
})
return err == nil
}

View File

@ -0,0 +1,51 @@
package services
import (
"errors"
"fmt"
"git.solsynth.dev/hydrogen/interactive/pkg/internal/database"
"git.solsynth.dev/hydrogen/interactive/pkg/internal/models"
"git.solsynth.dev/hydrogen/passport/pkg/proto"
"gorm.io/gorm"
"reflect"
)
func LinkAccount(userinfo *proto.Userinfo) (models.Account, error) {
var account models.Account
if userinfo == nil {
return account, fmt.Errorf("remote userinfo was not found")
}
if err := database.C.Where(&models.Account{
ExternalID: uint(userinfo.Id),
}).First(&account).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
account = models.Account{
Name: userinfo.Name,
Nick: userinfo.Nick,
Avatar: userinfo.Avatar,
Banner: userinfo.Banner,
Description: userinfo.GetDescription(),
EmailAddress: userinfo.Email,
PowerLevel: 0,
ExternalID: uint(userinfo.Id),
}
return account, database.C.Save(&account).Error
}
return account, err
}
prev := account
account.Name = userinfo.Name
account.Nick = userinfo.Nick
account.Avatar = userinfo.Avatar
account.Banner = userinfo.Banner
account.Description = userinfo.GetDescription()
account.EmailAddress = userinfo.Email
var err error
if !reflect.DeepEqual(prev, account) {
err = database.C.Save(&account).Error
}
return account, err
}

View File

@ -0,0 +1,77 @@
package services
import (
"errors"
"git.solsynth.dev/hydrogen/interactive/pkg/internal/database"
"git.solsynth.dev/hydrogen/interactive/pkg/internal/models"
"gorm.io/gorm"
)
func ListCategory() ([]models.Category, error) {
var categories []models.Category
err := database.C.Find(&categories).Error
return categories, err
}
func GetCategory(alias string) (models.Category, error) {
var category models.Category
if err := database.C.Where(models.Category{Alias: alias}).First(&category).Error; err != nil {
return category, err
}
return category, nil
}
func GetCategoryWithID(id uint) (models.Category, error) {
var category models.Category
if err := database.C.Where(models.Category{
BaseModel: models.BaseModel{ID: id},
}).First(&category).Error; err != nil {
return category, err
}
return category, nil
}
func NewCategory(alias, name, description string) (models.Category, error) {
category := models.Category{
Alias: alias,
Name: name,
Description: description,
}
err := database.C.Save(&category).Error
return category, err
}
func EditCategory(category models.Category, alias, name, description string) (models.Category, error) {
category.Alias = alias
category.Name = name
category.Description = description
err := database.C.Save(&category).Error
return category, err
}
func DeleteCategory(category models.Category) error {
return database.C.Delete(category).Error
}
func GetTagOrCreate(alias, name string) (models.Tag, error) {
var tag models.Tag
if err := database.C.Where(models.Category{Alias: alias}).First(&tag).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
tag = models.Tag{
Alias: alias,
Name: name,
}
err := database.C.Save(&tag).Error
return tag, err
}
return tag, err
}
return tag, nil
}

View File

@ -0,0 +1,23 @@
package services
import (
"git.solsynth.dev/hydrogen/interactive/pkg/internal/database"
"github.com/rs/zerolog/log"
"time"
)
func DoAutoDatabaseCleanup() {
deadline := time.Now().Add(60 * time.Minute)
log.Debug().Time("deadline", deadline).Msg("Now cleaning up entire database...")
var count int64
for _, model := range database.AutoMaintainRange {
tx := database.C.Unscoped().Delete(model, "deleted_at >= ?", deadline)
if tx.Error != nil {
log.Error().Err(tx.Error).Msg("An error occurred when running auth context cleanup...")
}
count += tx.RowsAffected
}
log.Debug().Int64("affected", count).Msg("Clean up entire database accomplished.")
}

View File

@ -0,0 +1,12 @@
package services
import "golang.org/x/crypto/bcrypt"
func HashPassword(raw string) string {
data, _ := bcrypt.GenerateFromPassword([]byte(raw), 12)
return string(data)
}
func VerifyPassword(text string, password string) bool {
return bcrypt.CompareHashAndPassword([]byte(password), []byte(text)) == nil
}

View File

@ -0,0 +1,81 @@
package services
import (
"fmt"
"github.com/gofiber/fiber/v2"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/spf13/viper"
)
type PayloadClaims struct {
jwt.RegisteredClaims
Type string `json:"typ"`
}
const (
JwtAccessType = "access"
JwtRefreshType = "refresh"
)
const (
CookieAccessKey = "passport_auth_key"
CookieRefreshKey = "passport_refresh_key"
)
func EncodeJwt(id string, typ, sub string, aud []string, exp time.Time) (string, error) {
tk := jwt.NewWithClaims(jwt.SigningMethodHS512, PayloadClaims{
jwt.RegisteredClaims{
Subject: sub,
Audience: aud,
Issuer: fmt.Sprintf("https://%s", viper.GetString("domain")),
ExpiresAt: jwt.NewNumericDate(exp),
NotBefore: jwt.NewNumericDate(time.Now()),
IssuedAt: jwt.NewNumericDate(time.Now()),
ID: id,
},
typ,
})
return tk.SignedString([]byte(viper.GetString("secret")))
}
func DecodeJwt(str string) (PayloadClaims, error) {
var claims PayloadClaims
tk, err := jwt.ParseWithClaims(str, &claims, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return []byte(viper.GetString("secret")), nil
})
if err != nil {
return claims, err
}
if data, ok := tk.Claims.(*PayloadClaims); ok {
return *data, nil
} else {
return claims, fmt.Errorf("unexpected token payload: not payload claims type")
}
}
func SetJwtCookieSet(c *fiber.Ctx, access, refresh string) {
c.Cookie(&fiber.Cookie{
Name: CookieAccessKey,
Value: access,
Domain: viper.GetString("security.cookie_domain"),
SameSite: viper.GetString("security.cookie_samesite"),
Expires: time.Now().Add(60 * time.Minute),
Path: "/",
})
c.Cookie(&fiber.Cookie{
Name: CookieRefreshKey,
Value: refresh,
Domain: viper.GetString("security.cookie_domain"),
SameSite: viper.GetString("security.cookie_samesite"),
Expires: time.Now().Add(24 * 30 * time.Hour),
Path: "/",
})
}

View File

@ -0,0 +1,51 @@
package services
import (
"crypto/tls"
"fmt"
"net/smtp"
"net/textproto"
"github.com/jordan-wright/email"
"github.com/spf13/viper"
)
func SendMail(target string, subject string, content string) error {
mail := &email.Email{
To: []string{target},
From: viper.GetString("mailer.name"),
Subject: subject,
Text: []byte(content),
Headers: textproto.MIMEHeader{},
}
return mail.SendWithTLS(
fmt.Sprintf("%s:%d", viper.GetString("mailer.smtp_host"), viper.GetInt("mailer.smtp_port")),
smtp.PlainAuth(
"",
viper.GetString("mailer.username"),
viper.GetString("mailer.password"),
viper.GetString("mailer.smtp_host"),
),
&tls.Config{ServerName: viper.GetString("mailer.smtp_host")},
)
}
func SendMailHTML(target string, subject string, content string) error {
mail := &email.Email{
To: []string{target},
From: viper.GetString("mailer.name"),
Subject: subject,
HTML: []byte(content),
Headers: textproto.MIMEHeader{},
}
return mail.SendWithTLS(
fmt.Sprintf("%s:%d", viper.GetString("mailer.smtp_host"), viper.GetInt("mailer.smtp_port")),
smtp.PlainAuth(
"",
viper.GetString("mailer.username"),
viper.GetString("mailer.password"),
viper.GetString("mailer.smtp_host"),
),
&tls.Config{ServerName: viper.GetString("mailer.smtp_host")},
)
}

View File

@ -0,0 +1,337 @@
package services
import (
"errors"
"fmt"
"time"
"git.solsynth.dev/hydrogen/interactive/pkg/internal/database"
"git.solsynth.dev/hydrogen/interactive/pkg/internal/models"
"git.solsynth.dev/hydrogen/passport/pkg/proto"
"github.com/rs/zerolog/log"
"github.com/samber/lo"
"github.com/spf13/viper"
"gorm.io/gorm"
)
func FilterPostWithCategory(tx *gorm.DB, alias string) *gorm.DB {
return tx.Joins("JOIN post_categories ON posts.id = post_categories.post_id").
Joins("JOIN post_categories ON post_categories.id = post_categories.category_id").
Where("post_categories.alias = ?", alias)
}
func FilterPostWithTag(tx *gorm.DB, alias string) *gorm.DB {
return tx.Joins("JOIN post_tags ON posts.id = post_tags.post_id").
Joins("JOIN post_tags ON post_tags.id = post_tags.category_id").
Where("post_tags.alias = ?", alias)
}
func FilterWithRealm(tx *gorm.DB, id uint) *gorm.DB {
if id > 0 {
return tx.Where("realm_id = ?", id)
} else {
return tx.Where("realm_id IS NULL")
}
}
func FilterPostReply(tx *gorm.DB, replyTo ...uint) *gorm.DB {
if len(replyTo) > 0 && replyTo[0] > 0 {
return tx.Where("reply_id = ?", replyTo[0])
} else {
return tx.Where("reply_id IS NULL")
}
}
func FilterPostWithPublishedAt(tx *gorm.DB, date time.Time) *gorm.DB {
return tx.Where("published_at <= ? OR published_at IS NULL", date)
}
func GetPostWithAlias(alias string, ignoreLimitation ...bool) (models.Post, error) {
tx := database.C
if len(ignoreLimitation) == 0 || !ignoreLimitation[0] {
tx = FilterPostWithPublishedAt(tx, time.Now())
}
var item models.Post
if err := tx.
Where("alias = ?", alias).
Preload("Author").
Preload("ReplyTo").
Preload("ReplyTo.Author").
Preload("RepostTo").
Preload("RepostTo.Author").
First(&item).Error; err != nil {
return item, err
}
return item, nil
}
func GetPost(id uint, ignoreLimitation ...bool) (models.Post, error) {
tx := database.C
if len(ignoreLimitation) == 0 || !ignoreLimitation[0] {
tx = FilterPostWithPublishedAt(tx, time.Now())
}
var item models.Post
if err := tx.
Where("id = ?", id).
Preload("Author").
Preload("ReplyTo").
Preload("ReplyTo.Author").
Preload("RepostTo").
Preload("RepostTo.Author").
First(&item).Error; err != nil {
return item, err
}
return item, nil
}
func CountPost(tx *gorm.DB) (int64, error) {
var count int64
if err := tx.Model(&models.Post{}).Count(&count).Error; err != nil {
return count, err
}
return count, nil
}
func CountPostReply(id uint) int64 {
var count int64
if err := database.C.Model(&models.Post{}).
Where("reply_id = ?", id).
Count(&count).Error; err != nil {
return 0
}
return count
}
func CountPostReactions(id uint) int64 {
var count int64
if err := database.C.Model(&models.Reaction{}).
Where("post_id = ?", id).
Count(&count).Error; err != nil {
return 0
}
return count
}
func ListPostReactions(id uint) (map[string]int64, error) {
var reactions []struct {
Symbol string
Count int64
}
if err := database.C.Model(&models.Reaction{}).
Select("symbol, COUNT(id) as count").
Where("post_id = ?", id).
Group("symbol").
Scan(&reactions).Error; err != nil {
return map[string]int64{}, err
}
return lo.SliceToMap(reactions, func(item struct {
Symbol string
Count int64
},
) (string, int64) {
return item.Symbol, item.Count
}), nil
}
func ListPost(tx *gorm.DB, take int, offset int, noReact ...bool) ([]*models.Post, error) {
if take > 20 {
take = 20
}
var items []*models.Post
if err := tx.
Limit(take).Offset(offset).
Order("created_at DESC").
Preload("Author").
Preload("ReplyTo").
Preload("ReplyTo.Author").
Preload("RepostTo").
Preload("RepostTo.Author").
Find(&items).Error; err != nil {
return items, err
}
idx := lo.Map(items, func(item *models.Post, index int) uint {
return item.ID
})
if len(noReact) <= 0 || !noReact[0] {
var reactions []struct {
PostID uint
Symbol string
Count int64
}
if err := database.C.Model(&models.Reaction{}).
Select("post_id, symbol, COUNT(id) as count").
Where("post_id IN (?)", idx).
Group("post_id, symbol").
Scan(&reactions).Error; err != nil {
return items, err
}
itemMap := lo.SliceToMap(items, func(item *models.Post) (uint, *models.Post) {
return item.ID, item
})
list := map[uint]map[string]int64{}
for _, info := range reactions {
if _, ok := list[info.PostID]; !ok {
list[info.PostID] = make(map[string]int64)
}
list[info.PostID][info.Symbol] = info.Count
}
for k, v := range list {
if post, ok := itemMap[k]; ok {
post.ReactionList = v
}
}
}
if len(noReact) <= 0 || !noReact[0] {
var replies []struct {
PostID uint
Count int64
}
if err := database.C.Model(&models.Post{}).
Select("id as post_id, COUNT(id) as count").
Where("reply_id IN (?)", idx).
Group("post_id").
Scan(&replies).Error; err != nil {
return items, err
}
itemMap := lo.SliceToMap(items, func(item *models.Post) (uint, *models.Post) {
return item.ID, item
})
list := map[uint]int64{}
for _, info := range replies {
list[info.PostID] = info.Count
}
for k, v := range list {
if post, ok := itemMap[k]; ok {
post.ReplyCount = v
}
}
}
return items, nil
}
func InitPostCategoriesAndTags(item models.Post) (models.Post, error) {
var err error
for idx, category := range item.Categories {
item.Categories[idx], err = GetCategory(category.Alias)
if err != nil {
return item, err
}
}
for idx, tag := range item.Tags {
item.Tags[idx], err = GetTagOrCreate(tag.Alias, tag.Name)
if err != nil {
return item, err
}
}
return item, nil
}
func NewPost(user models.Account, item models.Post) (models.Post, error) {
item, err := InitPostCategoriesAndTags(item)
if err != nil {
return item, err
}
if item.RealmID != nil {
_, err := GetRealmMember(*item.RealmID, user.ExternalID)
if err != nil {
return item, fmt.Errorf("you aren't a part of that realm: %v", err)
}
}
if err := database.C.Save(&item).Error; err != nil {
return item, err
}
// Notify the original poster its post has been replied
if item.ReplyID != nil {
var op models.Post
if err := database.C.
Where("id = ?", item.ReplyID).
Preload("Author").
First(&op).Error; err == nil {
if op.Author.ID != user.ID {
postUrl := fmt.Sprintf("https://%s/posts/%s", viper.GetString("domain"), item.Alias)
err := NotifyPosterAccount(
op.Author,
fmt.Sprintf("%s replied you", user.Nick),
fmt.Sprintf("%s (%s) replied your post #%s.", user.Nick, user.Name, op.Alias),
&proto.NotifyLink{Label: "Related post", Url: postUrl},
)
if err != nil {
log.Error().Err(err).Msg("An error occurred when notifying user...")
}
}
}
}
return item, nil
}
func EditPost(item models.Post) (models.Post, error) {
item, err := InitPostCategoriesAndTags(item)
if err != nil {
return item, err
}
err = database.C.Save(&item).Error
return item, err
}
func DeletePost(item models.Post) error {
return database.C.Delete(&item).Error
}
func ReactPost(user models.Account, reaction models.Reaction) (bool, models.Reaction, error) {
if err := database.C.Where(reaction).First(&reaction).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
var op models.Post
if err := database.C.
Where("id = ?", reaction.PostID).
Preload("Author").
First(&op).Error; err == nil {
if op.Author.ID != user.ID {
postUrl := fmt.Sprintf("https://%s/posts/%s", viper.GetString("domain"), op.Alias)
err := NotifyPosterAccount(
op.Author,
fmt.Sprintf("%s reacted your post", user.Nick),
fmt.Sprintf("%s (%s) reacted your post a %s", user.Nick, user.Name, reaction.Symbol),
&proto.NotifyLink{Label: "Related post", Url: postUrl},
)
if err != nil {
log.Error().Err(err).Msg("An error occurred when notifying user...")
}
}
}
return true, reaction, database.C.Save(&reaction).Error
} else {
return true, reaction, err
}
} else {
return false, reaction, database.C.Delete(&reaction).Error
}
}

View File

@ -0,0 +1,112 @@
package services
import (
"context"
"errors"
"fmt"
"git.solsynth.dev/hydrogen/interactive/pkg/internal/database"
"git.solsynth.dev/hydrogen/interactive/pkg/internal/gap"
"git.solsynth.dev/hydrogen/interactive/pkg/internal/models"
"git.solsynth.dev/hydrogen/passport/pkg/proto"
"github.com/samber/lo"
"gorm.io/gorm"
"reflect"
)
func GetRealm(id uint) (models.Realm, error) {
var realm models.Realm
pc, err := gap.H.DiscoverServiceGRPC("Hydrogen.Passport")
if err != nil {
return realm, err
}
response, err := proto.NewRealmsClient(pc).GetRealm(context.Background(), &proto.RealmLookupRequest{
Id: lo.ToPtr(uint64(id)),
})
if err != nil {
return realm, err
}
return LinkRealm(response)
}
func GetRealmWithAlias(alias string) (models.Realm, error) {
var realm models.Realm
pc, err := gap.H.DiscoverServiceGRPC("Hydrogen.Passport")
if err != nil {
return realm, err
}
response, err := proto.NewRealmsClient(pc).GetRealm(context.Background(), &proto.RealmLookupRequest{
Alias: &alias,
})
if err != nil {
return realm, err
}
return LinkRealm(response)
}
func GetRealmMember(realmId uint, userId uint) (*proto.RealmMemberResponse, error) {
pc, err := gap.H.DiscoverServiceGRPC("Hydrogen.Passport")
if err != nil {
return nil, err
}
response, err := proto.NewRealmsClient(pc).GetRealmMember(context.Background(), &proto.RealmMemberLookupRequest{
RealmId: uint64(realmId),
UserId: lo.ToPtr(uint64(userId)),
})
if err != nil {
return nil, err
} else {
return response, nil
}
}
func ListRealmMember(realmId uint) ([]*proto.RealmMemberResponse, error) {
pc, err := gap.H.DiscoverServiceGRPC("Hydrogen.Passport")
if err != nil {
return nil, err
}
response, err := proto.NewRealmsClient(pc).ListRealmMember(context.Background(), &proto.RealmMemberLookupRequest{
RealmId: uint64(realmId),
})
if err != nil {
return nil, err
} else {
return response.Data, nil
}
}
func LinkRealm(info *proto.RealmResponse) (models.Realm, error) {
var realm models.Realm
if info == nil {
return realm, fmt.Errorf("remote realm info was not found")
}
if err := database.C.Where(&models.Realm{
ExternalID: uint(info.Id),
}).First(&realm).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
realm = models.Realm{
Alias: info.Alias,
Name: info.Name,
Description: info.Description,
IsPublic: info.IsPublic,
IsCommunity: info.IsCommunity,
ExternalID: uint(info.Id),
}
return realm, database.C.Save(&realm).Error
}
return realm, err
}
prev := realm
realm.Alias = info.Alias
realm.Name = info.Name
realm.Description = info.Description
realm.IsPublic = info.IsPublic
realm.IsCommunity = info.IsCommunity
var err error
if !reflect.DeepEqual(prev, realm) {
err = database.C.Save(&realm).Error
}
return realm, err
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

View File

@ -0,0 +1,28 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<link rel="icon" type="image/png" href="favicon.png">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100..700;1,100..700&display=swap" rel="stylesheet">
<title>Hydrogen.Interactive</title>
<style>
html, body {
padding: 0;
margin: 0;
font-family: Roboto Mono, monospace;
}
</style>
</head>
<body>
{{embed}}
</body>
</html>

View File

@ -0,0 +1,60 @@
<div class="container">
<div>
<img src="/favicon.png" width="128" height="128" alt="Icon"/>
<p class="caption text-blinking">Launching Solian... 🚀</p>
<p class="description">
Hold on a second... <br/>
We are redirecting you to our application...
</p>
</div>
</div>
<script>
function redirect() {
window.location.href = {{ .frontend }}
}
setTimeout(() => redirect(), 1850)
</script>
<style>
.container {
width: 100vw;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
}
.caption {
margin-top: 4px;
font-weight: 600;
}
.text-blinking {
animation: text-blinking ease-in-out infinite 1.5s;
}
.description {
margin-top: 4px;
font-size: 0.85rem;
}
p {
margin: 0;
}
@keyframes text-blinking {
0% {
opacity: 100%;
}
50% {
opacity: 10%;
}
100% {
opacity: 100%;
}
}
</style>