Social Graph

Friends, blocking, groups, presence tracking, real-time chat, and horizontal sharding in Aether's social system.

The social graph is the backbone of player interaction in Aether. It manages friendships, group membership, presence tracking, real-time chat, and user blocking with mutual invisibility. The aether-social crate provides all social logic as composable, in-memory modules with no external dependencies.

Key Concepts

  • Friendships -- Bidirectional relationships with a request/accept lifecycle.
  • Blocking -- Directional in storage but enforced bidirectionally for mutual invisibility.
  • Groups -- Shared activity containers with ownership, invitations, and capacity limits.
  • Presence -- Online/offline/in-world status with visibility controls.
  • Chat -- DM, group, and world channels with message history.
  • Sharding -- User-ID-based partitioning for horizontal scalability.

Architecture

The social system is composed through a facade pattern. The SocialService coordinates all subsystems and enforces cross-cutting concerns like block-list checks:

SocialService (facade)
  |-- FriendManager    (friend.rs)
  |-- GroupManager     (group.rs)
  |-- ChatManager      (chat.rs)
  |-- PresenceTracker  (presence.rs)
  |-- BlockList        (blocking.rs)

Each manager uses HashMap-based in-memory storage. The facade checks the block list before delegating to any manager, ensuring blocked users are consistently invisible across all social features.

User Blocking

Blocking is the foundation of social safety. When a user blocks another, the block is stored directionally but enforced bidirectionally:

use aether_social::{BlockList, SocialError};

let mut block_list = BlockList::new();

// Alice blocks Bob
block_list.block(alice_id, bob_id)?;

// Bidirectional check -- either direction returns true
assert!(block_list.is_blocked_either(alice_id, bob_id));
assert!(block_list.is_blocked_either(bob_id, alice_id));

// Unblock restores visibility
block_list.unblock(alice_id, bob_id)?;

Blocking has cascading effects across the social system:

  • Existing friendships are automatically removed.
  • Friend requests between blocked users are rejected.
  • Chat messages from blocked users are filtered.
  • Presence information becomes invisible to the blocked party.
  • Group invitations between blocked users are rejected.

Friend System

Friendships follow a request/accept lifecycle with strict validation rules:

use aether_social::FriendManager;

let mut friends = FriendManager::new();

// Send a friend request
friends.send_request(alice_id, bob_id)?;

// Bob sees the pending request
let pending = friends.get_pending_requests(bob_id);
assert!(pending.contains(&alice_id));

// Bob accepts
friends.accept_request(alice_id, bob_id)?;
assert!(friends.are_friends(alice_id, bob_id));

// Either party can unfriend
friends.unfriend(alice_id, bob_id)?;

The friend state machine enforces these transitions:

  • None -> Pending via send_request
  • Pending -> Accepted via accept_request
  • Pending -> Rejected via reject_request
  • Accepted -> None via unfriend

Validation rules prevent invalid operations:

  • Cannot send a request to yourself.
  • Cannot send a request if already friends.
  • Cannot send a request if either party has blocked the other.
  • Duplicate pending requests return an error.
  • Only the recipient can accept or reject a request.

Group Management

Groups support shared activities with ownership, invite controls, and capacity limits:

use aether_social::{GroupManager, GroupConfig};

let mut groups = GroupManager::new();

// Create a group
let config = GroupConfig {
    name: "Explorers Guild".to_string(),
    max_members: 50,
    invite_only: true,
};
let group_id = groups.create_group(alice_id, config)?;

// Invite a user (only members can invite for invite-only groups)
groups.invite_user(&group_id, alice_id, bob_id)?;

// Bob accepts the invite
groups.accept_invite(&group_id, bob_id)?;

// Query user's groups
let user_groups = groups.get_user_groups(bob_id);
assert!(user_groups.contains(&group_id));

// Owner disbands
groups.disband_group(&group_id, alice_id)?;

Group rules:

  • Only the owner can disband a group.
  • Only current members can invite others (for invite-only groups).
  • Public groups allow direct join without invitation.
  • Maximum member count is enforced.
  • If the owner leaves, the group is disbanded.
  • Blocked users cannot be invited.

Chat System

Chat supports DM, group, and world channels with message history:

use aether_social::{ChatManager, ChannelKind, MessageKind};

let mut chat = ChatManager::new();

// Create a DM channel between two users
let channel_id = chat.create_channel(
    ChannelKind::DirectMessage,
    vec![alice_id, bob_id],
);

// Send a text message
let msg = chat.send_message(&channel_id, alice_id, MessageKind::Text {
    content: "Hello!".to_string(),
})?;

// Retrieve message history
let history = chat.get_messages(&channel_id, 50);
assert_eq!(history.len(), 1);

Channel types:

KindMembersDescription
DirectMessageExactly 2Private conversation between two users
Group2+Tied to a social group
WorldDynamicLocation-based chat within a world

Chat enforces social rules:

  • Only channel members can send messages.
  • Blocked users cannot send messages to channels containing the blocker.
  • DM channels are restricted to exactly 2 members.

Presence Tracking

Presence tracks whether users are online, offline, or active in a specific world:

use aether_social::{PresenceTracker, PresenceState};

let mut presence = PresenceTracker::new();

// User comes online
presence.set_online(alice_id);

// User enters a world
presence.enter_world(alice_id, "world-42".to_string());

// Query presence
let state = presence.get_presence(alice_id);
// Returns: InWorld { location: "world-42" }

// Visibility-aware query (respects blocking)
let visible = presence.get_visible_presence(alice_id, viewer_id);
// Returns None if viewer is blocked by alice

Presence states:

  • Offline -- User is not connected.
  • Online -- User is connected but not in a world.
  • InWorld { location } -- User is active in a specific world.

Users can set their visibility to control who sees their presence:

use aether_social::PresenceVisibility;

presence.set_visibility(alice_id, PresenceVisibility::FriendsOnly)?;

Cross-Cutting Concerns

The SocialService facade composes all managers and enforces block-list checks at every entry point. Blocking a user automatically removes any friendship, and subsequent friend requests or chat messages between blocked users return SocialError::UserBlocked.

All social operations return typed errors through the SocialError enum, covering states like UserBlocked, AlreadyFriends, GroupFull, NotChannelMember, and others.

Horizontal Sharding

For production deployments, the social system supports user-ID-based sharding via ShardMapPolicy to distribute load across multiple service instances. Sharding is aligned with user ID partitioning so all of a user's social data (friends, groups, presence, chat membership) lives on the same shard, avoiding cross-shard queries for common operations.

The aether-social crate is zero-dependency, using only Rust standard library collections with u64 user IDs and String entity IDs.