♻️ Move dealer to nexus

This commit is contained in:
2024-11-02 13:23:27 +08:00
parent fce8669059
commit cef4764d8c
38 changed files with 454 additions and 550 deletions

View File

@ -0,0 +1,232 @@
package api
import (
"git.solsynth.dev/hypernet/nexus/pkg/nex/sec"
authm "git.solsynth.dev/hypernet/passport/pkg/authkit/models"
"sync"
"git.solsynth.dev/hydrogen/messaging/pkg/internal/database"
"git.solsynth.dev/hydrogen/messaging/pkg/internal/http/exts"
"git.solsynth.dev/hydrogen/messaging/pkg/internal/models"
"git.solsynth.dev/hydrogen/messaging/pkg/internal/services"
"github.com/gofiber/fiber/v2"
"github.com/google/uuid"
"github.com/spf13/viper"
)
var callLocks sync.Map
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 if res, err := services.GetCallParticipants(call); err != nil {
return c.JSON(call)
} else {
call.Participants = res
return c.JSON(call)
}
}
func startCall(c *fiber.Ctx) error {
if err := sec.EnsureGrantedPerm(c, "CreateCalls", true); err != nil {
return err
}
user := c.Locals("user").(authm.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())
} else if membership.PowerLevel < 0 {
return fiber.NewError(fiber.StatusForbidden, "you have not enough permission to create a call")
}
if _, ok := callLocks.Load(channel.ID); ok {
return fiber.NewError(fiber.StatusLocked, "there is already a call in creation progress for this channel")
} else {
callLocks.Store(channel.ID, true)
}
call, err := services.NewCall(channel, membership)
if err != nil {
callLocks.Delete(channel.ID)
return fiber.NewError(fiber.StatusBadRequest, err.Error())
} else {
_, _ = services.NewEvent(models.Event{
Uuid: uuid.NewString(),
Body: map[string]any{},
Type: "calls.start",
Channel: channel,
Sender: membership,
ChannelID: channel.ID,
SenderID: membership.ID,
})
callLocks.Delete(channel.ID)
return c.JSON(call)
}
}
func endCall(c *fiber.Ctx) error {
if err := sec.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("user").(authm.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 != membership.ID && membership.PowerLevel < 50 {
return fiber.NewError(fiber.StatusBadRequest, "only call founder or channel moderator can end this call")
}
if call, err := services.EndCall(call); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
} else {
_, _ = services.NewEvent(models.Event{
Uuid: uuid.NewString(),
Body: map[string]any{"last": call.EndedAt.Unix() - call.CreatedAt.Unix()},
Type: "calls.end",
Channel: channel,
Sender: membership,
ChannelID: channel.ID,
SenderID: membership.ID,
})
return c.JSON(call)
}
}
func kickParticipantInCall(c *fiber.Ctx) error {
if err := sec.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("user").(authm.Account)
alias := c.Params("channel")
var data struct {
Username string `json:"username" 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())
}
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 && membership.PowerLevel < 50 {
return fiber.NewError(fiber.StatusBadRequest, "only call founder or channel admin can kick participant in this call")
}
if err = services.KickParticipantInCall(call, data.Username); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
return c.SendStatus(fiber.StatusOK)
}
func exchangeCallToken(c *fiber.Ctx) error {
if err := sec.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("user").(authm.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"),
})
}
}

View File

@ -0,0 +1,256 @@
package api
import (
"fmt"
"git.solsynth.dev/hypernet/nexus/pkg/nex/sec"
authm "git.solsynth.dev/hypernet/passport/pkg/authkit/models"
"git.solsynth.dev/hydrogen/dealer/pkg/hyper"
"git.solsynth.dev/hydrogen/messaging/pkg/internal/http/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")
if err := sec.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("user").(authm.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 {
if err := sec.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("user").(authm.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 authm.Account
if err := database.C.Where(&hyper.BaseUser{
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 {
if err := sec.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("user").(authm.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 authm.Account
if err := database.C.Where(&hyper.BaseUser{
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 {
if err := sec.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("user").(authm.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.Name = user.Name
membership.Notify = data.NotifyLevel
if len(data.Nick) > 0 {
membership.Nick = data.Nick
} else {
membership.Nick = user.Nick
}
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 {
if err := sec.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("user").(authm.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 && !channel.IsCommunity {
return fiber.NewError(fiber.StatusBadRequest, "you were impossible to join a channel without related realm and non-community")
}
if channel.RealmID != nil {
if realm, err := services.GetRealmWithExtID(channel.Realm.ID); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("invalid channel, related realm was not found: %v", err))
} else if _, err := services.GetRealmMember(realm.ID, user.ID); 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 {
if err := sec.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("user").(authm.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)
}
}

View File

@ -0,0 +1,275 @@
package api
import (
"fmt"
"git.solsynth.dev/hypernet/nexus/pkg/nex/sec"
authm "git.solsynth.dev/hypernet/passport/pkg/authkit/models"
"git.solsynth.dev/hydrogen/dealer/pkg/hyper"
"git.solsynth.dev/hydrogen/messaging/pkg/internal/http/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"
"github.com/samber/lo"
)
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 {
if err := sec.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("user").(authm.Account)
alias := c.Params("channel")
var err error
var member models.ChannelMember
if val, ok := c.Locals("realm").(models.Realm); ok {
_, member, err = services.GetChannelIdentity(alias, user.ID, val)
} else {
_, member, err = services.GetChannelIdentity(alias, user.ID)
}
if err != nil {
return c.SendStatus(fiber.StatusForbidden)
}
return c.JSON(member)
}
func listChannel(c *fiber.Ctx) error {
var user *authm.Account
if err := sec.EnsureAuthenticated(c); err == nil {
user = lo.ToPtr(c.Locals("user").(authm.Account))
}
var err error
var channels []models.Channel
if val, ok := c.Locals("realm").(models.Realm); ok {
channels, err = services.ListChannel(user, val.ID)
} else {
channels, err = services.ListChannel(user)
}
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
return c.JSON(channels)
}
func listOwnedChannel(c *fiber.Ctx) error {
if err := sec.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("user").(authm.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 {
if err := sec.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("user").(authm.Account)
tx := database.C
isDirect := c.QueryBool("direct", false)
if isDirect {
tx = tx.Where("type = ?", models.ChannelTypeDirect)
} else {
tx = tx.Where("type = ?", models.ChannelTypeCommon)
}
var err error
var channels []models.Channel
if val, ok := c.Locals("realm").(models.Realm); ok {
channels, err = services.ListAvailableChannel(tx, user, val.ID)
} else {
channels, err = services.ListAvailableChannel(tx, user)
}
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
return c.JSON(channels)
}
func createChannel(c *fiber.Ctx) error {
if err := sec.EnsureGrantedPerm(c, "CreateChannels", true); err != nil {
return err
}
user := c.Locals("user").(authm.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"`
IsPublic bool `json:"is_public"`
IsCommunity bool `json:"is_community"`
}
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.ID, user.ID); 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,
AccountID: user.ID,
Type: models.ChannelTypeCommon,
IsPublic: data.IsPublic,
IsCommunity: data.IsCommunity,
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 {
if err := sec.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("user").(authm.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"`
IsPublic bool `json:"is_public"`
IsCommunity bool `json:"is_community"`
}
if err := exts.BindAndValidate(c, &data); err != nil {
return err
}
tx := database.C.Where(&models.Channel{BaseModel: hyper.BaseModel{ID: uint(id)}})
if val, ok := c.Locals("realm").(models.Realm); ok {
if info, err := services.GetRealmMember(val.ID, user.ID); 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.IsPublic, data.IsCommunity)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
return c.JSON(channel)
}
func deleteChannel(c *fiber.Ctx) error {
if err := sec.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("user").(authm.Account)
id, _ := c.ParamsInt("channelId", 0)
tx := database.C.Where(&models.Channel{BaseModel: hyper.BaseModel{ID: uint(id)}})
if val, ok := c.Locals("realm").(models.Realm); ok {
if info, err := services.GetRealmMember(val.ID, user.ID); 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)
}

