🚚 Rename http package to web

This commit is contained in:
2025-03-29 14:45:12 +08:00
parent ae2c141efa
commit c24ed1e7e6
17 changed files with 39 additions and 39 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/hypernet/messaging/pkg/internal/database"
"git.solsynth.dev/hypernet/messaging/pkg/internal/web/exts"
"git.solsynth.dev/hypernet/messaging/pkg/internal/models"
"git.solsynth.dev/hypernet/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,267 @@
package api
import (
"git.solsynth.dev/hypernet/messaging/pkg/internal/gap"
"git.solsynth.dev/hypernet/messaging/pkg/internal/web/exts"
"git.solsynth.dev/hypernet/nexus/pkg/nex/cruda"
"git.solsynth.dev/hypernet/nexus/pkg/nex/sec"
"git.solsynth.dev/hypernet/passport/pkg/authkit"
authm "git.solsynth.dev/hypernet/passport/pkg/authkit/models"
"strconv"
"git.solsynth.dev/hypernet/messaging/pkg/internal/database"
"git.solsynth.dev/hypernet/messaging/pkg/internal/models"
"git.solsynth.dev/hypernet/messaging/pkg/internal/services"
"github.com/gofiber/fiber/v2"
)
func listChannelMembers(c *fiber.Ctx) error {
alias := c.Params("channel")
take := c.QueryInt("take", 0)
offset := c.QueryInt("offset", 0)
var err error
var channel models.Channel
if val, ok := c.Locals("realm").(authm.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())
}
count, err := services.CountChannelMember(channel.ID)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
if members, err := services.ListChannelMember(channel.ID, take, offset); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
} else {
return c.JSON(fiber.Map{
"count": count,
"data": members,
})
}
}
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 {
Related string `json:"related" 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 !channel.IsPublic {
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 err error
var account authm.Account
var numericId int
if numericId, err = strconv.Atoi(data.Related); err == nil {
account, err = authkit.GetUser(gap.Nx, uint(numericId))
} else {
account, err = authkit.GetUserByName(gap.Nx, data.Related)
}
if err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
}
if err := services.AddChannelMemberWithCheck(account, user, 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")
memberId := c.Params("memberId")
numericId, err := strconv.Atoi(memberId)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "invalid member id")
}
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")
} else if channel.AccountID == user.ID {
return fiber.NewError(fiber.StatusBadRequest, "you cannot remove yourself from your own channel")
}
var member models.ChannelMember
if me, err := services.GetChannelMember(user, channel.ID); err != nil {
return fiber.NewError(fiber.StatusForbidden, err.Error())
} else if me.PowerLevel < 50 {
return fiber.NewError(fiber.StatusForbidden, "you must be a moderator of a channel to remove member from it")
}
if err := database.C.Where(&models.ChannelMember{
BaseModel: cruda.BaseModel{ID: uint(numericId)},
ChannelID: channel.ID,
}).First(&member).Error; err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
}
if err := services.RemoveChannelMember(member, channel); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
} else {
return c.SendStatus(fiber.StatusOK)
}
}
func deleteChannelIdentity(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 channel models.Channel
if val, ok := c.Locals("realm").(authm.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())
}
if err = services.RemoveChannelMember(membership, channel); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
} else {
return c.JSON(membership)
}
}
func editChannelIdentity(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"`
}
if err := exts.BindAndValidate(c, &data); err != nil {
return err
}
var err error
var channel models.Channel
if val, ok := c.Locals("realm").(authm.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
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 editChannelNotifyLevel(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 {
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").(authm.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 membership, err := services.EditChannelMember(membership); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
} else {
return c.JSON(membership)
}
}

View File

@ -0,0 +1,348 @@
package api
import (
"fmt"
"git.solsynth.dev/hypernet/messaging/pkg/internal/gap"
"git.solsynth.dev/hypernet/nexus/pkg/nex/sec"
"git.solsynth.dev/hypernet/passport/pkg/authkit"
authm "git.solsynth.dev/hypernet/passport/pkg/authkit/models"
"git.solsynth.dev/hypernet/messaging/pkg/internal/web/exts"
"git.solsynth.dev/hypernet/messaging/pkg/internal/database"
"git.solsynth.dev/hypernet/messaging/pkg/internal/models"
"git.solsynth.dev/hypernet/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").(authm.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 {
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").(authm.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 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").(authm.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 listPublicChannel(c *fiber.Ctx) error {
var err error
var channels []models.Channel
if val, ok := c.Locals("realm").(authm.Realm); ok {
channels, err = services.ListChannelPublic(val.ID)
} else {
channels, err = services.ListChannelPublic()
}
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").(authm.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 listOwnedChannelGlobalWide(c *fiber.Ctx) error {
if err := sec.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("user").(authm.Account)
channels, err := services.ListChannelWithUser(user, 0)
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").(authm.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 listAvailableChannelGlobalWide(c *fiber.Ctx) error {
if err := sec.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("user").(authm.Account)
channels, err := services.ListAvailableChannel(database.C, user, 0)
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 *authm.Realm
if val, ok := c.Locals("realm").(authm.Realm); ok {
if info, err := authkit.GetRealmMember(gap.Nx, 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.PowerLevel < 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"`
NewBelongsRealm *string `json:"new_belongs_realm"`
}
if err := exts.BindAndValidate(c, &data); err != nil {
return err
}
tx := database.C.Where("id = ?", id)
if val, ok := c.Locals("realm").(authm.Realm); ok {
if info, err := authkit.GetRealmMember(gap.Nx, 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.PowerLevel < 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")
}
}
if data.NewBelongsRealm != nil {
if *data.NewBelongsRealm == "global" {
channel.RealmID = nil
} else {
realm, err := authkit.GetRealmByAlias(gap.Nx, *data.NewBelongsRealm)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("requested channel with realm, but realm was not found: %v", err))
} else {
if info, err := authkit.GetRealmMember(gap.Nx, realm.ID, user.ID); err != nil {
return fiber.NewError(fiber.StatusForbidden, "you must be a part of that realm then can transfer channel related to it")
} else if info.PowerLevel < 50 {
return fiber.NewError(fiber.StatusForbidden, "you must be a moderator of that realm then can transfer channel related to it")
} else {
channel.RealmID = &realm.ID
}
}
}
}
channel.Alias = data.Alias
channel.Name = data.Name
channel.Description = data.Description
channel.IsPublic = data.IsPublic
channel.IsCommunity = data.IsCommunity
channel, err := services.EditChannel(channel)
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("id = ?", id)
if val, ok := c.Locals("realm").(authm.Realm); ok {
if info, err := authkit.GetRealmMember(gap.Nx, 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.PowerLevel < 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,84 @@
package api
import (
"fmt"
"git.solsynth.dev/hypernet/messaging/pkg/internal/gap"
"git.solsynth.dev/hypernet/nexus/pkg/nex/sec"
"git.solsynth.dev/hypernet/passport/pkg/authkit"
authm "git.solsynth.dev/hypernet/passport/pkg/authkit/models"
"git.solsynth.dev/hypernet/messaging/pkg/internal/web/exts"
"git.solsynth.dev/hypernet/messaging/pkg/internal/models"
"git.solsynth.dev/hypernet/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"`
}
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 *authm.Realm
if val, ok := c.Locals("realm").(authm.Realm); ok {
if info, err := authkit.GetRealmMember(gap.Nx, 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.PowerLevel < 50 {
return fiber.NewError(fiber.StatusForbidden, "you must be a moderator of that realm then can create channel related to it")
} else {
realm = &val
}
}
relatedUser, err := authkit.GetUser(gap.Nx, data.RelatedUser)
if 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))
}
if err := authkit.EnsureUserPermGranted(gap.Nx, user.ID, relatedUser.ID, "ChannelAdd", true); err != nil {
return fmt.Errorf("unable to add user into your channel due to access denied: %v", err)
}
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,168 @@
package api
import (
"fmt"
"git.solsynth.dev/hypernet/messaging/pkg/internal/database"
"git.solsynth.dev/hypernet/nexus/pkg/nex/sec"
authm "git.solsynth.dev/hypernet/passport/pkg/authkit/models"
"github.com/samber/lo"
"git.solsynth.dev/hypernet/messaging/pkg/internal/web/exts"
"git.solsynth.dev/hypernet/messaging/pkg/internal/models"
"git.solsynth.dev/hypernet/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").(authm.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.ID, 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").(authm.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 checkHasNewEvent(c *fiber.Ctx) error {
if err := sec.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("user").(authm.Account)
pivot := c.QueryInt("pivot", 0)
alias := c.Params("channel")
if pivot < 1 {
return fiber.NewError(fiber.StatusBadRequest, "pivot must be greater than zero")
}
var err error
var channel models.Channel
if val, ok := c.Locals("realm").(authm.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))
}
var count int64
if err = database.C.
Where("channel_id = ?", channel.ID).
Where("id > ?", pivot).
Model(&models.Event{}).Count(&count).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
} else {
return c.JSON(fiber.Map{
"up_to_date": lo.Ternary(count > 0, false, true),
"count": count,
})
}
}
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").(authm.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,160 @@
package api
import (
"git.solsynth.dev/hypernet/nexus/pkg/nex/sec"
authm "git.solsynth.dev/hypernet/passport/pkg/authkit/models"
"strings"
"git.solsynth.dev/hypernet/messaging/pkg/internal/web/exts"
"git.solsynth.dev/hypernet/messaging/pkg/internal/models"
"git.solsynth.dev/hypernet/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").(authm.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,
QuoteEventID: data.Body.QuoteEventID,
RelatedEventID: data.Body.RelatedEventID,
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").(authm.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").(authm.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,59 @@
package api
import (
"github.com/gofiber/fiber/v2"
)
func MapAPIs(app *fiber.App, baseURL string) {
api := app.Group(baseURL).Name("API")
{
quick := api.Group("/quick")
{
quick.Post("/:channelId/reply/:eventId", quickReply)
}
api.Get("/channels/me", listOwnedChannelGlobalWide)
api.Get("/channels/me/available", listAvailableChannelGlobalWide)
channels := api.Group("/channels/:realm").Use(realmMiddleware).Name("Channels API")
{
channels.Get("/", listChannel)
channels.Get("/public", listPublicChannel)
channels.Get("/me", listOwnedChannel)
channels.Get("/me/available", listAvailableChannel)
channels.Get("/:channel", getChannel)
channels.Get("/:channel/me", getChannelIdentity)
channels.Get("/:channel/members/me", getChannelIdentity)
channels.Put("/:channel/me", editChannelIdentity)
channels.Put("/:channel/me/notify", editChannelNotifyLevel)
channels.Put("/:channel/members/me/notify", editChannelNotifyLevel)
channels.Delete("/:channel/me", deleteChannelIdentity)
channels.Post("/", createChannel)
channels.Post("/dm", createDirectChannel)
channels.Put("/:channelId", editChannel)
channels.Delete("/:channelId", deleteChannel)
channels.Get("/:channel/members", listChannelMembers)
channels.Post("/:channel/members", addChannelMember)
channels.Delete("/:channel/members/:memberId", removeChannelMember)
channels.Get("/:channel/events", listEvent)
channels.Get("/:channel/events/update", checkHasNewEvent)
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,79 @@
package api
import (
"fmt"
"strings"
"git.solsynth.dev/hypernet/messaging/pkg/internal/web/exts"
"git.solsynth.dev/hypernet/messaging/pkg/internal/models"
"git.solsynth.dev/hypernet/messaging/pkg/internal/services"
"github.com/gofiber/fiber/v2"
"github.com/google/uuid"
jsoniter "github.com/json-iterator/go"
"github.com/samber/lo"
)
// quickReply is a simplified API for replying to a message
// It used in the iOS notification action and others
// It did not support all the features of the message event
// But it just works
func quickReply(c *fiber.Ctx) error {
replyTk := c.Query("replyToken")
if len(replyTk) == 0 {
return fiber.NewError(fiber.StatusBadRequest, "reply token is required")
}
claims, err := services.ParseReplyToken(replyTk)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("reply token is invaild: %v", err))
}
channelId, _ := c.ParamsInt("channelId", 0)
eventId, _ := c.ParamsInt("eventId", 0)
if claims.EventID != uint(eventId) {
return fiber.NewError(fiber.StatusBadRequest, "reply token is invaild, event id mismatch")
}
var data struct {
Type string `json:"type" validate:"required"`
Body models.EventMessageBody `json:"body"`
}
if err := exts.BindAndValidate(c, &data); err != nil {
return err
} else {
data.Body.QuoteEventID = lo.ToPtr(uint(eventId))
}
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")
}
channel, member, err := services.GetChannelIdentityWithID(uint(channelId), claims.UserID)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("channel / member not found: %v", err.Error()))
}
var parsed map[string]any
raw, _ := jsoniter.Marshal(data.Body)
_ = jsoniter.Unmarshal(raw, &parsed)
event, err := services.NewEvent(models.Event{
Uuid: uuid.NewString(),
Body: parsed,
Type: data.Type,
Sender: member,
Channel: channel,
QuoteEventID: data.Body.QuoteEventID,
RelatedEventID: data.Body.RelatedEventID,
ChannelID: channel.ID,
SenderID: member.ID,
})
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
return c.JSON(event)
}

View File

@ -0,0 +1,23 @@
package api
import (
"fmt"
"git.solsynth.dev/hypernet/messaging/pkg/internal/gap"
"git.solsynth.dev/hypernet/passport/pkg/authkit"
"github.com/gofiber/fiber/v2"
)
func realmMiddleware(c *fiber.Ctx) error {
realmAlias := c.Params("realm")
if len(realmAlias) > 0 && realmAlias != "global" {
realm, err := authkit.GetRealmByAlias(gap.Nx, 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,31 @@
package api
import (
"git.solsynth.dev/hypernet/nexus/pkg/nex/sec"
authm "git.solsynth.dev/hypernet/passport/pkg/authkit/models"
"git.solsynth.dev/hypernet/messaging/pkg/internal/database"
"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)
var result []struct {
ChannelID uint `json:"channel_id"`
UnreadMessageCount int `json:"count"`
}
if err := database.C.Table("channel_members cm").
Select("cm.channel_id, COUNT(m.id) AS unread_message_count").
Joins("JOIN events m ON m.channel_id = cm.channel_id").
Where("m.id > cm.reading_anchor AND cm.account_id = ?", user.ID).
Group("cm.channel_id").
Scan(&result).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
return c.JSON(result)
}

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,75 @@
package web
import (
"strings"
"git.solsynth.dev/hypernet/nexus/pkg/nex/sec"
"git.solsynth.dev/hypernet/passport/pkg/authkit"
"git.solsynth.dev/hypernet/messaging/pkg/internal/web/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,
ReadBufferSize: 5 * 1024 * 1024, // 5MB for large JWT
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...")
}
}