forked from adam/discord-bot-shtik
All checks were successful
gitea.arg.rip/vassago/pipeline/head This commit looks good
🎉
316 lines
12 KiB
C#
316 lines
12 KiB
C#
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();
|
|
}
|
|
}
|
|
|
|
///<param name="oauth">https://www.twitchapps.com/tmi/</param>
|
|
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<Channel>();
|
|
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<Channel>();
|
|
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);
|
|
}
|
|
}
|