mirror of
https://github.com/Alexander-D-Karpov/concord.git
synced 2026-03-16 22:04:15 +03:00
578 lines
16 KiB
Go
578 lines
16 KiB
Go
package friends
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
|
|
friendsv1 "github.com/Alexander-D-Karpov/concord/api/gen/go/friends/v1"
|
|
streamv1 "github.com/Alexander-D-Karpov/concord/api/gen/go/stream/v1"
|
|
"github.com/Alexander-D-Karpov/concord/internal/auth/interceptor"
|
|
"github.com/Alexander-D-Karpov/concord/internal/common/errors"
|
|
"github.com/Alexander-D-Karpov/concord/internal/events"
|
|
"github.com/Alexander-D-Karpov/concord/internal/users"
|
|
"github.com/google/uuid"
|
|
"google.golang.org/protobuf/types/known/timestamppb"
|
|
)
|
|
|
|
type Service struct {
|
|
repo *Repository
|
|
hub *events.Hub
|
|
usersRepo *users.Repository
|
|
presence *users.PresenceManager
|
|
}
|
|
|
|
func NewService(repo *Repository, hub *events.Hub, usersRepo *users.Repository, presence *users.PresenceManager) *Service {
|
|
return &Service{
|
|
repo: repo,
|
|
hub: hub,
|
|
usersRepo: usersRepo,
|
|
presence: presence,
|
|
}
|
|
}
|
|
|
|
func (s *Service) SendFriendRequest(ctx context.Context, toUserID string) (*FriendRequest, *users.User, *users.User, error) {
|
|
fromUserID := interceptor.GetUserID(ctx)
|
|
if fromUserID == "" {
|
|
return nil, nil, nil, errors.Unauthorized("user not authenticated")
|
|
}
|
|
fromUUID, err := uuid.Parse(fromUserID)
|
|
if err != nil {
|
|
return nil, nil, nil, errors.BadRequest("invalid user id")
|
|
}
|
|
toUUID, err := uuid.Parse(toUserID)
|
|
if err != nil {
|
|
return nil, nil, nil, errors.BadRequest("invalid target user id")
|
|
}
|
|
if fromUUID == toUUID {
|
|
return nil, nil, nil, errors.BadRequest("cannot send friend request to yourself")
|
|
}
|
|
|
|
dbCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
|
|
defer cancel()
|
|
|
|
alreadyFriends, err := s.repo.AreFriends(dbCtx, fromUUID, toUUID)
|
|
if err != nil {
|
|
return nil, nil, nil, errors.Internal("failed to check friendship status", err)
|
|
}
|
|
if alreadyFriends {
|
|
return nil, nil, nil, errors.Conflict("already friends with this user")
|
|
}
|
|
|
|
existingRequest, err := s.repo.GetFriendRequestBetweenUsers(dbCtx, fromUUID, toUUID)
|
|
if err != nil && err.Error() != "user not found" {
|
|
return nil, nil, nil, errors.Internal("failed to check existing requests", err)
|
|
}
|
|
if existingRequest != nil && existingRequest.Status == "pending" {
|
|
if existingRequest.FromUserID == fromUUID {
|
|
return nil, nil, nil, errors.Conflict("friend request already sent")
|
|
}
|
|
return nil, nil, nil, errors.Conflict("this user has already sent you a friend request")
|
|
}
|
|
|
|
blocked, err := s.repo.IsBlocked(dbCtx, toUUID, fromUUID)
|
|
if err != nil {
|
|
return nil, nil, nil, errors.Internal("failed to check block status", err)
|
|
}
|
|
if blocked {
|
|
return nil, nil, nil, errors.Forbidden("cannot send friend request to this user")
|
|
}
|
|
|
|
request, err := s.repo.CreateFriendRequest(dbCtx, fromUUID, toUUID)
|
|
if err != nil {
|
|
return nil, nil, nil, errors.Internal("failed to create friend request", err)
|
|
}
|
|
|
|
fromUser, err := s.usersRepo.GetByID(dbCtx, fromUUID)
|
|
if err != nil {
|
|
return nil, nil, nil, errors.Internal("failed to get from user", err)
|
|
}
|
|
|
|
toUser, err := s.usersRepo.GetByID(dbCtx, toUUID)
|
|
if err != nil {
|
|
return nil, nil, nil, errors.Internal("failed to get to user", err)
|
|
}
|
|
|
|
if s.hub != nil {
|
|
protoRequest := &friendsv1.FriendRequest{
|
|
Id: request.ID.String(),
|
|
FromUserId: fromUserID,
|
|
ToUserId: toUserID,
|
|
Status: friendsv1.FriendRequestStatus_FRIEND_REQUEST_STATUS_PENDING,
|
|
CreatedAt: timestamppb.New(request.CreatedAt),
|
|
UpdatedAt: timestamppb.New(request.UpdatedAt),
|
|
FromHandle: fromUser.Handle,
|
|
FromDisplayName: fromUser.DisplayName,
|
|
FromAvatarUrl: fromUser.AvatarURL,
|
|
ToHandle: toUser.Handle,
|
|
ToDisplayName: toUser.DisplayName,
|
|
ToAvatarUrl: toUser.AvatarURL,
|
|
}
|
|
|
|
toEvent := &streamv1.ServerEvent{
|
|
EventId: uuid.New().String(),
|
|
CreatedAt: timestamppb.Now(),
|
|
Payload: &streamv1.ServerEvent_FriendRequestCreated{
|
|
FriendRequestCreated: &streamv1.FriendRequestCreated{
|
|
Request: protoRequest,
|
|
},
|
|
},
|
|
}
|
|
|
|
fromEvent := &streamv1.ServerEvent{
|
|
EventId: uuid.New().String(),
|
|
CreatedAt: timestamppb.Now(),
|
|
Payload: &streamv1.ServerEvent_FriendRequestCreated{
|
|
FriendRequestCreated: &streamv1.FriendRequestCreated{
|
|
Request: protoRequest,
|
|
},
|
|
},
|
|
}
|
|
|
|
s.hub.BroadcastToUser(toUserID, toEvent)
|
|
s.hub.BroadcastToUser(fromUserID, fromEvent)
|
|
}
|
|
|
|
return request, fromUser, toUser, nil
|
|
}
|
|
|
|
func (s *Service) AcceptFriendRequest(ctx context.Context, requestID string) error {
|
|
userID := interceptor.GetUserID(ctx)
|
|
if userID == "" {
|
|
return errors.Unauthorized("user not authenticated")
|
|
}
|
|
userUUID, err := uuid.Parse(userID)
|
|
if err != nil {
|
|
return errors.BadRequest("invalid user id")
|
|
}
|
|
reqUUID, err := uuid.Parse(requestID)
|
|
if err != nil {
|
|
return errors.BadRequest("invalid request id")
|
|
}
|
|
|
|
req, err := s.repo.GetFriendRequest(ctx, reqUUID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if req.ToUserID != userUUID {
|
|
return errors.Forbidden("not authorized to accept this request")
|
|
}
|
|
if req.Status != "pending" {
|
|
return errors.Conflict("request already processed")
|
|
}
|
|
|
|
if err := s.repo.CreateFriendship(ctx, req.FromUserID, req.ToUserID); err != nil {
|
|
return err
|
|
}
|
|
if err := s.repo.UpdateRequestStatus(ctx, reqUUID, "accepted"); err != nil {
|
|
return err
|
|
}
|
|
|
|
if s.hub != nil {
|
|
fromUser, _ := s.usersRepo.GetByID(ctx, req.FromUserID)
|
|
toUser, _ := s.usersRepo.GetByID(ctx, req.ToUserID)
|
|
|
|
protoRequest := &friendsv1.FriendRequest{
|
|
Id: req.ID.String(),
|
|
FromUserId: req.FromUserID.String(),
|
|
ToUserId: req.ToUserID.String(),
|
|
Status: friendsv1.FriendRequestStatus_FRIEND_REQUEST_STATUS_ACCEPTED,
|
|
CreatedAt: timestamppb.New(req.CreatedAt),
|
|
UpdatedAt: timestamppb.Now(),
|
|
}
|
|
|
|
if fromUser != nil {
|
|
protoRequest.FromHandle = fromUser.Handle
|
|
protoRequest.FromDisplayName = fromUser.DisplayName
|
|
protoRequest.FromAvatarUrl = fromUser.AvatarURL
|
|
}
|
|
if toUser != nil {
|
|
protoRequest.ToHandle = toUser.Handle
|
|
protoRequest.ToDisplayName = toUser.DisplayName
|
|
protoRequest.ToAvatarUrl = toUser.AvatarURL
|
|
}
|
|
|
|
ev := &streamv1.ServerEvent{
|
|
EventId: uuid.New().String(),
|
|
CreatedAt: timestamppb.Now(),
|
|
Payload: &streamv1.ServerEvent_FriendRequestUpdated{
|
|
FriendRequestUpdated: &streamv1.FriendRequestUpdated{
|
|
Request: protoRequest,
|
|
},
|
|
},
|
|
}
|
|
s.hub.BroadcastToUser(req.FromUserID.String(), ev)
|
|
s.hub.BroadcastToUser(req.ToUserID.String(), ev)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) RejectFriendRequest(ctx context.Context, requestID string) error {
|
|
userID := interceptor.GetUserID(ctx)
|
|
if userID == "" {
|
|
return errors.Unauthorized("user not authenticated")
|
|
}
|
|
userUUID, err := uuid.Parse(userID)
|
|
if err != nil {
|
|
return errors.BadRequest("invalid user id")
|
|
}
|
|
reqUUID, err := uuid.Parse(requestID)
|
|
if err != nil {
|
|
return errors.BadRequest("invalid request id")
|
|
}
|
|
req, err := s.repo.GetFriendRequest(ctx, reqUUID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if req.ToUserID != userUUID {
|
|
return errors.Forbidden("not authorized to reject this request")
|
|
}
|
|
if req.Status != "pending" {
|
|
return errors.Conflict("request already processed")
|
|
}
|
|
|
|
if err := s.repo.UpdateRequestStatus(ctx, reqUUID, "rejected"); err != nil {
|
|
return err
|
|
}
|
|
|
|
if s.hub != nil {
|
|
fromUser, _ := s.usersRepo.GetByID(ctx, req.FromUserID)
|
|
toUser, _ := s.usersRepo.GetByID(ctx, req.ToUserID)
|
|
|
|
protoRequest := &friendsv1.FriendRequest{
|
|
Id: req.ID.String(),
|
|
FromUserId: req.FromUserID.String(),
|
|
ToUserId: req.ToUserID.String(),
|
|
Status: friendsv1.FriendRequestStatus_FRIEND_REQUEST_STATUS_REJECTED,
|
|
CreatedAt: timestamppb.New(req.CreatedAt),
|
|
UpdatedAt: timestamppb.Now(),
|
|
}
|
|
|
|
if fromUser != nil {
|
|
protoRequest.FromHandle = fromUser.Handle
|
|
protoRequest.FromDisplayName = fromUser.DisplayName
|
|
protoRequest.FromAvatarUrl = fromUser.AvatarURL
|
|
}
|
|
if toUser != nil {
|
|
protoRequest.ToHandle = toUser.Handle
|
|
protoRequest.ToDisplayName = toUser.DisplayName
|
|
protoRequest.ToAvatarUrl = toUser.AvatarURL
|
|
}
|
|
|
|
ev := &streamv1.ServerEvent{
|
|
EventId: uuid.New().String(),
|
|
CreatedAt: timestamppb.Now(),
|
|
Payload: &streamv1.ServerEvent_FriendRequestUpdated{
|
|
FriendRequestUpdated: &streamv1.FriendRequestUpdated{
|
|
Request: protoRequest,
|
|
},
|
|
},
|
|
}
|
|
s.hub.BroadcastToUser(req.FromUserID.String(), ev)
|
|
s.hub.BroadcastToUser(req.ToUserID.String(), ev)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) CancelFriendRequest(ctx context.Context, requestID string) error {
|
|
userID := interceptor.GetUserID(ctx)
|
|
if userID == "" {
|
|
return errors.Unauthorized("user not authenticated")
|
|
}
|
|
userUUID, err := uuid.Parse(userID)
|
|
if err != nil {
|
|
return errors.BadRequest("invalid user id")
|
|
}
|
|
reqUUID, err := uuid.Parse(requestID)
|
|
if err != nil {
|
|
return errors.BadRequest("invalid request id")
|
|
}
|
|
|
|
req, err := s.repo.GetFriendRequest(ctx, reqUUID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if req.FromUserID != userUUID {
|
|
return errors.Forbidden("not authorized to cancel this request")
|
|
}
|
|
if req.Status != "pending" {
|
|
return errors.Conflict("request already processed")
|
|
}
|
|
|
|
if err := s.repo.UpdateRequestStatus(ctx, reqUUID, "rejected"); err != nil {
|
|
return err
|
|
}
|
|
|
|
if s.hub != nil {
|
|
fromUser, _ := s.usersRepo.GetByID(ctx, req.FromUserID)
|
|
toUser, _ := s.usersRepo.GetByID(ctx, req.ToUserID)
|
|
|
|
protoRequest := &friendsv1.FriendRequest{
|
|
Id: req.ID.String(),
|
|
FromUserId: req.FromUserID.String(),
|
|
ToUserId: req.ToUserID.String(),
|
|
Status: friendsv1.FriendRequestStatus_FRIEND_REQUEST_STATUS_REJECTED,
|
|
CreatedAt: timestamppb.New(req.CreatedAt),
|
|
UpdatedAt: timestamppb.Now(),
|
|
}
|
|
|
|
if fromUser != nil {
|
|
protoRequest.FromHandle = fromUser.Handle
|
|
protoRequest.FromDisplayName = fromUser.DisplayName
|
|
protoRequest.FromAvatarUrl = fromUser.AvatarURL
|
|
}
|
|
if toUser != nil {
|
|
protoRequest.ToHandle = toUser.Handle
|
|
protoRequest.ToDisplayName = toUser.DisplayName
|
|
protoRequest.ToAvatarUrl = toUser.AvatarURL
|
|
}
|
|
|
|
ev := &streamv1.ServerEvent{
|
|
EventId: uuid.New().String(),
|
|
CreatedAt: timestamppb.Now(),
|
|
Payload: &streamv1.ServerEvent_FriendRequestUpdated{
|
|
FriendRequestUpdated: &streamv1.FriendRequestUpdated{
|
|
Request: protoRequest,
|
|
},
|
|
},
|
|
}
|
|
s.hub.BroadcastToUser(req.FromUserID.String(), ev)
|
|
s.hub.BroadcastToUser(req.ToUserID.String(), ev)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) RemoveFriend(ctx context.Context, friendUserID string) error {
|
|
userID := interceptor.GetUserID(ctx)
|
|
if userID == "" {
|
|
return errors.Unauthorized("user not authenticated")
|
|
}
|
|
|
|
userUUID, err := uuid.Parse(userID)
|
|
if err != nil {
|
|
return errors.BadRequest("invalid user id")
|
|
}
|
|
|
|
friendUUID, err := uuid.Parse(friendUserID)
|
|
if err != nil {
|
|
return errors.BadRequest("invalid friend user id")
|
|
}
|
|
|
|
if err := s.repo.DeleteFriendship(ctx, userUUID, friendUUID); err != nil {
|
|
return err
|
|
}
|
|
|
|
if s.hub != nil {
|
|
// notify the removed friend that the current user disappeared from their friend list
|
|
s.hub.BroadcastToUser(friendUserID, &streamv1.ServerEvent{
|
|
EventId: uuid.New().String(),
|
|
CreatedAt: timestamppb.Now(),
|
|
Payload: &streamv1.ServerEvent_FriendRemoved{
|
|
FriendRemoved: &streamv1.FriendRemoved{
|
|
UserId: userID,
|
|
RemovedBy: userID,
|
|
},
|
|
},
|
|
})
|
|
|
|
// notify the current user that the other user disappeared from their friend list
|
|
s.hub.BroadcastToUser(userID, &streamv1.ServerEvent{
|
|
EventId: uuid.New().String(),
|
|
CreatedAt: timestamppb.Now(),
|
|
Payload: &streamv1.ServerEvent_FriendRemoved{
|
|
FriendRemoved: &streamv1.FriendRemoved{
|
|
UserId: friendUserID,
|
|
RemovedBy: userID,
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) ListFriends(ctx context.Context) ([]*Friend, error) {
|
|
userID := interceptor.GetUserID(ctx)
|
|
if userID == "" {
|
|
return nil, errors.Unauthorized("user not authenticated")
|
|
}
|
|
|
|
userUUID, err := uuid.Parse(userID)
|
|
if err != nil {
|
|
return nil, errors.BadRequest("invalid user id")
|
|
}
|
|
|
|
friends, err := s.repo.ListFriends(ctx, userUUID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, friend := range friends {
|
|
presence := users.StatusOffline
|
|
if s.presence != nil {
|
|
presence = s.presence.GetStatus(friend.UserID)
|
|
}
|
|
|
|
friend.Status = users.EffectiveStatus(
|
|
users.NormalizeStatusPreference(friend.Status),
|
|
presence,
|
|
)
|
|
}
|
|
|
|
return friends, nil
|
|
}
|
|
|
|
func (s *Service) ListPendingRequests(ctx context.Context) (incoming, outgoing []*FriendRequestWithUser, err error) {
|
|
userID := interceptor.GetUserID(ctx)
|
|
if userID == "" {
|
|
return nil, nil, errors.Unauthorized("user not authenticated")
|
|
}
|
|
|
|
userUUID, err := uuid.Parse(userID)
|
|
if err != nil {
|
|
return nil, nil, errors.BadRequest("invalid user id")
|
|
}
|
|
|
|
incoming, err = s.repo.ListIncomingRequestsWithUsers(ctx, userUUID)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
outgoing, err = s.repo.ListOutgoingRequestsWithUsers(ctx, userUUID)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
return incoming, outgoing, nil
|
|
}
|
|
|
|
func (s *Service) BlockUser(ctx context.Context, blockedUserID string) error {
|
|
userID := interceptor.GetUserID(ctx)
|
|
if userID == "" {
|
|
return errors.Unauthorized("user not authenticated")
|
|
}
|
|
|
|
userUUID, err := uuid.Parse(userID)
|
|
if err != nil {
|
|
return errors.BadRequest("invalid user id")
|
|
}
|
|
|
|
blockedUUID, err := uuid.Parse(blockedUserID)
|
|
if err != nil {
|
|
return errors.BadRequest("invalid blocked user id")
|
|
}
|
|
|
|
if userUUID == blockedUUID {
|
|
return errors.BadRequest("cannot block yourself")
|
|
}
|
|
|
|
friendshipRemoved := false
|
|
if err := s.repo.DeleteFriendship(ctx, userUUID, blockedUUID); err == nil {
|
|
friendshipRemoved = true
|
|
} else if !errors.IsNotFound(err) {
|
|
return err
|
|
}
|
|
|
|
if err := s.repo.BlockUser(ctx, userUUID, blockedUUID); err != nil {
|
|
return err
|
|
}
|
|
|
|
if s.hub != nil {
|
|
if friendshipRemoved {
|
|
// blocked user should remove blocker from their friend list
|
|
s.hub.BroadcastToUser(blockedUserID, &streamv1.ServerEvent{
|
|
EventId: uuid.New().String(),
|
|
CreatedAt: timestamppb.Now(),
|
|
Payload: &streamv1.ServerEvent_FriendRemoved{
|
|
FriendRemoved: &streamv1.FriendRemoved{
|
|
UserId: userID,
|
|
RemovedBy: userID,
|
|
},
|
|
},
|
|
})
|
|
|
|
// blocker should remove blocked user from their friend list
|
|
s.hub.BroadcastToUser(userID, &streamv1.ServerEvent{
|
|
EventId: uuid.New().String(),
|
|
CreatedAt: timestamppb.Now(),
|
|
Payload: &streamv1.ServerEvent_FriendRemoved{
|
|
FriendRemoved: &streamv1.FriendRemoved{
|
|
UserId: blockedUserID,
|
|
RemovedBy: userID,
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
s.hub.BroadcastToUser(blockedUserID, &streamv1.ServerEvent{
|
|
EventId: uuid.New().String(),
|
|
CreatedAt: timestamppb.Now(),
|
|
Payload: &streamv1.ServerEvent_UserBlocked{
|
|
UserBlocked: &streamv1.UserBlocked{
|
|
BlockerId: userID,
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) UnblockUser(ctx context.Context, blockedUserID string) error {
|
|
userID := interceptor.GetUserID(ctx)
|
|
if userID == "" {
|
|
return errors.Unauthorized("user not authenticated")
|
|
}
|
|
|
|
userUUID, err := uuid.Parse(userID)
|
|
if err != nil {
|
|
return errors.BadRequest("invalid user id")
|
|
}
|
|
|
|
blockedUUID, err := uuid.Parse(blockedUserID)
|
|
if err != nil {
|
|
return errors.BadRequest("invalid blocked user id")
|
|
}
|
|
|
|
if err := s.repo.UnblockUser(ctx, userUUID, blockedUUID); err != nil {
|
|
return err
|
|
}
|
|
|
|
if s.hub != nil {
|
|
s.hub.BroadcastToUser(blockedUserID, &streamv1.ServerEvent{
|
|
EventId: uuid.New().String(),
|
|
CreatedAt: timestamppb.Now(),
|
|
Payload: &streamv1.ServerEvent_UserUnblocked{
|
|
UserUnblocked: &streamv1.UserUnblocked{
|
|
UnblockerId: userID,
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) ListBlockedUsers(ctx context.Context) ([]string, error) {
|
|
userID := interceptor.GetUserID(ctx)
|
|
if userID == "" {
|
|
return nil, errors.Unauthorized("user not authenticated")
|
|
}
|
|
|
|
userUUID, err := uuid.Parse(userID)
|
|
if err != nil {
|
|
return nil, errors.BadRequest("invalid user id")
|
|
}
|
|
|
|
blockedIDs, err := s.repo.ListBlockedUsers(ctx, userUUID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
result := make([]string, len(blockedIDs))
|
|
for i, id := range blockedIDs {
|
|
result[i] = id.String()
|
|
}
|
|
|
|
return result, nil
|
|
}
|