⬆️ Upgrade to support the latest version Hydrogen Project standard
This commit is contained in:
23
pkg/internal/database/migrator.go
Normal file
23
pkg/internal/database/migrator.go
Normal file
@ -0,0 +1,23 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/internal/models"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var DatabaseAutoActionRange = []any{
|
||||
&models.Account{},
|
||||
&models.Realm{},
|
||||
&models.Channel{},
|
||||
&models.ChannelMember{},
|
||||
&models.Call{},
|
||||
&models.Message{},
|
||||
}
|
||||
|
||||
func RunMigration(source *gorm.DB) error {
|
||||
if err := source.AutoMigrate(DatabaseAutoActionRange...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
28
pkg/internal/database/source.go
Normal file
28
pkg/internal/database/source.go
Normal 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
6
pkg/internal/embed.go
Normal file
@ -0,0 +1,6 @@
|
||||
package pkg
|
||||
|
||||
import "embed"
|
||||
|
||||
//go:embed views/*
|
||||
var FS embed.FS
|
12
pkg/internal/gap/client.go
Normal file
12
pkg/internal/gap/client.go
Normal 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
15
pkg/internal/gap/net.go
Normal 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
|
||||
}
|
40
pkg/internal/gap/server.go
Normal file
40
pkg/internal/gap/server.go
Normal 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.Messaging"
|
||||
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)
|
||||
}
|
26
pkg/internal/grpc/health.go
Normal file
26
pkg/internal/grpc/health.go
Normal 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
|
||||
}
|
33
pkg/internal/grpc/server.go
Normal file
33
pkg/internal/grpc/server.go
Normal file
@ -0,0 +1,33 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"git.solsynth.dev/hydrogen/paperclip/pkg/proto"
|
||||
"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 {
|
||||
proto.UnimplementedAttachmentsServer
|
||||
}
|
||||
|
||||
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
5
pkg/internal/meta.go
Normal file
@ -0,0 +1,5 @@
|
||||
package pkg
|
||||
|
||||
const (
|
||||
AppVersion = "1.0.0"
|
||||
)
|
18
pkg/internal/models/accounts.go
Normal file
18
pkg/internal/models/accounts.go
Normal file
@ -0,0 +1,18 @@
|
||||
package models
|
||||
|
||||
// Account profiles basically fetched from Hydrogen.Identity
|
||||
// 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"`
|
||||
Channels []Channel `json:"channels"`
|
||||
ExternalID uint `json:"external_id"`
|
||||
}
|
17
pkg/internal/models/base.go
Normal file
17
pkg/internal/models/base.go
Normal 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"`
|
||||
}
|
15
pkg/internal/models/calls.go
Normal file
15
pkg/internal/models/calls.go
Normal file
@ -0,0 +1,15 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
type Call struct {
|
||||
BaseModel
|
||||
|
||||
EndedAt *time.Time `json:"ended_at"`
|
||||
|
||||
ExternalID string `json:"external_id"`
|
||||
FounderID uint `json:"founder_id"`
|
||||
ChannelID uint `json:"channel_id"`
|
||||
Founder ChannelMember `json:"founder"`
|
||||
Channel Channel `json:"channel"`
|
||||
}
|
49
pkg/internal/models/channels.go
Normal file
49
pkg/internal/models/channels.go
Normal file
@ -0,0 +1,49 @@
|
||||
package models
|
||||
|
||||
type ChannelType = uint8
|
||||
|
||||
const (
|
||||
ChannelTypeCommon = ChannelType(iota)
|
||||
ChannelTypeDirect
|
||||
)
|
||||
|
||||
type Channel struct {
|
||||
BaseModel
|
||||
|
||||
Alias string `json:"alias"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Members []ChannelMember `json:"members"`
|
||||
Messages []Message `json:"messages"`
|
||||
Calls []Call `json:"calls"`
|
||||
Type ChannelType `json:"type"`
|
||||
Account Account `json:"account"`
|
||||
AccountID uint `json:"account_id"`
|
||||
IsEncrypted bool `json:"is_encrypted"`
|
||||
|
||||
Realm Realm `json:"realm"`
|
||||
RealmID *uint `json:"realm_id"`
|
||||
}
|
||||
|
||||
type NotifyLevel = int8
|
||||
|
||||
const (
|
||||
NotifyLevelAll = NotifyLevel(iota)
|
||||
NotifyLevelMentioned
|
||||
NotifyLevelNone
|
||||
)
|
||||
|
||||
type ChannelMember struct {
|
||||
BaseModel
|
||||
|
||||
ChannelID uint `json:"channel_id"`
|
||||
AccountID uint `json:"account_id"`
|
||||
Nick *string `json:"nick"`
|
||||
Channel Channel `json:"channel"`
|
||||
Account Account `json:"account"`
|
||||
Notify NotifyLevel `json:"notify"`
|
||||
PowerLevel int `json:"power_level"`
|
||||
|
||||
Calls []Call `json:"calls" gorm:"foreignKey:FounderID"`
|
||||
Messages []Message `json:"messages" gorm:"foreignKey:SenderID"`
|
||||
}
|
22
pkg/internal/models/messages.go
Normal file
22
pkg/internal/models/messages.go
Normal file
@ -0,0 +1,22 @@
|
||||
package models
|
||||
|
||||
import "gorm.io/datatypes"
|
||||
|
||||
const (
|
||||
MessageTextType = "m.text"
|
||||
)
|
||||
|
||||
type Message struct {
|
||||
BaseModel
|
||||
|
||||
Uuid string `json:"uuid"`
|
||||
Content datatypes.JSONMap `json:"content"`
|
||||
Type string `json:"type"`
|
||||
Attachments datatypes.JSONSlice[uint] `json:"attachments"`
|
||||
Channel Channel `json:"channel"`
|
||||
Sender ChannelMember `json:"sender"`
|
||||
ReplyID *uint `json:"reply_id"`
|
||||
ReplyTo *Message `json:"reply_to" gorm:"foreignKey:ReplyID"`
|
||||
ChannelID uint `json:"channel_id"`
|
||||
SenderID uint `json:"sender_id"`
|
||||
}
|
15
pkg/internal/models/realms.go
Normal file
15
pkg/internal/models/realms.go
Normal 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"`
|
||||
Channels []Channel `json:"channels"`
|
||||
IsPublic bool `json:"is_public"`
|
||||
IsCommunity bool `json:"is_community"`
|
||||
ExternalID uint `json:"external_id"`
|
||||
}
|
21
pkg/internal/models/unified.go
Normal file
21
pkg/internal/models/unified.go
Normal file
@ -0,0 +1,21 @@
|
||||
package models
|
||||
|
||||
import jsoniter "github.com/json-iterator/go"
|
||||
|
||||
type UnifiedCommand struct {
|
||||
Action string `json:"w"`
|
||||
Message string `json:"m"`
|
||||
Payload any `json:"p"`
|
||||
}
|
||||
|
||||
func UnifiedCommandFromError(err error) UnifiedCommand {
|
||||
return UnifiedCommand{
|
||||
Action: "error",
|
||||
Message: err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
func (v UnifiedCommand) Marshal() []byte {
|
||||
data, _ := jsoniter.Marshal(v)
|
||||
return data
|
||||
}
|
8
pkg/internal/models/utils.go
Normal file
8
pkg/internal/models/utils.go
Normal file
@ -0,0 +1,8 @@
|
||||
package models
|
||||
|
||||
import jsoniter "github.com/json-iterator/go"
|
||||
|
||||
func FitStruct(src any, out any) {
|
||||
raw, _ := jsoniter.Marshal(src)
|
||||
_ = jsoniter.Unmarshal(raw, out)
|
||||
}
|
140
pkg/internal/server/api/calls_api.go
Normal file
140
pkg/internal/server/api/calls_api.go
Normal file
@ -0,0 +1,140 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/internal/database"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/internal/models"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/internal/services"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func listCall(c *fiber.Ctx) error {
|
||||
take := c.QueryInt("take", 0)
|
||||
offset := c.QueryInt("offset", 0)
|
||||
alias := c.Params("channel")
|
||||
|
||||
var channel models.Channel
|
||||
if err := database.C.Where(&models.Channel{
|
||||
Alias: alias,
|
||||
}).First(&channel).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
if calls, err := services.ListCall(channel, take, offset); err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
} else {
|
||||
return c.JSON(calls)
|
||||
}
|
||||
}
|
||||
|
||||
func getOngoingCall(c *fiber.Ctx) error {
|
||||
alias := c.Params("channel")
|
||||
|
||||
var channel models.Channel
|
||||
if err := database.C.Where(&models.Channel{
|
||||
Alias: alias,
|
||||
}).First(&channel).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
if call, err := services.GetOngoingCall(channel); err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
} else {
|
||||
return c.JSON(call)
|
||||
}
|
||||
}
|
||||
|
||||
func startCall(c *fiber.Ctx) error {
|
||||
user := c.Locals("principal").(models.Account)
|
||||
alias := c.Params("channel")
|
||||
|
||||
var channel models.Channel
|
||||
if err := database.C.Where(&models.Channel{
|
||||
Alias: alias,
|
||||
}).First(&channel).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
var membership models.ChannelMember
|
||||
if err := database.C.Where(&models.ChannelMember{
|
||||
ChannelID: channel.ID,
|
||||
AccountID: user.ID,
|
||||
}).Find(&membership).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
call, err := services.NewCall(channel, membership)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
} else {
|
||||
return c.JSON(call)
|
||||
}
|
||||
}
|
||||
|
||||
func endCall(c *fiber.Ctx) error {
|
||||
user := c.Locals("principal").(models.Account)
|
||||
alias := c.Params("channel")
|
||||
|
||||
var channel models.Channel
|
||||
if err := database.C.Where(&models.Channel{
|
||||
Alias: alias,
|
||||
}).First(&channel).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
var membership models.ChannelMember
|
||||
if err := database.C.Where(&models.ChannelMember{
|
||||
ChannelID: channel.ID,
|
||||
AccountID: user.ID,
|
||||
}).Find(&membership).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
call, err := services.GetOngoingCall(channel)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
} else if call.FounderID != user.ID && channel.AccountID != user.ID {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "only call founder or channel owner can end this call")
|
||||
}
|
||||
|
||||
if call, err := services.EndCall(call); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
} else {
|
||||
return c.JSON(call)
|
||||
}
|
||||
}
|
||||
|
||||
func exchangeCallToken(c *fiber.Ctx) error {
|
||||
user := c.Locals("principal").(models.Account)
|
||||
alias := c.Params("channel")
|
||||
|
||||
var channel models.Channel
|
||||
if err := database.C.Where(&models.Channel{
|
||||
Alias: alias,
|
||||
}).First(&channel).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
var membership models.ChannelMember
|
||||
if err := database.C.Where(&models.ChannelMember{
|
||||
ChannelID: channel.ID,
|
||||
AccountID: user.ID,
|
||||
}).Find(&membership).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
call, err := services.GetOngoingCall(channel)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
tk, err := services.EncodeCallToken(user, call)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
} else {
|
||||
return c.JSON(fiber.Map{
|
||||
"token": tk,
|
||||
"endpoint": viper.GetString("calling.endpoint"),
|
||||
})
|
||||
}
|
||||
}
|
231
pkg/internal/server/api/channel_members_api.go
Normal file
231
pkg/internal/server/api/channel_members_api.go
Normal file
@ -0,0 +1,231 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/internal/server/exts"
|
||||
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/internal/database"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/internal/models"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/internal/services"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func listChannelMembers(c *fiber.Ctx) error {
|
||||
alias := c.Params("channel")
|
||||
|
||||
var err error
|
||||
var channel models.Channel
|
||||
if val, ok := c.Locals("realm").(models.Realm); ok {
|
||||
channel, err = services.GetChannelWithAlias(alias, val.ID)
|
||||
} else {
|
||||
channel, err = services.GetChannelWithAlias(alias)
|
||||
}
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
if members, err := services.ListChannelMember(channel.ID); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
} else {
|
||||
return c.JSON(members)
|
||||
}
|
||||
}
|
||||
|
||||
func getMyChannelMembership(c *fiber.Ctx) error {
|
||||
alias := c.Params("channel")
|
||||
user := c.Locals("principal").(models.Account)
|
||||
|
||||
var err error
|
||||
var channel models.Channel
|
||||
if val, ok := c.Locals("realm").(models.Realm); ok {
|
||||
channel, err = services.GetChannelWithAlias(alias, val.ID)
|
||||
} else {
|
||||
channel, err = services.GetChannelWithAlias(alias)
|
||||
}
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
if member, err := services.GetChannelMember(user, channel.ID); err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
} else {
|
||||
return c.JSON(member)
|
||||
}
|
||||
}
|
||||
|
||||
func addChannelMember(c *fiber.Ctx) error {
|
||||
user := c.Locals("principal").(models.Account)
|
||||
alias := c.Params("channel")
|
||||
|
||||
var data struct {
|
||||
Target string `json:"target" validate:"required"`
|
||||
}
|
||||
|
||||
if err := exts.BindAndValidate(c, &data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var channel models.Channel
|
||||
if err := database.C.Where(&models.Channel{
|
||||
Alias: alias,
|
||||
}).First(&channel).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
} else if channel.Type == models.ChannelTypeDirect {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "direct message member changes was not allowed")
|
||||
}
|
||||
|
||||
if member, err := services.GetChannelMember(user, channel.ID); err != nil {
|
||||
return fiber.NewError(fiber.StatusForbidden, err.Error())
|
||||
} else if member.PowerLevel < 50 {
|
||||
return fiber.NewError(fiber.StatusForbidden, "you must be a moderator of a channel to add member into it")
|
||||
}
|
||||
|
||||
var account models.Account
|
||||
if err := database.C.Where(&models.Account{
|
||||
Name: data.Target,
|
||||
}).First(&account).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
if err := services.AddChannelMemberWithCheck(account, channel); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
} else {
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
func removeChannelMember(c *fiber.Ctx) error {
|
||||
user := c.Locals("principal").(models.Account)
|
||||
alias := c.Params("channel")
|
||||
|
||||
var data struct {
|
||||
Target string `json:"target" validate:"required"`
|
||||
}
|
||||
|
||||
if err := exts.BindAndValidate(c, &data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var channel models.Channel
|
||||
if err := database.C.Where(&models.Channel{
|
||||
Alias: alias,
|
||||
AccountID: user.ID,
|
||||
}).First(&channel).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
} else if channel.Type == models.ChannelTypeDirect {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "direct message member changes was not allowed")
|
||||
}
|
||||
|
||||
if member, err := services.GetChannelMember(user, channel.ID); err != nil {
|
||||
return fiber.NewError(fiber.StatusForbidden, err.Error())
|
||||
} else if member.PowerLevel < 50 {
|
||||
return fiber.NewError(fiber.StatusForbidden, "you must be a moderator of a channel to remove member into it")
|
||||
}
|
||||
|
||||
var account models.Account
|
||||
if err := database.C.Where(&models.Account{
|
||||
Name: data.Target,
|
||||
}).First(&account).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
if err := services.RemoveChannelMember(account, channel); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
} else {
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
func editMyChannelMembership(c *fiber.Ctx) error {
|
||||
user := c.Locals("principal").(models.Account)
|
||||
alias := c.Params("channel")
|
||||
|
||||
var data struct {
|
||||
Nick string `json:"nick"`
|
||||
NotifyLevel int8 `json:"notify_level"`
|
||||
}
|
||||
|
||||
if err := exts.BindAndValidate(c, &data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var err error
|
||||
var channel models.Channel
|
||||
if val, ok := c.Locals("realm").(models.Realm); ok {
|
||||
channel, err = services.GetChannelWithAlias(alias, val.ID)
|
||||
} else {
|
||||
channel, err = services.GetChannelWithAlias(alias)
|
||||
}
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
var membership models.ChannelMember
|
||||
if err := database.C.Where(&models.ChannelMember{
|
||||
ChannelID: channel.ID,
|
||||
AccountID: user.ID,
|
||||
}).First(&membership).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
membership.Notify = data.NotifyLevel
|
||||
if len(data.Nick) > 0 {
|
||||
membership.Nick = &data.Nick
|
||||
} else {
|
||||
membership.Nick = nil
|
||||
}
|
||||
|
||||
if membership, err := services.EditChannelMember(membership); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
} else {
|
||||
return c.JSON(membership)
|
||||
}
|
||||
}
|
||||
|
||||
func joinChannel(c *fiber.Ctx) error {
|
||||
user := c.Locals("principal").(models.Account)
|
||||
alias := c.Params("channel")
|
||||
|
||||
var channel models.Channel
|
||||
if err := database.C.Where(&models.Channel{
|
||||
Alias: alias,
|
||||
}).Preload("Realm").First(&channel).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
} else if _, _, err := services.GetAvailableChannel(channel.ID, user); err == nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "you already joined the channel")
|
||||
} else if channel.RealmID == nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "you was impossible to join a channel without related realm")
|
||||
}
|
||||
|
||||
if realm, err := services.GetRealm(channel.Realm.ExternalID); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("invalid channel, related realm was not found: %v", err))
|
||||
} else if _, err := services.GetRealmMember(realm.ExternalID, user.ExternalID); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("you are not a part of the realm: %v", err))
|
||||
}
|
||||
|
||||
if err := services.AddChannelMember(user, channel); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
} else {
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
func leaveChannel(c *fiber.Ctx) error {
|
||||
user := c.Locals("principal").(models.Account)
|
||||
alias := c.Params("channel")
|
||||
|
||||
var channel models.Channel
|
||||
if err := database.C.Where(&models.Channel{
|
||||
Alias: alias,
|
||||
}).First(&channel).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
} else if user.ID == channel.AccountID {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "you cannot leave your own channel")
|
||||
}
|
||||
|
||||
if err := services.RemoveChannelMember(user, channel); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
} else {
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
||||
}
|
234
pkg/internal/server/api/channels_api.go
Normal file
234
pkg/internal/server/api/channels_api.go
Normal file
@ -0,0 +1,234 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/internal/server/exts"
|
||||
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/internal/database"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/internal/models"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/internal/services"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func getChannel(c *fiber.Ctx) error {
|
||||
alias := c.Params("channel")
|
||||
|
||||
var err error
|
||||
var channel models.Channel
|
||||
if val, ok := c.Locals("realm").(models.Realm); ok {
|
||||
channel, err = services.GetChannelWithAlias(alias, val.ID)
|
||||
} else {
|
||||
channel, err = services.GetChannelWithAlias(alias)
|
||||
}
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(channel)
|
||||
}
|
||||
|
||||
func getChannelIdentity(c *fiber.Ctx) error {
|
||||
user := c.Locals("principal").(models.Account)
|
||||
alias := c.Params("channel")
|
||||
|
||||
var err error
|
||||
var member models.ChannelMember
|
||||
if val, ok := c.Locals("realm").(models.Realm); ok {
|
||||
_, member, err = services.GetAvailableChannelWithAlias(alias, user, val.ID)
|
||||
} else {
|
||||
_, member, err = services.GetAvailableChannelWithAlias(alias, user)
|
||||
}
|
||||
if err != nil {
|
||||
return c.SendStatus(fiber.StatusForbidden)
|
||||
}
|
||||
|
||||
return c.JSON(member)
|
||||
}
|
||||
|
||||
func listChannel(c *fiber.Ctx) error {
|
||||
var err error
|
||||
var channels []models.Channel
|
||||
if val, ok := c.Locals("realm").(models.Realm); ok {
|
||||
channels, err = services.ListChannel(val.ID)
|
||||
} else {
|
||||
channels, err = services.ListChannel()
|
||||
}
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(channels)
|
||||
}
|
||||
|
||||
func listOwnedChannel(c *fiber.Ctx) error {
|
||||
user := c.Locals("principal").(models.Account)
|
||||
|
||||
var err error
|
||||
var channels []models.Channel
|
||||
if val, ok := c.Locals("realm").(models.Realm); ok {
|
||||
channels, err = services.ListChannelWithUser(user, val.ID)
|
||||
} else {
|
||||
channels, err = services.ListChannelWithUser(user)
|
||||
}
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(channels)
|
||||
}
|
||||
|
||||
func listAvailableChannel(c *fiber.Ctx) error {
|
||||
user := c.Locals("principal").(models.Account)
|
||||
|
||||
var err error
|
||||
var channels []models.Channel
|
||||
if val, ok := c.Locals("realm").(models.Realm); ok {
|
||||
channels, err = services.ListAvailableChannel(user, val.ID)
|
||||
} else {
|
||||
channels, err = services.ListAvailableChannel(user)
|
||||
}
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(channels)
|
||||
}
|
||||
|
||||
func createChannel(c *fiber.Ctx) error {
|
||||
user := c.Locals("principal").(models.Account)
|
||||
|
||||
var data struct {
|
||||
Alias string `json:"alias" validate:"required,lowercase,min=4,max=32"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
Description string `json:"description"`
|
||||
IsEncrypted bool `json:"is_encrypted"`
|
||||
}
|
||||
|
||||
if err := exts.BindAndValidate(c, &data); err != nil {
|
||||
return err
|
||||
} else if err = services.GetChannelAliasAvailability(data.Alias); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
var realm *models.Realm
|
||||
if val, ok := c.Locals("realm").(models.Realm); ok {
|
||||
if info, err := services.GetRealmMember(val.ExternalID, user.ExternalID); err != nil {
|
||||
return fiber.NewError(fiber.StatusForbidden, "you must be a part of that realm then can create channel related to it")
|
||||
} else if info.GetPowerLevel() < 50 {
|
||||
return fiber.NewError(fiber.StatusForbidden, "you must be a moderator of that realm then can create channel related to it")
|
||||
} else {
|
||||
realm = &val
|
||||
}
|
||||
}
|
||||
|
||||
channel := models.Channel{
|
||||
Alias: data.Alias,
|
||||
Name: data.Name,
|
||||
Description: data.Description,
|
||||
IsEncrypted: data.IsEncrypted,
|
||||
AccountID: user.ID,
|
||||
Type: models.ChannelTypeCommon,
|
||||
Members: []models.ChannelMember{
|
||||
{AccountID: user.ID, PowerLevel: 100},
|
||||
},
|
||||
}
|
||||
|
||||
if realm != nil {
|
||||
channel.RealmID = &realm.ID
|
||||
}
|
||||
|
||||
channel, err := services.NewChannel(channel)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(channel)
|
||||
}
|
||||
|
||||
func editChannel(c *fiber.Ctx) error {
|
||||
user := c.Locals("principal").(models.Account)
|
||||
id, _ := c.ParamsInt("channelId", 0)
|
||||
|
||||
var data struct {
|
||||
Alias string `json:"alias" validate:"required,min=4,max=32"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
Description string `json:"description"`
|
||||
IsEncrypted bool `json:"is_encrypted"`
|
||||
}
|
||||
|
||||
if err := exts.BindAndValidate(c, &data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tx := database.C.Where(&models.Channel{BaseModel: models.BaseModel{ID: uint(id)}})
|
||||
|
||||
if val, ok := c.Locals("realm").(models.Realm); ok {
|
||||
if info, err := services.GetRealmMember(val.ExternalID, user.ExternalID); err != nil {
|
||||
return fiber.NewError(fiber.StatusForbidden, "you must be a part of that realm then can edit channel related to it")
|
||||
} else if info.GetPowerLevel() < 50 {
|
||||
return fiber.NewError(fiber.StatusForbidden, "you must be a moderator of that realm then can edit channel related to it")
|
||||
} else {
|
||||
tx = tx.Where("realm_id = ?", val.ID)
|
||||
}
|
||||
} else {
|
||||
tx = tx.Where("realm_id IS NULL")
|
||||
}
|
||||
|
||||
var channel models.Channel
|
||||
if err := tx.First(&channel).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
if channel.RealmID != nil {
|
||||
if member, err := services.GetChannelMember(user, channel.ID); err != nil {
|
||||
return fiber.NewError(fiber.StatusForbidden, "you must be a part of this channel to edit it")
|
||||
} else if member.PowerLevel < 100 {
|
||||
return fiber.NewError(fiber.StatusForbidden, "you must be channel admin to edit it")
|
||||
}
|
||||
}
|
||||
|
||||
channel, err := services.EditChannel(channel, data.Alias, data.Name, data.Description, data.IsEncrypted)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(channel)
|
||||
}
|
||||
|
||||
func deleteChannel(c *fiber.Ctx) error {
|
||||
user := c.Locals("principal").(models.Account)
|
||||
id, _ := c.ParamsInt("channelId", 0)
|
||||
|
||||
tx := database.C.Where(&models.Channel{BaseModel: models.BaseModel{ID: uint(id)}})
|
||||
|
||||
if val, ok := c.Locals("realm").(models.Realm); ok {
|
||||
if info, err := services.GetRealmMember(val.ExternalID, user.ExternalID); err != nil {
|
||||
return fmt.Errorf("you must be a part of that realm then can delete channel related to it")
|
||||
} else if info.GetPowerLevel() < 50 {
|
||||
return fmt.Errorf("you must be a moderator of that realm then can delete channel related to it")
|
||||
} else {
|
||||
tx = tx.Where("realm_id = ?", val.ID)
|
||||
}
|
||||
} else {
|
||||
tx = tx.Where("(account_id = ? OR type = ?) AND realm_id IS NULL", user.ID, models.ChannelTypeDirect)
|
||||
}
|
||||
|
||||
var channel models.Channel
|
||||
if err := tx.First(&channel).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
if channel.Type == models.ChannelTypeDirect {
|
||||
if member, err := services.GetChannelMember(user, channel.ID); err != nil {
|
||||
return fiber.NewError(fiber.StatusForbidden, "you must related to this direct message if you want delete it")
|
||||
} else if member.PowerLevel < 100 {
|
||||
return fiber.NewError(fiber.StatusForbidden, "you must be a moderator of this direct message if you want delete it")
|
||||
}
|
||||
}
|
||||
|
||||
if err := services.DeleteChannel(channel); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
71
pkg/internal/server/api/direct_channels_api.go
Normal file
71
pkg/internal/server/api/direct_channels_api.go
Normal file
@ -0,0 +1,71 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/internal/server/exts"
|
||||
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/internal/database"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/internal/models"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/internal/services"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func createDirectChannel(c *fiber.Ctx) error {
|
||||
user := c.Locals("principal").(models.Account)
|
||||
|
||||
var data struct {
|
||||
Alias string `json:"alias" validate:"required,lowercase,min=4,max=32"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
Description string `json:"description"`
|
||||
RelatedUser uint `json:"related_user"`
|
||||
IsEncrypted bool `json:"is_encrypted"`
|
||||
}
|
||||
|
||||
if err := exts.BindAndValidate(c, &data); err != nil {
|
||||
return err
|
||||
} else if err = services.GetChannelAliasAvailability(data.Alias); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
var realm *models.Realm
|
||||
if val, ok := c.Locals("realm").(models.Realm); ok {
|
||||
if info, err := services.GetRealmMember(val.ExternalID, user.ExternalID); err != nil {
|
||||
return fiber.NewError(fiber.StatusForbidden, "you must be a part of that realm then can create channel related to it")
|
||||
} else if info.GetPowerLevel() < 50 {
|
||||
return fiber.NewError(fiber.StatusForbidden, "you must be a moderator of that realm then can create channel related to it")
|
||||
} else {
|
||||
realm = &val
|
||||
}
|
||||
}
|
||||
|
||||
var relatedUser models.Account
|
||||
if err := database.C.
|
||||
Where("external_id = ?", data.RelatedUser).
|
||||
First(&relatedUser).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("unable to find related user: %v", err))
|
||||
}
|
||||
|
||||
channel := models.Channel{
|
||||
Alias: data.Alias,
|
||||
Name: data.Name,
|
||||
Description: data.Description,
|
||||
IsEncrypted: data.IsEncrypted,
|
||||
AccountID: user.ID,
|
||||
Type: models.ChannelTypeDirect,
|
||||
Members: []models.ChannelMember{
|
||||
{AccountID: user.ID, PowerLevel: 100},
|
||||
{AccountID: relatedUser.ID, PowerLevel: 100},
|
||||
},
|
||||
}
|
||||
|
||||
if realm != nil {
|
||||
channel.RealmID = &realm.ID
|
||||
}
|
||||
|
||||
channel, err := services.NewChannel(channel)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(channel)
|
||||
}
|
49
pkg/internal/server/api/index.go
Normal file
49
pkg/internal/server/api/index.go
Normal file
@ -0,0 +1,49 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/gofiber/contrib/websocket"
|
||||
"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)
|
||||
|
||||
channels := api.Group("/channels/:realm").Use(realmMiddleware).Name("Channels API")
|
||||
{
|
||||
channels.Get("/", listChannel)
|
||||
channels.Get("/me", listOwnedChannel)
|
||||
channels.Get("/me/available", listAvailableChannel)
|
||||
channels.Get("/:channel", getChannel)
|
||||
channels.Get("/:channel/me", getChannelIdentity)
|
||||
|
||||
channels.Post("/", createChannel)
|
||||
channels.Post("/dm", createDirectChannel)
|
||||
channels.Put("/:channelId", editChannel)
|
||||
channels.Delete("/:channelId", deleteChannel)
|
||||
|
||||
channels.Get("/:channel/members", listChannelMembers)
|
||||
channels.Get("/:channel/members/me", getMyChannelMembership)
|
||||
channels.Put("/:channel/members/me", editMyChannelMembership)
|
||||
channels.Post("/:channel/members", addChannelMember)
|
||||
channels.Post("/:channel/members/me", joinChannel)
|
||||
channels.Delete("/:channel/members", removeChannelMember)
|
||||
channels.Delete("/:channel/members/me", leaveChannel)
|
||||
|
||||
channels.Get("/:channel/messages", listMessage)
|
||||
channels.Post("/:channel/messages", newMessage)
|
||||
channels.Put("/:channel/messages/:messageId", editMessage)
|
||||
channels.Delete("/:channel/messages/:messageId", deleteMessage)
|
||||
|
||||
channels.Get("/:channel/calls", listCall)
|
||||
channels.Get("/:channel/calls/ongoing", getOngoingCall)
|
||||
channels.Post("/:channel/calls", startCall)
|
||||
channels.Delete("/:channel/calls/ongoing", endCall)
|
||||
channels.Post("/:channel/calls/ongoing/token", exchangeCallToken)
|
||||
}
|
||||
|
||||
api.Get("/ws", websocket.New(messageGateway))
|
||||
}
|
||||
}
|
208
pkg/internal/server/api/messages_api.go
Normal file
208
pkg/internal/server/api/messages_api.go
Normal file
@ -0,0 +1,208 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/internal/database"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/internal/models"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/internal/server/exts"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/internal/services"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func listMessage(c *fiber.Ctx) error {
|
||||
user := c.Locals("principal").(models.Account)
|
||||
take := c.QueryInt("take", 0)
|
||||
offset := c.QueryInt("offset", 0)
|
||||
alias := c.Params("channel")
|
||||
|
||||
var err error
|
||||
var channel models.Channel
|
||||
if val, ok := c.Locals("realm").(models.Realm); ok {
|
||||
channel, err = services.GetChannelWithAlias(alias, val.ID)
|
||||
} else {
|
||||
channel, err = services.GetChannelWithAlias(alias)
|
||||
}
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
} else if _, _, err := services.GetAvailableChannel(channel.ID, user); err != nil {
|
||||
return fiber.NewError(fiber.StatusForbidden, fmt.Sprintf("you need join the channel before you read the messages: %v", err))
|
||||
}
|
||||
|
||||
count := services.CountMessage(channel)
|
||||
messages, err := services.ListMessage(channel, take, offset)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"count": count,
|
||||
"data": messages,
|
||||
})
|
||||
}
|
||||
|
||||
func newMessage(c *fiber.Ctx) error {
|
||||
user := c.Locals("principal").(models.Account)
|
||||
alias := c.Params("channel")
|
||||
|
||||
var data struct {
|
||||
Uuid string `json:"uuid" validate:"required"`
|
||||
Type string `json:"type" validate:"required"`
|
||||
Content map[string]any `json:"content"`
|
||||
Attachments []uint `json:"attachments"`
|
||||
ReplyTo *uint `json:"reply_to"`
|
||||
}
|
||||
|
||||
if err := exts.BindAndValidate(c, &data); err != nil {
|
||||
return err
|
||||
} else if len(data.Uuid) < 36 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "message uuid was not valid")
|
||||
}
|
||||
|
||||
if len(data.Attachments) == 0 {
|
||||
if data.Type == models.MessageTextType {
|
||||
if val, ok := data.Content["value"].(string); ok && len(strings.TrimSpace(val)) == 0 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "you cannot send an empty message")
|
||||
} else if !ok {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "invalid content of text message")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, attachment := range data.Attachments {
|
||||
if !services.CheckAttachmentByIDExists(attachment, "m.attachment") {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("attachment %d not found", attachment))
|
||||
}
|
||||
}
|
||||
|
||||
var err error
|
||||
var channel models.Channel
|
||||
var member models.ChannelMember
|
||||
if val, ok := c.Locals("realm").(models.Realm); ok {
|
||||
channel, member, err = services.GetAvailableChannelWithAlias(alias, user, val.ID)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
} else {
|
||||
channel, member, err = services.GetAvailableChannelWithAlias(alias, user)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
} else if member.PowerLevel < 0 {
|
||||
return fiber.NewError(fiber.StatusForbidden, "you have not enough permission to send message")
|
||||
}
|
||||
}
|
||||
|
||||
message := models.Message{
|
||||
Uuid: data.Uuid,
|
||||
Content: data.Content,
|
||||
Sender: member,
|
||||
Channel: channel,
|
||||
ChannelID: channel.ID,
|
||||
SenderID: member.ID,
|
||||
Attachments: data.Attachments,
|
||||
Type: data.Type,
|
||||
}
|
||||
|
||||
var replyTo models.Message
|
||||
if data.ReplyTo != nil {
|
||||
if err := database.C.Where("id = ?", data.ReplyTo).First(&replyTo).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("message to reply was not found: %v", err))
|
||||
} else {
|
||||
message.ReplyTo = &replyTo
|
||||
message.ReplyID = &replyTo.ID
|
||||
}
|
||||
}
|
||||
|
||||
if message, err = services.NewMessage(message); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(message)
|
||||
}
|
||||
|
||||
func editMessage(c *fiber.Ctx) error {
|
||||
user := c.Locals("principal").(models.Account)
|
||||
alias := c.Params("channel")
|
||||
messageId, _ := c.ParamsInt("messageId", 0)
|
||||
|
||||
var data struct {
|
||||
Type string `json:"type" validate:"required"`
|
||||
Content map[string]any `json:"content"`
|
||||
Attachments []uint `json:"attachments"`
|
||||
ReplyTo *uint `json:"reply_to"`
|
||||
}
|
||||
|
||||
if err := exts.BindAndValidate(c, &data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, attachment := range data.Attachments {
|
||||
if !services.CheckAttachmentByIDExists(attachment, "m.attachment") {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("attachment %d not found", attachment))
|
||||
}
|
||||
}
|
||||
|
||||
var err error
|
||||
var channel models.Channel
|
||||
var member models.ChannelMember
|
||||
if val, ok := c.Locals("realm").(models.Realm); ok {
|
||||
channel, member, err = services.GetAvailableChannelWithAlias(alias, user, val.ID)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
} else {
|
||||
channel, member, err = services.GetAvailableChannelWithAlias(alias, user)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
var message models.Message
|
||||
if message, err = services.GetMessageWithPrincipal(channel, member, uint(messageId)); err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
message.Attachments = data.Attachments
|
||||
message.Content = data.Content
|
||||
message.Type = data.Type
|
||||
|
||||
message, err = services.EditMessage(message)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(message)
|
||||
}
|
||||
|
||||
func deleteMessage(c *fiber.Ctx) error {
|
||||
user := c.Locals("principal").(models.Account)
|
||||
alias := c.Params("channel")
|
||||
messageId, _ := c.ParamsInt("messageId", 0)
|
||||
|
||||
var err error
|
||||
var channel models.Channel
|
||||
var member models.ChannelMember
|
||||
if val, ok := c.Locals("realm").(models.Realm); ok {
|
||||
channel, member, err = services.GetAvailableChannelWithAlias(alias, user, val.ID)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
} else {
|
||||
channel, member, err = services.GetAvailableChannelWithAlias(alias, user)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
var message models.Message
|
||||
if message, err = services.GetMessageWithPrincipal(channel, member, uint(messageId)); err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
message, err = services.DeleteMessage(message)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(message)
|
||||
}
|
48
pkg/internal/server/api/messages_ws.go
Normal file
48
pkg/internal/server/api/messages_ws.go
Normal file
@ -0,0 +1,48 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/internal/models"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/internal/services"
|
||||
"github.com/gofiber/contrib/websocket"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func messageGateway(c *websocket.Conn) {
|
||||
user := c.Locals("principal").(models.Account)
|
||||
|
||||
// Push connection
|
||||
services.ClientRegister(user, c)
|
||||
log.Debug().Uint("user", user.ID).Msg("New websocket connection established...")
|
||||
|
||||
// Event loop
|
||||
var task models.UnifiedCommand
|
||||
|
||||
var messageType int
|
||||
var packet []byte
|
||||
var err error
|
||||
|
||||
for {
|
||||
if messageType, packet, err = c.ReadMessage(); err != nil {
|
||||
break
|
||||
} else if err := jsoniter.Unmarshal(packet, &task); err != nil {
|
||||
_ = c.WriteMessage(messageType, models.UnifiedCommand{
|
||||
Action: "error",
|
||||
Message: "unable to unmarshal your command, requires json request",
|
||||
}.Marshal())
|
||||
continue
|
||||
}
|
||||
|
||||
message := services.DealCommand(task, user)
|
||||
|
||||
if message != nil {
|
||||
if err = c.WriteMessage(messageType, message.Marshal()); err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pop connection
|
||||
services.ClientUnregister(user, c)
|
||||
log.Debug().Uint("user", user.ID).Msg("A websocket connection disconnected...")
|
||||
}
|
21
pkg/internal/server/api/realms_api.go
Normal file
21
pkg/internal/server/api/realms_api.go
Normal file
@ -0,0 +1,21 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/internal/services"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func realmMiddleware(c *fiber.Ctx) error {
|
||||
realmAlias := c.Params("realm")
|
||||
if len(realmAlias) > 0 && realmAlias != "global" {
|
||||
realm, err := services.GetRealmWithAlias(realmAlias)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("requested channel with realm, but realm was not found: %v", err))
|
||||
} else {
|
||||
c.Locals("realm", realm)
|
||||
}
|
||||
}
|
||||
|
||||
return c.Next()
|
||||
}
|
33
pkg/internal/server/api/users_api.go
Normal file
33
pkg/internal/server/api/users_api.go
Normal file
@ -0,0 +1,33 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/internal/database"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/internal/models"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func getUserinfo(c *fiber.Ctx) error {
|
||||
user := c.Locals("principal").(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)
|
||||
}
|
19
pkg/internal/server/exts/auth.go
Normal file
19
pkg/internal/server/exts/auth.go
Normal file
@ -0,0 +1,19 @@
|
||||
package exts
|
||||
|
||||
import (
|
||||
"git.solsynth.dev/hydrogen/messaging/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()
|
||||
}
|
18
pkg/internal/server/exts/utils.go
Normal file
18
pkg/internal/server/exts/utils.go
Normal 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
|
||||
}
|
86
pkg/internal/server/server.go
Normal file
86
pkg/internal/server/server.go
Normal file
@ -0,0 +1,86 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/internal"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/internal/gap"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/internal/server/api"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/internal/server/exts"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gofiber/fiber/v2/middleware/favicon"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/cors"
|
||||
"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"
|
||||
)
|
||||
|
||||
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.Messaging",
|
||||
AppName: "Hydrogen.Messaging",
|
||||
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...")
|
||||
}
|
||||
}
|
60
pkg/internal/services/accounts.go
Normal file
60
pkg/internal/services/accounts.go
Normal file
@ -0,0 +1,60 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/internal/database"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/internal/gap"
|
||||
"time"
|
||||
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/internal/models"
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/proto"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
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 NotifyAccountMessager(user models.Account, t, s, c string, realtime bool, forcePush bool, 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: fmt.Sprintf("messaging.%s", t),
|
||||
Subject: s,
|
||||
Content: c,
|
||||
Links: links,
|
||||
RecipientId: uint64(user.ExternalID),
|
||||
IsRealtime: realtime,
|
||||
IsForcePush: forcePush,
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
22
pkg/internal/services/attachments.go
Normal file
22
pkg/internal/services/attachments.go
Normal file
@ -0,0 +1,22 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.solsynth.dev/hydrogen/messaging/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.Passport")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
_, err = proto.NewAttachmentsClient(pc).CheckAttachmentExists(context.Background(), &proto.AttachmentLookupRequest{
|
||||
Id: lo.ToPtr(uint64(id)),
|
||||
Usage: &usage,
|
||||
})
|
||||
|
||||
return err == nil
|
||||
}
|
51
pkg/internal/services/auth.go
Normal file
51
pkg/internal/services/auth.go
Normal file
@ -0,0 +1,51 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/internal/database"
|
||||
"git.solsynth.dev/hydrogen/messaging/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(account, prev) {
|
||||
err = database.C.Save(&account).Error
|
||||
}
|
||||
|
||||
return account, err
|
||||
}
|
172
pkg/internal/services/calls.go
Normal file
172
pkg/internal/services/calls.go
Normal file
@ -0,0 +1,172 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/internal/database"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/internal/models"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/livekit/protocol/auth"
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/samber/lo"
|
||||
"github.com/spf13/viper"
|
||||
"gorm.io/gorm"
|
||||
"time"
|
||||
)
|
||||
|
||||
func ListCall(channel models.Channel, take, offset int) ([]models.Call, error) {
|
||||
var calls []models.Call
|
||||
if err := database.C.
|
||||
Where(models.Call{ChannelID: channel.ID}).
|
||||
Limit(take).
|
||||
Offset(offset).
|
||||
Preload("Founder").
|
||||
Preload("Founder.Account").
|
||||
Preload("Channel").
|
||||
Order("created_at DESC").
|
||||
Find(&calls).Error; err != nil {
|
||||
return calls, err
|
||||
} else {
|
||||
return calls, nil
|
||||
}
|
||||
}
|
||||
|
||||
func GetCall(channel models.Channel, id uint) (models.Call, error) {
|
||||
var call models.Call
|
||||
if err := database.C.
|
||||
Where(models.Call{
|
||||
BaseModel: models.BaseModel{ID: id},
|
||||
ChannelID: channel.ID,
|
||||
}).
|
||||
Preload("Founder").
|
||||
Preload("Founder.Account").
|
||||
Preload("Channel").
|
||||
Order("created_at DESC").
|
||||
First(&call).Error; err != nil {
|
||||
return call, err
|
||||
} else {
|
||||
return call, nil
|
||||
}
|
||||
}
|
||||
|
||||
func GetOngoingCall(channel models.Channel) (models.Call, error) {
|
||||
var call models.Call
|
||||
if err := database.C.
|
||||
Where(models.Call{ChannelID: channel.ID}).
|
||||
Where("ended_at IS NULL").
|
||||
Preload("Founder").
|
||||
Preload("Channel").
|
||||
Order("created_at DESC").
|
||||
First(&call).Error; err != nil {
|
||||
return call, err
|
||||
} else {
|
||||
return call, nil
|
||||
}
|
||||
}
|
||||
|
||||
func NewCall(channel models.Channel, founder models.ChannelMember) (models.Call, error) {
|
||||
call := models.Call{
|
||||
ExternalID: channel.Alias,
|
||||
FounderID: founder.ID,
|
||||
ChannelID: channel.ID,
|
||||
Founder: founder,
|
||||
Channel: channel,
|
||||
}
|
||||
|
||||
if _, err := GetOngoingCall(channel); err == nil || !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return call, fmt.Errorf("this channel already has an ongoing call")
|
||||
}
|
||||
|
||||
_, err := Lk.CreateRoom(context.Background(), &livekit.CreateRoomRequest{
|
||||
Name: call.ExternalID,
|
||||
EmptyTimeout: viper.GetUint32("calling.empty_timeout_duration"),
|
||||
MaxParticipants: viper.GetUint32("calling.max_participants"),
|
||||
})
|
||||
if err != nil {
|
||||
return call, fmt.Errorf("remote livekit error: %v", err)
|
||||
}
|
||||
|
||||
var members []models.ChannelMember
|
||||
if err := database.C.Save(&call).Error; err != nil {
|
||||
return call, err
|
||||
} else if err = database.C.Where(models.ChannelMember{
|
||||
ChannelID: call.ChannelID,
|
||||
}).Preload("Account").Find(&members).Error; err == nil {
|
||||
channel := call.Channel
|
||||
call, _ = GetCall(call.Channel, call.ID)
|
||||
for _, member := range members {
|
||||
if member.ID != call.Founder.ID {
|
||||
err = NotifyAccountMessager(member.Account,
|
||||
"incomingCall",
|
||||
fmt.Sprintf("Call in #%s", channel.Alias),
|
||||
fmt.Sprintf("%s started a new call", call.Founder.Account.Name),
|
||||
false,
|
||||
true,
|
||||
)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("An error occurred when trying notify user.")
|
||||
}
|
||||
}
|
||||
PushCommand(member.AccountID, models.UnifiedCommand{
|
||||
Action: "calls.new",
|
||||
Payload: call,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return call, nil
|
||||
}
|
||||
|
||||
func EndCall(call models.Call) (models.Call, error) {
|
||||
call.EndedAt = lo.ToPtr(time.Now())
|
||||
|
||||
if _, err := Lk.DeleteRoom(context.Background(), &livekit.DeleteRoomRequest{
|
||||
Room: call.ExternalID,
|
||||
}); err != nil {
|
||||
log.Error().Err(err).Msg("Unable to delete room at livekit side")
|
||||
}
|
||||
|
||||
var members []models.ChannelMember
|
||||
if err := database.C.Save(&call).Error; err != nil {
|
||||
return call, err
|
||||
} else if err = database.C.Where(models.ChannelMember{
|
||||
ChannelID: call.ChannelID,
|
||||
}).Preload("Account").Find(&members).Error; err == nil {
|
||||
call, _ = GetCall(call.Channel, call.ID)
|
||||
for _, member := range members {
|
||||
PushCommand(member.AccountID, models.UnifiedCommand{
|
||||
Action: "calls.end",
|
||||
Payload: call,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return call, nil
|
||||
}
|
||||
|
||||
func EncodeCallToken(user models.Account, call models.Call) (string, error) {
|
||||
isAdmin := false
|
||||
if user.ID == call.FounderID || user.ID == call.Channel.AccountID {
|
||||
isAdmin = true
|
||||
}
|
||||
|
||||
grant := &auth.VideoGrant{
|
||||
Room: call.ExternalID,
|
||||
RoomJoin: true,
|
||||
RoomAdmin: isAdmin,
|
||||
}
|
||||
|
||||
metadata, _ := jsoniter.Marshal(user)
|
||||
|
||||
duration := time.Second * time.Duration(viper.GetInt("calling.token_duration"))
|
||||
tk := auth.NewAccessToken(viper.GetString("calling.api_key"), viper.GetString("calling.api_secret"))
|
||||
tk.AddGrant(grant).
|
||||
SetIdentity(user.Name).
|
||||
SetName(user.Nick).
|
||||
SetMetadata(string(metadata)).
|
||||
SetValidFor(duration)
|
||||
|
||||
return tk.ToJWT()
|
||||
}
|
77
pkg/internal/services/channel_members.go
Normal file
77
pkg/internal/services/channel_members.go
Normal file
@ -0,0 +1,77 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/internal/database"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/internal/models"
|
||||
)
|
||||
|
||||
func ListChannelMember(channelId uint) ([]models.ChannelMember, error) {
|
||||
var members []models.ChannelMember
|
||||
|
||||
if err := database.C.
|
||||
Where(&models.ChannelMember{ChannelID: channelId}).
|
||||
Preload("Account").
|
||||
Find(&members).Error; err != nil {
|
||||
return members, err
|
||||
}
|
||||
|
||||
return members, nil
|
||||
}
|
||||
|
||||
func GetChannelMember(user models.Account, channelId uint) (models.ChannelMember, error) {
|
||||
var member models.ChannelMember
|
||||
|
||||
if err := database.C.
|
||||
Where(&models.ChannelMember{AccountID: user.ID, ChannelID: channelId}).
|
||||
Find(&member).Error; err != nil {
|
||||
return member, err
|
||||
}
|
||||
|
||||
return member, nil
|
||||
}
|
||||
|
||||
func AddChannelMemberWithCheck(user models.Account, target models.Channel) error {
|
||||
if _, err := GetAccountFriend(user.ID, target.AccountID, 1); err != nil {
|
||||
return fmt.Errorf("you only can invite your friends to your channel")
|
||||
}
|
||||
|
||||
member := models.ChannelMember{
|
||||
ChannelID: target.ID,
|
||||
AccountID: user.ID,
|
||||
}
|
||||
|
||||
err := database.C.Save(&member).Error
|
||||
return err
|
||||
}
|
||||
|
||||
func AddChannelMember(user models.Account, target models.Channel) error {
|
||||
member := models.ChannelMember{
|
||||
ChannelID: target.ID,
|
||||
AccountID: user.ID,
|
||||
}
|
||||
|
||||
err := database.C.Save(&member).Error
|
||||
return err
|
||||
}
|
||||
|
||||
func EditChannelMember(membership models.ChannelMember) (models.ChannelMember, error) {
|
||||
if err := database.C.Save(&membership).Error; err != nil {
|
||||
return membership, err
|
||||
}
|
||||
return membership, nil
|
||||
}
|
||||
|
||||
func RemoveChannelMember(user models.Account, target models.Channel) error {
|
||||
var member models.ChannelMember
|
||||
|
||||
if err := database.C.Where(&models.ChannelMember{
|
||||
ChannelID: target.ID,
|
||||
AccountID: user.ID,
|
||||
}).First(&member).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return database.C.Delete(&member).Error
|
||||
}
|
177
pkg/internal/services/channels.go
Normal file
177
pkg/internal/services/channels.go
Normal file
@ -0,0 +1,177 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/internal/database"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/internal/models"
|
||||
"github.com/samber/lo"
|
||||
"github.com/spf13/viper"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func GetChannelAliasAvailability(alias string) error {
|
||||
if !regexp.MustCompile("^[a-z0-9-]+$").MatchString(alias) {
|
||||
return fmt.Errorf("channel alias should only contains lowercase letters, numbers, and hyphens")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetChannel(id uint) (models.Channel, error) {
|
||||
var channel models.Channel
|
||||
tx := database.C.Where(models.Channel{
|
||||
BaseModel: models.BaseModel{ID: id},
|
||||
}).Preload("Account").Preload("Realm")
|
||||
tx = PreloadDirectChannelMembers(tx)
|
||||
if err := tx.First(&channel).Error; err != nil {
|
||||
return channel, err
|
||||
}
|
||||
|
||||
return channel, nil
|
||||
}
|
||||
|
||||
func GetChannelWithAlias(alias string, realmId ...uint) (models.Channel, error) {
|
||||
var channel models.Channel
|
||||
tx := database.C.Where(models.Channel{Alias: alias}).Preload("Account").Preload("Realm")
|
||||
if len(realmId) > 0 {
|
||||
tx = tx.Where("realm_id = ?", realmId)
|
||||
} else {
|
||||
tx = tx.Where("realm_id IS NULL")
|
||||
}
|
||||
tx = PreloadDirectChannelMembers(tx)
|
||||
if err := tx.First(&channel).Error; err != nil {
|
||||
return channel, err
|
||||
}
|
||||
|
||||
return channel, nil
|
||||
}
|
||||
|
||||
func GetAvailableChannelWithAlias(alias string, user models.Account, realmId ...uint) (models.Channel, models.ChannelMember, error) {
|
||||
var err error
|
||||
var member models.ChannelMember
|
||||
var channel models.Channel
|
||||
if channel, err = GetChannelWithAlias(alias, realmId...); err != nil {
|
||||
return channel, member, err
|
||||
}
|
||||
|
||||
if err := database.C.Where(models.ChannelMember{
|
||||
AccountID: user.ID,
|
||||
ChannelID: channel.ID,
|
||||
}).First(&member).Error; err != nil {
|
||||
return channel, member, fmt.Errorf("channel principal not found: %v", err.Error())
|
||||
}
|
||||
|
||||
return channel, member, nil
|
||||
}
|
||||
|
||||
func GetAvailableChannel(id uint, user models.Account) (models.Channel, models.ChannelMember, error) {
|
||||
var err error
|
||||
var member models.ChannelMember
|
||||
var channel models.Channel
|
||||
if channel, err = GetChannel(id); err != nil {
|
||||
return channel, member, err
|
||||
}
|
||||
tx := database.C.Where(models.ChannelMember{
|
||||
AccountID: user.ID,
|
||||
ChannelID: channel.ID,
|
||||
})
|
||||
if err := tx.First(&member).Error; err != nil {
|
||||
return channel, member, fmt.Errorf("channel principal not found: %v", err.Error())
|
||||
}
|
||||
|
||||
return channel, member, nil
|
||||
}
|
||||
|
||||
func PreloadDirectChannelMembers(tx *gorm.DB) *gorm.DB {
|
||||
return tx.Preload("Members", func(db *gorm.DB) *gorm.DB {
|
||||
return db.Joins(
|
||||
fmt.Sprintf(
|
||||
"JOIN %schannels AS c ON c.type = ?",
|
||||
viper.GetString("database.prefix"),
|
||||
),
|
||||
models.ChannelTypeDirect,
|
||||
)
|
||||
}).Preload("Members.Account")
|
||||
}
|
||||
|
||||
func ListChannel(realmId ...uint) ([]models.Channel, error) {
|
||||
var channels []models.Channel
|
||||
tx := database.C.Preload("Account").Preload("Realm")
|
||||
if len(realmId) > 0 {
|
||||
tx = tx.Where("realm_id = ?", realmId)
|
||||
}
|
||||
|
||||
tx = PreloadDirectChannelMembers(tx)
|
||||
|
||||
if err := tx.Find(&channels).Error; err != nil {
|
||||
return channels, err
|
||||
}
|
||||
|
||||
return channels, nil
|
||||
}
|
||||
|
||||
func ListChannelWithUser(user models.Account, realmId ...uint) ([]models.Channel, error) {
|
||||
var channels []models.Channel
|
||||
tx := database.C.Where(&models.Channel{AccountID: user.ID}).Preload("Realm")
|
||||
if len(realmId) > 0 {
|
||||
tx = tx.Where("realm_id = ?", realmId)
|
||||
}
|
||||
|
||||
tx = PreloadDirectChannelMembers(tx)
|
||||
|
||||
if err := tx.Find(&channels).Error; err != nil {
|
||||
return channels, err
|
||||
}
|
||||
|
||||
return channels, nil
|
||||
}
|
||||
|
||||
func ListAvailableChannel(user models.Account, realmId ...uint) ([]models.Channel, error) {
|
||||
var channels []models.Channel
|
||||
var members []models.ChannelMember
|
||||
if err := database.C.Where(&models.ChannelMember{
|
||||
AccountID: user.ID,
|
||||
}).Find(&members).Error; err != nil {
|
||||
return channels, err
|
||||
}
|
||||
|
||||
idx := lo.Map(members, func(item models.ChannelMember, index int) uint {
|
||||
return item.ChannelID
|
||||
})
|
||||
|
||||
tx := database.C.Preload("Realm").Where("id IN ?", idx)
|
||||
if len(realmId) > 0 {
|
||||
tx = tx.Where("realm_id = ?", realmId)
|
||||
}
|
||||
|
||||
tx = PreloadDirectChannelMembers(tx)
|
||||
|
||||
if err := tx.Find(&channels).Error; err != nil {
|
||||
return channels, err
|
||||
}
|
||||
|
||||
return channels, nil
|
||||
}
|
||||
|
||||
func NewChannel(channel models.Channel) (models.Channel, error) {
|
||||
err := database.C.Save(&channel).Error
|
||||
return channel, err
|
||||
}
|
||||
|
||||
func EditChannel(channel models.Channel, alias, name, description string, isEncrypted bool) (models.Channel, error) {
|
||||
channel.Alias = alias
|
||||
channel.Name = name
|
||||
channel.Description = description
|
||||
if !channel.IsEncrypted {
|
||||
channel.IsEncrypted = isEncrypted
|
||||
}
|
||||
|
||||
err := database.C.Save(&channel).Error
|
||||
|
||||
return channel, err
|
||||
}
|
||||
|
||||
func DeleteChannel(channel models.Channel) error {
|
||||
return database.C.Delete(&channel).Error
|
||||
}
|
25
pkg/internal/services/cleaner.go
Normal file
25
pkg/internal/services/cleaner.go
Normal file
@ -0,0 +1,25 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/internal/database"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func DoAutoDatabaseCleanup() {
|
||||
deadline := time.Now().Add(60 * time.Minute)
|
||||
log.Debug().Time("deadline", deadline).Msg("Now cleaning up entire database...")
|
||||
|
||||
// Deal soft-deletion
|
||||
var count int64
|
||||
for _, model := range database.DatabaseAutoActionRange {
|
||||
tx := database.C.Unscoped().Delete(model, "deleted_at >= ?", deadline)
|
||||
if tx.Error != nil {
|
||||
log.Error().Err(tx.Error).Msg("An error occurred when running database cleanup...")
|
||||
}
|
||||
count += tx.RowsAffected
|
||||
}
|
||||
|
||||
log.Debug().Int64("affected", count).Msg("Clean up entire database accomplished.")
|
||||
}
|
47
pkg/internal/services/connections.go
Normal file
47
pkg/internal/services/connections.go
Normal file
@ -0,0 +1,47 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/internal/models"
|
||||
"github.com/gofiber/contrib/websocket"
|
||||
)
|
||||
|
||||
var (
|
||||
wsMutex sync.Mutex
|
||||
wsConn = make(map[uint]map[*websocket.Conn]bool)
|
||||
)
|
||||
|
||||
func ClientRegister(user models.Account, conn *websocket.Conn) {
|
||||
wsMutex.Lock()
|
||||
if wsConn[user.ID] == nil {
|
||||
wsConn[user.ID] = make(map[*websocket.Conn]bool)
|
||||
}
|
||||
wsConn[user.ID][conn] = true
|
||||
wsMutex.Unlock()
|
||||
}
|
||||
|
||||
func ClientUnregister(user models.Account, conn *websocket.Conn) {
|
||||
wsMutex.Lock()
|
||||
if wsConn[user.ID] == nil {
|
||||
wsConn[user.ID] = make(map[*websocket.Conn]bool)
|
||||
}
|
||||
delete(wsConn[user.ID], conn)
|
||||
wsMutex.Unlock()
|
||||
}
|
||||
|
||||
func PushCommand(userId uint, task models.UnifiedCommand) {
|
||||
for conn := range wsConn[userId] {
|
||||
_ = conn.WriteMessage(1, task.Marshal())
|
||||
}
|
||||
}
|
||||
|
||||
func DealCommand(task models.UnifiedCommand, user models.Account) *models.UnifiedCommand {
|
||||
switch task.Action {
|
||||
default:
|
||||
return &models.UnifiedCommand{
|
||||
Action: "error",
|
||||
Message: "command not found",
|
||||
}
|
||||
}
|
||||
}
|
12
pkg/internal/services/encryptor.go
Normal file
12
pkg/internal/services/encryptor.go
Normal 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
|
||||
}
|
81
pkg/internal/services/jwt.go
Normal file
81
pkg/internal/services/jwt.go
Normal 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 = "identity_auth_key"
|
||||
CookieRefreshKey = "identity_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: "/",
|
||||
})
|
||||
}
|
18
pkg/internal/services/livekit.go
Normal file
18
pkg/internal/services/livekit.go
Normal file
@ -0,0 +1,18 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
lksdk "github.com/livekit/server-sdk-go"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var Lk *lksdk.RoomServiceClient
|
||||
|
||||
func SetupLiveKit() {
|
||||
host := "https://" + viper.GetString("calling.endpoint")
|
||||
|
||||
Lk = lksdk.NewRoomServiceClient(
|
||||
host,
|
||||
viper.GetString("calling.api_key"),
|
||||
viper.GetString("calling.api_secret"),
|
||||
)
|
||||
}
|
51
pkg/internal/services/mailer.go
Normal file
51
pkg/internal/services/mailer.go
Normal 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")},
|
||||
)
|
||||
}
|
181
pkg/internal/services/messages.go
Normal file
181
pkg/internal/services/messages.go
Normal file
@ -0,0 +1,181 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/internal/database"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/internal/models"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
func CountMessage(channel models.Channel) int64 {
|
||||
var count int64
|
||||
if err := database.C.Where(models.Message{
|
||||
ChannelID: channel.ID,
|
||||
}).Model(&models.Message{}).Count(&count).Error; err != nil {
|
||||
return 0
|
||||
} else {
|
||||
return count
|
||||
}
|
||||
}
|
||||
|
||||
func ListMessage(channel models.Channel, take int, offset int) ([]models.Message, error) {
|
||||
if take > 100 {
|
||||
take = 100
|
||||
}
|
||||
|
||||
var messages []models.Message
|
||||
if err := database.C.
|
||||
Where(models.Message{
|
||||
ChannelID: channel.ID,
|
||||
}).Limit(take).Offset(offset).
|
||||
Order("created_at DESC").
|
||||
Preload("ReplyTo").
|
||||
Preload("ReplyTo.Sender").
|
||||
Preload("ReplyTo.Sender.Account").
|
||||
Preload("Sender").
|
||||
Preload("Sender.Account").
|
||||
Find(&messages).Error; err != nil {
|
||||
return messages, err
|
||||
} else {
|
||||
return messages, nil
|
||||
}
|
||||
}
|
||||
|
||||
func GetMessage(channel models.Channel, id uint) (models.Message, error) {
|
||||
var message models.Message
|
||||
if err := database.C.
|
||||
Where(models.Message{
|
||||
BaseModel: models.BaseModel{ID: id},
|
||||
ChannelID: channel.ID,
|
||||
}).
|
||||
Preload("ReplyTo").
|
||||
Preload("ReplyTo.Sender").
|
||||
Preload("ReplyTo.Sender.Account").
|
||||
Preload("Sender").
|
||||
Preload("Sender.Account").
|
||||
First(&message).Error; err != nil {
|
||||
return message, err
|
||||
} else {
|
||||
return message, nil
|
||||
}
|
||||
}
|
||||
|
||||
func GetMessageWithPrincipal(channel models.Channel, member models.ChannelMember, id uint) (models.Message, error) {
|
||||
var message models.Message
|
||||
if err := database.C.Where(models.Message{
|
||||
BaseModel: models.BaseModel{ID: id},
|
||||
ChannelID: channel.ID,
|
||||
SenderID: member.ID,
|
||||
}).First(&message).Error; err != nil {
|
||||
return message, err
|
||||
} else {
|
||||
return message, nil
|
||||
}
|
||||
}
|
||||
|
||||
func NewMessage(message models.Message) (models.Message, error) {
|
||||
var members []models.ChannelMember
|
||||
if err := database.C.Save(&message).Error; err != nil {
|
||||
return message, err
|
||||
} else if err = database.C.Where(models.ChannelMember{
|
||||
ChannelID: message.ChannelID,
|
||||
}).Preload("Account").Find(&members).Error; err == nil {
|
||||
channel := message.Channel
|
||||
message, _ = GetMessage(message.Channel, message.ID)
|
||||
for _, member := range members {
|
||||
PushCommand(member.AccountID, models.UnifiedCommand{
|
||||
Action: "messages.new",
|
||||
Payload: message,
|
||||
})
|
||||
|
||||
if member.ID != message.SenderID {
|
||||
switch member.Notify {
|
||||
case models.NotifyLevelNone:
|
||||
continue
|
||||
case models.NotifyLevelMentioned:
|
||||
if message.ReplyTo != nil && member.ID == message.ReplyTo.SenderID {
|
||||
break
|
||||
}
|
||||
if val, ok := message.Content["mentioned_users"]; ok {
|
||||
if usernames, ok := val.([]string); ok {
|
||||
if lo.Contains(usernames, member.Account.Name) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
var displayText string
|
||||
if message.Content["algorithm"] == "plain" {
|
||||
displayText, _ = message.Content["value"].(string)
|
||||
} else {
|
||||
displayText = "*encrypted message*"
|
||||
}
|
||||
|
||||
if len(displayText) == 0 {
|
||||
displayText = fmt.Sprintf("%d attachment(s)", len(message.Attachments))
|
||||
}
|
||||
|
||||
err = NotifyAccountMessager(member.Account,
|
||||
"incomingMessage",
|
||||
fmt.Sprintf("%s in #%s", message.Sender.Account.Nick, channel.Alias),
|
||||
fmt.Sprintf("%s", displayText),
|
||||
true,
|
||||
false,
|
||||
)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("An error occurred when trying notify user.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return message, nil
|
||||
}
|
||||
|
||||
func EditMessage(message models.Message) (models.Message, error) {
|
||||
var members []models.ChannelMember
|
||||
if err := database.C.Save(&message).Error; err != nil {
|
||||
return message, err
|
||||
} else if err = database.C.Where(models.ChannelMember{
|
||||
ChannelID: message.ChannelID,
|
||||
}).Find(&members).Error; err == nil {
|
||||
message, _ = GetMessage(models.Channel{
|
||||
BaseModel: models.BaseModel{ID: message.Channel.ID},
|
||||
}, message.ID)
|
||||
for _, member := range members {
|
||||
PushCommand(member.AccountID, models.UnifiedCommand{
|
||||
Action: "messages.update",
|
||||
Payload: message,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return message, nil
|
||||
}
|
||||
|
||||
func DeleteMessage(message models.Message) (models.Message, error) {
|
||||
prev, _ := GetMessage(models.Channel{
|
||||
BaseModel: models.BaseModel{ID: message.Channel.ID},
|
||||
}, message.ID)
|
||||
|
||||
var members []models.ChannelMember
|
||||
if err := database.C.Delete(&message).Error; err != nil {
|
||||
return message, err
|
||||
} else if err = database.C.Where(models.ChannelMember{
|
||||
ChannelID: message.ChannelID,
|
||||
}).Find(&members).Error; err == nil {
|
||||
for _, member := range members {
|
||||
PushCommand(member.AccountID, models.UnifiedCommand{
|
||||
Action: "messages.burnt",
|
||||
Payload: prev,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return message, nil
|
||||
}
|
112
pkg/internal/services/realms.go
Normal file
112
pkg/internal/services/realms.go
Normal file
@ -0,0 +1,112 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/internal/database"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/internal/gap"
|
||||
"git.solsynth.dev/hydrogen/messaging/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
|
||||
}
|
BIN
pkg/internal/views/favicon.png
Normal file
BIN
pkg/internal/views/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 72 KiB |
28
pkg/internal/views/index.gohtml
Normal file
28
pkg/internal/views/index.gohtml
Normal 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>
|
60
pkg/internal/views/open.gohtml
Normal file
60
pkg/internal/views/open.gohtml
Normal 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>
|
Reference in New Issue
Block a user