Table of Contents

CL.SocialConnect

Discord webhooks and Steam Web API integration — send rich Discord notifications and read Steam player profiles, bans, and game libraries.

CL.SocialConnect adds two third-party integrations to a CodeLogic 4 application: outbound Discord webhooks and read access to the Steam Web API, plus Steam ticket authentication for game-server scenarios. The library exposes a single SocialConnectLibrary surface with three services — Discord, Steam, and Auth — each independently toggled by configuration. It has no external NuGet dependencies; HTTP is handled with System.Net.Http.Json.

Package CodeLogic.SocialConnect
Library class CL.SocialConnect.SocialConnectLibrary
Config file config.socialconnect.json (section socialconnect)
Dependencies None (uses System.Net.Http.Json)

Install & load

dotnet add package CodeLogic.SocialConnect
using CL.SocialConnect;

await Libraries.LoadAsync<SocialConnectLibrary>();   // register before ConfigureAsync()
await CodeLogic.ConfigureAsync();
await CodeLogic.StartAsync();

var social = Libraries.Get<SocialConnectLibrary>();

The three services

The library surfaces three services. Each property throws InvalidOperationException if accessed while its service is disabled or uninitialized, so pair every access with the matching Has* guard when the service might be off.

Property Type Guard Default state
Discord DiscordWebhookService HasDiscord enabled
Steam SteamProfileService HasSteam disabled
Auth SteamAuthenticationService HasSteamAuth disabled
if (social.HasDiscord)
    await social.Discord.SendMessageAsync("hello");

if (social.HasSteam)
{
    Result<SteamPlayer> p = await social.Steam.GetPlayerAsync("76561198012345678");
}