View File

@ -0,0 +1,82 @@
package api
import (
"fmt"
"git.solsynth.dev/hypernet/nexus/pkg/nex/sec"
authm "git.solsynth.dev/hypernet/passport/pkg/authkit/models"
"git.solsynth.dev/hydrogen/messaging/pkg/internal/http/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 {
if err := sec.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("user").(authm.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.ID, user.ID); 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 authm.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))
}
if ch, err := services.GetDirectChannelByUser(user, relatedUser); err == nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("you already have a direct with that user #%d", ch.ID))
}
channel := models.Channel{
Alias: data.Alias,
Name: data.Name,
Description: data.Description,
IsPublic: false,
IsCommunity: false,
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)
}

View File

@ -0,0 +1,127 @@
package api
import (
"fmt"
"git.solsynth.dev/hypernet/nexus/pkg/nex/sec"
authm "git.solsynth.dev/hypernet/passport/pkg/authkit/models"
"git.solsynth.dev/hydrogen/messaging/pkg/internal/http/exts"
"git.solsynth.dev/hydrogen/messaging/pkg/internal/models"
"git.solsynth.dev/hydrogen/messaging/pkg/internal/services"
"github.com/gofiber/fiber/v2"
)
func getEvent(c *fiber.Ctx) error {
if err := sec.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("user").(authm.Account)
alias := c.Params("channel")
id, _ := c.ParamsInt("eventId")
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))
}
event, err := services.GetEvent(channel, uint(id))
if err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
}
return c.JSON(event)
}
func listEvent(c *fiber.Ctx) error {
if err := sec.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("user").(authm.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.CountEvent(channel)
events, err := services.ListEvent(channel, take, offset)
if err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
}
return c.JSON(fiber.Map{
"count": count,
"data": events,
})
}
func newRawEvent(c *fiber.Ctx) error {
if err := sec.EnsureGrantedPerm(c, "CreateMessagingRawEvent", true); err != nil {
return err
}
user := c.Locals("user").(authm.Account)
alias := c.Params("channel")
var data struct {
Uuid string `json:"uuid" validate:"required"`
Type string `json:"type" validate:"required"`
Body map[string]any `json:"body"`
}
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")
}
var err error
var channel models.Channel
var member models.ChannelMember
if val, ok := c.Locals("realm").(models.Realm); ok {
channel, member, err = services.GetChannelIdentity(alias, user.ID, val)
} else {
channel, member, err = services.GetChannelIdentity(alias, user.ID)
}
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")
}
event := models.Event{
Uuid: data.Uuid,
Body: data.Body,
Type: data.Type,
Sender: member,
Channel: channel,
ChannelID: channel.ID,
SenderID: member.ID,
}
if event, err = services.NewEvent(event); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
return c.JSON(event)
}

