Slyv.Social (1.3.0)
Installation
dotnet nuget add source --name logikonline --username your_username --password your_token dotnet add package --source logikonline --version 1.3.0 Slyv.SocialAbout this package
Social media integration library with comprehensive Bluesky/AT Protocol support. Features include timeline reading, post creation and management, list management, follower analysis, feed aggregation, and content moderation. Supports Slyv-specific metadata for tracking analyzed posts and maintaining bidirectional links between social media content and Slyv analysis features.
Slyv.Social
A comprehensive .NET library for integrating with decentralized social media platforms.
Overview
Slyv.Social provides a unified, type-safe interface for interacting with federated and decentralized social networks:
- Bluesky - AT Protocol integration
- Mastodon - ActivityPub/Fediverse integration
Quick Start
Installation
Add the library to your project and configure services:
// Program.cs
using Slyv.Social.Services;
services.AddHttpClient<IBlueskyService, BlueskyService>();
services.AddHttpClient<IMastodonService, MastodonService>();
services.Configure<BlueskyServiceOptions>(config.GetSection("BlueskyService"));
services.Configure<MastodonServiceOptions>(config.GetSection("MastodonService"));
Configuration
{
"BlueskyService": {
"ApiBase": "https://bsky.social/xrpc",
"MaxPostLength": 300,
"MaxTimelineLimit": 100,
"MaxFollowersPageSize": 100,
"DefaultLimit": 50,
"MaxRetries": 3,
"CacheDurationMinutes": 5,
"EnableCaching": true,
"MaxProfileBatchSize": 25
},
"MastodonService": {
"InstanceBase": "https://mastodon.social",
"DefaultLimit": 40,
"MaxLimit": 80,
"CacheMinutes": 5,
"MaxRetries": 3,
"MinMsBetweenRequests": 100
}
}
Feature Reference
Authentication
Bluesky Authentication
private readonly IBlueskyService _bluesky;
// Authenticate with handle and app password
await _bluesky.AuthenticateAsync("user.bsky.social", "app-password");
// Check authentication status
bool isAuthenticated = await _bluesky.IsAuthenticatedAsync();
// Sign out
await _bluesky.SignOutAsync();
Mastodon Authentication
private readonly IMastodonService _mastodon;
// Authenticate with instance URL and access token
await _mastodon.AuthenticateAsync("https://mastodon.social", "access-token");
// Check authentication status
bool isAuthenticated = await _mastodon.IsAuthenticatedAsync();
// Sign out
await _mastodon.SignOutAsync();
Posts & Status Updates
Bluesky Posts
// Simple text post
var post = await _bluesky.CreatePostAsync("Hello Bluesky!", "en");
// Post with custom facets (mentions, links, hashtags)
var customPost = await _bluesky.CreatePostWithCustomFacetAsync(
text: "Check out @user.bsky.social",
customFacetType: "app.bsky.richtext.facet#mention",
customFacetPayload: new { did = "did:plc:..." },
charStart: 10,
charLength: 20);
// Post with encoded metadata link (invisible link with custom data)
var metadataPost = await _bluesky.CreatePostWithEncodedMetadataLinkAsync(
text: "Feeling great!",
customFacetType: "app.slyv.emotion",
customFacetPayload: new { mood = "happy", intensity = 0.8 });
// Post with images
var imagePost = await _bluesky.CreateImagePostWithEncodedMetadataAsync(
text: "Check out these photos!",
imagePaths: new[] { "/path/to/image1.jpg", "/path/to/image2.jpg" },
customFacetType: "app.slyv.photo",
customFacetPayload: new { category = "travel" });
// Post with link preview
var linkPost = await _bluesky.CreateLinkPostWithEncodedMetadataAsync(
text: "Interesting article",
visibleUrl: "https://example.com/article",
linkText: "Read more",
customFacetType: "app.slyv.bookmark",
customFacetPayload: new { saved = true },
overrideTitle: "Custom Title",
overrideDescription: "Custom description");
// Get a specific post
var fetchedPost = await _bluesky.GetPostAsync("at://did:plc:.../app.bsky.feed.post/...");
// Delete a post
bool deleted = await _bluesky.DeletePostAsync("at://did:plc:.../app.bsky.feed.post/...");
Mastodon Statuses
// Simple status
var status = await _mastodon.PostStatusAsync("Hello Mastodon!");
// Status with options
var detailedStatus = await _mastodon.PostStatusAsync(
status: "My post content",
inReplyToId: "reply-to-id",
visibility: "public", // "public", "unlisted", "private", "direct"
mediaPaths: new[] { "media-id-1", "media-id-2" },
language: "en",
sensitive: false,
spoilerText: "Content warning text");
// Upload media
var mediaId = await _mastodon.UploadMediaAsync(
filePath: "/path/to/image.jpg",
description: "Alt text for accessibility",
focus: "0,0"); // Focal point for cropping
// Delete status
bool deleted = await _mastodon.DeleteStatusAsync("status-id");
Timelines & Feeds
Bluesky Timelines
// Get home timeline (authenticated user's feed)
var timeline = await _bluesky.GetTimelineAsync(
limit: 50,
cursor: null,
includeReplies: false);
// Get user's posts
var userPosts = await _bluesky.GetUserPostsAsync(
actor: "user.bsky.social",
limit: 50,
cursor: null);
Mastodon Timelines
// Get home timeline
var homeTimeline = await _mastodon.GetHomeTimelineAsync(
limit: 40,
maxId: null,
sinceId: null,
onlyMedia: false);
// Get user timeline
var userTimeline = await _mastodon.GetUserTimelineAsync(
accountId: "account-id",
limit: 40,
maxId: null,
sinceId: null,
excludeReplies: false,
onlyMedia: false);
// Get list timeline
var listTimeline = await _mastodon.GetListTimelineAsync(
listId: "list-id",
limit: 40,
maxId: null,
sinceId: null);
Social Interactions
Bluesky Interactions
// Like a post
bool liked = await _bluesky.LikePostAsync(
postUri: "at://did:plc:.../app.bsky.feed.post/...",
cid: "post-cid");
// Unlike a post
bool unliked = await _bluesky.UnlikePostAsync("at://did:plc:.../app.bsky.feed.post/...");
// Repost (boost/share)
bool reposted = await _bluesky.RepostAsync(
postUri: "at://did:plc:.../app.bsky.feed.post/...",
cid: "post-cid");
// Un-repost
bool unreposted = await _bluesky.UnrepostAsync("at://did:plc:.../app.bsky.feed.post/...");
Mastodon Interactions
// Favourite (like) a status
bool favourited = await _mastodon.FavouriteAsync("status-id");
// Unfavourite
bool unfavourited = await _mastodon.UnfavouriteAsync("status-id");
// Reblog (boost/share)
bool reblogged = await _mastodon.ReblogAsync("status-id");
// Unreblog
bool unreblogged = await _mastodon.UnreblogAsync("status-id");
Profiles & Accounts
Bluesky Profiles
// Get authenticated user's profile
var myProfile = await _bluesky.GetProfileAsync();
// Get another user's profile
var userProfile = await _bluesky.GetProfileAsync("user.bsky.social");
// Get multiple profiles at once (batch operation)
var profiles = await _bluesky.GetProfilesAsync(new[] {
"user1.bsky.social",
"user2.bsky.social",
"did:plc:..."
});
Mastodon Accounts
// Get authenticated user's account
var myAccount = await _mastodon.GetMyAccountAsync();
// Get another user's account
var account = await _mastodon.GetAccountAsync("account-id-or-acct");
Followers & Following
Bluesky
// Get followers with cursor-based pagination
var (followers, nextCursor) = await _bluesky.GetFollowersAsync(
actor: "user.bsky.social",
limit: 50,
cursor: null,
enrichWithCounts: true); // Include follower/following counts
// Get following
var (following, cursor) = await _bluesky.GetFollowingAsync(
actor: "user.bsky.social",
limit: 50,
cursor: null);
// Follow a user
bool followed = await _bluesky.FollowAsync("did:plc:...");
// Unfollow a user
bool unfollowed = await _bluesky.UnfollowAsync("did:plc:...");
Mastodon
// Get followers
var followers = await _mastodon.GetFollowersAsync(
accountId: "account-id",
limit: 40,
maxId: null);
// Get following
var following = await _mastodon.GetFollowingAsync(
accountId: "account-id",
limit: 40,
maxId: null);
Search
Bluesky Search
// Search posts
var posts = await _bluesky.SearchPostsAsync(
query: "bluesky",
limit: 50);
// Search users
var users = await _bluesky.SearchUsersAsync(
query: "john",
limit: 50);
Mastodon Search
// Search statuses
var statuses = await _mastodon.SearchStatusesAsync(
query: "mastodon",
limit: 20);
// Search accounts
var accounts = await _mastodon.SearchAccountsAsync(
query: "john",
limit: 20);
Lists Management
Bluesky Lists
Bluesky supports two types of lists:
- Curation Lists (
curatelist) - For organizing content - Moderation Lists (
modlist) - For moderation purposes
// Get all lists for a user
var (lists, cursor) = await _bluesky.GetListsAsync(
actor: "user.bsky.social",
limit: 50,
cursor: null);
// Get a specific list
var list = await _bluesky.GetListAsync("at://did:plc:.../app.bsky.graph.list/...");
// Create a curation list
var newList = await _bluesky.CreateListAsync(
name: "Tech Experts",
purpose: "curatelist", // or "modlist"
description: "People I follow for tech insights");
// Get list members
var (members, nextCursor) = await _bluesky.GetListMembersAsync(
listUri: newList.Uri,
limit: 100,
cursor: null);
// Add user to list
bool added = await _bluesky.AddToListAsync(
listUri: newList.Uri,
subjectDid: "did:plc:...");
// Remove user from list
bool removed = await _bluesky.RemoveFromListAsync(
listUri: newList.Uri,
subjectDid: "did:plc:...");
// Update list metadata
bool updated = await _bluesky.UpdateListAsync(
listUri: newList.Uri,
name: "Tech & Science Experts",
description: "Updated description");
// Delete list
bool deleted = await _bluesky.DeleteListAsync(newList.Uri);
Mastodon Lists
Mastodon lists are:
- Private only - Not discoverable by others
- Followers only - Can only add accounts you follow
// Get all lists
var lists = await _mastodon.GetListsAsync();
// Get specific list
var list = await _mastodon.GetListAsync("list-id");
// Create list
var newList = await _mastodon.CreateListAsync(
title: "Close Friends",
repliesPolicy: "list", // "followed", "list", or "none"
exclusive: false); // If true, removes members from home timeline
// Get list members
var members = await _mastodon.GetListAccountsAsync(
listId: "list-id",
limit: 40,
maxId: null);
// Get list timeline
var timeline = await _mastodon.GetListTimelineAsync(
listId: "list-id",
limit: 40,
maxId: null,
sinceId: null);
// Add accounts to list (batch operation)
bool added = await _mastodon.AddAccountsToListAsync(
"list-id",
"account-id-1",
"account-id-2",
"account-id-3");
// Remove accounts from list (batch operation)
bool removed = await _mastodon.RemoveAccountsFromListAsync(
"list-id",
"account-id-1");
// Update list
var updated = await _mastodon.UpdateListAsync(
listId: "list-id",
title: "Best Friends",
repliesPolicy: "followed",
exclusive: true);
// Delete list
bool deleted = await _mastodon.DeleteListAsync("list-id");
Trending & News
Bluesky Trending
Note: Bluesky trending APIs are in the unspecced namespace and may change.
// Get trending topics (hashtags)
var topics = await _bluesky.GetTrendingTopicsAsync(
limit: 10,
cursor: null);
foreach (var topic in topics)
{
Console.WriteLine($"Topic: {topic.Topic}");
Console.WriteLine($"Link: {topic.Link}");
Console.WriteLine($"Post Count: {topic.PostCount}");
}
// Get detailed trends with metrics
var trends = await _bluesky.GetTrendsAsync(
limit: 10,
cursor: null);
foreach (var trend in trends)
{
Console.WriteLine($"Trend: {trend.Topic}");
Console.WriteLine($"Posts: {trend.PostCount}");
// Additional metrics in trend.AdditionalData
}
Mastodon Trending
// Get trending tags (hashtags)
var tags = await _mastodon.GetTrendingTagsAsync(
limit: 10,
offset: 0);
foreach (var tag in tags)
{
Console.WriteLine($"#{tag.Name}");
Console.WriteLine($"URL: {tag.Url}");
// View usage history (past 7 days)
foreach (var history in tag.History)
{
Console.WriteLine($" {history.Day}: {history.Uses} uses by {history.Accounts} accounts");
}
}
// Get trending links (News)
var links = await _mastodon.GetTrendingLinksAsync(
limit: 10,
offset: 0);
foreach (var link in links)
{
Console.WriteLine($"Title: {link.Title}");
Console.WriteLine($"URL: {link.Url}");
Console.WriteLine($"Provider: {link.ProviderName}");
Console.WriteLine($"Description: {link.Description}");
// Shares over time
foreach (var history in link.History)
{
Console.WriteLine($" {history.Day}: {history.Uses} shares");
}
}
// Get trending statuses (popular posts)
var statuses = await _mastodon.GetTrendingStatusesAsync(
limit: 20,
offset: 0);
foreach (var status in statuses)
{
Console.WriteLine($"@{status.Account.Acct}");
Console.WriteLine($"Content: {status.ContentHtml}");
Console.WriteLine($"Engagement: {status.FavouritesCount} favs, {status.ReblogsCount} boosts");
}
Custom Metadata (Bluesky Only)
Slyv.Social includes helpers for encoding custom metadata in Bluesky posts using invisible links.
using Slyv.Social.Helpers;
// Encode metadata into a URL
var metadataUrl = SlyvMetadataHelper.EncodeMetadataToUrl(
facetType: "app.slyv.emotion",
payload: new { mood = "happy", intensity = 0.8, tags = new[] { "joy", "content" } });
// Decode metadata from a URL
var decoded = SlyvMetadataHelper.DecodeMetadataFromUrl(metadataUrl);
Console.WriteLine($"Type: {decoded.FacetType}");
Console.WriteLine($"Data: {decoded.PayloadJson}");
// Check if a post has Slyv metadata
var post = await _bluesky.GetPostAsync("at://...");
bool hasMetadata = SlyvMetadataHelper.HasSlyvMetadata(post);
// Extract all Slyv metadata from a post
var metadata = SlyvMetadataHelper.ExtractSlyvMetadata(post);
foreach (var meta in metadata)
{
Console.WriteLine($"Found metadata: {meta.FacetType}");
}
// Extract emotion data specifically
foreach (var meta in metadata)
{
var emotion = SlyvMetadataHelper.ExtractEmotionData(meta);
if (emotion != null)
{
Console.WriteLine($"Mood: {emotion.Mood}");
Console.WriteLine($"Intensity: {emotion.Intensity}");
Console.WriteLine($"Tags: {string.Join(", ", emotion.Tags)}");
}
}
Advanced Features
Caching
Both services implement intelligent caching to reduce API calls:
// Cache is automatic, but you can clear it manually
_bluesky.ClearCache();
_mastodon.ClearCache();
Cache configuration:
- Bluesky:
CacheDurationMinutes(default: 5 minutes) - Mastodon:
CacheMinutes(default: 5 minutes)
Rate Limiting
Built-in rate limiting prevents API throttling:
- Bluesky: Automatic retry with exponential backoff (configurable via
MaxRetries) - Mastodon: Minimum delay between requests (configurable via
MinMsBetweenRequests)
Batch Operations
Optimize API usage with batch operations:
// Bluesky: Fetch multiple profiles at once
var profiles = await _bluesky.GetProfilesAsync(new[] {
"user1.bsky.social",
"user2.bsky.social",
"user3.bsky.social"
});
// Mastodon: Add/remove multiple accounts from list at once
await _mastodon.AddAccountsToListAsync(
"list-id",
"account1",
"account2",
"account3");
Pagination
Handle large result sets with cursor/offset-based pagination:
// Bluesky cursor-based pagination
string? cursor = null;
do
{
var (followers, nextCursor) = await _bluesky.GetFollowersAsync(
limit: 100,
cursor: cursor);
// Process followers...
cursor = nextCursor;
} while (cursor != null);
// Mastodon offset-based pagination
int offset = 0;
const int limit = 20;
while (true)
{
var tags = await _mastodon.GetTrendingTagsAsync(
limit: limit,
offset: offset);
if (tags.Count == 0) break;
// Process tags...
offset += limit;
}
Error Handling
Services use non-throwing error handling - check return values:
// Operations return null on failure
var post = await _bluesky.CreatePostAsync("Hello!");
if (post == null)
{
// Check logs for error details
// Reasons: not authenticated, network error, API error, etc.
}
// Boolean operations return false on failure
bool success = await _mastodon.FavouriteAsync("status-id");
if (!success)
{
// Operation failed - check logs
}
All errors are logged with structured logging using ILogger.
Configuration Reference
BlueskyServiceOptions
| Property | Type | Default | Description |
|---|---|---|---|
ApiBase |
string | https://bsky.social/xrpc |
AT Protocol API endpoint |
MaxPostLength |
int | 300 |
Maximum characters per post |
MaxTimelineLimit |
int | 100 |
Maximum items per timeline request |
MaxFollowersPageSize |
int | 100 |
Maximum followers per page |
DefaultLimit |
int | 50 |
Default limit for paginated requests |
MaxRetries |
int | 3 |
Maximum retry attempts for failed requests |
CacheDurationMinutes |
int | 5 |
Cache TTL in minutes |
EnableCaching |
bool | true |
Enable/disable caching |
MaxProfileBatchSize |
int | 25 |
Maximum profiles per batch request |
MastodonServiceOptions
| Property | Type | Default | Description |
|---|---|---|---|
InstanceBase |
string | https://mastodon.social |
Mastodon instance URL (no trailing slash) |
DefaultLimit |
int | 40 |
Default page size |
MaxLimit |
int | 80 |
Maximum page size (instance-dependent) |
CacheMinutes |
int | 5 |
Cache TTL in minutes |
MaxRetries |
int | 3 |
Maximum retry attempts |
MinMsBetweenRequests |
int | 100 |
Minimum milliseconds between requests |
Testing
Mock services for unit testing:
using Moq;
using Slyv.Social.Services;
using Slyv.Social.Models;
// Mock BlueskyService
var mockBluesky = new Mock<IBlueskyService>();
mockBluesky
.Setup(x => x.CreatePostAsync(It.IsAny<string>(), It.IsAny<string>()))
.ReturnsAsync(new BlueskyPost
{
Uri = "at://test/post/123",
Cid = "test-cid",
Text = "Test post"
});
// Mock MastodonService
var mockMastodon = new Mock<IMastodonService>();
mockMastodon
.Setup(x => x.PostStatusAsync(
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<IEnumerable<string>>(),
It.IsAny<string>(),
It.IsAny<bool>(),
It.IsAny<string>()))
.ReturnsAsync(new MastoStatus
{
Id = "test-status-123",
ContentHtml = "Test status"
});
Best Practices
1. Always Check Authentication
if (!await _bluesky.IsAuthenticatedAsync())
{
await _bluesky.AuthenticateAsync(handle, password);
}
2. Handle Null Returns
var post = await _bluesky.CreatePostAsync("Hello!");
if (post != null)
{
Console.WriteLine($"Posted: {post.Uri}");
}
3. Use Batch Operations When Possible
// Good: Single batch request
var profiles = await _bluesky.GetProfilesAsync(userHandles);
// Avoid: Multiple individual requests
foreach (var handle in userHandles)
{
var profile = await _bluesky.GetProfileAsync(handle);
}
4. Respect Rate Limits
// Services handle rate limiting automatically
// But avoid rapid-fire requests when possible
foreach (var item in items)
{
await Task.Delay(100); // Optional extra delay for safety
await _bluesky.CreatePostAsync(item.Text);
}
5. Use Caching Wisely
// Clear cache when fresh data is needed
_bluesky.ClearCache();
var freshProfile = await _bluesky.GetProfileAsync("user.bsky.social");
Platform Comparison
| Feature | Bluesky | Mastodon |
|---|---|---|
| Authentication | Handle + App Password | Instance URL + Access Token |
| Post Length | 300 characters | Varies by instance (500-5000) |
| Media | Images in posts | Images + Video + Audio |
| Lists | Public/Private, Curation/Moderation | Private only, Followers only |
| Trending | Unspecced API | Official API |
| Search | Posts + Users | Statuses + Accounts + Tags |
| Custom Metadata | Via facets | Via API extensions |
| Pagination | Cursor-based | Offset + Max/Since ID |
License
Part of the Slyv project. See main repository for license information.
v1.3.0: Added Cid and Alt properties to VideoEmbed for native video playback support. v1.2.0: Added Images property to RecordEmbed for accessing quoted post images. v1.1.0: Added GetPostThreadAsync (Bluesky) and GetStatusContextAsync (Mastodon) for fetching post threads with replies.
Dependencies
| ID | Version | Target Framework |
|---|---|---|
| Slyv.Abstractions | 1.0.0 | net9.0 |
| Slyv.Goals | 1.0.0 | net9.0 |
| Microsoft.EntityFrameworkCore | 9.0.10 | net9.0 |
| Microsoft.Extensions.Caching.Abstractions | 9.0.10 | net9.0 |
| Microsoft.Extensions.Configuration.Abstractions | 9.0.10 | net9.0 |
| Microsoft.Extensions.DependencyInjection | 9.0.10 | net9.0 |
| Microsoft.Extensions.DependencyInjection.Abstractions | 9.0.10 | net9.0 |
| Microsoft.Extensions.Http | 9.0.10 | net9.0 |
| Microsoft.Extensions.Logging.Abstractions | 9.0.10 | net9.0 |
| Microsoft.Extensions.Options | 9.0.10 | net9.0 |
| System.Text.Json | 9.0.10 | net9.0 |
| idunno.Bluesky | 1.1.0 | net9.0 |