Discord webhook methods return a plain Result (no value — they either delivered or they didn't). Steam profile and auth methods return Result<T>. Always check IsSuccess before reading .Value, and read Error?.Message on failure.

Discord webhooks

DiscordWebhookService posts to Discord webhook URLs. Every method accepts an optional webhookUrl; when omitted it falls back to Discord.DefaultWebhookUrl from config. The configured DefaultUsername and DefaultAvatarUrl are applied automatically unless the message overrides them.

Plain text

SendMessageAsync(content, webhookUrl?) sends a plain-text message (max 2000 characters).

Result r = await social.Discord.SendMessageAsync("Deployment finished ✅");
if (r.IsFailure)
    log.Warn(r.Error?.Message);

// Override the destination per call:
await social.Discord.SendMessageAsync("Alert!", "https://discord.com/api/webhooks/aaa/bbb");

Embeds

SendEmbedAsync(embeds, content?, webhookUrl?) sends up to 10 rich embeds, optionally with leading text.

var embed = new DiscordEmbed
{
    Title       = "Build #482",
    Description = "All checks passed.",
    Color       = 0x2ECC71,   // decimal int colour (green)
};

Result r = await social.Discord.SendEmbedAsync(new[] { embed });

Full payload

SendAsync(message, webhookUrl?) posts a complete DiscordWebhookMessage, giving you control over username, avatar, TTS, mentions, and multiple embeds in one call.

var message = new DiscordWebhookMessage
{
    Content          = "Release notes:",
    Username         = "Release Bot",
    AvatarUrl        = "https://example.com/bot.png",
    Tts              = false,
    AllowedMentions  = DiscordAllowedMentions.None,   // suppress all pings
    Embeds           = new[] { embed },
};

Result r = await social.Discord.SendAsync(message);

All three methods publish a WebhookSentEvent after the request completes.

Discord model reference

public sealed class DiscordWebhookMessage
{
    public string? Content { get; set; }
    public string? Username { get; set; }
    public string? AvatarUrl { get; set; }
    public bool Tts { get; set; } = false;
    public IEnumerable<DiscordEmbed>? Embeds { get; set; }
    public DiscordAllowedMentions? AllowedMentions { get; set; }
}

public sealed class DiscordAllowedMentions
{
    public IEnumerable<string>? Parse { get; set; }   // "roles" | "users" | "everyone"
    public IEnumerable<string>? Roles { get; set; }
    public IEnumerable<string>? Users { get; set; }

    public static DiscordAllowedMentions None { get; }   // ping nobody
    public static DiscordAllowedMentions All  { get; }   // allow all mentions
}

public sealed class DiscordEmbed
{
    public string? Title { get; set; }
    public string? Description { get; set; }
    public string? Url { get; set; }
    public int? Color { get; set; }                 // decimal int, e.g. 0xFF0000 (red)
    public DateTimeOffset? Timestamp { get; set; }
    public DiscordEmbedFooter? Footer { get; set; }
    public DiscordEmbedImage? Image { get; set; }
    public DiscordEmbedImage? Thumbnail { get; set; }
    public DiscordEmbedAuthor? Author { get; set; }
    public IEnumerable<DiscordEmbedField>? Fields { get; set; }
}

public sealed class DiscordEmbedField
{
    public string Name { get; set; }    // required
    public string Value { get; set; }   // required
    public bool Inline { get; set; } = false;
}

public sealed class DiscordEmbedImage
{
    public string Url { get; set; }     // required
    public int? Width { get; set; }
    public int? Height { get; set; }
}

public sealed class DiscordEmbedAuthor
{
    public string Name { get; set; }    // required
    public string? Url { get; set; }
    public string? IconUrl { get; set; }
}

public sealed class DiscordEmbedFooter
{
    public string Text { get; set; }    // required
    public string? IconUrl { get; set; }
}

Embed Color is a decimal integer, not a CSS hex string. The hex literal 0xFF0000 is the cleanest way to write it in C#.

A rich embed

var embed = new DiscordEmbed
{
    Title       = "Server Status",
    Description = "Nightly maintenance complete.",
    Url         = "https://status.example.com",
    Color       = 0x5865F2,                       // Discord blurple
    Timestamp   = DateTimeOffset.UtcNow,
    Author      = new DiscordEmbedAuthor
    {
        Name    = "Ops",
        IconUrl = "https://example.com/ops.png",
    },
    Thumbnail   = new DiscordEmbedImage { Url = "https://example.com/icon.png" },
    Fields      = new[]
    {
        new DiscordEmbedField { Name = "Region",  Value = "eu-west", Inline = true },
        new DiscordEmbedField { Name = "Uptime",  Value = "99.98%",  Inline = true },
        new DiscordEmbedField { Name = "Notes",   Value = "No incidents." },
    },
    Footer      = new DiscordEmbedFooter { Text = "Reported automatically" },
};

await social.Discord.SendEmbedAsync(new[] { embed }, content: "📣 Status update");

Steam profiles

SteamProfileService reads the Steam Web API. It requires Steam.Enabled = true and a valid Steam.ApiKey. Results are cached for CacheTtlSeconds (see Caching).

Players

GetPlayerAsync(steamId) returns a SteamPlayer for a 64-bit Steam ID.

Result<SteamPlayer> r = await social.Steam.GetPlayerAsync("76561198012345678");
if (r.IsSuccess)
{
    SteamPlayer p = r.Value;
    Console.WriteLine($"{p.PersonaName} ({(p.IsPublic ? "public" : "private")})");
    if (p.IsInGame)
        Console.WriteLine($"Playing: {p.GameExtraInfo}");
}

Bans

GetPlayerBansAsync(steamId) returns a SteamPlayerBans.

Result<SteamPlayerBans> r = await social.Steam.GetPlayerBansAsync("76561198012345678");
if (r.IsSuccess && r.Value.HasAnyBan)
    Console.WriteLine($"VAC bans: {r.Value.NumberOfVacBans}");

Owned games

GetOwnedGamesAsync(steamId, includeAppInfo = true) returns a List<SteamGame>. With includeAppInfo set, each game carries its Name and icon; turn it off for a lighter response when you only need playtimes.

Result<List<SteamGame>> r = await social.Steam.GetOwnedGamesAsync("76561198012345678");
if (r.IsSuccess)
{
    foreach (var game in r.Value.OrderByDescending(g => g.PlaytimeForever).Take(5))
        Console.WriteLine($"{game.Name}: {game.TotalPlaytime.TotalHours:F0}h");
}

Each profile read publishes a SteamProfileFetchedEvent carrying a FromCache flag.

Steam model reference

public sealed class SteamPlayer
{
    public string SteamId { get; }
    public int CommunityVisibilityState { get; }   // 3 = public
    public int ProfileState { get; }
    public string PersonaName { get; }
    public string ProfileUrl { get; }
    public string Avatar { get; }          // 32px
    public string AvatarMedium { get; }    // 64px
    public string AvatarFull { get; }      // 184px
    public int PersonaState { get; }       // 0=Offline, 1=Online … 6=LookingToPlay
    public string? RealName { get; }
    public string? PrimaryClanId { get; }
    public long? TimeCreated { get; }       // unix seconds
    public string? GameId { get; }
    public string? GameExtraInfo { get; }
    public string? LocCountryCode { get; }
    public string? LocStateCode { get; }

    // Computed
    public bool IsPublic { get; }            // CommunityVisibilityState == 3
    public bool IsInGame { get; }            // GameId is set
    public DateTime? AccountCreated { get; } // from TimeCreated
}

public sealed class SteamPlayerBans
{
    public string SteamId { get; }
    public bool CommunityBanned { get; }
    public bool VacBanned { get; }
    public int NumberOfVacBans { get; }
    public int DaysSinceLastBan { get; }
    public int NumberOfGameBans { get; }
    public string EconomyBan { get; }       // "none" | "probation" | "banned"

    // Computed
    public bool HasAnyBan { get; }
}

public sealed class SteamGame
{
    public int AppId { get; }
    public string? Name { get; }            // requires includeAppInfo
    public int PlaytimeForever { get; }     // minutes
    public int? Playtime2Weeks { get; }     // minutes
    public string? ImgIconUrl { get; }
    public long? RtimeLastPlayed { get; }   // unix seconds

    // Computed
    public TimeSpan TotalPlaytime { get; }       // from PlaytimeForever
    public TimeSpan? RecentPlaytime { get; }     // from Playtime2Weeks
    public DateTime? LastPlayed { get; }         // from RtimeLastPlayed
    public string? GetIconUrl();                  // full CDN icon URL
}

Steam authentication

SteamAuthenticationService validates a Steam session ticket — typically used by a game server to confirm a connecting client's identity. It requires Steam.AuthEnabled = true and a configured Steam.AppId.

AuthenticateAsync(ticket, appId?) validates the ticket; the optional appId overrides Steam.AppId for that call.

if (social.HasSteamAuth)
{
    Result<SteamAuthResult> r = await social.Auth.AuthenticateAsync(sessionTicket);
    if (r.IsSuccess && r.Value.IsAuthenticated)
        Console.WriteLine($"Verified {r.Value.SteamId}");
}

Each call publishes a SteamAuthenticatedEvent.

public sealed class SteamAuthResult
{
    public string SteamId { get; }
    public bool IsAuthenticated { get; }
    public string? OwnerSteamId { get; }
    public string? ErrorDescription { get; }
    public int VacBanned { get; }        // 0 / 1
    public int PublisherBanned { get; }  // 0 / 1
}

Caching

SteamProfileService caches the results of GetPlayerAsync, GetPlayerBansAsync, and GetOwnedGamesAsync for CacheTtlSeconds (default 300). Cached reads publish SteamProfileFetchedEvent with FromCache = true. Call Steam.ClearCache() to drop all cached entries — useful after you know a profile has changed, or in tests.

social.Steam.ClearCache();

Configuration

The library writes config.socialconnect.json (section socialconnect) with defaults on first run.

{
  "Enabled": true,
  "Discord": {
    "Enabled": true,
    "DefaultWebhookUrl": "https://discord.com/api/webhooks/xxx/yyy",
    "TimeoutSeconds": 10,
    "DefaultUsername": "MyApp",
    "DefaultAvatarUrl": "https://example.com/bot.png"
  },
  "Steam": {
    "Enabled": false,
    "ApiKey": "your-steam-web-api-key",
    "AuthEnabled": false,
    "AppId": "",
    "CacheTtlSeconds": 300,
    "TimeoutSeconds": 15,
    "ApiBaseUrl": "https://api.steampowered.com"
  }
}
Setting Type Default Description
Enabled bool true Master switch for the whole library.
Discord.Enabled bool true Create the Discord webhook service.
Discord.DefaultWebhookUrl string "" Webhook used when a call omits webhookUrl. Secret.
Discord.TimeoutSeconds int 10 HTTP timeout for Discord requests (1–120).
Discord.DefaultUsername string "" Default sender name applied to outgoing messages.
Discord.DefaultAvatarUrl string "" Default sender avatar applied to outgoing messages.
Steam.Enabled bool false Create the Steam profile service. Requires ApiKey.
Steam.ApiKey string "" Steam Web API key (secret). Get one at steamcommunity.com/dev/apikey.
Steam.AuthEnabled bool false Create the Steam auth service. Requires AppId.
Steam.AppId string "" Your game's Steam App ID (used by ticket auth).
Steam.CacheTtlSeconds int 300 Profile cache lifetime in seconds. Must be > 0.
Steam.TimeoutSeconds int 15 HTTP timeout for Steam requests (1–120).
Steam.ApiBaseUrl string https://api.steampowered.com Steam Web API base URL.

ValidationSteam.Enabled requires a non-empty ApiKey; Steam.AuthEnabled requires a non-empty AppId; CacheTtlSeconds must be greater than 0. Steam (and Steam auth) are disabled by default.

Events

All events implement IEvent and are published to the CodeLogic event bus.

Event Published when Payload
WebhookSentEvent A Discord webhook request completes. WebhookUrl, Success, SentAt, StatusCode?, ErrorMessage?
SteamProfileFetchedEvent A Steam profile/bans/games read completes. SteamId, FetchedAt, FromCache (default false)
SteamAuthenticatedEvent A ticket authentication completes. SteamId, Success, AuthenticatedAt, AppId?, ErrorMessage?

Health check

HealthCheckAsync() reports a HealthStatus based on which services are available and configured. Disabled services do not fail the check.

var status = await social.HealthCheckAsync();
// status.Status : Healthy | Degraded | Unhealthy

See also