From 72a162b67abcca33998f23a58284187dc51afb0d Mon Sep 17 00:00:00 2001 From: cohlexyz <142505474+cohlexyz@users.noreply.github.com> Date: Fri, 19 Dec 2025 07:42:57 +0100 Subject: [PATCH] Add !hostess command (#12) * Add basic !hostess command * Add openrouter integration --- KfChatDotNetBot/Commands/MemeCommands.cs | 89 +++++++++++++++++++ KfChatDotNetBot/Services/BotServices.cs | 11 ++- KfChatDotNetBot/Services/OpenRouter.cs | 105 +++++++++++++++++++++++ KfChatDotNetBot/Settings/BuiltIn.cs | 2 + 4 files changed, 206 insertions(+), 1 deletion(-) create mode 100644 KfChatDotNetBot/Services/OpenRouter.cs diff --git a/KfChatDotNetBot/Commands/MemeCommands.cs b/KfChatDotNetBot/Commands/MemeCommands.cs index 516e1ca..d019b53 100644 --- a/KfChatDotNetBot/Commands/MemeCommands.cs +++ b/KfChatDotNetBot/Commands/MemeCommands.cs @@ -2,6 +2,7 @@ using System.Text.RegularExpressions; using Humanizer; using KfChatDotNetBot.Models; using KfChatDotNetBot.Models.DbModels; +using KfChatDotNetBot.Services; using KfChatDotNetBot.Settings; using KfChatDotNetWsClient.Models.Events; using NLog; @@ -320,4 +321,92 @@ public class JuiceSportsCommand : ICommand "[img]https://i.ddos.lgbt/u/KAwWMW.webp[/img][br]⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀" + "[img]https://i.ddos.lgbt/u/uCuSOw.gif[/img][br]", true); } +} + + +public class HostessCommand : ICommand +{ + public List Patterns => [ + new Regex("^hostess", RegexOptions.IgnoreCase), + ]; + public string? HelpText => "Ask the hostess for help"; + public UserRight RequiredRight => UserRight.Loser; + public TimeSpan Timeout => TimeSpan.FromSeconds(10); + public RateLimitOptionsModel? RateLimitOptions => new RateLimitOptionsModel + { + MaxInvocations = 1, + Window = TimeSpan.FromSeconds(30) + }; + + private static string[] StaticResponses = [ + "For questions regarding your current contract please contact us at contact@bossmanjack.com", + "Unspecified error", + "Have you considered giving us a review on TrustPilot?", + "We are sincerely sorry to hear that you are not having a positive experience on our platform. Please be assured that we take matters of fairness and transparency very seriously.", + "At the Kasino, we prioritize strict adherence to regulatory requirements to maintain the security and integrity of our platform.", + "There are currently no hosts online to serve your request.", + "We would like to assist you further and understand better the issue. Due to that, we have requested further information.", + "When it comes to RTP, it is important to understand that this number is calculated based on at least 1 million bets. So, over a session of a few thousand bets, anything can happen, which is exactly what makes gambling exciting.", + "We understand that gambling involves risks, and while some players may experience periods of winning and losing, we strive to provide resources and tools to support responsible gambling practices.", + "Thank you for taking the time to leave a 5-star review! We're thrilled to have provided you with a great experience.", + "Please rest assured that our platform operates with certified random number generators to ensure fairness and transparency in all gaming outcomes. We do not manipulate the odds or monitor games to favor any particular outcome.", + "We would like to inform you that we have responded to your recent post.", + "All of our Kasino originals are 100% probably fair and each and every single bet placed at our any games are verifiable.", + "We want to emphasize that our games are developed with the highest standards of integrity and fairness.", + "Stop harrassing me", + ]; + + private static string[] LLMPrompts = [ + "You are a hostess for a virtual casino. You've just gotten a message from a customer with cripling gambling addiction issues. Respond in a smug and condescending manner.", + "You are an overworked fastfood worker at a drive-thru. A confused gambling addict just arrived. Respond with at most two sentences." + ]; + + public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments, CancellationToken ctx) + { + await using var db = new ApplicationDbContext(); + var activeExclusion = await Money.GetActiveExclusionAsync(user.Id, ctx); + if (db.Exclusions.Any(e => e.Id == user.Id)) + { + await botInstance.SendChatMessageAsync("You are currently excluded from using hostess service.", true); + return; + } + + var random = new Random(); + + if (random.NextDouble() < 0.06) + { + // ignore 6% of requests like the old hostess command + return; + } + + if (botInstance.BotServices.OpenRouter == null || random.NextDouble() < 0.3) + { + var response = StaticResponses[random.Next(0, StaticResponses.Length)]; + await botInstance.SendChatMessageAsync(response, true); + } + else + { + var msg = message.MessageRaw.Replace("hostess", "").Trim(); + if (string.IsNullOrWhiteSpace(msg)) + { + msg = "I need help with my gambling addiction."; + } + + var llmResponse = await botInstance.BotServices.OpenRouter.GetResponseAsync( + LLMPrompts[random.Next(0, LLMPrompts.Length)], + msg, + model: "deepseek/deepseek-v3.2", + Temperature: 1.0f + (float)((random.NextDouble() - 0.3) * 0.5) + ); + if (llmResponse == null) + { + var fallback = StaticResponses[random.Next(0, StaticResponses.Length)]; + await botInstance.SendChatMessageAsync(fallback, true); + } + else + { + await botInstance.SendChatMessageAsync(llmResponse, true, ChatBot.LengthLimitBehavior.TruncateExactly); + } + } + } } \ No newline at end of file diff --git a/KfChatDotNetBot/Services/BotServices.cs b/KfChatDotNetBot/Services/BotServices.cs index d3eeeae..64a5b3c 100644 --- a/KfChatDotNetBot/Services/BotServices.cs +++ b/KfChatDotNetBot/Services/BotServices.cs @@ -39,6 +39,7 @@ public class BotServices private PeerTube? _peerTubeStatusCheck; private Owncast? _owncastStatusCheck; private ShuffleDotUs? _shuffleDotUs; + public OpenRouter? OpenRouter; private Task? _websocketWatchdog; private Task? _howlggGetUserTimer; @@ -89,7 +90,8 @@ public class BotServices BuildDLiveStatusCheck(), BuildPeerTubeLiveStatusCheck(), BuildOwncastLiveStatusCheck(), - BuildShuffleDotUs() + BuildShuffleDotUs(), + BuildOpenRouter() ]; try { @@ -105,6 +107,13 @@ public class BotServices _websocketWatchdog = WebsocketWatchdog(); _howlggGetUserTimer = HowlggGetUserTimer(); } + + private async Task BuildOpenRouter() + { + _logger.Debug("Building OpenRouter client"); + var proxySetting = await SettingsProvider.GetValueAsync(BuiltIn.Keys.Proxy); + OpenRouter = new OpenRouter(proxySetting.Value); + } private async Task BuildShuffle() { diff --git a/KfChatDotNetBot/Services/OpenRouter.cs b/KfChatDotNetBot/Services/OpenRouter.cs new file mode 100644 index 0000000..90e7128 --- /dev/null +++ b/KfChatDotNetBot/Services/OpenRouter.cs @@ -0,0 +1,105 @@ +using System.Net; +using NLog; +using System.Text.Json.Serialization; +using System.Text.Json; +using System.Net.Http.Headers; +using System.Text; +using KfChatDotNetBot.Settings; + +public class OpenRouter(string? proxy = null) +{ + + class OrResponse + { + [JsonPropertyName("id")] public string Id { get; set; } = string.Empty; + [JsonPropertyName("provider")] public string Provider { get; set; } = string.Empty; + [JsonPropertyName("model")] public string Model { get; set; } = string.Empty; + [JsonPropertyName("object")] public string Object { get; set; } = string.Empty; + [JsonPropertyName("created")] public long Created { get; set; } + [JsonPropertyName("choices")] public List Choices { get; set; } = new List(); + [JsonPropertyName("usage")] public Usage Usage { get; set; } = new Usage(); + } + + class OrChoice + { + [JsonPropertyName("logprobs")] public object? Logprobs { get; set; } + [JsonPropertyName("finish_reason")] public string FinishReason { get; set; } = string.Empty; + [JsonPropertyName("native_finish_reason")] public string NativeFinishReason { get; set; } = string.Empty; + [JsonPropertyName("index")] public int Index { get; set; } + [JsonPropertyName("message")] public OrMessage Message { get; set; } = new OrMessage(); + } + + class OrMessage + { + [JsonPropertyName("role")] public string Role { get; set; } = string.Empty; + [JsonPropertyName("content")] public string Content { get; set; } = string.Empty; + [JsonPropertyName("refusal")] public object? Refusal { get; set; } + [JsonPropertyName("reasoning")] public object? Reasoning { get; set; } + } + + class Usage + { + [JsonPropertyName("prompt_tokens")] public int PromptTokens { get; set; } + [JsonPropertyName("completion_tokens")] public int CompletionTokens { get; set; } + [JsonPropertyName("total_tokens")] public int TotalTokens { get; set; } + } + + + private Logger _logger = LogManager.GetCurrentClassLogger(); + private Uri _orEndpoint = new Uri("https://openrouter.ai/api/v1/chat/completions"); + private string? _proxy = proxy; + + public async Task GetResponseAsync(string prompt, string question, string model = "openrouter-gpt4-1106", float Temperature = 0.7f) + { + _logger.Info("Sending request to OpenRouter"); + var handler = new HttpClientHandler { AutomaticDecompression = DecompressionMethods.All }; + if (_proxy != null) + { + handler.UseProxy = true; + handler.Proxy = new WebProxy(_proxy); + _logger.Debug($"Configured to use proxy {_proxy}"); + } + using var client = new HttpClient(handler); + + try + { + List<(string role, string content)> msg = [("system", prompt), ("user", question)]; + var keySetting = await SettingsProvider.GetValueAsync(BuiltIn.Keys.OpenrouterApiKey); + if (keySetting == null || string.IsNullOrWhiteSpace(keySetting.Value)) + { + _logger.Error("OpenRouter API key is not set in settings."); + return null; + } + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", keySetting.Value); + + var payload = new + { + model, + temperature = Temperature, + messages = msg.ConvertAll(m => new { m.role, m.content }) + }; + var json = JsonSerializer.Serialize(payload); + var content = new StringContent(json, Encoding.UTF8, "application/json"); + + + var response = await client.PostAsync("https://openrouter.ai/api/v1/chat/completions", content); + response.EnsureSuccessStatusCode(); + var responseBody = await response.Content.ReadAsStringAsync(); + var responseData = JsonSerializer.Deserialize(responseBody); + + if (responseData == null || responseData.Choices.Count == 0) + { + _logger.Error("No response from OpenRouter."); + return null; + } + return responseData.Choices[0].Message.Content; + + } + catch (Exception ex) + { + _logger.Error(ex, "Error while communicating with OpenRouter."); + } + + return null; + } +} \ No newline at end of file diff --git a/KfChatDotNetBot/Settings/BuiltIn.cs b/KfChatDotNetBot/Settings/BuiltIn.cs index 8cb7249..01e6283 100644 --- a/KfChatDotNetBot/Settings/BuiltIn.cs +++ b/KfChatDotNetBot/Settings/BuiltIn.cs @@ -453,6 +453,8 @@ public static class BuiltIn public static string KasinoDiceCleanupDelay = "Kasino.Dice.CleanupDelay"; [BuiltInSetting("Delay in milliseconds before cleaning up wheel", SettingValueType.Text, "30000", WholeNumberRegex)] public static string KasinoWheelCleanupDelay = "Kasino.Wheel.CleanupDelay"; + [BuiltInSetting("Openrouter API key for hostess command", SettingValueType.Text, isSecret: true)] + public static string OpenrouterApiKey = "Openrouter.ApiKey"; } }