Compare commits

..

No commits in common. "master" and "v20260221-e27536d" have entirely different histories.

43 changed files with 1466 additions and 3068 deletions

View File

@ -8,20 +8,11 @@ import "google/api/annotations.proto";
import "google/protobuf/timestamp.proto";
import "common/v1/types.proto";
message UploadAttachment {
string filename = 1;
string content_type = 2;
bytes data = 3;
int32 width = 4;
int32 height = 5;
}
message SendMessageRequest {
string room_id = 1;
string content = 2;
string reply_to_id = 3;
repeated string mention_user_ids = 4;
repeated UploadAttachment attachments = 5;
}
message SendMessageResponse {
@ -124,17 +115,6 @@ message SearchMessagesResponse {
bool has_more = 2;
}
message ListMessagesSinceRequest {
string room_id = 1;
string after_message_id = 2;
int32 limit = 3;
}
message ListMessagesSinceResponse {
repeated concord.common.v1.Message messages = 1;
bool has_more = 2;
}
message MarkAsReadRequest {
string room_id = 1;
string message_id = 2;
@ -246,12 +226,6 @@ service ChatService {
};
}
rpc ListMessagesSince(ListMessagesSinceRequest) returns (ListMessagesSinceResponse) {
option (google.api.http) = {
get: "/v1/rooms/{room_id}/messages/since/{after_message_id}"
};
}
rpc MarkAsRead(MarkAsReadRequest) returns (MarkAsReadResponse) {
option (google.api.http) = {
post: "/v1/rooms/{room_id}/read"

View File

@ -22,7 +22,6 @@ message User {
string status = 6;
string bio = 7;
string avatar_thumbnail_url = 8;
string status_preference = 9;
}
message AvatarEntry {

View File

@ -80,19 +80,10 @@ message ListDMChannelsResponse {
int32 total_unread = 3;
}
message SendDMAttachment {
string filename = 1;
string content_type = 2;
bytes data = 3;
int32 width = 4;
int32 height = 5;
}
message SendDMRequest {
string channel_id = 1;
string content = 2;
string reply_to_id = 3;
repeated SendDMAttachment attachments = 4;
}
message SendDMResponse {

View File

@ -1,28 +0,0 @@
syntax = "proto3";
package concord.unfurl.v1;
option go_package = "github.com/Alexander-D-Karpov/concord/api/gen/go/unfurl/v1;unfurlv1";
import "google/api/annotations.proto";
message UnfurlRequest {
string url = 1;
}
message UnfurlResponse {
string url = 1;
string title = 2;
string description = 3;
string image = 4;
string site_name = 5;
string favicon = 6;
}
service UnfurlService {
rpc Unfurl(UnfurlRequest) returns (UnfurlResponse) {
option (google.api.http) = {
get: "/v1/unfurl"
};
}
}

View File

@ -11,12 +11,10 @@ import (
"syscall"
"time"
unfurlv1 "github.com/Alexander-D-Karpov/concord/api/gen/go/unfurl/v1"
"github.com/Alexander-D-Karpov/concord/internal/readtracking"
"github.com/Alexander-D-Karpov/concord/internal/security"
"github.com/Alexander-D-Karpov/concord/internal/swagger"
"github.com/Alexander-D-Karpov/concord/internal/typing"
"github.com/Alexander-D-Karpov/concord/internal/unfurl"
"github.com/Alexander-D-Karpov/concord/internal/version"
"github.com/joho/godotenv"
"go.uber.org/zap"
@ -117,6 +115,10 @@ func run() error {
}
logger.Info("migrations applied successfully")
if _, err := database.Pool.Exec(ctx, `UPDATE users SET status = 'offline'`); err != nil {
logger.Warn("failed to reset user statuses", zap.Error(err))
}
var cacheClient *cache.Cache
if cfg.Redis.Enabled {
cacheClient, err = cache.New(
@ -171,11 +173,10 @@ func run() error {
cfg.RateLimit.RequestsPerMinute,
cfg.RateLimit.Burst,
true,
cfg.RateLimit.BypassToken,
)
logger.Info("rate limiting enabled")
} else {
rateLimiter = ratelimit.NewLimiter(nil, 500, 100, false, cfg.RateLimit.BypassToken)
rateLimiter = ratelimit.NewLimiter(nil, 500, 100, false)
}
rateLimitInterceptor := ratelimit.NewInterceptor(rateLimiter)
@ -191,13 +192,9 @@ func run() error {
if cacheClient != nil {
usersRepo = users.NewRepositoryWithCache(database.Pool, cacheClient)
}
eventsHub := events.NewHub(logger, database.Pool, aside)
presenceManager := users.NewPresenceManager(usersRepo, eventsHub)
defer presenceManager.Stop()
usersService := users.NewService(usersRepo, eventsHub, presenceManager, cfg.Storage.Path, cfg.Storage.URL)
usersService := users.NewService(usersRepo, eventsHub, cfg.Storage.Path, cfg.Storage.URL)
usersHandler := users.NewHandler(usersService)
roomsRepo := rooms.NewRepository(database.Pool)
@ -220,6 +217,9 @@ func run() error {
membershipService := membership.NewService(roomsRepo, eventsHub, aside)
membershipHandler := membership.NewHandler(membershipService)
presenceManager := users.NewPresenceManager(usersRepo, eventsHub)
defer presenceManager.Stop()
streamHandler := stream.NewHandler(eventsHub, presenceManager)
voiceAssignService := voiceassign.NewService(database.Pool, jwtManager, cacheClient)
@ -231,7 +231,7 @@ func run() error {
if cacheClient != nil {
friendsRepo = friends.NewRepositoryWithCache(database.Pool, cacheClient)
}
friendsService := friends.NewService(friendsRepo, eventsHub, usersRepo, presenceManager)
friendsService := friends.NewService(friendsRepo, eventsHub, usersRepo)
friendsHandler := friends.NewHandler(friendsService)
adminService := admin.NewService(database.Pool, roomsRepo, eventsHub, logger)
@ -239,14 +239,11 @@ func run() error {
dmRepo := dm.NewRepository(database.Pool)
dmMsgRepo := dm.NewMessageRepository(database.Pool, snowflakeGen)
dmService := dm.NewService(dmRepo, dmMsgRepo, usersRepo, eventsHub, voiceAssignService, presenceManager, logger)
dmService := dm.NewService(dmRepo, dmMsgRepo, usersRepo, eventsHub, voiceAssignService, logger)
dmHandler := dm.NewHandler(dmService, storageService)
dmHandler.SetReadTrackingService(readTrackingSvc)
dmHandler.SetTypingService(typingSvc)
unfurlService := unfurl.NewService(cacheClient)
unfurlHandler := unfurl.NewHandler(unfurlService)
var oauthManager *oauth.Manager
if len(cfg.Auth.OAuth) > 0 {
oauthManager = oauth.NewManager(cfg.Auth)
@ -314,7 +311,6 @@ func run() error {
friendsv1.RegisterFriendsServiceServer(grpcServer, friendsHandler)
adminv1.RegisterAdminServiceServer(grpcServer, adminHandler)
dmv1.RegisterDMServiceServer(grpcServer, dmHandler)
unfurlv1.RegisterUnfurlServiceServer(grpcServer, unfurlHandler)
reflection.Register(grpcServer)
listener, err := net.Listen("tcp", fmt.Sprintf(":%d", cfg.Server.GRPCPort))

View File

@ -11,7 +11,6 @@ import (
"time"
"github.com/Alexander-D-Karpov/concord/internal/version"
"github.com/Alexander-D-Karpov/concord/internal/voice/status"
"github.com/joho/godotenv"
"go.uber.org/zap"
@ -81,12 +80,13 @@ func run() error {
sessionManager := session.NewManager()
roomManager := room.NewManager()
// Router needs access to both session and room state
voiceRouter := router.NewRouter(sessionManager, logger)
// Metrics and health
metrics := telemetry.NewMetrics(logger)
telemetryLogger := telemetry.NewLogger(logger)
voiceRouter := router.NewRouter(sessionManager, logger, metrics)
healthServer := health.NewServer(logger)
healthServer.RegisterCheck("sessions", func(ctx context.Context) error {
sessions := sessionManager.GetAllSessions()
@ -147,14 +147,6 @@ func run() error {
)
netinfo.PrintAccessBanner(advertised, "Concord Voice Server")
statusSrv := status.NewServer(sessionManager, jwtManager, metrics, logger)
go func() {
err := statusSrv.Start(ctx, cfg.Voice.StatusPort)
if err != nil {
logger.Error("status server error", zap.Error(err))
}
}()
var registrar *discovery.Registrar
if cfg.Voice.RegistryURL != "" {
publicAddr := advertised.PublicHost

View File

@ -1,60 +0,0 @@
cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY=
cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU=
cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 h1:UQUsRi8WTzhZntp5313l+CHIAT95ojUI2lpP/ExlZa4=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0/go.mod h1:Cz6ft6Dkn3Et6l2v2a9/RpN7epQ1GtDlO6lj8bEcOvw=
github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjHpqDjYY=
github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE=
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc=
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 h1:aQ3y1lwWyqYPiWZThqv1aFbZMiM9vblcSArJRf2Irls=
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M=
github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA=
github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A=
github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw=
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI=
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4=
github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8=
github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU=
github.com/go-jose/go-jose/v4 v4.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI=
github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo=
github.com/golang/glog v1.2.5 h1:DrW6hGnjIhtvhOIiAKT6Psh/Kd/ldepEa81DKeiRJ5I=
github.com/golang/glog v1.2.5/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
github.com/rogpeppe/fastuuid v1.2.0 h1:Ppwyp6VYCF1nvBTXL3trRso7mXMlRrw9ooo375wvi2s=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE=
github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc=
github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU=
github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM=
github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4=
go.opentelemetry.io/contrib/detectors/gcp v1.36.0 h1:F7q2tNlCaHY9nMKHR6XH9/qkp8FktLnIcy6jJNyOCQw=
go.opentelemetry.io/contrib/detectors/gcp v1.36.0/go.mod h1:IbBN8uAIIx734PTonTPxAxnjc2pQTxWNkwfstZ+6H2k=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=

View File

@ -33,8 +33,8 @@ func NewHandler(service *Service, storage *storage.Storage, readTrackingSvc *rea
}
func (h *Handler) SendMessage(ctx context.Context, req *chatv1.SendMessageRequest) (*chatv1.SendMessageResponse, error) {
if req.RoomId == "" || (req.Content == "" && len(req.Attachments) == 0) {
return nil, errors.ToGRPCError(errors.BadRequest("room_id and content or attachments are required"))
if req.RoomId == "" || req.Content == "" {
return nil, errors.ToGRPCError(errors.BadRequest("room_id and content are required"))
}
var replyToID *int64
@ -55,36 +55,7 @@ func (h *Handler) SendMessage(ctx context.Context, req *chatv1.SendMessageReques
mentionIDs = append(mentionIDs, uid)
}
var attachments []Attachment
for _, att := range req.Attachments {
if len(att.Data) == 0 {
continue
}
fileInfo, err := h.storage.Store(ctx, att.Data, att.Filename, att.ContentType)
if err != nil {
return nil, errors.ToGRPCError(errors.BadRequest("failed to store attachment: " + err.Error()))
}
width := int(att.Width)
height := int(att.Height)
if fileInfo.Width > 0 {
width = fileInfo.Width
height = fileInfo.Height
}
attachments = append(attachments, Attachment{
ID: uuid.MustParse(fileInfo.ID),
URL: fileInfo.URL,
Filename: att.Filename,
ContentType: fileInfo.ContentType,
Size: fileInfo.Size,
Width: width,
Height: height,
})
}
msg, err := h.service.SendMessage(ctx, req.RoomId, req.Content, replyToID, mentionIDs, attachments)
msg, err := h.service.SendMessage(ctx, req.RoomId, req.Content, replyToID, mentionIDs)
if err != nil {
return nil, errors.ToGRPCError(err)
}
@ -94,37 +65,6 @@ func (h *Handler) SendMessage(ctx context.Context, req *chatv1.SendMessageReques
}, nil
}
func (h *Handler) ListMessagesSince(ctx context.Context, req *chatv1.ListMessagesSinceRequest) (*chatv1.ListMessagesSinceResponse, error) {
if req.RoomId == "" || req.AfterMessageId == "" {
return nil, errors.ToGRPCError(errors.BadRequest("room_id and after_message_id are required"))
}
afterID, err := strconv.ParseInt(req.AfterMessageId, 10, 64)
if err != nil {
return nil, errors.ToGRPCError(errors.BadRequest("invalid after_message_id"))
}
limit := int(req.Limit)
if limit <= 0 || limit > 200 {
limit = 100
}
messages, hasMore, err := h.service.ListMessages(ctx, req.RoomId, nil, &afterID, limit)
if err != nil {
return nil, errors.ToGRPCError(err)
}
protoMessages := make([]*commonv1.Message, len(messages))
for i, msg := range messages {
protoMessages[i] = toProtoMessage(msg)
}
return &chatv1.ListMessagesSinceResponse{
Messages: protoMessages,
HasMore: hasMore,
}, nil
}
func (h *Handler) EditMessage(ctx context.Context, req *chatv1.EditMessageRequest) (*chatv1.EditMessageResponse, error) {
if req.RoomId == "" || req.MessageId == "" || req.Content == "" {
return nil, errors.ToGRPCError(errors.BadRequest("room_id, message_id and content are required"))

File diff suppressed because it is too large Load Diff

View File

@ -73,7 +73,7 @@ func (s *Service) isMember(ctx context.Context, roomID, userID uuid.UUID) (bool,
return true, nil
}
func (s *Service) SendMessage(ctx context.Context, roomID, content string, replyToID *int64, mentionIDs []uuid.UUID, attachments []Attachment) (*Message, error) {
func (s *Service) SendMessage(ctx context.Context, roomID, content string, replyToID *int64, mentionIDs []uuid.UUID) (*Message, error) {
userID := interceptor.GetUserID(ctx)
if userID == "" {
return nil, errors.Unauthorized("user not authenticated")
@ -102,10 +102,9 @@ func (s *Service) SendMessage(ctx context.Context, roomID, content string, reply
}
msg := &Message{
RoomID: roomUUID,
AuthorID: authorUUID,
Content: content,
Attachments: attachments,
RoomID: roomUUID,
AuthorID: authorUUID,
Content: content,
}
if replyToID != nil {
@ -140,34 +139,19 @@ func (s *Service) SendMessage(ctx context.Context, roomID, content string, reply
}
if s.hub != nil {
var protoAttachments []*commonv1.MessageAttachment
for _, att := range msg.Attachments {
protoAttachments = append(protoAttachments, &commonv1.MessageAttachment{
Id: att.ID.String(),
Url: att.URL,
Filename: att.Filename,
ContentType: att.ContentType,
Size: att.Size,
Width: int32(att.Width),
Height: int32(att.Height),
CreatedAt: timestamppb.New(att.CreatedAt),
})
}
event := &streamv1.ServerEvent{
EventId: uuid.New().String(),
CreatedAt: timestamppb.Now(),
Payload: &streamv1.ServerEvent_MessageCreated{
MessageCreated: &streamv1.MessageCreated{
Message: &commonv1.Message{
Id: strconv.FormatInt(msg.ID, 10),
RoomId: msg.RoomID.String(),
AuthorId: msg.AuthorID.String(),
Content: msg.Content,
CreatedAt: timestamppb.New(msg.CreatedAt),
ReplyToId: replyToIDStr,
Mentions: mentionStrings,
Attachments: protoAttachments,
Id: strconv.FormatInt(msg.ID, 10),
RoomId: msg.RoomID.String(),
AuthorId: msg.AuthorID.String(),
Content: msg.Content,
CreatedAt: timestamppb.New(msg.CreatedAt),
ReplyToId: replyToIDStr,
Mentions: mentionStrings,
},
},
},

View File

@ -70,7 +70,6 @@ type VoiceConfig struct {
Secret string
RegistryURL string
PublicHost string
StatusPort int
}
type LoggingConfig struct {
@ -98,7 +97,6 @@ type RateLimitConfig struct {
Enabled bool
RequestsPerMinute int
Burst int
BypassToken string
}
type EmailConfig struct {
@ -152,7 +150,6 @@ func Load() (*Config, error) {
Secret: getEnv("VOICE_SECRET", "change-me-voice-server-secret"),
RegistryURL: getEnv("REGISTRY_URL", "localhost:9090"),
PublicHost: getEnv("VOICE_PUBLIC_HOST", "localhost"),
StatusPort: getEnvInt("VOICE_STATUS_PORT", 9092),
},
Logging: LoggingConfig{
Level: getEnv("LOG_LEVEL", "info"),
@ -172,7 +169,6 @@ func Load() (*Config, error) {
Enabled: getEnvBool("RATE_LIMIT_ENABLED", true),
RequestsPerMinute: getEnvInt("RATE_LIMIT_REQUESTS_PER_MINUTE", 60),
Burst: getEnvInt("RATE_LIMIT_BURST", 10),
BypassToken: getEnv("RATE_LIMIT_BYPASS_TOKEN", ""),
},
Storage: StorageConfig{
Path: getEnv("STORAGE_PATH", "./uploads"),

View File

@ -103,40 +103,11 @@ func (h *Handler) SendDM(ctx context.Context, req *dmv1.SendDMRequest) (*dmv1.Se
if req.ChannelId == "" {
return nil, errors.ToGRPCError(errors.BadRequest("channel_id is required"))
}
if req.Content == "" && len(req.Attachments) == 0 {
return nil, errors.ToGRPCError(errors.BadRequest("content or attachments are required"))
if req.Content == "" {
return nil, errors.ToGRPCError(errors.BadRequest("content is required"))
}
var attachments []DMAttachment
for _, att := range req.Attachments {
if len(att.Data) == 0 {
continue
}
fileInfo, err := h.storage.Store(ctx, att.Data, att.Filename, att.ContentType)
if err != nil {
return nil, errors.ToGRPCError(errors.BadRequest("failed to store attachment: " + err.Error()))
}
width := int(att.Width)
height := int(att.Height)
if fileInfo.Width > 0 {
width = fileInfo.Width
height = fileInfo.Height
}
attachments = append(attachments, DMAttachment{
ID: uuid.MustParse(fileInfo.ID),
URL: fileInfo.URL,
Filename: att.Filename,
ContentType: fileInfo.ContentType,
Size: fileInfo.Size,
Width: width,
Height: height,
})
}
msg, err := h.service.SendMessage(ctx, req.ChannelId, req.Content, req.ReplyToId, attachments, nil)
msg, err := h.service.SendMessage(ctx, req.ChannelId, req.Content, req.ReplyToId, nil, nil)
if err != nil {
return nil, errors.ToGRPCError(err)
}

View File

@ -22,44 +22,20 @@ type Service struct {
usersRepo *users.Repository
hub *events.Hub
voiceAssign *voiceassign.Service
presence *users.PresenceManager
logger *zap.Logger
}
func NewService(
repo *Repository,
msgRepo *MessageRepository,
usersRepo *users.Repository,
hub *events.Hub,
voiceAssign *voiceassign.Service,
presence *users.PresenceManager,
logger *zap.Logger,
) *Service {
func NewService(repo *Repository, msgRepo *MessageRepository, usersRepo *users.Repository, hub *events.Hub, voiceAssign *voiceassign.Service, logger *zap.Logger) *Service {
return &Service{
repo: repo,
msgRepo: msgRepo,
usersRepo: usersRepo,
hub: hub,
voiceAssign: voiceAssign,
presence: presence,
logger: logger,
}
}
func (s *Service) decorateChannelStatuses(channels []*DMChannelWithUser) {
for _, ch := range channels {
presence := users.StatusOffline
if s.presence != nil {
presence = s.presence.GetStatus(ch.OtherUserID)
}
ch.OtherUserStatus = users.EffectiveStatus(
users.NormalizeStatusPreference(ch.OtherUserStatus),
presence,
)
}
}
func (s *Service) GetOrCreateDM(ctx context.Context, otherUserID string) (*DMChannel, error) {
userID := interceptor.GetUserID(ctx)
if userID == "" {
@ -104,13 +80,7 @@ func (s *Service) ListDMs(ctx context.Context) ([]*DMChannelWithUser, error) {
return nil, errors.BadRequest("invalid user id")
}
channels, err := s.repo.ListByUser(ctx, userUUID)
if err != nil {
return nil, err
}
s.decorateChannelStatuses(channels)
return channels, nil
return s.repo.ListByUser(ctx, userUUID)
}
func (s *Service) CloseDM(ctx context.Context, channelID string) error {

View File

@ -99,9 +99,8 @@ func (r *Repository) CreateFriendRequest(ctx context.Context, fromUserID, toUser
query := `
INSERT INTO friend_requests (id, from_user_id, to_user_id, status)
VALUES ($1, $2, $3, $4)
ON CONFLICT (from_user_id, to_user_id)
DO UPDATE SET status = 'pending', updated_at = NOW()
RETURNING id, created_at, updated_at
ON CONFLICT (from_user_id, to_user_id) DO UPDATE SET status = 'pending', updated_at = NOW()
RETURNING created_at, updated_at
`
err := r.pool.QueryRow(ctx, query,
@ -109,7 +108,7 @@ func (r *Repository) CreateFriendRequest(ctx context.Context, fromUserID, toUser
req.FromUserID,
req.ToUserID,
req.Status,
).Scan(&req.ID, &req.CreatedAt, &req.UpdatedAt)
).Scan(&req.CreatedAt, &req.UpdatedAt)
if err != nil {
return nil, err

File diff suppressed because it is too large Load Diff

View File

@ -13,7 +13,6 @@ import (
friendsv1 "github.com/Alexander-D-Karpov/concord/api/gen/go/friends/v1"
membershipv1 "github.com/Alexander-D-Karpov/concord/api/gen/go/membership/v1"
roomsv1 "github.com/Alexander-D-Karpov/concord/api/gen/go/rooms/v1"
unfurlv1 "github.com/Alexander-D-Karpov/concord/api/gen/go/unfurl/v1"
usersv1 "github.com/Alexander-D-Karpov/concord/api/gen/go/users/v1"
"github.com/Alexander-D-Karpov/concord/internal/middleware"
"github.com/Alexander-D-Karpov/concord/internal/version"
@ -63,7 +62,6 @@ func (g *Gateway) Init(ctx context.Context) error {
callv1.RegisterCallServiceHandlerFromEndpoint,
friendsv1.RegisterFriendsServiceHandlerFromEndpoint,
dmv1.RegisterDMServiceHandlerFromEndpoint,
unfurlv1.RegisterUnfurlServiceHandlerFromEndpoint,
}
for _, register := range handlers {
@ -124,7 +122,7 @@ func (g *Gateway) Start(ctx context.Context, port int) error {
func customMatcher(key string) (string, bool) {
switch key {
case "authorization", "x-request-id", "x-correlation-id", "grpc-timeout", "x-concord-ratelimit-bypass":
case "authorization", "x-request-id", "x-correlation-id", "grpc-timeout":
return key, true
default:
return runtime.DefaultHeaderMatcher(key)

View File

@ -1,13 +0,0 @@
DROP INDEX IF EXISTS idx_messages_content_search;
CREATE INDEX IF NOT EXISTS idx_messages_fts
ON messages USING gin(to_tsvector('simple', content))
WHERE deleted_at IS NULL;
DROP INDEX IF EXISTS idx_dm_messages_content_search;
CREATE INDEX IF NOT EXISTS idx_dm_messages_fts
ON dm_messages USING gin(to_tsvector('simple', content))
WHERE deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_users_handle_lower ON users(lower(handle));

File diff suppressed because it is too large Load Diff

View File

@ -11,8 +11,6 @@ import (
"google.golang.org/grpc/status"
)
const BypassMetadataKey = "x-concord-ratelimit-bypass"
type Interceptor struct {
limiter *Limiter
}
@ -28,10 +26,6 @@ func (i *Interceptor) Unary() grpc.UnaryServerInterceptor {
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler,
) (interface{}, error) {
if i.limiter.ShouldBypass(ctx) {
return handler(ctx, req)
}
key := i.getKey(ctx, info.FullMethod)
allowed, err := i.limiter.Allow(ctx, key)
@ -55,11 +49,6 @@ func (i *Interceptor) Stream() grpc.StreamServerInterceptor {
handler grpc.StreamHandler,
) error {
ctx := ss.Context()
if i.limiter.ShouldBypass(ctx) {
return handler(srv, ss)
}
key := i.getKey(ctx, info.FullMethod)
allowed, err := i.limiter.Allow(ctx, key)

View File

@ -2,15 +2,12 @@ package ratelimit
import (
"context"
"crypto/subtle"
"fmt"
"strings"
"sync"
"time"
"github.com/Alexander-D-Karpov/concord/internal/infra/cache"
"golang.org/x/time/rate"
"google.golang.org/grpc/metadata"
)
type Limiter struct {
@ -20,8 +17,6 @@ type Limiter struct {
localCache map[string]*rate.Limiter
mu sync.RWMutex
cleanupDone chan struct{}
bypassToken string
}
type LimitConfig struct {
@ -29,11 +24,10 @@ type LimitConfig struct {
Burst int
}
func NewLimiter(cache *cache.Cache, requestsPerMinute, burst int, enabled bool, bypassToken string) *Limiter {
func NewLimiter(cache *cache.Cache, requestsPerMinute, burst int, enabled bool) *Limiter {
l := &Limiter{
cache: cache,
enabled: enabled,
bypassToken: strings.TrimSpace(bypassToken),
cache: cache,
enabled: enabled,
limits: map[string]LimitConfig{
"default": {
RequestsPerMinute: requestsPerMinute,
@ -63,30 +57,6 @@ func NewLimiter(cache *cache.Cache, requestsPerMinute, burst int, enabled bool,
return l
}
func (l *Limiter) ShouldBypass(ctx context.Context) bool {
if l.bypassToken == "" {
return false
}
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return false
}
values := md.Get(BypassMetadataKey)
if len(values) == 0 {
return false
}
for _, candidate := range values {
if subtle.ConstantTimeCompare([]byte(strings.TrimSpace(candidate)), []byte(l.bypassToken)) == 1 {
return true
}
}
return false
}
func (l *Limiter) Allow(ctx context.Context, key string) (bool, error) {
if !l.enabled {
return true, nil

View File

@ -451,13 +451,8 @@ func (r *Repository) AssignVoiceServer(ctx context.Context, roomID, serverID uui
return err
}
func (r *Repository) CreateRoomInvite(
ctx context.Context,
roomID uuid.UUID,
invitedUserID uuid.UUID,
invitedBy uuid.UUID,
) (*RoomInvite, error) {
inv := &RoomInvite{
func (r *Repository) CreateRoomInvite(ctx context.Context, roomID, invitedUserID, invitedBy uuid.UUID) (*RoomInvite, error) {
invite := &RoomInvite{
ID: uuid.New(),
RoomID: roomID,
InvitedUserID: invitedUserID,
@ -466,30 +461,24 @@ func (r *Repository) CreateRoomInvite(
}
query := `
INSERT INTO room_invites (id, room_id, invited_user_id, invited_by, status)
VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (room_id, invited_user_id)
DO UPDATE SET
status = 'pending',
invited_by = EXCLUDED.invited_by,
updated_at = NOW()
RETURNING id, created_at, updated_at
`
INSERT INTO room_invites (id, room_id, invited_user_id, invited_by, status)
VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (room_id, invited_user_id) DO UPDATE SET
invited_by = $4,
status = 'pending',
updated_at = NOW()
RETURNING created_at, updated_at
`
err := r.pool.QueryRow(
ctx,
query,
inv.ID,
inv.RoomID,
inv.InvitedUserID,
inv.InvitedBy,
inv.Status,
).Scan(&inv.ID, &inv.CreatedAt, &inv.UpdatedAt)
if err != nil {
return nil, err
}
err := r.pool.QueryRow(ctx, query,
invite.ID,
invite.RoomID,
invite.InvitedUserID,
invite.InvitedBy,
invite.Status,
).Scan(&invite.CreatedAt, &invite.UpdatedAt)
return inv, nil
return invite, err
}
func (r *Repository) GetRoomInvite(ctx context.Context, id uuid.UUID) (*RoomInvite, error) {

View File

@ -2,7 +2,6 @@ package typing
import (
"context"
"sync"
"time"
streamv1 "github.com/Alexander-D-Karpov/concord/api/gen/go/stream/v1"
@ -16,8 +15,6 @@ type Service struct {
repo *Repository
hub *events.Hub
usersRepo *users.Repository
rateMu sync.Mutex
lastTyped map[string]time.Time
}
func NewService(repo *Repository, hub *events.Hub, usersRepo *users.Repository) *Service {
@ -25,40 +22,10 @@ func NewService(repo *Repository, hub *events.Hub, usersRepo *users.Repository)
repo: repo,
hub: hub,
usersRepo: usersRepo,
lastTyped: make(map[string]time.Time),
}
}
const typingRateLimit = 2 * time.Second
func (s *Service) checkTypingRate(userID uuid.UUID, targetID uuid.UUID) bool {
key := userID.String() + ":" + targetID.String()
s.rateMu.Lock()
defer s.rateMu.Unlock()
now := time.Now()
if last, ok := s.lastTyped[key]; ok && now.Sub(last) < typingRateLimit {
return false
}
s.lastTyped[key] = now
if len(s.lastTyped) > 10000 {
cutoff := now.Add(-typingRateLimit * 2)
for k, t := range s.lastTyped {
if t.Before(cutoff) {
delete(s.lastTyped, k)
}
}
}
return true
}
func (s *Service) StartTypingInRoom(ctx context.Context, userID, roomID uuid.UUID) error {
if !s.checkTypingRate(userID, roomID) {
return nil
}
if err := s.repo.SetTypingInRoom(ctx, userID, roomID); err != nil {
return err
}
@ -77,10 +44,6 @@ func (s *Service) StopTypingInRoom(ctx context.Context, userID, roomID uuid.UUID
}
func (s *Service) StartTypingInDM(ctx context.Context, userID, channelID uuid.UUID, otherUserID uuid.UUID) error {
if !s.checkTypingRate(userID, channelID) {
return nil
}
if err := s.repo.SetTypingInDM(ctx, userID, channelID); err != nil {
return err
}

View File

@ -1,37 +0,0 @@
package unfurl
import (
"context"
unfurlv1 "github.com/Alexander-D-Karpov/concord/api/gen/go/unfurl/v1"
"github.com/Alexander-D-Karpov/concord/internal/common/errors"
)
type Handler struct {
unfurlv1.UnimplementedUnfurlServiceServer
service *Service
}
func NewHandler(service *Service) *Handler {
return &Handler{service: service}
}
func (h *Handler) Unfurl(ctx context.Context, req *unfurlv1.UnfurlRequest) (*unfurlv1.UnfurlResponse, error) {
if req.Url == "" {
return nil, errors.ToGRPCError(errors.BadRequest("url is required"))
}
preview, err := h.service.Unfurl(ctx, req.Url)
if err != nil {
return nil, errors.ToGRPCError(errors.BadRequest("failed to unfurl: " + err.Error()))
}
return &unfurlv1.UnfurlResponse{
Url: preview.URL,
Title: preview.Title,
Description: preview.Description,
Image: preview.Image,
SiteName: preview.SiteName,
Favicon: preview.Favicon,
}, nil
}

File diff suppressed because it is too large Load Diff

View File

@ -31,46 +31,16 @@ type ProcessedAvatar struct {
Height int
}
var magicBytes = map[string][]byte{
"image/jpeg": {0xFF, 0xD8, 0xFF},
"image/png": {0x89, 0x50, 0x4E, 0x47},
"image/gif": {0x47, 0x49, 0x46},
"image/webp": {0x52, 0x49, 0x46, 0x46},
}
func ValidateImageMagic(data []byte) (string, error) {
for mime, magic := range magicBytes {
if len(data) >= len(magic) {
match := true
for i, b := range magic {
if data[i] != b {
match = false
break
}
}
if match {
return mime, nil
}
}
}
return "", fmt.Errorf("unrecognized image format")
}
func ProcessAvatarImage(data []byte) (*ProcessedAvatar, error) {
if len(data) > MaxAvatarBytes {
return nil, fmt.Errorf("image too large: %d bytes (max %d)", len(data), MaxAvatarBytes)
}
if _, err := ValidateImageMagic(data); err != nil {
return nil, fmt.Errorf("invalid image: %w", err)
}
src, _, err := image.Decode(bytes.NewReader(data))
if err != nil {
return nil, fmt.Errorf("decode image: %w", err)
}
// Re-encoding to JPEG strips all EXIF/metadata automatically
bounds := src.Bounds()
w, h := bounds.Dx(), bounds.Dy()

View File

@ -160,7 +160,6 @@ func toProtoUser(user *User) *commonv1.User {
AvatarThumbnailUrl: user.AvatarThumbnailURL,
CreatedAt: timestamppb.New(user.CreatedAt),
Status: user.Status,
StatusPreference: user.StatusPreference,
}
if user.Bio != "" {
proto.Bio = user.Bio

File diff suppressed because it is too large Load Diff

View File

@ -25,7 +25,6 @@ type User struct {
OAuthSubject *string
CreatedAt time.Time
DeletedAt *time.Time
StatusPreference string
}
type UserAvatar struct {
@ -468,24 +467,3 @@ func (r *Repository) GetLatestUserAvatar(ctx context.Context, userID uuid.UUID)
}
return av, err
}
func (r *Repository) GetStatusPreference(ctx context.Context, userID uuid.UUID) (string, error) {
var status string
err := r.pool.QueryRow(ctx,
`SELECT COALESCE(status, 'online') FROM users WHERE id = $1`,
userID,
).Scan(&status)
if err != nil {
return "", err
}
switch status {
case StatusDND:
return StatusDND, nil
case StatusOffline:
return StatusOffline, nil
default:
return StatusOnline, nil
}
}

File diff suppressed because it is too large Load Diff

View File

@ -27,11 +27,11 @@ var concordeFleet = []string{
const (
APIMajor = 0
APIMinor = 3
APIMinor = 1
APIPatch = 0
VoiceMajor = 0
VoiceMinor = 2
VoiceMinor = 1
VoicePatch = 0
)

View File

@ -27,7 +27,6 @@ const (
PacketTypeRR = 0x0c
PacketTypeParticipantLeft = 0x0d
PacketTypeSubscribe = 0x0e
PacketTypeQualityReport = 0x10
FlagMarker = 0x01
FlagKeyframe = 0x02
@ -279,27 +278,16 @@ type ReceiverReport struct {
}
type ParticipantLeftPayload struct {
UserID string `json:"user_id"`
RoomID string `json:"room_id"`
SSRC uint32 `json:"ssrc"`
VideoSSRC uint32 `json:"video_ssrc,omitempty"`
ScreenSSRC uint32 `json:"screen_ssrc,omitempty"`
UserID string `json:"user_id"`
RoomID string `json:"room_id"`
SSRC uint32 `json:"ssrc"`
VideoSSRC uint32 `json:"video_ssrc,omitempty"`
}
type SubscribePayload struct {
Subscriptions []uint32 `json:"subscriptions"` // List of SSRCs to subscribe to
}
type QualityReportPayload struct {
SSRC uint32 `json:"ssrc"`
UserID string `json:"user_id"`
RoomID string `json:"room_id"`
Quality int `json:"quality"`
RTTMs float64 `json:"rtt_ms"`
PacketLoss float64 `json:"packet_loss"`
JitterMs float64 `json:"jitter_ms"`
}
func ParseNack(data []byte) (*NackPayload, error) {
if len(data) < 7 {
return nil, ErrTooSmall

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,49 +0,0 @@
package udp
import (
"sync"
"sync/atomic"
)
type packetBuffer struct {
buf []byte
n int
refs atomic.Int32
pool *sync.Pool
}
func newPacketPool() sync.Pool {
p := sync.Pool{}
p.New = func() interface{} {
return &packetBuffer{
buf: make([]byte, maxPacketLen),
pool: &p,
}
}
return p
}
func (p *packetBuffer) PrepareForRead() []byte {
p.n = 0
p.refs.Store(1)
return p.buf[:cap(p.buf)]
}
func (p *packetBuffer) SetLen(n int) {
p.n = n
}
func (p *packetBuffer) Bytes() []byte {
return p.buf[:p.n]
}
func (p *packetBuffer) Retain() {
p.refs.Add(1)
}
func (p *packetBuffer) Release() {
if p.refs.Add(-1) == 0 {
p.n = 0
p.pool.Put(p)
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -31,7 +31,7 @@ INCLUDES="-I. \
-I$PROTO_DEPS_DIR/grpc-gateway \
-I$PROTO_DEPS_DIR/protobuf/src"
for dir in common/v1 auth/v1 users/v1 rooms/v1 membership/v1 chat/v1 call/v1 registry/v1 admin/v1 friends/v1 dm/v1 unfurl/v1; do
for dir in common/v1 auth/v1 users/v1 rooms/v1 membership/v1 chat/v1 call/v1 registry/v1 admin/v1 friends/v1 dm/v1; do
echo "Generating for $dir..."
protoc $INCLUDES \
@ -67,7 +67,6 @@ protoc $INCLUDES \
call/v1/*.proto \
friends/v1/*.proto \
dm/v1/*.proto \
admin/v1/*.proto \
unfurl/v1/*.proto
admin/v1/*.proto
echo "Protobuf generation complete!"

View File

@ -1,21 +0,0 @@
module github.com/Alexander-D-Karpov/concord/tools/voicetest
go 1.25.1
require (
github.com/Alexander-D-Karpov/concord v0.0.0
golang.org/x/crypto v0.43.0
google.golang.org/grpc v1.76.0
)
require (
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect
golang.org/x/net v0.45.0 // indirect
golang.org/x/sys v0.37.0 // indirect
golang.org/x/text v0.34.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4 // indirect
google.golang.org/protobuf v1.36.10 // indirect
)
replace github.com/Alexander-D-Karpov/concord => ../../

View File

@ -1,42 +0,0 @@
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM=
golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4 h1:8XJ4pajGwOlasW+L13MnEGA8W4115jJySQtVfS2/IBU=
google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4/go.mod h1:NnuHhy+bxcg30o7FnVAZbXsPHUDQ9qKWAQKCD7VxFtk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4 h1:i8QOKZfYg6AbGVZzUAY3LrNWCKF8O6zFisU9Wl9RER4=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ=
google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=

File diff suppressed because it is too large Load Diff