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);
}
}