Skip to content

Friends

Purpose

FriendService owns the friendship graph and the live location feed that depends on it. The friendship model is a single undirected row in friendships, with the requester stored in user_id_1 and the recipient in user_id_2. Status is one of pending, accepted, or blocked. The same service also handles per-user location updates and exposes the multi-friend "where is everyone" feed used by the Discovery map and radar.

Responsibilities

  • Send / accept / reject / cancel a friend request (single row, status flip)
  • Unfriend (delete the row, both sides cache-invalidated)
  • List friends (with pagination), pending requests received, pending requests sent
  • Batch friendship-status lookup (GetFriendshipStatuses for a list of target IDs)
  • Cache the caller's friend list in Redis (user:friends:{userID}, 10-min TTL) — invalidated on accept/unfriend
  • Update the caller's location in Redis (user:location:{userID}) with privacy-toggle short-circuit and "arrived_at" preservation across updates within 50m
  • Compose the GetFriendLocations feed: friend list × user:location: × presence: × active_location: with a single Redis MGET
  • Log every friend action (friend_request_sent, friend_request_accepted, location_checkin) through ActivityLogService

HTTP endpoints

Method Path Auth Description
POST /api/v1/friends/request JWT Send a friend request
POST /api/v1/friends/accept JWT Accept a pending request
POST /api/v1/friends/reject JWT Reject or cancel a request
DELETE /api/v1/friends/:id JWT Unfriend
GET /api/v1/friends JWT List accepted friends (limit / offset for paging)
GET /api/v1/friends/requests JWT Pending requests received
GET /api/v1/friends/sent JWT Pending requests sent
POST /api/v1/location/update JWT Update caller's location (Redis only, no DB write)
GET /api/v1/location/friends JWT Friend live-location feed (optional lat/lng/radius filter)

Mutual-friends count

Mutual-friends data is computed by DiscoveryService (GetUserProfile, SearchUsers), not by FriendService. The friend service exposes only the raw graph.

Key types

type FriendService struct {
    friendRepo   repository.FriendRepository
    userRepo     repository.UserRepository
    redisService RedisService
    activityLog  ActivityLogService
}

type Location struct {
    Lat       float64
    Lng       float64
    UpdatedAt time.Time
    ArrivedAt time.Time // preserved across updates within 50m
}

type FriendLocationInfo struct {
    Location
    FriendID          string
    FriendName        string
    AvatarURL         string
    LastActiveAt      *time.Time
    IsOnline          bool
    IsActivelySharing bool
}

// Friendship statuses
const (
    FriendshipStatusPending  = "pending"
    FriendshipStatusAccepted = "accepted"
    FriendshipStatusBlocked  = "blocked"
)

Data model

  • friendshipsid, user_id_1 (requester), user_id_2 (recipient), status, created_at. A single row per pair; sender/recipient ordering matters only for "pending sent" vs. "pending received" UI.
  • No location data is persisted in PostgreSQL. Live location lives entirely in Redis at user:location:{userID} with a 24-hour TTL.

Dependencies

  • repository.FriendRepository — graph reads/writes (FindBetweenUsers, ListFriends, ListPendingRequests, ListSentRequests, GetFriendshipsWithUsers)
  • repository.UserRepositoryIsLocationShared flag on the user record, friend list page enrichment
  • RedisService — friend list cache, location store, presence + active-location lookups
  • ActivityLogService — async logging

Notable behavior

Privacy is one-way

The caller's is_location_shared flag controls whether others see them. It does not restrict what the caller can see — GetFriendLocations always returns visible friend locations regardless of the caller's own sharing setting. Only friends whose own is_location_shared is true appear in the result.

Stay-duration preservation

UpdateLocation checks the prior Redis value; if the new coords are within ~50m (< 0.05 km) of the previous, ArrivedAt is preserved. This lets the UI show "been here X minutes" without a separate journal.

Single-MGET friend feed

GetFriendLocations issues exactly one Redis MGET per call with 3N keys: user:location:, presence:, and active_location: for each friend. There is no per-friend round-trip — so the feed scales linearly with the friend list and the network cost stays flat.

AcceptRequest takes the requester's ID, not the request ID

For historical reasons, POST /friends/accept takes a friend_id (the other user's UUID), not the friendship row's ID. The service then uses FindBetweenUsers(userID, friendID) to resolve the row and confirms UserID2 == userID before flipping status. The same is true for RejectRequest and Unfriend.

Where to look