View File

@ -0,0 +1,158 @@
package api
import (
"git.solsynth.dev/hypernet/nexus/pkg/nex/sec"
authm "git.solsynth.dev/hypernet/passport/pkg/authkit/models"
"strings"
"git.solsynth.dev/hydrogen/messaging/pkg/internal/http/exts"
"git.solsynth.dev/hydrogen/messaging/pkg/internal/models"
"git.solsynth.dev/hydrogen/messaging/pkg/internal/services"
"github.com/gofiber/fiber/v2"
jsoniter "github.com/json-iterator/go"
)
func newMessageEvent(c *fiber.Ctx) error {
if err := sec.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("user").(authm.Account)
alias := c.Params("channel")
var data struct {
Uuid string `json:"uuid" validate:"required"`
Type string `json:"type" validate:"required"`
Body models.EventMessageBody `json:"body"`
}
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")
}
data.Body.Text = strings.TrimSpace(data.Body.Text)
if len(data.Body.Text) == 0 && len(data.Body.Attachments) == 0 {
return fiber.NewError(fiber.StatusBadRequest, "empty message was not allowed")
}
var err error
var channel models.Channel
var member models.ChannelMember
if val, ok := c.Locals("realm").(models.Realm); ok {
channel, member, err = services.GetChannelIdentity(alias, user.ID, val)
} else {
channel, member, err = services.GetChannelIdentity(alias, user.ID)
}
if err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
} else if member.PowerLevel < 0 {
return fiber.NewError(fiber.StatusForbidden, "unable to send message, access denied")
}
var parsed map[string]any
raw, _ := jsoniter.Marshal(data.Body)
_ = jsoniter.Unmarshal(raw, &parsed)
event := models.Event{
Uuid: data.Uuid,
Body: parsed,
Type: data.Type,
Sender: member,
Channel: channel,
ChannelID: channel.ID,
SenderID: member.ID,
}
if event, err = services.NewEvent(event); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
return c.JSON(event)
}
func editMessageEvent(c *fiber.Ctx) error {
if err := sec.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("user").(authm.Account)
alias := c.Params("channel")
messageId, _ := c.ParamsInt("messageId", 0)
var data struct {
Uuid string `json:"uuid" validate:"required"`
Type string `json:"type" validate:"required"`
Body models.EventMessageBody `json:"body"`
}
if err := exts.BindAndValidate(c, &data); err != nil {
return err
}
if len(data.Body.Text) == 0 && len(data.Body.Attachments) == 0 {
return fiber.NewError(fiber.StatusBadRequest, "you cannot send an empty message")
}
var err error
var channel models.Channel
var member models.ChannelMember
if val, ok := c.Locals("realm").(models.Realm); ok {
channel, member, err = services.GetChannelIdentity(alias, user.ID, val)
} else {
channel, member, err = services.GetChannelIdentity(alias, user.ID)
}
if err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
}
var event models.Event
if event, err = services.GetEventWithSender(channel, member, uint(messageId)); err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
}
event, err = services.EditMessage(event, data.Body)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
return c.JSON(event)
}
func deleteMessageEvent(c *fiber.Ctx) error {
if err := sec.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("user").(authm.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.GetChannelIdentity(alias, user.ID, val)
} else {
channel, member, err = services.GetChannelIdentity(alias, user.ID)
}
if err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
}
var event models.Event
if event, err = services.GetEventWithSender(channel, member, uint(messageId)); err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
}
event, err = services.DeleteMessage(event)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
return c.JSON(event)
}

