CL.SocialConnect
Discord webhooks and Steam integration — send rich Discord notifications, read Steam player profiles, bans, and game libraries, and add "Sign in through Steam" to a website.
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 and Steam sign-in (OpenID 2.0) for websites. The library exposes a single SocialConnectLibrary surface with four services — Discord, Steam, Auth, and OpenId — 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
Coloris a decimal integer, not a CSS hex string. The hex literal0xFF0000is 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
}
Steam sign-in (OpenID)
SteamOpenIdService (social.OpenId) implements "Sign in through Steam" for websites with Steam's OpenID 2.0 provider. It is available whenever Steam is enabled and Steam.OpenIdEnabled is true (the default). OpenID itself needs no API key. Steam only returns the player's 64-bit Steam ID; use social.Steam.GetPlayerAsync afterwards for the name and avatar.
The flow has two steps:
- Redirect.
BuildLoginUrl(returnTo, realm?)returns thehttps://steamcommunity.com/openid/login?…URL.returnTomust be an absolute http(s) URL without a fragment.realm(what Steam shows the user) defaults to the scheme and host ofreturnTo, andreturnTomust start with it. - Callback. Steam sends the browser back to
returnTowith a signed assertion.VerifyAsync(query, expectedReturnTo)takes the raw query string or the decoded parameters, and returnsResult<SteamOpenIdResult>.
// Start: bind the attempt to this browser with a random state kept in a cookie.
var state = Convert.ToHexString(RandomNumberGenerator.GetBytes(32));
var returnTo = $"https://example.com/auth/steam/callback?state={state}";
// …store state in an HttpOnly cookie, then redirect:
string loginUrl = social.OpenId.BuildLoginUrl(returnTo);
// Callback: rebuild the exact return_to from the cookie's state, then verify.
Result<SteamOpenIdResult> result = await social.OpenId.VerifyAsync(request.QueryString, returnTo);
if (result.IsSuccess)
SignIn(result.Value.SteamId);
VerifyAsync checks the assertion locally first, then asks Steam to confirm it (check_authentication, only an exact is_valid:true line counts). The local checks are:
openid.modeisid_res(cancelgivessocial.steam.openid_cancelled).- The namespace and
op_endpointare Steam's. return_toequalsexpectedReturnToexactly.claimed_idequalsidentityand ishttps://steamcommunity.com/openid/id/7656119…(a SteamID64).op_endpoint,claimed_id,identity,return_toandresponse_nonceare among the signed fields, and a signature is present.- No
openid.*parameter appears twice.
Because Steam signs return_to, putting a per-attempt state in it and checking it against a cookie stops a callback taken from someone else's login from being replayed into another browser.
| Error code | Meaning |
|---|---|
social.steam.openid_cancelled |
The user cancelled at Steam. |
social.steam.openid_return_to |
return_to does not match this sign-in. |
social.steam.openid_identity |
Missing or malformed Steam identity. |
social.steam.openid_invalid |
Not a valid Steam assertion (mode, endpoint, signed fields, repeated parameters). |
social.steam.openid_rejected |
Steam did not confirm the assertion. |
social.steam.openid_unavailable / social.steam.openid_timeout |
Steam could not be reached. |
Each verification publishes a SteamOpenIdVerifiedEvent.
public sealed class SteamOpenIdResult
{
public string SteamId { get; } // "76561197960435530"
public string ClaimedId { get; } // "https://steamcommunity.com/openid/id/76561197960435530"
}
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": "",
"OpenIdEnabled": true,
"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.OpenIdEnabled |
bool |
true |
Create the Steam sign-in (OpenID) service while Steam is enabled. OpenID itself needs no key. |
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. |
Validation — Steam.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.
Changing config while running
With CodeLogic 4.9+, config.socialconnect.json applies live when changed through Libraries.UpdateConfigAsync, Libraries.ReloadConfigAsync, or the config file watcher. Only the service whose settings changed is rebuilt:
- Discord settings:
social.Discordis replaced. - Steam settings:
social.Steam,social.Authandsocial.OpenIdare replaced; the profile cache starts empty. - Enabled flags: each service, and the whole library, can be switched on or off.
A replaced service is disposed only after its request timeout, so requests already running on it finish. Read social.Discord / social.Steam / social.Auth / social.OpenId when you use them rather than keeping an old instance.
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? |
SteamOpenIdVerifiedEvent |
A Steam sign-in callback is verified or refused. | SteamId, Success, VerifiedAt, ErrorMessage? |
Health check
HealthCheckAsync() reports a HealthStatus listing the active services (Active services: Discord, Steam, SteamOpenId). Services are only absent when their config switches them off, so an app with every service off is Healthy (No services enabled); only a call before initialization is Unhealthy.
var status = await social.HealthCheckAsync();
// status.Status : Healthy | Degraded | Unhealthy
See also
- Getting Started — load, configure, and use any
CL.*library. - API Reference — generated type/member documentation.
- Package on NuGet