diff --git a/KfChatDotNetBot/Commands/Kasino/SlotsCommand.cs b/KfChatDotNetBot/Commands/Kasino/SlotsCommand.cs new file mode 100644 index 0000000..65a51d3 --- /dev/null +++ b/KfChatDotNetBot/Commands/Kasino/SlotsCommand.cs @@ -0,0 +1,531 @@ +using System.Text.RegularExpressions; +using KfChatDotNetBot.Extensions; +using KfChatDotNetBot.Models; +using KfChatDotNetBot.Models.DbModels; +using KfChatDotNetBot.Services; +using KfChatDotNetBot.Settings; +using KfChatDotNetWsClient.Models.Events; +using System.Net.Http.Headers; +using System.Runtime.InteropServices; +using System.Numerics; +using RandN; +using RandN.Compat; +using Raylib_cs; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Formats.Webp; + +namespace KfChatDotNetBot.Commands.Kasino; + +[KasinoCommand] +[WagerCommand] +public class SlotsCommand : ICommand +{ + public List Patterns => [ + new Regex(@"^slots (?\d+)$", RegexOptions.IgnoreCase), + new Regex(@"^slots (?\d+\.\d+)$", RegexOptions.IgnoreCase), + new Regex("^slots$", RegexOptions.IgnoreCase) + ]; + + public string? HelpText => "!slots [bet amount]"; + public UserRight RequiredRight => UserRight.Loser; + public TimeSpan Timeout => TimeSpan.FromSeconds(5); + public RateLimitOptionsModel? RateLimitOptions => new() + { + MaxInvocations = 1, + Window = TimeSpan.FromSeconds(30) + }; + + public async Task RunCommand(ChatBot botInstance, MessageModel messagen, UserDbModel user, + GroupCollection arguments, + CancellationToken ctx) + { + + if (!arguments.TryGetValue("amount", out var amount)) //if user just enters !keno + { + await botInstance.SendChatMessageAsync( + $"{user.FormatUsername()}, you need to bet something to play. !slots [bet]", + true, autoDeleteAfter: TimeSpan.FromSeconds(30)); + return; + } + + var wager = Convert.ToDecimal(amount.Value); + var gambler = await Money.GetGamblerEntityAsync(user.Id, ct: ctx); + if (gambler == null) + throw new InvalidOperationException($"Caught a null when retrieving gambler for {user.KfUsername}"); + if (gambler.Balance < wager) + { + await botInstance.SendChatMessageAsync( + $"{user.FormatUsername()}, your balance of {await gambler.Balance.FormatKasinoCurrencyAsync()} isn't enough for this wager.", + true, autoDeleteAfter: TimeSpan.FromSeconds(30)); + return; + } + + + Raylib.SetConfigFlags(ConfigFlags.HiddenWindow); + Raylib.InitWindow(500,900,"KiwiSlot"); + + var board = new KiwiSlotBoard(wager); + board.LoadAssets(); + board.ExecuteGameLoop(); + var finalImage = board.GenerateAnimatedWebp(board.SlotFrames); + + board.UnloadAssets(); + Raylib.CloseWindow(); + if (finalImage == null) + { + throw new InvalidOperationException("finalImage was null"); + } + var imageUrl = await Zipline.Upload(finalImage, new MediaTypeHeaderValue("image/webp"), "1h", ctx); + if (imageUrl is null) + { + await botInstance.SendChatMessageAsync($"{user.FormatUsername()}, failed to upload slot image.", true, autoDeleteAfter:TimeSpan.FromSeconds(30)); + return; + } + await botInstance.SendChatMessageAsync($"[img]{imageUrl}[/img]", true, autoDeleteAfter:TimeSpan.FromMinutes(3)); //posts image to chat + var winnings = Convert.ToDecimal(board.RunningTotalDisplay); + var colors = + await SettingsProvider.GetMultipleValuesAsync([ + BuiltIn.Keys.KiwiFarmsGreenColor, BuiltIn.Keys.KiwiFarmsRedColor + ]); + decimal newBalance; + if (winnings == 0) //dud spin + { + newBalance = await Money.NewWagerAsync(gambler.Id, wager, -wager, WagerGame.Keno, ct: ctx); + await botInstance.SendChatMessageAsync( + $"{user.FormatUsername()} you [color={colors[BuiltIn.Keys.KiwiFarmsRedColor].Value}]lost[/color]. Current balance: {await newBalance.FormatKasinoCurrencyAsync()}", true, autoDeleteAfter:TimeSpan.FromSeconds(30)); + return; + } + //if you win + var featureAddOn = board.GotFeature ? "Congrats on the feature." : ""; + newBalance = await Money.NewWagerAsync(gambler.Id, wager, Convert.ToDecimal(winnings), WagerGame.Keno, ct: ctx); + await botInstance.SendChatMessageAsync($"{user.FormatUsername()}, you [color={colors[BuiltIn.Keys.KiwiFarmsGreenColor].Value}win[/color]! Current balance: {await newBalance.FormatKasinoCurrencyAsync()}" + + $"{featureAddOn}", true, autoDeleteAfter:TimeSpan.FromSeconds(30)); + } + + private class KiwiSlotBoard + { + private const char WILD = 'K', FEATURE = 'L', EXPANDER = 'M'; + public readonly List SlotFrames = []; + private readonly Dictionary _symbolTextures = new(); + private readonly Dictionary _expanderTextures = new(); + private Texture2D _headerTexture; + + private readonly char[,] _preboard = new char[5, 5]; + private char[,] _board = new char[5, 5]; + private readonly decimal _userBet; + public decimal RunningTotalDisplay; + public bool GotFeature; + private int _activeFeatureTier; + private int _currentFeatureSpin; // Tracks progress through the feature + private bool _showGoldCircle; + + private string SlotSkin = "Default"; + + private readonly RandomShim _rand = RandomShim.Create(StandardRng.Create()); + private static readonly List ExpanderWild = + ['N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '1', '2']; + private readonly Dictionary _multiTable = new() + { + { 'N', 2 }, { 'O', 3 }, { 'P', 4 }, { 'Q', 5 }, { 'R', 6 }, { 'S', 7 }, + { 'T', 8 }, { 'U', 9 }, { 'V', 10 }, { 'W', 15 }, { 'X', 20 }, { 'Y', 25 }, + { 'Z', 50 }, { '1', 100 }, { '2', 200 } + }; + private readonly Dictionary _payoutTable = new() + { + { "A3", 0.2 }, { "A4", 1.0 }, { "A5", 5.0 }, { "B3", 0.2 }, { "B4", 1.0 }, { "B5", 5.0 }, + { "C3", 0.3 }, { "C4", 1.5 }, { "C5", 7.5 }, { "D3", 0.3 }, { "D4", 1.5 }, { "D5", 7.5 }, + { "E3", 0.4 }, { "E4", 2.0 }, { "E5", 10.0 }, { "F3", 1.0 }, { "F4", 5.0 }, { "F5", 15.0 }, + { "G3", 1.0 }, { "G4", 5.0 }, { "G5", 15.0 }, { "H3", 1.5 }, { "H4", 7.5 }, { "H5", 17.5 }, + { "I3", 1.5 }, { "I4", 7.5 }, { "I5", 17.5 }, { "J3", 2.0 }, { "J4", 10.0 }, { "J5", 20.0 }, + { "K5", 25.0 }, { "L5", 25.0 }, { "M5", 25.0 } + }; + private readonly List<(int row, int col)[]> _payoutLines = new() + { + new[] { (0, 0), (0, 1), (0, 2), (0, 3), (0, 4) }, + new[] { (1, 0), (1, 1), (1, 2), (1, 3), (1, 4) }, + new[] { (2, 0), (2, 1), (2, 2), (2, 3), (2, 4) }, + new[] { (3, 0), (3, 1), (3, 2), (3, 3), (3, 4) }, + new[] { (4, 0), (4, 1), (4, 2), (4, 3), (4, 4) }, + new[] { (0, 0), (1, 1), (2, 2), (3, 3), (4, 4) }, + new[] { (4, 0), (3, 1), (2, 2), (1, 3), (0, 4) }, + new[] { (1, 0), (0, 1), (1, 2), (0, 3), (1, 4) }, + new[] { (2, 0), (1, 1), (2, 2), (1, 3), (2, 4) }, + new[] { (3, 0), (2, 1), (3, 2), (2, 3), (3, 4) }, + new[] { (4, 0), (3, 1), (4, 2), (3, 3), (4, 4) }, + new[] { (0, 0), (1, 1), (0, 2), (1, 3), (0, 4) }, + new[] { (1, 0), (2, 1), (1, 2), (2, 3), (1, 4) }, + new[] { (2, 0), (3, 1), (2, 2), (3, 3), (2, 4) }, + new[] { (3, 0), (4, 1), (3, 2), (4, 3), (3, 4) }, + new[] { (2, 0), (1, 1), (0, 2), (1, 3), (2, 4) }, + new[] { (3, 0), (2, 1), (1, 2), (2, 3), (3, 4) }, + new[] { (2, 0), (3, 1), (4, 2), (3, 3), (2, 4) }, + new[] { (1, 0), (2, 1), (3, 2), (2, 3), (1, 4) } + }; + + public KiwiSlotBoard(decimal bet) { + _userBet = bet; + RunningTotalDisplay = 0; + } + + public void LoadAssets() + { + var assetPath = Path.Join(AppDomain.CurrentDomain.BaseDirectory, "Assets", SlotSkin); + _headerTexture = Raylib.LoadTexture(Path.Combine(assetPath, "header.png")); + foreach (var c in "ABCDEFGHIJKL") _symbolTextures[c] = Raylib.LoadTexture(Path.Combine(assetPath, $"{c}.png")); + for (var i = 1; i <= 5; i++) _expanderTextures[i] = Raylib.LoadTexture(Path.Combine(assetPath, $"exp{i}.png")); + } + + public void UnloadAssets() + { + Raylib.UnloadTexture(_headerTexture); + foreach (var t in _symbolTextures.Values) Raylib.UnloadTexture(t); + foreach (var t in _expanderTextures.Values) Raylib.UnloadTexture(t); + } + + public void ExecuteGameLoop(int featureSpins = 0) + { + if (featureSpins is not 0) GotFeature = true; + GeneratePreBoard(featureSpins); + + var fCount = 0; + for (var i = 0; i < 5; i++) + for (var j = 0; j < 5; j++) + if (_preboard[i, j] == FEATURE) fCount++; + + if (featureSpins == 0) + { + _showGoldCircle = false; + _activeFeatureTier = fCount >= 5 ? 5 : (fCount >= 3 ? fCount : 0); + _currentFeatureSpin = 0; + } + else + { + _showGoldCircle = true; + _currentFeatureSpin = featureSpins; + } + + ConsoleDisplay(); + + var totalSpins = _activeFeatureTier switch { 3 => 3, 4 => 5, 5 => 10, _ => 0 }; + if (featureSpins == 0) + for (var s = 1; s <= totalSpins; s++) ExecuteGameLoop(s); + } + + + public void GeneratePreBoard(int feature = 0, char rigged = '0') + { + var fCount = 0; + var exCols = new HashSet(); + var riggedCounter = 0; + var maxWinRiggedCounter = 0; + for (var i = 0; i < 5; i++) + { + for (var j = 0; j < 5; j++) + { + if (rigged == '0') + { + /* + * LOWEST - A, B + * LOW - C, D + * LOWMID - E + * MID - F, G + * HIGH - H, I + * HIGHEST - J + * WILD - K + * FEATURE - L + * EXPANDER - M + * EXPANDERWILD multis 2 - 10, 15, 20, 25, 50, 100, 200x - N O P Q R S T U V W X Y Z 1 2 + */ + var r = _rand.NextDouble()*100.6; + if (feature!=0 && j > 2) r*=1.05; + if (r < 22) _preboard[i, j] = 'A'; + else if (r < 44) _preboard[i, j] = 'B'; + else if (r < 52) _preboard[i, j] = 'C'; + else if (r < 66) _preboard[i, j] = 'D'; + else if (r < 78) _preboard[i, j] = 'E'; + else if (r < 84) _preboard[i, j] = 'F'; + else if (r < 89) _preboard[i, j] = 'G'; + else if (r < 92) _preboard[i, j] = 'H'; + else if (r < 95) _preboard[i, j] = 'I'; + else if (r < 97) _preboard[i, j] = 'J'; + else if (r < 98.5) _preboard[i, j] = WILD; + else if (r < j switch {<=2 => 99, _ => 99.5}) { if (!exCols.Contains(j)) { _preboard[i, j] = EXPANDER; exCols.Add(j); } else _preboard[i, j] = WILD; } + else + { + if (fCount < 5) { _preboard[i, j] = FEATURE; fCount++; } + else _preboard[i, j] = WILD; + } + } + else + { + if (riggedCounter < 5 || (rigged != EXPANDER && rigged != FEATURE)) + { + _preboard[i, j] = rigged; + riggedCounter++; + } + else if (rigged == EXPANDER && maxWinRiggedCounter < 5) + { + _preboard[i, j] = FEATURE; + maxWinRiggedCounter++; + } + else if (rigged == EXPANDER && maxWinRiggedCounter >= 5) + { + _preboard[i,j] = WILD; + } + else + { + _preboard[i, j] = 'A'; + rigged = '0'; + } + } + } + } + } + + private void RenderFrame(int dropOffset = 500, List? activeWins = null) + { + var target = Raylib.LoadRenderTexture(500, 900); + Raylib.BeginTextureMode(target); + Raylib.ClearBackground(Raylib_cs.Color.Black); + + Raylib.DrawTexture(_headerTexture, 0, 0, Raylib_cs.Color.White); + + Raylib.BeginScissorMode(0, 200, 500, 500); + var occupied = new bool[5, 5]; + for (var j = 0; j < 5; j++) { + for (var i = 0; i < 5; i++) { + if (occupied[i, j]) continue; + var sym = _board[i, j]; + int x = j * 100, currentY = (200 + (i * 100)) - (500 - dropOffset); + if (sym == EXPANDER || _multiTable.ContainsKey(sym)) { + var h = 0; + for (var k = i; k < 5; k++) { if (_board[k, j] == sym) h++; else break; } + if (_expanderTextures.TryGetValue(h, out var texture)) { + Raylib.DrawTexture(texture, x, currentY, Raylib_cs.Color.White); + if (_multiTable.TryGetValue(sym, out var mVal)) { + var mText = $"x{mVal}"; + const int fsM = 30; var twM = Raylib.MeasureText(mText, fsM); + Raylib.DrawText(mText, x + 50 - twM / 2 + 2, currentY + (h * 50) - fsM / 2 + 2, fsM, Raylib_cs.Color.Black); + Raylib.DrawText(mText, x + 50 - twM / 2, currentY + (h * 50) - fsM / 2, fsM, Raylib_cs.Color.Yellow); + } + } + for (var k = 0; k < h; k++) occupied[i + k, j] = true; + } else if (_symbolTextures.TryGetValue(sym, out var texture)) Raylib.DrawTexture(texture, x, currentY, Raylib_cs.Color.White); + } + } + + if (activeWins != null) { + foreach (var win in activeWins) { + for (var i = 0; i < win.Path.Length - 1; i++) { + var s = new Vector2(win.Path[i].col * 100 + 50, 200 + (win.Path[i].row * 100) + 50); + var e = new Vector2(win.Path[i+1].col * 100 + 50, 200 + (win.Path[i+1].row * 100) + 50); + Raylib.DrawLineEx(s, e, 8.0f, Raylib_cs.Color.White); + } + var amt = $"${win.Amount:F2}"; + const int fsW = 25; var twW = Raylib.MeasureText(amt, fsW); + int tx = win.Path[win.Path.Length / 2].col * 100 + 50, ty = 200 + (win.Path[win.Path.Length / 2].row * 100) + 50; + Raylib.DrawRectangle(tx - twW / 2 - 5, ty - fsW / 2 - 2, twW + 10, fsW + 4, new Raylib_cs.Color(0, 0, 0, 200)); + Raylib.DrawText(amt, tx - twW / 2, ty - fsW / 2, fsW, Raylib_cs.Color.Green); + } + } + Raylib.EndScissorMode(); + + // FOOTER + Raylib.DrawRectangle(0, 700, 500, 200, new Raylib_cs.Color(15, 15, 15, 255)); + Raylib.DrawLineEx(new Vector2(0, 700), new Vector2(500, 700), 2, Raylib_cs.Color.Gold); + + // Top Row UI - Compacted + Raylib.DrawText("BET", 20, 710, 12, Raylib_cs.Color.LightGray); + Raylib.DrawText($"${_userBet:F2}", 20, 725, 18, Raylib_cs.Color.White); + + // SPIN COUNTER (Progress) - Center between Bet and Win + if (_currentFeatureSpin > 0) + { + var totalSpins = _activeFeatureTier switch { 3 => 3, 4 => 5, 5 => 10, _ => 0 }; + var spinProgress = $"SPIN {_currentFeatureSpin}/{totalSpins}"; + var spinFs = 20; + var spinTw = Raylib.MeasureText(spinProgress, spinFs); + Raylib.DrawText(spinProgress, 250 - (spinTw / 2), 715, spinFs, Raylib_cs.Color.SkyBlue); + } + + var tallyStr = $"WIN: ${RunningTotalDisplay:F2}"; + var tallySize = 26; + var tallyWidth = Raylib.MeasureText(tallyStr, tallySize); + Raylib.DrawText(tallyStr, 480 - tallyWidth, 712, tallySize, Raylib_cs.Color.Gold); + + // Feature Symbol Area + var iconY = 760; + var textY = 865; + int[] xCoords = { 80, 250, 420 }; + int[] tiers = { 3, 4, 5 }; + + for (var i = 0; i < 3; i++) + { + var tier = tiers[i]; + var x = xCoords[i] - 50; + + if (_showGoldCircle && _activeFeatureTier == tier) + { + Raylib.DrawCircle(x + 50, iconY + 50, 48, Raylib_cs.Color.Gold); + } + + if (_symbolTextures.ContainsKey(FEATURE)) + { + Raylib.DrawTexture(_symbolTextures[FEATURE], x, iconY, Raylib_cs.Color.White); + } + + var tierLabel = $"x{tier}"; + var fsL = 20; + var twL = Raylib.MeasureText(tierLabel, fsL); + Raylib.DrawText(tierLabel, x + 50 - twL / 2, textY, fsL, Raylib_cs.Color.White); + } + + Raylib.EndTextureMode(); + var finalImage = Raylib.LoadImageFromTexture(target.Texture); + Raylib.ImageFlipVertical(ref finalImage); + SlotFrames.Add(finalImage); + Raylib.UnloadRenderTexture(target); + } + + private void ConsoleDisplay(bool riggedMaxWin = false) + { + // 1. Initial Setup and Drop Animation + _board = (char[,])_preboard.Clone(); + for (var offset = 0; offset <= 500; offset += 50) RenderFrame(offset); + for (var offset = 500; offset <= 520; offset += 20) RenderFrame(offset); + for (var offset = 520; offset >= 500; offset -= 20) RenderFrame(offset); + + // 2. Handle Expander Multipliers + var multis = new List(_multiTable.Keys); + for (var j = 0; j < 5; j++) { + for (var i = 0; i < 5; i++) + { + if (_preboard[i, j] != EXPANDER) continue; + + var hitWild = false; + for (var c = i; c < 5; c++) if (_preboard[c, j] == WILD) hitWild = true; + + char mSym; + if (!riggedMaxWin) + { + mSym = hitWild ? multis[_rand.Next(multis.Count)] : EXPANDER; + } + else mSym = '2'; + + for (var row = i; row < 5; row++) { + _board[row, j] = mSym; + // Brief pause frames for expansion effect + for(var f = 0; f < 1; f++) RenderFrame(); + } + break; + } + } + + // 3. Winning Line Calculations and Accurate Accumulation + var winners = GetWinningLinesCoordsWithPayouts(); + + // Calculate the final target to prevent rounding errors + var totalToWinThisSpin = winners.Sum(w => w.Amount); + var finalTarget = RunningTotalDisplay + totalToWinThisSpin; + + // Iterate through each winning line + for (var i = 0; i < winners.Count; i++) { + var currentWin = winners[i]; + var increment = currentWin.Amount / (decimal)10.0; + + // Process 10 frames of animation per winning line + for (var f = 0; f < 10; f++) { + RunningTotalDisplay += increment; + + // If there's a next win, we show both currently active and next for smoothness + if (i < winners.Count - 1 && f > 7) { + RenderFrame(500, [currentWin, winners[i + 1]]); + } else { + RenderFrame(500, [currentWin]); + } + } + } + + // FINAL SNAP: Ensure floating point precision hasn't left us at 199.99 instead of 200.00 + RunningTotalDisplay = finalTarget; + + // 4. Feature Trigger Visualization + if (_activeFeatureTier >= 3) + { + _showGoldCircle = true; + // Hold on the final board state longer if a feature triggered + for (var f = 0; f < 10; f++) RenderFrame(); + } + else + { + // Short pause before the next spin/end of animation + for (var f = 0; f < 5; f++) RenderFrame(); + } + } + + private List GetWinningLinesCoordsWithPayouts() + { + var results = new List(); + foreach (var line in _payoutLines) { + var checker = '0'; + var count = 0; double multi = 0; var special = true; + foreach (var (r, c) in line) { + var cell = _board[r, c]; + if (cell == WILD || cell == FEATURE || cell == EXPANDER || ExpanderWild.Contains(cell)) continue; + checker = cell; special = false; break; //finds the first valid symbol in the payline + } + if (!special) { + foreach (var (r, c) in line) { + var ch = _board[r, c]; + if (ch == checker || ch == WILD || ch == FEATURE || ExpanderWild.Contains(ch) || ch == EXPANDER) { + count++; + if (ExpanderWild.Contains(ch)) multi += _multiTable[ch]; + } else if (count < 3) { count = 0; break; } else break; + } + } + else { + checker = _board[line[0].row, line[0].col]; + count = 5; + foreach (var (r, c) in line) if (ExpanderWild.Contains(_board[r, c])) multi += _multiTable[_board[r, c]]; + } + if (count >= 3) { + if (multi == 0) multi = 1; + if (_payoutTable.TryGetValue($"{checker}{count}", out var baseWin)) { + var path = new (int, int)[count]; Array.Copy(line, path, count); + results.Add(new WinDetail { Path = path, Amount = _userBet * (decimal)baseWin * (decimal)multi }); + } + } + } + return results; + } + + public unsafe MemoryStream? GenerateAnimatedWebp(List frames) + { + if (frames.Count == 0) return null; + using var animated = new Image(500, 900); + foreach (var rImg in frames) { + var bytes = new byte[rImg.Width * rImg.Height * 4]; + Marshal.Copy((IntPtr)rImg.Data, bytes, 0, bytes.Length); + using var frame = SixLabors.ImageSharp.Image.LoadPixelData(bytes, rImg.Width, rImg.Height); + frame.Frames.RootFrame.Metadata.GetWebpMetadata().FrameDelay = 2; + animated.Frames.AddFrame(frame.Frames.RootFrame); + } + if (animated.Frames.Count > 1) animated.Frames.RemoveFrame(0); + // Create a MemoryStream that stays open for the caller + var outputStream = new MemoryStream(); + animated.Save(outputStream, new WebpEncoder { Quality = 80 }); + + // IMPORTANT: Reset position to the start so the uploader reads the whole file + outputStream.Position = 0; + return outputStream; + } + } + + private class WinDetail + { + public required (int row, int col)[] Path { get; set; } + public decimal Amount { get; set; } + } +} + diff --git a/KfChatDotNetBot/KfChatDotNetBot.csproj b/KfChatDotNetBot/KfChatDotNetBot.csproj index 2a9c472..1b47c65 100644 --- a/KfChatDotNetBot/KfChatDotNetBot.csproj +++ b/KfChatDotNetBot/KfChatDotNetBot.csproj @@ -6,6 +6,7 @@ enable enable default + true @@ -25,6 +26,8 @@ + + diff --git a/KfChatDotNetBot/Services/Zipline.cs b/KfChatDotNetBot/Services/Zipline.cs new file mode 100644 index 0000000..778f6af --- /dev/null +++ b/KfChatDotNetBot/Services/Zipline.cs @@ -0,0 +1,61 @@ +using System.Net; +using System.Net.Http.Headers; +using System.Net.Http.Json; +using System.Text.Json; +using KfChatDotNetBot.Settings; +using NLog; + +namespace KfChatDotNetBot.Services; + +public static class Zipline +{ + public static async Task Upload(Stream content, MediaTypeHeaderValue mimeType, string? expiration = null, CancellationToken ct = default) + { + var logger = LogManager.GetCurrentClassLogger(); + var settings = + await SettingsProvider.GetMultipleValuesAsync([ + BuiltIn.Keys.Proxy, BuiltIn.Keys.ZiplineUrl, BuiltIn.Keys.ZiplineKey + ]); + + if (settings[BuiltIn.Keys.ZiplineKey].Value == null) + { + throw new InvalidOperationException("ZiplineKey is not defined"); + } + + var handler = new HttpClientHandler(); + if (settings[BuiltIn.Keys.Proxy].Value != null) + { + handler.Proxy = new WebProxy(settings[BuiltIn.Keys.Proxy].Value); + handler.UseProxy = true; + } + + using var client = new HttpClient(handler); + using var formContent = new MultipartFormDataContent(); + var fileContent = new StreamContent(content); + fileContent.Headers.ContentType = mimeType; + formContent.Add(fileContent); + client.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", settings[BuiltIn.Keys.ZiplineKey].Value); + if (expiration != null) + { + client.DefaultRequestHeaders.Add("x-zipline-expiration", expiration); + } + + var response = await client.PostAsync($"{settings[BuiltIn.Keys.ZiplineUrl].Value}/api/upload", formContent, ct); + var json = await response.Content.ReadFromJsonAsync(cancellationToken: ct); + string url; + try + { + url = json.GetProperty("files")[0].GetString() ?? + throw new InvalidOperationException("Caught null when grabbing Zipline result"); + } + catch (Exception e) + { + logger.Error("Caught exception when attempting to upload to Zipline. Raw JSON response followed by exception"); + logger.Error(json.GetRawText); + logger.Error(e); + throw; + } + + return url; + } +} \ No newline at end of file diff --git a/KfChatDotNetBot/Settings/BuiltIn.cs b/KfChatDotNetBot/Settings/BuiltIn.cs index ce79bf1..4983312 100644 --- a/KfChatDotNetBot/Settings/BuiltIn.cs +++ b/KfChatDotNetBot/Settings/BuiltIn.cs @@ -459,6 +459,10 @@ public static class BuiltIn public static string YouTubeApiKey = "YouTube.ApiKey"; [BuiltInSetting("Openrouter API key for hostess command", SettingValueType.Text, isSecret: true)] public static string OpenrouterApiKey = "Openrouter.ApiKey"; + [BuiltInSetting("API key for Zipline", SettingValueType.Text, isSecret: true)] + public static string ZiplineKey = "Zipline.Key"; + [BuiltInSetting("Base URL for Zipline", SettingValueType.Text, defaultValue: "https://i.ddos.lgbt")] + public static string ZiplineUrl = "Zipline.Url"; } }