View File

@ -0,0 +1,49 @@
package api
import (
"github.com/gofiber/fiber/v2"
)
func MapAPIs(app *fiber.App, baseURL string) {
api := app.Group(baseURL).Name("API")
{
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/events", listEvent)
channels.Get("/:channel/events/:eventId", getEvent)
channels.Post("/:channel/events", newRawEvent)
channels.Post("/:channel/messages", newMessageEvent)
channels.Put("/:channel/messages/:messageId", editMessageEvent)
channels.Delete("/:channel/messages/:messageId", deleteMessageEvent)
channels.Get("/:channel/calls", listCall)
channels.Get("/:channel/calls/ongoing", getOngoingCall)
channels.Post("/:channel/calls", startCall)
channels.Delete("/:channel/calls/ongoing", endCall)
channels.Delete("/:channel/calls/ongoing/participant", kickParticipantInCall)
channels.Post("/:channel/calls/ongoing/token", exchangeCallToken)
}
api.Get("/whats-new", getWhatsNew)
}
}

View File

@ -0,0 +1,22 @@
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()
}

View File

@ -0,0 +1,70 @@
package api
import (
"fmt"
"git.solsynth.dev/hypernet/nexus/pkg/nex/sec"
authm "git.solsynth.dev/hypernet/passport/pkg/authkit/models"
"git.solsynth.dev/hydrogen/messaging/pkg/internal/database"
"git.solsynth.dev/hydrogen/messaging/pkg/internal/models"
"github.com/gofiber/fiber/v2"
)
func getWhatsNew(c *fiber.Ctx) error {
if err := sec.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("user").(authm.Account)
pivot := c.QueryInt("pivot", 0)
if pivot < 0 {
return fiber.NewError(fiber.StatusBadRequest, "pivot must be greater than zero")
}
take := c.QueryInt("take", 10)
if take > 100 {
take = 100
}
tx := database.C
var lookupRange []uint
var ignoreRange []uint
var channelMembers []models.ChannelMember
if err := database.C.Where("account_id = ?", user.ID).Select("id", "channel_id").Find(&channelMembers).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("unable to get channel identity of you: %v", err))
} else {
for _, member := range channelMembers {
lookupRange = append(lookupRange, member.ChannelID)
ignoreRange = append(ignoreRange, member.ID)
}
}
tx = tx.Where("channel_id IN ?", lookupRange)
tx = tx.Where("sender_id NOT IN ?", ignoreRange)
tx = tx.Where("id > ?", pivot)
countTx := tx
var count int64
if err := countTx.Model(&models.Event{}).Count(&count).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
var items []models.Event
if err := tx.
Limit(take).
Order("created_at DESC").
Preload("Sender").
Preload("Sender.Account").
Preload("Channel").
Preload("Channel.Realm").
Find(&items).Error; err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
return c.JSON(fiber.Map{
"count": count,
"data": items,
})
}

View File

@ -0,0 +1,22 @@
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
}
func ValidateStruct(in any) error {
return validation.Struct(in)
}

View File

@ -0,0 +1,73 @@
package http
import (
"git.solsynth.dev/hypernet/nexus/pkg/nex/sec"
"git.solsynth.dev/hypernet/passport/pkg/authkit"
"strings"
"git.solsynth.dev/hydrogen/messaging/pkg/internal/http/api"
"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"
jsoniter "github.com/json-iterator/go"
"github.com/rs/zerolog/log"
"github.com/spf13/viper"
)
var IReader *sec.InternalTokenReader
type App struct {
app *fiber.App
}
func NewServer() *App {
app := fiber.New(fiber.Config{
DisableStartupMessage: true,
EnableIPValidation: true,
ServerHeader: "Hypernet.Messaging",
AppName: "Hypernet.Messaging",
ProxyHeader: fiber.HeaderXForwardedFor,
JSONEncoder: jsoniter.ConfigCompatibleWithStandardLibrary.Marshal,
JSONDecoder: jsoniter.ConfigCompatibleWithStandardLibrary.Unmarshal,
BodyLimit: 50 * 1024 * 1024,
EnablePrintRoutes: viper.GetBool("debug.print_routes"),
})
app.Use(idempotency.New())
app.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
},
}))
app.Use(logger.New(logger.Config{
Format: "${status} | ${latency} | ${method} ${path}\n",
Output: log.Logger,
}))
app.Use(sec.ContextMiddleware(IReader))
app.Use(authkit.ParseAccountMiddleware)
api.MapAPIs(app, "/api")
return &App{
app: app,
}
}
func (v *App) Listen() {
if err := v.app.Listen(viper.GetString("bind")); err != nil {
log.Fatal().Err(err).Msg("An error occurred when starting http...")
}
}