ActivityPub actions

This commit is contained in:
2025-12-28 22:18:50 +08:00
parent 9f4a7a3fe8
commit 95472df02b
5 changed files with 1249 additions and 3 deletions

View File

@@ -1,8 +1,5 @@
using DysonNetwork.Shared.Models; using DysonNetwork.Shared.Models;
using DysonNetwork.Sphere.ActivityPub;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using NodaTime;
using System.Net.Http.Headers;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;

View File

@@ -0,0 +1,416 @@
using DysonNetwork.Shared.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using NodaTime;
namespace DysonNetwork.Sphere.ActivityPub;
[ApiController]
[Route("api/activitypub")]
[Authorize]
public class ActivityPubFollowController(
AppDatabase db,
ActivityPubDeliveryService deliveryService,
IConfiguration configuration,
ILogger<ActivityPubFollowController> logger
) : ControllerBase
{
private string Domain => configuration["ActivityPub:Domain"] ?? "localhost";
[HttpPost("follow")]
public async Task<ActionResult> FollowRemoteUser([FromBody] FollowRequest request)
{
var currentUser = GetCurrentUser();
if (currentUser == null)
return Unauthorized(new { error = "Not authenticated" });
var publisher = await db.Publishers
.Include(p => p.Members)
.Where(p => p.Members.Any(m => m.AccountId == currentUser))
.FirstOrDefaultAsync();
if (publisher == null)
return BadRequest(new { error = "User doesn't have a publisher" });
logger.LogInformation("User {UserId} wants to follow {TargetActor}",
currentUser, request.TargetActorUri);
var success = await deliveryService.SendFollowActivityAsync(
publisher.Id,
request.TargetActorUri
);
if (success)
{
return Ok(new
{
success = true,
message = "Follow request sent. Waiting for acceptance.",
targetActorUri = request.TargetActorUri
});
}
return BadRequest(new { error = "Failed to send follow request" });
}
[HttpPost("unfollow")]
public async Task<ActionResult> UnfollowRemoteUser([FromBody] UnfollowRequest request)
{
var currentUser = GetCurrentUser();
if (currentUser == null)
return Unauthorized(new { error = "Not authenticated" });
var publisher = await db.Publishers
.Include(p => p.Members)
.Where(p => p.Members.Any(m => m.AccountId == currentUser))
.FirstOrDefaultAsync();
if (publisher == null)
return BadRequest(new { error = "User doesn't have a publisher" });
var success = await deliveryService.SendUndoActivityAsync(
"Follow",
request.TargetActorUri,
publisher.Id
);
if (success)
{
return Ok(new
{
success = true,
message = "Unfollowed successfully"
});
}
return BadRequest(new { error = "Failed to unfollow" });
}
[HttpGet("following")]
public async Task<ActionResult<List<SnFediverseActor>>> GetFollowing(
[FromQuery] int limit = 50
)
{
var currentUser = GetCurrentUser();
if (currentUser == null)
return Unauthorized();
var publisher = await db.Publishers
.Include(p => p.Members)
.Where(p => p.Members.Any(m => m.AccountId == currentUser))
.FirstOrDefaultAsync();
if (publisher == null)
return Ok(new List<SnFediverseActor>());
var actors = await db.FediverseRelationships
.Include(r => r.TargetActor)
.Where(r =>
r.IsLocalActor &&
r.LocalPublisherId == publisher.Id &&
r.IsFollowing &&
r.State == RelationshipState.Accepted)
.OrderByDescending(r => r.FollowedAt)
.Select(r => r.TargetActor)
.Take(limit)
.ToListAsync();
return Ok(actors);
}
[HttpGet("followers")]
public async Task<ActionResult<List<SnFediverseActor>>> GetFollowers(
[FromQuery] int limit = 50
)
{
var currentUser = GetCurrentUser();
if (currentUser == null)
return Unauthorized();
var publisher = await db.Publishers
.Include(p => p.Members)
.Where(p => p.Members.Any(m => m.AccountId == currentUser))
.FirstOrDefaultAsync();
if (publisher == null)
return Ok(new List<SnFediverseActor>());
var actors = await db.FediverseRelationships
.Include(r => r.Actor)
.Where(r =>
!r.IsLocalActor &&
r.LocalPublisherId == publisher.Id &&
r.IsFollowedBy &&
r.State == RelationshipState.Accepted)
.OrderByDescending(r => r.FollowedAt ?? r.CreatedAt)
.Select(r => r.Actor)
.Take(limit)
.ToListAsync();
return Ok(actors);
}
[HttpGet("search")]
public async Task<ActionResult<List<SnFediverseActor>>> SearchRemoteUsers(
[FromQuery] string query,
[FromQuery] int limit = 20
)
{
if (string.IsNullOrWhiteSpace(query))
return BadRequest(new { error = "Query is required" });
var actors = await db.FediverseActors
.Where(a =>
a.Username.Contains(query) ||
a.DisplayName != null && a.DisplayName.Contains(query))
.OrderByDescending(a => a.LastActivityAt ?? a.CreatedAt)
.Take(limit)
.ToListAsync();
return Ok(actors);
}
[HttpGet("relationships")]
public async Task<ActionResult<RelationshipsSummary>> GetRelationships()
{
var currentUser = GetCurrentUser();
if (currentUser == null)
return Unauthorized();
var publisher = await db.Publishers
.Include(p => p.Members)
.Where(p => p.Members.Any(m => m.AccountId == currentUser))
.FirstOrDefaultAsync();
if (publisher == null)
return NotFound(new { error = "Publisher not found" });
var actorUrl = $"https://{Domain}/activitypub/actors/{publisher.Name}";
var followingCount = await db.FediverseRelationships
.CountAsync(r =>
r.IsLocalActor &&
r.LocalPublisherId == publisher.Id &&
r.IsFollowing &&
r.State == RelationshipState.Accepted);
var followersCount = await db.FediverseRelationships
.CountAsync(r =>
!r.IsLocalActor &&
r.LocalPublisherId == publisher.Id &&
r.IsFollowedBy &&
r.State == RelationshipState.Accepted);
var pendingCount = await db.FediverseRelationships
.CountAsync(r =>
r.IsLocalActor &&
r.LocalPublisherId == publisher.Id &&
r.State == RelationshipState.Pending);
var relationships = await db.FediverseRelationships
.Include(r => r.TargetActor)
.Where(r => r.IsLocalActor && r.LocalPublisherId == publisher.Id)
.OrderByDescending(r => r.FollowedAt ?? r.CreatedAt)
.Take(20)
.ToListAsync();
return Ok(new RelationshipsSummary
{
ActorUri = actorUrl,
FollowingCount = followingCount,
FollowersCount = followersCount,
PendingCount = pendingCount,
Relationships = relationships.Select(r => new RelationshipSummaryItem
{
Actor = r.TargetActor,
State = r.State,
IsFollowing = r.IsFollowing,
FollowedAt = r.FollowedAt,
TargetActorUri = r.TargetActor.Uri,
Username = r.TargetActor.Username,
DisplayName = r.TargetActor.DisplayName
}).ToList()
});
}
[HttpGet("check/{username}")]
[AllowAnonymous]
public async Task<ActionResult<ActorCheckResult>> CheckActor(string username)
{
var actorUrl = GetActorUrl(username);
var existingActor = await db.FediverseActors
.Include(snFediverseActor => snFediverseActor.Instance)
.FirstOrDefaultAsync(a => a.Uri == actorUrl);
if (existingActor != null)
{
return Ok(new ActorCheckResult
{
Exists = true,
Actor = existingActor,
ActorUri = existingActor.Uri,
Username = existingActor.Username,
DisplayName = existingActor.DisplayName,
Bio = existingActor.Bio,
AvatarUrl = existingActor.AvatarUrl,
InstanceDomain = existingActor.Instance.Domain,
PublicKeyExists = !string.IsNullOrEmpty(existingActor.PublicKey),
LastActivityAt = existingActor.LastActivityAt,
IsLocal = false
});
}
try
{
var httpClient = new HttpClient();
var response = await httpClient.GetAsync(actorUrl);
if (!response.IsSuccessStatusCode)
{
return Ok(new ActorCheckResult
{
Exists = false,
ActorUri = actorUrl,
Error = $"Actor not accessible: {response.StatusCode}"
});
}
var json = await response.Content.ReadAsStringAsync();
var actorData = System.Text.Json.JsonDocument.Parse(json);
var preferredUsername = actorData.RootElement.GetProperty("preferredUsername").GetString();
var displayName = actorData.RootElement.TryGetProperty("name", out var nameProp)
? nameProp.GetString()
: null;
var bio = actorData.RootElement.TryGetProperty("summary", out var bioProp)
? bioProp.GetString()
: null;
var avatarUrl = actorData.RootElement.TryGetProperty("icon", out var iconProp)
? iconProp.GetProperty("url").GetString()
: null;
var publicKeyPem = actorData.RootElement.GetProperty("publicKey")
.GetProperty("publicKeyPem").GetString();
var domain = ExtractDomain(actorUrl);
var instance = await db.FediverseInstances
.FirstOrDefaultAsync(i => i.Domain == domain);
if (instance == null)
{
instance = new SnFediverseInstance
{
Domain = domain,
Name = domain
};
db.FediverseInstances.Add(instance);
await db.SaveChangesAsync();
}
var actor = new SnFediverseActor
{
Uri = actorUrl,
Username = username,
DisplayName = displayName,
Bio = bio,
AvatarUrl = avatarUrl,
PublicKey = publicKeyPem,
InstanceId = instance.Id
};
db.FediverseActors.Add(actor);
await db.SaveChangesAsync();
return Ok(new ActorCheckResult
{
Exists = true,
Actor = actor,
ActorUri = actorUrl,
Username = username,
DisplayName = displayName,
Bio = bio,
AvatarUrl = avatarUrl,
InstanceDomain = domain,
PublicKeyExists = !string.IsNullOrEmpty(publicKeyPem),
IsDiscoverable = true,
IsLocal = false,
LastActivityAt = SystemClock.Instance.GetCurrentInstant()
});
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to check actor {ActorUri}", actorUrl);
return Ok(new ActorCheckResult
{
Exists = false,
ActorUri = actorUrl,
Error = ex.Message
});
}
}
private Guid? GetCurrentUser()
{
HttpContext.Items.TryGetValue("CurrentUser", out var currentUser);
return currentUser as Guid?;
}
private string ExtractDomain(string actorUri)
{
var uri = new Uri(actorUri);
return uri.Host;
}
private string GetActorUrl(string username)
{
return $"https://{Domain}/activitypub/actors/{username}";
}
}
public class FollowRequest
{
public string TargetActorUri { get; set; } = string.Empty;
}
public class UnfollowRequest
{
public string TargetActorUri { get; set; } = string.Empty;
}
public class RelationshipsSummary
{
public string ActorUri { get; set; } = string.Empty;
public int FollowingCount { get; set; }
public int FollowersCount { get; set; }
public int PendingCount { get; set; }
public List<RelationshipSummaryItem> Relationships { get; set; } = new();
}
public class RelationshipSummaryItem
{
public SnFediverseActor Actor { get; set; } = null!;
public RelationshipState State { get; set; }
public bool IsFollowing { get; set; }
public Instant? FollowedAt { get; set; }
public string TargetActorUri { get; set; } = string.Empty;
public string Username { get; set; } = string.Empty;
public string? DisplayName { get; set; }
}
public class ActorCheckResult
{
public bool Exists { get; set; }
public SnFediverseActor? Actor { get; set; }
public string ActorUri { get; set; } = string.Empty;
public string Username { get; set; } = string.Empty;
public string? DisplayName { get; set; }
public string? Bio { get; set; }
public string? AvatarUrl { get; set; }
public string? InstanceDomain { get; set; }
public bool PublicKeyExists { get; set; }
public bool IsLocal { get; set; }
public bool IsDiscoverable { get; set; }
public Instant? LastActivityAt { get; set; }
public string? Error { get; set; }
}

