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
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
}
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. |
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.
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
- Getting Started — load, configure, and use any
CL.*library. - API Reference — generated type/member documentation.
- Package on NuGet