Slyv.Social (1.3.0)

Published 2026-01-06 19:54:19 +00:00 by logikonline

Installation

dotnet nuget add source --name logikonline --username your_username --password your_token 
dotnet add package --source logikonline --version 1.3.0 Slyv.Social

About 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 posts
var posts = await _bluesky.SearchPostsAsync(
    query: "bluesky",
    limit: 50);

// Search users
var users = await _bluesky.SearchUsersAsync(
    query: "john",
    limit: 50);
// 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");

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
}
// 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
Details
NuGet
2026-01-06 19:54:19 +00:00
0
David H. Friedel Jr.
347 KiB
Assets (4)
Versions (1) View all
1.3.0 2026-01-06