View File

@@ -107,6 +107,8 @@ public static class ServiceCollectionExtensions
services.AddScoped<ActivityPubSignatureService>(); services.AddScoped<ActivityPubSignatureService>();
services.AddScoped<ActivityPubActivityProcessor>(); services.AddScoped<ActivityPubActivityProcessor>();
services.AddScoped<ActivityPubDeliveryService>(); services.AddScoped<ActivityPubDeliveryService>();
services.AddScoped<ActivityPubFollowController>();
services.AddScoped<ActivityPubController>();
var translationProvider = configuration["Translation:Provider"]?.ToLower(); var translationProvider = configuration["Translation:Provider"]?.ToLower();
switch (translationProvider) switch (translationProvider)

View File

@@ -0,0 +1,425 @@
# Follow Feature - User Guide
## Quick Start: How to Follow Fediverse Users
### Method 1: Via Search (Recommended)
1. Go to the search bar in Solar Network
2. Type the user's full address: `@username@domain.com`
- Example: `@alice@mastodon.social`
- Example: `@bob@pleroma.site`
3. Click on their profile in search results
4. Click the "Follow" button
5. Wait for acceptance (usually immediate)
6. ✅ Done! Their posts will now appear in your timeline
### Method 2: Via Profile URL
1. If you know their profile URL, visit it directly:
- Example: `https://mastodon.social/@alice`
2. Look for the "Follow" button on their profile
3. Click it to follow
4. ✅ You're now following them!
## What Happens When You Follow Someone
### The Technical Flow
```
You click "Follow"
Solar Network creates Follow Activity
Follow Activity is signed with your private key
Solar Network sends Follow to their instance's inbox
Their instance verifies your signature
Their instance processes the Follow
Their instance sends Accept Activity back
Solar Network receives and processes Accept
Relationship is stored in database
Their public posts federate to Solar Network
```
### What You'll See
-**"Following..."** (while waiting for acceptance)
-**"Following" ✓** (when accepted)
-**Their posts** in your home timeline
-**Their likes, replies, boosts** on your posts
## Different Types of Accounts
### Regular Users
- Full ActivityPub support
- Follows work both ways
- Content federates normally
- Example: `@alice@mastodon.social`
### Locked Accounts
- User must manually approve followers
- You'll see "Pending" after clicking follow
- User receives notification to approve/deny
- Example: `@private@pleroma.site`
### Bot/Service Accounts
- Automated content accounts
- Often auto-accept follows
- Example: `@newsbot@botsin.space`
### Organizational Accounts
- Group or team accounts
- Example: `@team@company.social`
## Managing Your Follows
### View Who You're Following
**Go to**: Following page or `GET /api/activitypub/following`
You'll see:
- Username
- Display name
- Profile picture
- When you followed them
- Their instance (e.g., "Mastodon")
### Unfollowing Someone
**Method 1: Via UI**
1. Go to their profile
2. Click "Following" button (shows as active)
3. Click to unfollow
**Method 2: Via API**
```bash
curl -X POST http://solar.local:5000/api/activitypub/unfollow \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"targetActorUri": "https://mastodon.social/users/alice"
}'
```
**Response**:
```json
{
"success": true,
"message": "Unfollowed successfully"
}
```
### View Your Followers
**Go to**: Followers page or `GET /api/activitypub/followers`
You'll see:
- Users following you
- Their instance
- When they started following
- Whether they're local or from another instance
## Searching Fediverse Users
### How Search Works
1. **Type in search bar**: `@username@domain.com`
2. **Solar Network queries their instance**:
- Fetches their actor profile
- Checks if they're discoverable
3. **Shows results**:
- Profile picture
- Display name
- Bio
- Instance name
### Supported Search Formats
| Format | Example | Works? |
|--------|---------|--------|
| Full handle | `@alice@mastodon.social` | ✅ Yes |
| Username only | `alice` | ⚠️ May search local users first |
| Full URL | `https://mastodon.social/@alice` | ✅ Yes |
## Privacy Considerations
### Public Posts
- **What**: Posts visible to everyone
- **Federation**: ✅ Federates to all followers
- **Timeline**: Visible in public federated timelines
- **Example**: General updates, thoughts, content you want to share
### Private Posts
- **What**: Posts only visible to followers
- **Federation**: ✅ Federates to followers (including remote)
- **Timeline**: Only visible to your followers
- **Example**: Personal updates, questions
### Unlisted Posts
- **What**: Posts not in public timelines
- **Federation**: ✅ Federates but marked unlisted
- **Timeline**: Only followers see it
- **Example**: Limited audience content
### Followers-Only Posts
- **What**: Posts only to followers, no federated boost
- **Federation**: ⚠️ May not federate fully
- **Timeline**: Only your followers
- **Example**: Very sensitive content
## Following Etiquette
### Best Practices
1. **Check before following**:
- Read their bio and recent posts
- Make sure they're who you think they are
- Check if their content aligns with your interests
2. **Start with interactions**:
- Like a few posts first
- Reply thoughtfully
- Share interesting content
- Then follow if you want to see more
3. **Respect instance culture**:
- Each instance has its own norms
- Read their community guidelines
- Be mindful of local rules
4. **Don't spam**:
- Don't mass-follow users
- Don't send unwanted DMs
- Don't repeatedly like old posts
5. **Use appropriate post visibility**:
- Public for general content
- Unlisted for updates to followers
- Private for sensitive topics
### Red Flags to Watch
1. **Suspicious accounts**:
- Newly created with generic content
- Only posting promotional links
- Unusual following patterns
2. **Instances with poor moderation**:
- Lots of spam in public timelines
- Harassment goes unaddressed
- You may want to block the instance
3. **Content warnings not respected**:
- Users posting unmarked sensitive content
- You can report/block these users
## Troubleshooting
### "Follow button doesn't work"
**Possible causes**:
1. User doesn't exist
2. Instance is down
3. Network connectivity issue
**What to do**:
1. Verify the username/domain is correct
2. Try searching for them again
3. Check your internet connection
4. Try again in a few minutes
### "User doesn't appear in Following list"
**Possible causes**:
1. Follow was rejected (locked account)
2. Follow is still pending
3. Error in federation
**What to do**:
1. Check if their account is locked
2. Wait a few minutes for acceptance
3. Check your ActivityPub logs
4. Try following again
### "Can't find a user via search"
**Possible causes**:
1. Username/domain is wrong
2. User's instance is blocking your instance
3. User's profile is not discoverable
**What to do**:
1. Double-check the spelling
2. Try their full URL: `https://instance.com/@username`
3. Check if they're from a blocked instance
4. Contact them directly for their handle
## API Reference
### Follow a Remote User
**Endpoint**: `POST /api/activitypub/follow`
**Request**:
```json
{
"targetActorUri": "https://mastodon.social/users/alice"
}
```
**Success Response**:
```json
{
"success": true,
"message": "Follow request sent. Waiting for acceptance.",
"targetActorUri": "https://mastodon.social/users/alice"
}
```
### Get Your Following
**Endpoint**: `GET /api/activitypub/following?limit=50`
**Response**:
```json
{
"users": [
{
"actorUri": "https://mastodon.social/users/alice",
"username": "alice",
"displayName": "Alice Smith",
"bio": "I love tech!",
"avatarUrl": "https://...",
"followedAt": "2024-01-15T10:30:00Z",
"isLocal": false,
"instanceDomain": "mastodon.social"
}
]
}
```
### Get Your Followers
**Endpoint**: `GET /api/activitypub/followers?limit=50`
**Response**:
```json
{
"users": [
{
"actorUri": "https://pleroma.site/users/bob",
"username": "bob",
"displayName": "Bob Jones",
"bio": "Federated user following me",
"avatarUrl": "https://...",
"followedAt": "2024-01-10T14:20:00Z",
"isLocal": false,
"instanceDomain": "pleroma.site"
}
]
}
```
### Search Users
**Endpoint**: `GET /api/activitypub/search?query=@alice@domain.com&limit=20`
**Response**:
```json
{
"users": [
{
"actorUri": "https://mastodon.social/users/alice",
"username": "alice",
"displayName": "Alice Smith",
"bio": "Tech enthusiast",
"avatarUrl": "https://...",
"isLocal": false,
"instanceDomain": "mastodon.social"
}
]
}
```
## Real Examples
### Example 1: Following a Mastodon User
**What you do**:
1. Search for `@alice@mastodon.social`
2. Click on Alice's profile
3. Click "Follow" button
4. Wait 1-2 seconds
5. ✅ Alice appears in your "Following" list
6. ✅ Alice's public posts appear in your timeline
**What happens technically**:
- Solar Network sends Follow to Alice's Mastodon instance
- Alice's Mastodon auto-accepts (unless locked)
- Mastodon sends Accept back to Solar Network
- Relationship stored in both databases
- Alice's future posts federate to Solar Network
### Example 2: Following a Locked Account
**What you do**:
1. Search for `@private@pleroma.site`
2. Click "Follow" button
3. ✅ See "Following..." (pending)
4. Wait for user to approve
**What happens technically**:
- Solar Network sends Follow to private@pleroma.site
- Private user receives notification
- Private user manually approves the request
- Private user's instance sends Accept
- ✅ Now following!
### Example 3: Following a Bot Account
**What you do**:
1. Search for `@news@botsin.space`
2. Click "Follow" button
3. ✅ Immediately following (bots auto-accept)
**What happens technically**:
- Follow is auto-accepted
- News posts appear in your timeline
- Regular updates from the bot
## Key Differences from Traditional Social Media
| Aspect | Traditional Social | ActivityPub |
|---------|------------------|-------------|
| Central server | ❌ No | ✅ Yes (per instance) |
| Multiple platforms | ❌ No | ✅ Yes (Mastodon, Pleroma, etc.) |
| Data ownership | ❌ On their servers | ✅ On your server |
| Blocking | ❌ One platform | ✅ Per instance |
| Migration | ❌ Difficult | ✅ Use your own domain |
| Federation | ❌ No | ✅ Built-in |
## Getting Help
If you have issues following users:
1. **Check the main guide**: See `HOW_TO_FOLLOW_FEDIVERSE_USERS.md`
2. **Check your logs**: Look for ActivityPub errors
3. **Test the API**: Use curl to test follow endpoints directly
4. **Verify the user**: Make sure the user exists on their instance
## Summary
Following fediverse users in Solar Network:
1. **Simple**: Just search and click "Follow"
2. **Works both ways**: You can follow them, they can follow you
3. **Works across instances**: Mastodon, Pleroma, Lemmy, etc.
4. **Federated content**: Their posts appear in your timeline
5. **Full interactions**: Like, reply, boost their posts
It works just like following on any other social platform, but with the added benefit of being able to follow users on completely different services! 🌍

