This specification defines the updated friend request protocol for Decentraland, replacing the previous implementation described in ADR-137. The new protocol leverages the existing Social Service infrastructure and follows the established patterns for real-time communication, providing a more robust and maintainable solution for managing friendship relationships between users.
The complete protocol definition can be found in the Decentraland Protocol repository.
The original friend request implementation described in ADR-137 was designed to work with Matrix as the underlying communication system. However, as outlined in ADR-208, Decentraland has moved away from Matrix due to its complexity and cost. The current Social Service implementation provides a more efficient and scalable solution for managing social interactions.
This updated protocol leverages the existing Social Service infrastructure, which already
handles friendship management through the UpsertFriendship RPC call and real-time
updates via WebSocket subscriptions. The implementation follows the same patterns established
for private voice chats in
ADR-284.
This section specifies the interaction between the Explorer and the Social Service for managing friend requests, following the established patterns for real-time communication.
Sending a friend request is done by performing the UpsertFriendship RPC call with
the request action to the Social Service. The request includes the target user's
address and an optional message.
sequenceDiagram
participant U1 as Requester
participant SS as Social Service
U1->>SS: UpsertFriendship(request: { user: "0x...", message: "Hello!" })
alt User is blocked or invalid
SS-->>U1: InvalidFriendshipAction error
else Request is successful
SS-->>U1: Accepted response with friendship details
end
The RPC call is specified in the protocol as:
message UpsertFriendshipPayload {
message RequestPayload {
User user = 1;
optional string message = 3;
}
message AcceptPayload { User user = 1; }
message RejectPayload { User user = 1; }
message DeletePayload { User user = 1; }
message CancelPayload { User user = 1; }
oneof action {
RequestPayload request = 1;
AcceptPayload accept = 2;
RejectPayload reject = 4;
DeletePayload delete = 5;
CancelPayload cancel = 6;
}
}
message UpsertFriendshipResponse {
message Accepted {
string id = 1;
int64 created_at = 2;
FriendProfile friend = 3;
optional string message = 4;
}
oneof response {
Accepted accepted = 1;
InvalidFriendshipAction invalid_friendship_action = 2;
InternalServerError internal_server_error = 3;
InvalidRequest invalid_request = 4;
}
}
rpc UpsertFriendship(UpsertFriendshipPayload) returns
(UpsertFriendshipResponse) {}
Accepting a friend request is done by performing the UpsertFriendship RPC call
with the accept action to the Social Service with the requester's address.
sequenceDiagram
participant U2 as Recipient
participant SS as Social Service
U2->>SS: UpsertFriendship(accept: { user: "0x..." })
alt Request doesn't exist or user is blocked
SS-->>U2: InvalidFriendshipAction error
else Request is accepted successfully
SS-->>U2: Accepted response with friendship details
end
Rejecting a friend request is done by performing the UpsertFriendship RPC call
with the reject action to the Social Service with the requester's address.
sequenceDiagram
participant U2 as Recipient
participant SS as Social Service
U2->>SS: UpsertFriendship(reject: { user: "0x..." })
alt Request doesn't exist or user is blocked
SS-->>U2: InvalidFriendshipAction error
else Request is rejected successfully
SS-->>U2: Accepted response with friendship details
end
Canceling a sent friend request is done by performing the UpsertFriendship RPC
call with the cancel action to the Social Service with the recipient's address.
sequenceDiagram
participant U1 as Requester
participant SS as Social Service
U1->>SS: UpsertFriendship(cancel: { user: "0x..." })
alt Request doesn't exist or user is blocked
SS-->>U1: InvalidFriendshipAction error
else Request is canceled successfully
SS-->>U1: Accepted response with friendship details
end
Deleting an existing friendship is done by performing the UpsertFriendship RPC
call with the delete action to the Social Service with the friend's address.
sequenceDiagram
participant U1 as User
participant SS as Social Service
U1->>SS: UpsertFriendship(delete: { user: "0x..." })
alt Friendship doesn't exist or user is blocked
SS-->>U1: InvalidFriendshipAction error
else Friendship is deleted successfully
SS-->>U1: Accepted response with friendship details
end
Retrieving pending friend requests is done through separate RPC calls for sent and received requests.
sequenceDiagram
participant U as User
participant SS as Social Service
U->>SS: GetPendingFriendshipRequests(pagination: { limit: 50, offset: 0 })
SS-->>U: PaginatedFriendshipRequestsResponse with received requests
message GetFriendshipRequestsPayload {
optional Pagination pagination = 1;
}
message FriendshipRequestResponse {
FriendProfile friend = 1;
int64 created_at = 2;
optional string message = 3;
string id = 4;
}
message FriendshipRequests {
repeated FriendshipRequestResponse requests = 1;
}
message PaginatedFriendshipRequestsResponse {
oneof response {
FriendshipRequests requests = 1;
InternalServerError internal_server_error = 2;
}
optional PaginatedResponse pagination_data = 3;
}
rpc GetPendingFriendshipRequests(GetFriendshipRequestsPayload) returns
(PaginatedFriendshipRequestsResponse) {}
sequenceDiagram
participant U as User
participant SS as Social Service
U->>SS: GetSentFriendshipRequests(pagination: { limit: 50, offset: 0 })
SS-->>U: PaginatedFriendshipRequestsResponse with sent requests
rpc GetSentFriendshipRequests(GetFriendshipRequestsPayload) returns
(PaginatedFriendshipRequestsResponse) {}
Retrieving the user's current friends list is done through the GetFriends RPC
call.
sequenceDiagram
participant U as User
participant SS as Social Service
U->>SS: GetFriends(pagination: { limit: 50, offset: 0 })
SS-->>U: PaginatedFriendsProfilesResponse with friends list
message GetFriendsPayload {
optional Pagination pagination = 1;
}
message PaginatedFriendsProfilesResponse {
repeated FriendProfile friends = 1;
PaginatedResponse pagination_data = 2;
}
rpc GetFriends(GetFriendsPayload) returns (PaginatedFriendsProfilesResponse) {}
Retrieving mutual friends between two users is done through the
GetMutualFriends RPC call.
sequenceDiagram
participant U1 as User
participant SS as Social Service
U1->>SS: GetMutualFriends(user: "0x...", pagination: { limit: 50, offset: 0 })
SS-->>U1: PaginatedFriendsProfilesResponse with mutual friends
message GetMutualFriendsPayload {
User user = 1;
optional Pagination pagination = 2;
}
rpc GetMutualFriends(GetMutualFriendsPayload)
returns (PaginatedFriendsProfilesResponse) {}
Checking the friendship status between two users is done by performing the
GetFriendshipStatus RPC call.
sequenceDiagram
participant U1 as User
participant SS as Social Service
U1->>SS: GetFriendshipStatus(user: "0x...")
SS-->>U1: GetFriendshipStatusResponse with status
message GetFriendshipStatusPayload {
User user = 1;
}
enum FriendshipStatus {
REQUEST_SENT = 0;
REQUEST_RECEIVED = 1;
CANCELED = 2;
ACCEPTED = 3;
REJECTED = 4;
DELETED = 5;
BLOCKED = 6;
NONE = 7;
BLOCKED_BY = 8;
}
message GetFriendshipStatusResponse {
message Ok {
FriendshipStatus status = 1;
optional string message = 2;
}
oneof response {
Ok accepted = 1;
InternalServerError internal_server_error = 2;
InvalidRequest invalid_request = 3;
}
}
rpc GetFriendshipStatus(GetFriendshipStatusPayload) returns
(GetFriendshipStatusResponse) {}
Subscribing to friendship updates is critical for real-time friend request management. This
subscription is done by performing the SubscribeToFriendshipUpdates RPC call to
the Social Service.
sequenceDiagram
participant U1 as User 1
participant U2 as User 2
participant SS as Social Service
U1->>SS: Subscribe to friendship updates
U2->>SS: Subscribe to friendship updates
alt Friend request sent by U1
U1->>SS: UpsertFriendship(request: { user: U2, message: "Hello!" })
SS-->>U2: FriendshipUpdate(request: RequestResponse)
else Friend request accepted by U2
U2->>SS: UpsertFriendship(accept: { user: U1 })
SS-->>U1: FriendshipUpdate(accept: AcceptResponse)
else Friend request rejected by U2
U2->>SS: UpsertFriendship(reject: { user: U1 })
SS-->>U1: FriendshipUpdate(reject: RejectResponse)
else Friend request canceled by U1
U1->>SS: UpsertFriendship(cancel: { user: U2 })
SS-->>U2: FriendshipUpdate(cancel: CancelResponse)
else Friendship deleted by U1
U1->>SS: UpsertFriendship(delete: { user: U2 })
SS-->>U2: FriendshipUpdate(delete: DeleteResponse)
else User U2 blocked by U1
U1->>SS: BlockUser(user: U2)
SS-->>U2: FriendshipUpdate(block: BlockResponse)
end
message FriendshipUpdate {
message RequestResponse {
FriendProfile friend = 1;
int64 created_at = 2;
optional string message = 3;
string id = 4;
}
message AcceptResponse { User user = 1; }
message RejectResponse { User user = 1; }
message DeleteResponse { User user = 1; }
message CancelResponse { User user = 1; }
message BlockResponse { User user = 1; }
oneof update {
RequestResponse request = 1;
AcceptResponse accept = 2;
RejectResponse reject = 3;
DeleteResponse delete = 4;
CancelResponse cancel = 5;
BlockResponse block = 6;
}
}
rpc SubscribeToFriendshipUpdates(google.protobuf.Empty) returns
(stream FriendshipUpdate) {}
Subscribing to friend connectivity updates provides real-time information about friends' online status.
sequenceDiagram
participant U as User
participant SS as Social Service
U->>SS: Subscribe to friend connectivity updates
alt Friend goes online/offline/away
SS-->>U: FriendConnectivityUpdate(friend: {...}, status: ONLINE/OFFLINE/AWAY)
end
enum ConnectivityStatus {
ONLINE = 0;
OFFLINE = 1;
AWAY = 2;
}
message FriendConnectivityUpdate {
FriendProfile friend = 1;
ConnectivityStatus status = 2;
}
rpc SubscribeToFriendConnectivityUpdates(google.protobuf.Empty) returns
(stream FriendConnectivityUpdate) {}
The protocol also supports blocking and unblocking users, which affects friend request functionality.
sequenceDiagram
participant U1 as Blocker
participant SS as Social Service
U1->>SS: BlockUser(user: "0x...")
alt User doesn't exist
SS-->>U1: ProfileNotFound error
else User is blocked successfully
SS-->>U1: BlockUserResponse with blocked user profile
end
message BlockUserPayload {
User user = 1;
}
message BlockUserResponse {
message Ok {
BlockedUserProfile profile = 1;
}
oneof response {
Ok ok = 1;
InternalServerError internal_server_error = 2;
InvalidRequest invalid_request = 3;
ProfileNotFound profile_not_found = 4;
}
}
rpc BlockUser(BlockUserPayload) returns (BlockUserResponse) {}
sequenceDiagram
participant U1 as Unblocker
participant SS as Social Service
U1->>SS: UnblockUser(user: "0x...")
alt User doesn't exist
SS-->>U1: ProfileNotFound error
else User is unblocked successfully
SS-->>U1: UnblockUserResponse with unblocked user profile
end
message UnblockUserPayload {
User user = 1;
}
message UnblockUserResponse {
message Ok {
BlockedUserProfile profile = 1;
}
oneof response {
Ok ok = 1;
InternalServerError internal_server_error = 2;
InvalidRequest invalid_request = 3;
ProfileNotFound profile_not_found = 4;
}
}
rpc UnblockUser(UnblockUserPayload) returns (UnblockUserResponse) {}
Retrieving the list of blocked users is done through the GetBlockedUsers RPC
call.
sequenceDiagram
participant U as User
participant SS as Social Service
U->>SS: GetBlockedUsers(pagination: { limit: 50, offset: 0 })
SS-->>U: GetBlockedUsersResponse with blocked users list
message GetBlockedUsersPayload {
optional Pagination pagination = 1;
}
message GetBlockedUsersResponse {
repeated BlockedUserProfile profiles = 1;
PaginatedResponse pagination_data = 2;
}
rpc GetBlockedUsers(GetBlockedUsersPayload)
returns (GetBlockedUsersResponse) {}
Checking the blocking status between users is done through the
GetBlockingStatus RPC call.
sequenceDiagram
participant U as User
participant SS as Social Service
U->>SS: GetBlockingStatus()
SS-->>U: GetBlockingStatusResponse with blocking information
message GetBlockingStatusResponse {
repeated string blocked_users = 1;
repeated string blocked_by_users = 2;
}
rpc GetBlockingStatus(google.protobuf.Empty)
returns (GetBlockingStatusResponse) {}
sequenceDiagram
participant U as User
participant SS as Social Service
U->>SS: Subscribe to block updates
alt User is blocked/unblocked
SS-->>U: BlockUpdate(address: "0x...", is_blocked: true/false)
end
message BlockUpdate {
string address = 1;
bool is_blocked = 2;
}
rpc SubscribeToBlockUpdates(google.protobuf.Empty) returns
(stream BlockUpdate) {}
The client implementation carries the crucial task of maintaining friend request functionality and respecting user-defined social settings.
The client must maintain a subscription to friendship updates to receive real-time notifications about friend request changes. This subscription should be established upon connecting to the Social Service and maintained throughout the session.
The client must handle various error scenarios gracefully. All error types are defined in the
decentraland/social_service/errors.proto
file and include:
The client must maintain local state for:
The client must respect blocking functionality when displaying friend requests and managing friendships. This includes:
This updated protocol replaces the previous implementation described in ADR-137. Key changes include:
UpsertFriendship RPC call with different action types
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 and RFC 8174.