⬆️ Upgrade to support the latest version Hydrogen Project standard
This commit is contained in:
@ -1,44 +0,0 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
pcpb "git.solsynth.dev/hydrogen/paperclip/pkg/grpc/proto"
|
||||
idpb "git.solsynth.dev/hydrogen/passport/pkg/grpc/proto"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
var Attachments pcpb.AttachmentsClient
|
||||
|
||||
func ConnectPaperclip() error {
|
||||
addr := viper.GetString("paperclip.grpc_endpoint")
|
||||
if conn, err := grpc.Dial(addr, grpc.WithTransportCredentials(insecure.NewCredentials())); err != nil {
|
||||
return err
|
||||
} else {
|
||||
Attachments = pcpb.NewAttachmentsClient(conn)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
Realms idpb.RealmsClient
|
||||
Friendships idpb.FriendshipsClient
|
||||
Notify idpb.NotifyClient
|
||||
Auth idpb.AuthClient
|
||||
)
|
||||
|
||||
func ConnectPassport() error {
|
||||
addr := viper.GetString("passport.grpc_endpoint")
|
||||
if conn, err := grpc.Dial(addr, grpc.WithTransportCredentials(insecure.NewCredentials())); err != nil {
|
||||
return err
|
||||
} else {
|
||||
Realms = idpb.NewRealmsClient(conn)
|
||||
Friendships = idpb.NewFriendshipsClient(conn)
|
||||
Notify = idpb.NewNotifyClient(conn)
|
||||
Auth = idpb.NewAuthClient(conn)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/models"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/internal/models"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
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)
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
package server
|
||||
package api
|
||||
|
||||
import (
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/database"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/models"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/services"
|
||||
"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"
|
||||
)
|
@ -1,11 +1,12 @@
|
||||
package server
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/internal/server/exts"
|
||||
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/database"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/models"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/services"
|
||||
"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"
|
||||
)
|
||||
|
||||
@ -60,7 +61,7 @@ func addChannelMember(c *fiber.Ctx) error {
|
||||
Target string `json:"target" validate:"required"`
|
||||
}
|
||||
|
||||
if err := BindAndValidate(c, &data); err != nil {
|
||||
if err := exts.BindAndValidate(c, &data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -101,7 +102,7 @@ func removeChannelMember(c *fiber.Ctx) error {
|
||||
Target string `json:"target" validate:"required"`
|
||||
}
|
||||
|
||||
if err := BindAndValidate(c, &data); err != nil {
|
||||
if err := exts.BindAndValidate(c, &data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -144,7 +145,7 @@ func editMyChannelMembership(c *fiber.Ctx) error {
|
||||
NotifyLevel int8 `json:"notify_level"`
|
||||
}
|
||||
|
||||
if err := BindAndValidate(c, &data); err != nil {
|
||||
if err := exts.BindAndValidate(c, &data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -1,11 +1,12 @@
|
||||
package server
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/internal/server/exts"
|
||||
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/database"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/models"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/services"
|
||||
"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"
|
||||
)
|
||||
|
||||
@ -103,7 +104,7 @@ func createChannel(c *fiber.Ctx) error {
|
||||
IsEncrypted bool `json:"is_encrypted"`
|
||||
}
|
||||
|
||||
if err := BindAndValidate(c, &data); err != nil {
|
||||
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())
|
||||
@ -155,7 +156,7 @@ func editChannel(c *fiber.Ctx) error {
|
||||
IsEncrypted bool `json:"is_encrypted"`
|
||||
}
|
||||
|
||||
if err := BindAndValidate(c, &data); err != nil {
|
||||
if err := exts.BindAndValidate(c, &data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -1,11 +1,12 @@
|
||||
package server
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/internal/server/exts"
|
||||
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/database"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/models"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/services"
|
||||
"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"
|
||||
)
|
||||
|
||||
@ -20,7 +21,7 @@ func createDirectChannel(c *fiber.Ctx) error {
|
||||
IsEncrypted bool `json:"is_encrypted"`
|
||||
}
|
||||
|
||||
if err := BindAndValidate(c, &data); err != nil {
|
||||
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())
|
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))
|
||||
}
|
||||
}
|
@ -1,10 +1,11 @@
|
||||
package server
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/database"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/models"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/services"
|
||||
"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"
|
||||
)
|
||||
@ -52,7 +53,7 @@ func newMessage(c *fiber.Ctx) error {
|
||||
ReplyTo *uint `json:"reply_to"`
|
||||
}
|
||||
|
||||
if err := BindAndValidate(c, &data); err != nil {
|
||||
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")
|
||||
@ -131,7 +132,7 @@ func editMessage(c *fiber.Ctx) error {
|
||||
ReplyTo *uint `json:"reply_to"`
|
||||
}
|
||||
|
||||
if err := BindAndValidate(c, &data); err != nil {
|
||||
if err := exts.BindAndValidate(c, &data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
package server
|
||||
package api
|
||||
|
||||
import (
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/models"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/services"
|
||||
"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"
|
@ -1,8 +1,8 @@
|
||||
package server
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/services"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/internal/services"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
@ -1,8 +1,8 @@
|
||||
package server
|
||||
package api
|
||||
|
||||
import (
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/database"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/models"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/internal/database"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/internal/models"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
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()
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package server
|
||||
package exts
|
||||
|
||||
import (
|
||||
"github.com/go-playground/validator/v10"
|
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...")
|
||||
}
|
||||
}
|
@ -3,12 +3,12 @@ package services
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/database"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/internal/database"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/internal/gap"
|
||||
"time"
|
||||
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/grpc"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/models"
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/grpc/proto"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/internal/models"
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/proto"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
@ -25,7 +25,11 @@ func GetAccountFriend(userId, relatedId uint, status int) (*proto.FriendshipResp
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
||||
defer cancel()
|
||||
|
||||
return grpc.Friendships.GetFriendship(ctx, &proto.FriendshipTwoSideLookupRequest{
|
||||
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),
|
||||
@ -36,7 +40,11 @@ func NotifyAccountMessager(user models.Account, t, s, c string, realtime bool, f
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
||||
defer cancel()
|
||||
|
||||
_, err := grpc.Notify.NotifyUser(ctx, &proto.NotifyRequest{
|
||||
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),
|
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
|
||||
}
|
@ -1,17 +1,13 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/database"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/grpc"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/models"
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/grpc/proto"
|
||||
"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) {
|
||||
@ -53,24 +49,3 @@ func LinkAccount(userinfo *proto.Userinfo) (models.Account, error) {
|
||||
|
||||
return account, err
|
||||
}
|
||||
|
||||
func Authenticate(atk, rtk string) (models.Account, string, string, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
||||
defer cancel()
|
||||
|
||||
var err error
|
||||
var user models.Account
|
||||
reply, err := grpc.Auth.Authenticate(ctx, &proto.AuthRequest{
|
||||
AccessToken: atk,
|
||||
RefreshToken: &rtk,
|
||||
})
|
||||
if err != nil {
|
||||
return user, reply.GetAccessToken(), reply.GetRefreshToken(), err
|
||||
} else if !reply.IsValid {
|
||||
return user, reply.GetAccessToken(), reply.GetRefreshToken(), fmt.Errorf("invalid authorization context")
|
||||
}
|
||||
|
||||
user, err = LinkAccount(reply.Userinfo)
|
||||
|
||||
return user, reply.GetAccessToken(), reply.GetRefreshToken(), err
|
||||
}
|
@ -4,8 +4,8 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/database"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/models"
|
||||
"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"
|
@ -3,8 +3,8 @@ package services
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/database"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/models"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/internal/database"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/internal/models"
|
||||
)
|
||||
|
||||
func ListChannelMember(channelId uint) ([]models.ChannelMember, error) {
|
@ -4,8 +4,8 @@ import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/database"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/models"
|
||||
"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"
|
@ -3,7 +3,7 @@ package services
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/database"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/internal/database"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
@ -3,7 +3,7 @@ package services
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/models"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/internal/models"
|
||||
"github.com/gofiber/contrib/websocket"
|
||||
)
|
||||
|
@ -3,8 +3,8 @@ package services
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/database"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/models"
|
||||
"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"
|
||||
)
|
@ -4,10 +4,10 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/database"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/grpc"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/models"
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/grpc/proto"
|
||||
"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"
|
||||
@ -15,7 +15,11 @@ import (
|
||||
|
||||
func GetRealm(id uint) (models.Realm, error) {
|
||||
var realm models.Realm
|
||||
response, err := grpc.Realms.GetRealm(context.Background(), &proto.RealmLookupRequest{
|
||||
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 {
|
||||
@ -26,7 +30,11 @@ func GetRealm(id uint) (models.Realm, error) {
|
||||
|
||||
func GetRealmWithAlias(alias string) (models.Realm, error) {
|
||||
var realm models.Realm
|
||||
response, err := grpc.Realms.GetRealm(context.Background(), &proto.RealmLookupRequest{
|
||||
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 {
|
||||
@ -36,7 +44,11 @@ func GetRealmWithAlias(alias string) (models.Realm, error) {
|
||||
}
|
||||
|
||||
func GetRealmMember(realmId uint, userId uint) (*proto.RealmMemberResponse, error) {
|
||||
response, err := grpc.Realms.GetRealmMember(context.Background(), &proto.RealmMemberLookupRequest{
|
||||
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)),
|
||||
})
|
||||
@ -48,7 +60,11 @@ func GetRealmMember(realmId uint, userId uint) (*proto.RealmMemberResponse, erro
|
||||
}
|
||||
|
||||
func ListRealmMember(realmId uint) ([]*proto.RealmMemberResponse, error) {
|
||||
response, err := grpc.Realms.ListRealmMember(context.Background(), &proto.RealmMemberLookupRequest{
|
||||
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 {
|
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 72 KiB |
@ -1,18 +1,19 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/internal/gap"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/internal/grpc"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/services"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/internal/services"
|
||||
"github.com/robfig/cron/v3"
|
||||
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/grpc"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/server"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/internal/server"
|
||||
|
||||
messaging "git.solsynth.dev/hydrogen/messaging/pkg"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/database"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/internal"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/internal/database"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/viper"
|
||||
@ -44,30 +45,32 @@ func main() {
|
||||
|
||||
// Connect other services
|
||||
services.SetupLiveKit()
|
||||
if err := grpc.ConnectPassport(); err != nil {
|
||||
log.Fatal().Err(err).Msg("An error occurred when connecting to passport...")
|
||||
}
|
||||
if err := grpc.ConnectPaperclip(); err != nil {
|
||||
log.Fatal().Err(err).Msg("An error occurred when connecting to paperclip...")
|
||||
if err := gap.Register(); err != nil {
|
||||
log.Fatal().Err(err).Msg("An error occurred when connecting to consul...")
|
||||
} else {
|
||||
gap.NewHyperClient()
|
||||
}
|
||||
|
||||
// Server
|
||||
server.NewServer()
|
||||
go server.Listen()
|
||||
|
||||
grpc.NewGRPC()
|
||||
go grpc.ListenGRPC()
|
||||
|
||||
// Configure timed tasks
|
||||
quartz := cron.New(cron.WithLogger(cron.VerbosePrintfLogger(&log.Logger)))
|
||||
quartz.AddFunc("@every 60m", services.DoAutoDatabaseCleanup)
|
||||
quartz.Start()
|
||||
|
||||
// Messages
|
||||
log.Info().Msgf("Messaging v%s is started...", messaging.AppVersion)
|
||||
log.Info().Msgf("Messaging v%s is started...", pkg.AppVersion)
|
||||
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-quit
|
||||
|
||||
log.Info().Msgf("Messaging v%s is quitting...", messaging.AppVersion)
|
||||
log.Info().Msgf("Messaging v%s is quitting...", pkg.AppVersion)
|
||||
|
||||
quartz.Stop()
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/services"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func authMiddleware(c *fiber.Ctx) error {
|
||||
var token string
|
||||
if cookie := c.Cookies(services.CookieAccessKey); len(cookie) > 0 {
|
||||
token = cookie
|
||||
}
|
||||
if header := c.Get(fiber.HeaderAuthorization); len(header) > 0 {
|
||||
tk := strings.Replace(header, "Bearer", "", 1)
|
||||
token = strings.TrimSpace(tk)
|
||||
}
|
||||
if query := c.Query("tk"); len(query) > 0 {
|
||||
token = strings.TrimSpace(query)
|
||||
}
|
||||
|
||||
c.Locals("token", token)
|
||||
|
||||
if err := authFunc(c); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
func authFunc(c *fiber.Ctx, overrides ...string) error {
|
||||
var token string
|
||||
if len(overrides) > 0 {
|
||||
token = overrides[0]
|
||||
} else {
|
||||
if tk, ok := c.Locals("token").(string); !ok {
|
||||
return fiber.NewError(fiber.StatusUnauthorized)
|
||||
} else {
|
||||
token = tk
|
||||
}
|
||||
}
|
||||
|
||||
rtk := c.Cookies(services.CookieRefreshKey)
|
||||
if user, atk, rtk, err := services.Authenticate(token, rtk); err == nil {
|
||||
if atk != token {
|
||||
services.SetJwtCookieSet(c, atk, rtk)
|
||||
}
|
||||
c.Locals("principal", user)
|
||||
return nil
|
||||
} else {
|
||||
return fiber.NewError(fiber.StatusUnauthorized, err.Error())
|
||||
}
|
||||
}
|
@ -1,122 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg"
|
||||
"github.com/gofiber/contrib/websocket"
|
||||
"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.Get("/.well-known", getMetadata)
|
||||
|
||||
api := A.Group("/api").Name("API")
|
||||
{
|
||||
api.Get("/users/me", authMiddleware, getUserinfo)
|
||||
api.Get("/users/:accountId", getOthersInfo)
|
||||
|
||||
channels := api.Group("/channels/:realm").Use(realmMiddleware).Name("Channels API")
|
||||
{
|
||||
channels.Get("/", listChannel)
|
||||
channels.Get("/me", authMiddleware, listOwnedChannel)
|
||||
channels.Get("/me/available", authMiddleware, listAvailableChannel)
|
||||
channels.Get("/:channel", getChannel)
|
||||
channels.Get("/:channel/me", authMiddleware, getChannelIdentity)
|
||||
|
||||
channels.Post("/", authMiddleware, createChannel)
|
||||
channels.Post("/dm", authMiddleware, createDirectChannel)
|
||||
channels.Put("/:channelId", authMiddleware, editChannel)
|
||||
channels.Delete("/:channelId", authMiddleware, deleteChannel)
|
||||
|
||||
channels.Get("/:channel/members", listChannelMembers)
|
||||
channels.Get("/:channel/members/me", authMiddleware, getMyChannelMembership)
|
||||
channels.Put("/:channel/members/me", authMiddleware, editMyChannelMembership)
|
||||
channels.Post("/:channel/members", authMiddleware, addChannelMember)
|
||||
channels.Post("/:channel/members/me", authMiddleware, joinChannel)
|
||||
channels.Delete("/:channel/members", authMiddleware, removeChannelMember)
|
||||
channels.Delete("/:channel/members/me", authMiddleware, leaveChannel)
|
||||
|
||||
channels.Get("/:channel/messages", authMiddleware, listMessage)
|
||||
channels.Post("/:channel/messages", authMiddleware, newMessage)
|
||||
channels.Put("/:channel/messages/:messageId", authMiddleware, editMessage)
|
||||
channels.Delete("/:channel/messages/:messageId", authMiddleware, deleteMessage)
|
||||
|
||||
channels.Get("/:channel/calls", listCall)
|
||||
channels.Get("/:channel/calls/ongoing", getOngoingCall)
|
||||
channels.Post("/:channel/calls", authMiddleware, startCall)
|
||||
channels.Delete("/:channel/calls/ongoing", authMiddleware, endCall)
|
||||
channels.Post("/:channel/calls/ongoing/token", authMiddleware, exchangeCallToken)
|
||||
}
|
||||
|
||||
api.Get("/ws", authMiddleware, websocket.New(messageGateway))
|
||||
}
|
||||
|
||||
A.Use(favicon.New(favicon.Config{
|
||||
FileSystem: http.FS(pkg.FS),
|
||||
File: "views/favicon.png",
|
||||
URL: "/favicon.png",
|
||||
}))
|
||||
|
||||
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...")
|
||||
}
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func getMetadata(c *fiber.Ctx) error {
|
||||
return c.JSON(fiber.Map{
|
||||
"name": viper.GetString("name"),
|
||||
"domain": viper.GetString("domain"),
|
||||
"components": fiber.Map{
|
||||
"identity": viper.GetString("identity.endpoint"),
|
||||
},
|
||||
})
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/grpc"
|
||||
pcpb "git.solsynth.dev/hydrogen/paperclip/pkg/grpc/proto"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
func GetAttachmentByID(id uint) (*pcpb.Attachment, error) {
|
||||
return grpc.Attachments.GetAttachment(context.Background(), &pcpb.AttachmentLookupRequest{
|
||||
Id: lo.ToPtr(uint64(id)),
|
||||
})
|
||||
}
|
||||
|
||||
func GetAttachmentByUUID(uuid string) (*pcpb.Attachment, error) {
|
||||
return grpc.Attachments.GetAttachment(context.Background(), &pcpb.AttachmentLookupRequest{
|
||||
Uuid: &uuid,
|
||||
})
|
||||
}
|
||||
|
||||
func CheckAttachmentByIDExists(id uint, usage string) bool {
|
||||
_, err := grpc.Attachments.CheckAttachmentExists(context.Background(), &pcpb.AttachmentLookupRequest{
|
||||
Id: lo.ToPtr(uint64(id)),
|
||||
Usage: &usage,
|
||||
})
|
||||
|
||||
return err == nil
|
||||
}
|
Reference in New Issue
Block a user