View File

@@ -0,0 +1,406 @@
# How to Follow (Subscribe to) Fediverse Users in Solar Network
## Overview
In ActivityPub terminology, "subscribing" to a user is called **"following"**. This guide explains how users in Solar Network can follow users from other federated services (Mastodon, Pleroma, etc.).
## User Guide: How to Follow Fediverse Users
### Method 1: Via Search (Recommended)
1. **Search for the user**:
- Type their full address in the search bar: `@username@domain.com`
- Example: `@alice@mastodon.social`
- Example: `@bob@pleroma.site`
2. **View their profile**:
- Click on the search result
- You'll see their profile, bio, and recent posts
3. **Click "Follow" button**:
- Solar Network sends a Follow activity to their instance
- The remote instance will send back an Accept
- The user now appears in your "Following" list
### Method 2: Via Profile URL
1. **Visit their profile directly**:
- If you know their profile URL, visit it directly
- Example: `https://mastodon.social/@alice`
2. **Look for "Follow" button**:
- Click it to follow
3. **Confirm the follow**:
- Solar Network will send the follow request
- Wait for acceptance (usually immediate)
## What Happens Behind the Scenes
### The Follow Flow
```
User clicks "Follow"
Solar Network creates Follow Activity
Solar Network signs with publisher's private key
Solar Network sends to remote user's inbox
Remote instance verifies signature
Remote instance processes the Follow
Remote instance sends Accept Activity back
Solar Network receives and processes Accept
Relationship is established!
```
### Timeline Integration
Once you're following a user:
- ✅ Their public posts appear in your "Home" timeline
- ✅ Their posts are federated to your followers
- ✅ Their likes, replies, and boosts are visible
- ✅ You can interact with their content
## Following Different Types of Accounts
### Individual Users
- **What**: Regular users like you
- **Example**: `@alice@mastodon.social`
- **Works**: ✅ Full support
### Organizational/Bot Accounts
- **What**: Groups, bots, or organizations
- **Example**: `@official@newsbot.site`
- **Works**: ✅ Full support
### Locked Accounts
- **What**: Users who manually approve followers
- **Example**: `@private@pleroma.site`
- **Works**: ✅ Follow request sent, waits for approval
## Managing Your Follows
### View Who You're Following
**API Endpoint**: `GET /api/activitypub/following`
**Response Example**:
```json
{
"users": [
{
"actorUri": "https://mastodon.social/users/alice",
"username": "alice",
"displayName": "Alice Smith",
"bio": "I love tech and coffee! ☕",
"avatarUrl": "https://cdn.mastodon.social/avatars/...",
"followedAt": "2024-01-15T10:30:00Z",
"isLocal": false,
"instanceDomain": "mastodon.social"
}
]
}
```
### Unfollowing Someone
**API Endpoint**: `POST /api/activitypub/unfollow`
**Request Body**:
```json
{
"targetActorUri": "https://mastodon.social/users/alice"
}
```
**Response**:
```json
{
"success": true,
"message": "Unfollowed successfully"
}
```
## Searching Fediverse Users
**API Endpoint**: `GET /api/activitypub/search?query=@username@domain.com`
**Response Example**:
```json
{
"users": [
{
"actorUri": "https://mastodon.social/users/alice",
"username": "alice",
"displayName": "Alice Smith",
"bio": "Software developer | Mastodon user",
"avatarUrl": "https://cdn.mastodon.social/avatars/...",
"isLocal": false,
"instanceDomain": "mastodon.social"
}
]
}
```
## Follow States
| State | Meaning | What User Sees |
|--------|---------|----------------|
| Pending | Follow request sent, waiting for response | "Following..." (loading) |
| Accepted | Remote user accepted | "Following" ✓ |
| Rejected | Remote user declined | "Follow" button available again |
| Failed | Error occurred | "Error following" message |
## Privacy & Visibility
### Public Posts
- ✅ Federate to your followers automatically
- ✅ Appear in remote instances' timelines
- ✅ Can be boosted/liked by remote users
### Private Posts
- ❌ Do not federate
- ❌ Only visible to your local followers
- ❌ Not sent to remote instances
### Unlisted Posts
- ⚠️ Federate but not in public timelines
- ⚠️ Only visible to followers
## Best Practices for Users
### When Following Someone
1. **Check their profile first**:
- Make sure they're who you think they are
- Read their bio to understand their content
2. **Start with a few interactions**:
- Like a few posts
- Reply to something interesting
- Don't overwhelm their timeline
3. **Respect their instance's rules**:
- Each instance has its own guidelines
- Read community rules before interacting
4. **Report spam/harassment**:
- Use instance blocking features
- Report to instance admins
### Following Across Instances
1. **Use their full address**:
- `@username@instance.com`
- This helps identify which instance they're on
2. **Be aware of instance culture**:
- Each instance has its own norms
- Some are more technical, others more casual
3. **Check if they're from your instance**:
- Local users show `isLocal: true`
- Usually faster interaction
## Troubleshooting
### "Follow button doesn't work"
**Possible Causes**:
1. User doesn't exist
2. Instance is down
3. Network issue
**Solutions**:
1. Verify the user's address is correct
2. Check if the instance is accessible
3. Check your internet connection
4. Try again in a few minutes
### "User doesn't appear in Following list"
**Possible Causes**:
1. Follow was rejected
2. Still waiting for acceptance (locked accounts)
3. Error in federation
**Solutions**:
1. Check the follow status via API
2. Try following again
3. Check if their account is locked
4. Contact support if issue persists
### "Can't find a user"
**Possible Causes**:
1. Wrong username or domain
2. User doesn't exist
3. Instance blocking your instance
**Solutions**:
1. Double-check the address
2. Try searching from a different instance
3. Contact the user directly for their handle
## API Reference
### Follow a Remote User
**Endpoint**: `POST /api/activitypub/follow`
**Request**:
```json
{
"targetActorUri": "https://mastodon.social/users/alice"
}
```
**Response**: `200 OK`
```json
{
"success": true,
"message": "Follow request sent. Waiting for acceptance.",
"targetActorUri": "https://mastodon.social/users/alice"
}
```
### Get Following List
**Endpoint**: `GET /api/activitypub/following?limit=50`
**Response**: `200 OK`
```json
{
"users": [
{
"actorUri": "https://mastodon.social/users/alice",
"username": "alice",
"displayName": "Alice Smith",
"bio": "...",
"avatarUrl": "...",
"followedAt": "2024-01-15T10:30:00Z",
"isLocal": false,
"instanceDomain": "mastodon.social"
}
]
}
```
### Get Followers List
**Endpoint**: `GET /api/activitypub/followers?limit=50`
**Response**: `200 OK`
```json
{
"users": [
{
"actorUri": "https://mastodon.social/users/alice",
"username": "alice",
"displayName": "Alice Smith",
"bio": "...",
"avatarUrl": "...",
"isLocal": false,
"instanceDomain": "mastodon.social"
}
]
}
```
### Search Users
**Endpoint**: `GET /api/activitypub/search?query=alice&limit=20`
**Response**: `200 OK`
```json
{
"users": [
{
"actorUri": "https://mastodon.social/users/alice",
"username": "alice",
"displayName": "Alice Smith",
"bio": "...",
"avatarUrl": "...",
"isLocal": false,
"instanceDomain": "mastodon.social"
}
]
}
```
## What's Different About ActivityPub Following?
Unlike traditional social media:
| Feature | Traditional Social | ActivityPub |
|---------|------------------|-------------|
| Central server | ✅ Yes | ❌ No - federated |
| All users on same platform | ✅ Yes | ❌ No - multiple platforms |
| Blocked instances | ❌ No | ✅ Yes - instance blocking |
| Following across platforms | ❌ No | ✅ Yes - works with Mastodon, Pleroma, etc. |
| Your data stays on your server | ❌ Maybe | ✅ Yes - you control your data |
## User Experience Considerations
### Making It Easy
1. **Auto-discovery**:
- When users search for `@username`, suggest `@username@domain.com`
- Offer to search the fediverse
2. **Clear UI feedback**:
- Show "Follow request sent..."
- Show "They accepted!" notification
- Show "Follow request rejected" message
3. **Helpful tooltips**:
- Explain what ActivityPub is
- Show which instance a user is from
- Explain locked accounts
4. **Profile badges**:
- Show instance icon/logo
- Show if user is from same instance
- Show if user is verified
## Examples
### Following a Mastodon User
**User searches**: `@alice@mastodon.social`
**What happens**:
1. Solar Network fetches Alice's actor profile
2. Solar Network stores Alice in `fediverse_actors`
3. Solar Network sends Follow to Alice's inbox
4. Alice's instance accepts
5. Solar Network stores relationship in `fediverse_relationships`
6. Alice's posts now appear in user's timeline
### Following a Local User
**User searches**: `@bob`
**What happens**:
1. Solar Network finds Bob's publisher
2. Relationship created locally (no federation needed)
3. Bob's posts appear in user's timeline immediately
4. Same as traditional social media following
## Summary
Following fediverse users in Solar Network:
1. **Search by `@username@domain.com`** - Works for any ActivityPub instance
2. **Click "Follow"** - Sends federated follow request
3. **Wait for acceptance** - Remote user can approve or auto-accept
4. **See their posts in your timeline** - Content federates to you
5. **Interact normally** - Like, reply, boost, etc.
All of this is handled automatically by the ActivityPub implementation!