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 (
GetFriendshipStatusesfor 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
GetFriendLocationsfeed: friend list ×user:location:×presence:×active_location:with a single RedisMGET - Log every friend action (
friend_request_sent,friend_request_accepted,location_checkin) throughActivityLogService
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¶
friendships—id,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.UserRepository—IsLocationSharedflag on the user record, friend list page enrichmentRedisService— friend list cache, location store, presence + active-location lookupsActivityLogService— 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.