using RestSharp; using System.Security.Cryptography.X509Certificates; using System.Text.RegularExpressions; using TwitchLib.Api.Helix.Models.Users.GetUsers; using TwitchLib.Api; using TwitchLib.Client.Events; using TwitchLib.Client.Models; using TwitchLib.Client; using TwitchLib.Communication.Clients; using TwitchLib.Communication.Models; using vassago.Behavior; using vassago.Models; namespace vassago.TwitchInterface; internal class unifiedTwitchMessage { public unifiedTwitchMessage(ChatMessage chatMessage){} } public class TwitchInterface { internal const string PROTOCOL = "twitch"; private static SemaphoreSlim channelSetupSemaphpore = new SemaphoreSlim(1, 1); private Channel protocolAsChannel; private Account selfAccountInProtocol; TwitchClient client; private async Task SetupTwitchChannel() { await channelSetupSemaphpore.WaitAsync(); try { protocolAsChannel = Rememberer.SearchChannel(c => c.ParentChannel == null && c.Protocol == PROTOCOL); if (protocolAsChannel == null) { protocolAsChannel = new Channel() { DisplayName = "twitch (itself)", MeannessFilterLevel = Enumerations.MeannessFilterLevel.Medium, LewdnessFilterLevel = Enumerations.LewdnessFilterLevel.G, MaxTextChars = 500, MaxAttachmentBytes = 0, LinksAllowed = false, ReactionsPossible = false, ExternalId = null, Protocol = PROTOCOL, SubChannels = [] }; protocolAsChannel.DisplayName = "twitch (itself)"; protocolAsChannel.SendMessage = (t) => { throw new InvalidOperationException($"twitch itself cannot accept text"); }; protocolAsChannel.SendFile = (f, t) => { throw new InvalidOperationException($"twitch itself cannot send file"); }; protocolAsChannel = Rememberer.RememberChannel(protocolAsChannel); Console.WriteLine($"protocol as channle added; {protocolAsChannel}"); } else { Console.WriteLine($"twitch, channel with id {protocolAsChannel.Id}, already exists"); } //protocolAsChan } finally { channelSetupSemaphpore.Release(); } } ///https://www.twitchapps.com/tmi/ public async Task Init(TwitchConfig tc) { await SetupTwitchChannel(); WebSocketClient customClient = new WebSocketClient(new ClientOptions { MessagesAllowedInPeriod = 750, ThrottlingPeriod = TimeSpan.FromSeconds(30) } ); client = new TwitchClient(customClient); client.Initialize(new ConnectionCredentials(tc.username, tc.oauth, capabilities: new Capabilities())); client.OnLog += Client_OnLog; client.OnJoinedChannel += Client_OnJoinedChannel; client.OnMessageReceived += Client_OnMessageReceivedAsync; client.OnWhisperReceived += Client_OnWhisperReceivedAsync; client.OnConnected += Client_OnConnected; client.Connect(); Console.WriteLine("twitch client 1 connected"); } private async void Client_OnWhisperReceivedAsync(object sender, OnWhisperReceivedArgs e) { //data received Console.WriteLine($"whisper#{e.WhisperMessage.Username}[{DateTime.Now}][{e.WhisperMessage.DisplayName} [id={e.WhisperMessage.Username}]][msg id: {e.WhisperMessage.MessageId}] {e.WhisperMessage.Message}"); //translate to internal, upsert var m = UpsertMessage(e.WhisperMessage); m.Reply = (t) => { return Task.Run(() => { client.SendWhisper(e.WhisperMessage.Username, t); }); }; m.Channel.ChannelType = vassago.Models.Enumerations.ChannelType.DM; //act on await Behaver.Instance.ActOn(m); m.ActedOn = true; //TODO: remember it again? } private async void Client_OnMessageReceivedAsync(object sender, OnMessageReceivedArgs e) { //data eived Console.WriteLine($"#{e.ChatMessage.Channel}[{DateTime.Now}][{e.ChatMessage.DisplayName} [id={e.ChatMessage.Username}]][msg id: {e.ChatMessage.Id}] {e.ChatMessage.Message}"); //translate to internal, upsert var m = UpsertMessage(e.ChatMessage); m.Reply = (t) => { return Task.Run(() => { client.SendReply(e.ChatMessage.Channel, e.ChatMessage.Id, t); }); }; m.Channel.ChannelType = vassago.Models.Enumerations.ChannelType.Normal; //act on await Behaver.Instance.ActOn(m); m.ActedOn = true; //TODO: remember again? } private void Client_OnConnected(object sender, OnConnectedArgs e) { Console.WriteLine($"twitch marking selfaccount as seeninchannel {protocolAsChannel.Id}"); selfAccountInProtocol = UpsertAccount(e.BotUsername, protocolAsChannel); selfAccountInProtocol.DisplayName = e.BotUsername; Behaver.Instance.MarkSelf(selfAccountInProtocol); Console.WriteLine($"Connected to {e.AutoJoinChannel}"); } private void Client_OnJoinedChannel(object sender, OnJoinedChannelArgs e) { client.SendMessage(e.Channel, "beep boop"); } private void Client_OnLog(object sender, OnLogArgs e) { Console.WriteLine($"{e.DateTime.ToString()}: {e.BotUsername} - {e.Data}"); } private Account UpsertAccount(string username, Channel inChannel) { Console.WriteLine($"upserting twitch account. username: {username}. inChannel: {inChannel?.Id}"); var acc = Rememberer.SearchAccount(ui => ui.ExternalId == username && ui.SeenInChannel.ExternalId == inChannel.ExternalId); Console.WriteLine($"upserting twitch account, retrieved {acc?.Id}."); if (acc != null) { Console.WriteLine($"acc's usser: {acc.IsUser?.Id}"); } acc ??= new Account() { IsUser = Rememberer.SearchUser( u => u.Accounts.Any(a => a.ExternalId == username && a.Protocol == PROTOCOL)) ?? new vassago.Models.User() }; acc.Username = username; acc.ExternalId = username; //acc.IsBot = false? there is a way to tell, but you have to go back through the API acc.Protocol = PROTOCOL; acc.SeenInChannel = inChannel; Console.WriteLine($"we asked rememberer to search for acc's user. {acc.IsUser?.Id}"); if (acc.IsUser != null) { Console.WriteLine($"user has record of {acc.IsUser.Accounts?.Count ?? 0} accounts"); } acc.IsUser ??= new vassago.Models.User() { Accounts = [acc] }; if (inChannel.Users?.Count > 0) { Console.WriteLine($"channel has {inChannel.Users.Count} accounts"); } Rememberer.RememberAccount(acc); inChannel.Users ??= []; if (!inChannel.Users.Contains(acc)) { inChannel.Users.Add(acc); Rememberer.RememberChannel(inChannel); } return acc; } private Channel UpsertChannel(string channelName) { Channel c = Rememberer.SearchChannel(ci => ci.ExternalId == channelName && ci.Protocol == PROTOCOL); if (c == null) { Console.WriteLine($"couldn't find channel under protocol {PROTOCOL} with externalId {channelName}"); c = new Channel() { Users = [] }; } c.DisplayName = channelName; c.ExternalId = channelName; c.ChannelType = vassago.Models.Enumerations.ChannelType.Normal; c.Messages ??= []; c.Protocol = PROTOCOL; c.ParentChannel = protocolAsChannel; c.SubChannels = c.SubChannels ?? new List(); c.SendMessage = (t) => { return Task.Run(() => { client.SendMessage(channelName, t); }); }; c.SendFile = (f, t) => { throw new InvalidOperationException($"twitch cannot send files"); }; c = Rememberer.RememberChannel(c); var selfAccountInChannel = c.Users?.FirstOrDefault(a => a.ExternalId == selfAccountInProtocol.ExternalId); if(selfAccountInChannel == null) { selfAccountInChannel = UpsertAccount(selfAccountInProtocol.Username, c); } return c; } private Channel UpsertDMChannel(string whisperWith) { Channel c = Rememberer.SearchChannel(ci => ci.ExternalId == $"w_{whisperWith}" && ci.Protocol == PROTOCOL); if (c == null) { Console.WriteLine($"couldn't find channel under protocol {PROTOCOL}, whisper with {whisperWith}"); c = new Channel() { Users = [] }; } c.DisplayName = $"Whisper: {whisperWith}"; c.ExternalId = $"w_{whisperWith}"; c.ChannelType = vassago.Models.Enumerations.ChannelType.DM; c.Messages ??= []; c.Protocol = PROTOCOL; c.ParentChannel = protocolAsChannel; c.SubChannels = c.SubChannels ?? new List(); c.SendMessage = (t) => { return Task.Run(() => { try { client.SendWhisper(whisperWith, t); } catch(Exception e) { Console.Error.WriteLine(e); } }); }; c.SendFile = (f, t) => { throw new InvalidOperationException($"twitch cannot send files"); }; c = Rememberer.RememberChannel(c); var selfAccountInChannel = c.Users.FirstOrDefault(a => a.ExternalId == selfAccountInProtocol.ExternalId); if(selfAccountInChannel == null) { selfAccountInChannel = UpsertAccount(selfAccountInChannel.Username, c); } return c; } //n.b., I see you future adam. "we should unify these, they're redundant". //ah, but that's the trick, they aren't! twitchlib has a common base class, but //none of the features we care about are on it! private Message UpsertMessage(ChatMessage chatMessage) { var m = Rememberer.SearchMessage(mi => mi.ExternalId == chatMessage.Id && mi.Protocol == PROTOCOL) ?? new() { Protocol = PROTOCOL, Timestamp = (DateTimeOffset)DateTime.SpecifyKind(DateTime.UtcNow, DateTimeKind.Utc) }; m.Content = chatMessage.Message; m.ExternalId = chatMessage.Id; m.Channel = UpsertChannel(chatMessage.Channel); m.Author = UpsertAccount(chatMessage.Username, m.Channel); m.MentionsMe = Regex.IsMatch(m.Content?.ToLower(), $"@\\b{selfAccountInProtocol.Username.ToLower()}\\b"); m.Reply = (t) => { return Task.Run(() => { client.SendReply(chatMessage.Channel, chatMessage.Id, t); }); }; m.React = (e) => { throw new InvalidOperationException($"twitch cannot react"); }; Rememberer.RememberMessage(m); return m; } //n.b., I see you future adam. "we should unify these, they're redundant". //ah, but that's the trick, they aren't! twitchlib has a common base class, but //none of the features we care about are on it! private Message UpsertMessage(WhisperMessage whisperMessage) { //WhisperMessage.Id corresponds to chatMessage.Id. \*eye twitch* var m = Rememberer.SearchMessage(mi => mi.ExternalId == whisperMessage.MessageId && mi.Protocol == PROTOCOL) ?? new() { Protocol = PROTOCOL, Timestamp = (DateTimeOffset)DateTime.SpecifyKind(DateTime.UtcNow, DateTimeKind.Utc) }; m.Content = whisperMessage.Message; m.ExternalId = whisperMessage.MessageId; m.Channel = UpsertDMChannel(whisperMessage.Username); m.Author = UpsertAccount(whisperMessage.Username, m.Channel); m.MentionsMe = Regex.IsMatch(m.Content?.ToLower(), $"@\\b{selfAccountInProtocol.Username.ToLower()}\\b"); m.Reply = (t) => { return Task.Run(() => { client.SendWhisper(whisperMessage.Username, t); }); }; m.React = (e) => { throw new InvalidOperationException($"twitch cannot react"); }; Rememberer.RememberMessage(m); return m; } public string AttemptJoin(string channelTarget) { client.JoinChannel(channelTarget); return $"attempt join {channelTarget} - o7"; } internal void AttemptLeave(string channelTarget) { client.SendMessage(channelTarget, "o7"); client.LeaveChannel(channelTarget); } }