Editor/Discord/DiscordWebhook.cs

Editor utility that posts images to Discord via webhook. Builds a multipart/form-data request with optional JSON payload (content, username, avatar, embed) and uploads an image attachment to one or multiple webhook URLs.

Http CallsNetworking
🌐 (runtime webhook URL passed into methods)
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
using Sandbox;

namespace Editor.SuperShot;

public static class DiscordWebhook
{
	public static async Task<bool> PostImage( string webhookUrl, byte[] imageBytes, string filename, DiscordMessage message, string mimeType = "image/png" )
	{
		if ( string.IsNullOrWhiteSpace( webhookUrl ) )
		{
			Log.Warning( "[Supershot] No Discord webhook URL configured." );
			return false;
		}

		if ( imageBytes is null || imageBytes.Length == 0 )
		{
			Log.Warning( "[Supershot] Nothing to post to Discord." );
			return false;
		}

		try
		{
			using var form = new MultipartFormDataContent();

			// A null message means image-only: send an empty payload so Discord posts just the attachment.
			var payload = message is null ? "{}" : BuildPayload( message, filename );
			form.Add( new StringContent( payload, Encoding.UTF8, "application/json" ), "payload_json" );

			var file = new ByteArrayContent( imageBytes );
			file.Headers.ContentType = new MediaTypeHeaderValue( mimeType );
			form.Add( file, "files[0]", filename );

			await Http.RequestStringAsync( webhookUrl, "POST", form );
			Log.Info( $"[Supershot] Posted {filename} to Discord." );
			return true;
		}
		catch ( Exception e )
		{
			Log.Error( $"[Supershot] Discord post failed: {e.Message}" );
			return false;
		}
	}

	public static async Task<int> PostImageToMany( IEnumerable<DiscordWebhookConfig> webhooks, byte[] imageBytes, string filename, DiscordMessage message, string mimeType = "image/png" )
	{
		int sent = 0;
		foreach ( var wh in webhooks )
		{
			if ( wh is null || !wh.IsConfigured )
				continue;

			if ( await PostImage( wh.Url, imageBytes, filename, wh.ResolveMessage( message ), mimeType ) )
				sent++;
		}
		return sent;
	}

	static string BuildPayload( DiscordMessage message, string filename )
	{
		var payload = new Dictionary<string, object>();

		if ( !string.IsNullOrWhiteSpace( message.Content ) )
			payload["content"] = message.Content;

		if ( !string.IsNullOrWhiteSpace( message.Username ) )
			payload["username"] = message.Username;

		if ( !string.IsNullOrWhiteSpace( message.AvatarUrl ) )
			payload["avatar_url"] = message.AvatarUrl;

		if ( message.UseEmbed )
		{
			var embed = new Dictionary<string, object>
			{
				["color"] = ColorToInt( message.EmbedColor ),
				["image"] = new Dictionary<string, object> { ["url"] = $"attachment://{filename}" }
			};

			if ( !string.IsNullOrWhiteSpace( message.EmbedTitle ) )
				embed["title"] = message.EmbedTitle;

			if ( !string.IsNullOrWhiteSpace( message.EmbedDescription ) )
				embed["description"] = message.EmbedDescription;

			if ( !string.IsNullOrWhiteSpace( message.EmbedFooter ) )
				embed["footer"] = new Dictionary<string, object> { ["text"] = message.EmbedFooter };

			if ( message.EmbedTimestamp )
				embed["timestamp"] = DateTime.UtcNow.ToString( "o" );

			payload["embeds"] = new[] { embed };
		}

		return Json.Serialize( payload );
	}

	static int ColorToInt( Color c )
	{
		int r = (int)(MathX.Clamp( c.r, 0f, 1f ) * 255f);
		int g = (int)(MathX.Clamp( c.g, 0f, 1f ) * 255f);
		int b = (int)(MathX.Clamp( c.b, 0f, 1f ) * 255f);
		return (r << 16) | (g << 8) | b;
	}
}