connects to twitch. twitchsummon happens.

I think twitchunsummon doesn't?
This commit is contained in:
adam 2025-04-19 11:11:39 -04:00
parent 5ff601a60c
commit 1d70074d71
5 changed files with 224 additions and 137 deletions

View File

@ -24,7 +24,7 @@ public class Message
public Channel Channel { get; set; }
//TODO: these are nicities to make it OOP, but it couples them with their respective platform interfaces (and connections!)
[NonSerialized]
public Func<string, Task> Reply;

View File

@ -15,16 +15,22 @@ using System.Reactive.Linq;
namespace vassago.ProtocolInterfaces.DiscordInterface;
//data received
//translate data to internal type
//store
//ship off to behaver
public class DiscordInterface
{
internal const string PROTOCOL = "discord";
internal static string PROTOCOL { get => "discord"; }
internal DiscordSocketClient client;
private bool eventsSignedUp = false;
private static readonly SemaphoreSlim discordChannelSetup = new(1, 1);
private Channel protocolAsChannel;
public async Task Init(string token)
public async Task Init(string config)
{
var token = config;
await SetupDiscordChannel();
client = new DiscordSocketClient(new DiscordSocketConfig() { GatewayIntents = GatewayIntents.All });
@ -147,6 +153,7 @@ public class DiscordInterface
}
await Behaver.Instance.ActOn(m);
m.ActedOn = true; // for its own ruposess it might act on it later, but either way, fuck it, we checked.
// ...but we don't save?
}
private Task UserJoined(SocketGuildUser arg)
@ -312,7 +319,8 @@ public class DiscordInterface
c = Rememberer.RememberChannel(c);
var selfAccountInChannel = c.Users.FirstOrDefault(a => a.ExternalId == client.CurrentUser.Id.ToString());
//Console.WriteLine($"no one knows how to make good tooling. c.users.first, which needs client currentuser id tostring. c: {c}, c.Users {c.Users}, client: {client}, client.CurrentUser: {client.CurrentUser}, client.currentUser.Id: {client.CurrentUser.Id}");
var selfAccountInChannel = c.Users?.FirstOrDefault(a => a.ExternalId == client.CurrentUser.Id.ToString());
if(selfAccountInChannel == null)
{
selfAccountInChannel = UpsertAccount(client.CurrentUser, c);

View File

@ -1,11 +1,11 @@
using RestSharp;
using System.Security.Cryptography.X509Certificates;
using System.Text.RegularExpressions;
using RestSharp;
using TwitchLib.Api;
using TwitchLib.Api.Helix.Models.Users.GetUsers;
using TwitchLib.Client;
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;
@ -13,27 +13,26 @@ using vassago.Models;
namespace vassago.TwitchInterface;
internal class unifiedTwitchMessage
{
public unifiedTwitchMessage(ChatMessage chatMessage){}
}
public class TwitchInterface
{
internal const string PROTOCOL = "twitch";
private bool eventsSignedUp = false;
private ChattingContext _db;
private static SemaphoreSlim twitchChannelSetup = new SemaphoreSlim(1, 1);
private static SemaphoreSlim channelSetupSemaphpore = new SemaphoreSlim(1, 1);
private Channel protocolAsChannel;
private Account selfAccountInProtocol;
TwitchClient client;
TwitchAPI api;
public TwitchInterface()
{
_db = new ChattingContext();
}
private async Task SetupTwitchChannel()
{
await twitchChannelSetup.WaitAsync();
await channelSetupSemaphpore.WaitAsync();
try
{
protocolAsChannel = _db.Channels.FirstOrDefault(c => c.ParentChannel == null && c.Protocol == PROTOCOL);
protocolAsChannel = Rememberer.SearchChannel(c => c.ParentChannel == null && c.Protocol == PROTOCOL);
if (protocolAsChannel == null)
{
protocolAsChannel = new Channel()
@ -47,17 +46,23 @@ public class TwitchInterface
ReactionsPossible = false,
ExternalId = null,
Protocol = PROTOCOL,
SubChannels = new List<Channel>()
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"); };
_db.Channels.Add(protocolAsChannel);
_db.SaveChanges();
protocolAsChannel = Rememberer.RememberChannel(protocolAsChannel);
Console.WriteLine($"protocol as channle added; {protocolAsChannel}");
}
else
{
Console.WriteLine($"twitch, channel with id {protocolAsChannel.Id}, already exists");
}
//protocolAsChan
}
finally
{
twitchChannelSetup.Release();
channelSetupSemaphpore.Release();
}
}
@ -81,69 +86,46 @@ public class TwitchInterface
client.OnWhisperReceived += Client_OnWhisperReceivedAsync;
client.OnConnected += Client_OnConnected;
Console.WriteLine("twitch client 1 connecting...");
client.Connect();
Console.WriteLine("twitch client 1 connected");
// Console.WriteLine("twitch API client connecting...");
// api = new TwitchAPI();
// Console.WriteLine("can I just use the same creds as the other client?");
// api.Settings.ClientId = tc.username;
// api.Settings.AccessToken = tc.oauth;
// try{
// var neckbreads = await api.Helix.Moderation.GetModeratorsAsync("silvermeddlists");
// Console.WriteLine($"{neckbreads?.Data?.Count()} shabby beards that need to be given up on");
// }
// catch(Exception e){
// Console.Error.WriteLine(e);
// }
// Console.WriteLine("k.");
}
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}");
var old = _db.Messages.FirstOrDefault(m => m.ExternalId == e.WhisperMessage.MessageId && m.Protocol == PROTOCOL);
if (old != null)
{
Console.WriteLine($"[whisperreceived]: {e.WhisperMessage.MessageId}? already seent it. Internal id: {old.Id}");
return;
}
var m = UpsertMessage(e.WhisperMessage);
m.Channel.ChannelType = vassago.Models.Enumerations.ChannelType.DM;
m.MentionsMe = Regex.IsMatch(e.WhisperMessage.Message?.ToLower(), $"\\b@{e.WhisperMessage.BotUsername.ToLower()}\\b");
await _db.SaveChangesAsync();
//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);
await _db.SaveChangesAsync();
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}");
var old = _db.Messages.FirstOrDefault(m => m.ExternalId == e.ChatMessage.Id && m.Protocol == PROTOCOL);
if (old != null)
{
Console.WriteLine($"[messagereceived]: {e.ChatMessage.Id}? already seent it");
return;
}
Console.WriteLine($"[messagereceived]: {e.ChatMessage.Id}? new to me.");
//translate to internal, upsert
var m = UpsertMessage(e.ChatMessage);
m.MentionsMe = Regex.IsMatch(e.ChatMessage.Message?.ToLower(), $"@{e.ChatMessage.BotUsername.ToLower()}\\b") ||
e.ChatMessage.ChatReply?.ParentUserLogin == e.ChatMessage.BotUsername;
await _db.SaveChangesAsync();
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);
await _db.SaveChangesAsync();
m.ActedOn = true;
//TODO: remember again?
}
private async void Client_OnConnected(object sender, OnConnectedArgs e)
private void Client_OnConnected(object sender, OnConnectedArgs e)
{
Console.WriteLine($"twitch marking selfaccount as seeninchannel {protocolAsChannel.Id}");
var selfAccount = UpsertAccount(e.BotUsername, protocolAsChannel.Id);
Behaver.Instance.MarkSelf(selfAccount);
await _db.SaveChangesAsync();
selfAccountInProtocol = UpsertAccount(e.BotUsername, protocolAsChannel);
selfAccountInProtocol.DisplayName = e.BotUsername;
Behaver.Instance.MarkSelf(selfAccountInProtocol);
Console.WriteLine($"Connected to {e.AutoJoinChannel}");
}
@ -158,61 +140,95 @@ public class TwitchInterface
Console.WriteLine($"{e.DateTime.ToString()}: {e.BotUsername} - {e.Data}");
}
private Account UpsertAccount(string username, Guid inChannel)
private Account UpsertAccount(string username, Channel inChannel)
{
var seenInChannel = _db.Channels.FirstOrDefault(c => c.Id == inChannel);
var acc = _db.Accounts.FirstOrDefault(ui => ui.ExternalId == username && ui.SeenInChannel.Id == inChannel);
if (acc == null)
var acc = Rememberer.SearchAccount(ui => ui.ExternalId == username && ui.SeenInChannel.ExternalId == inChannel.ToString());
Console.WriteLine($"upserting account, retrieved {acc?.Id}.");
if (acc != null)
{
acc = new Account();
acc.SeenInChannel = seenInChannel;
_db.Accounts.Add(acc);
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 =
//acc.IsBot = false? there is a way to tell, but you have to go back through the API
acc.Protocol = PROTOCOL;
acc.SeenInChannel = inChannel;
acc.IsUser = _db.Users.FirstOrDefault(u => u.Accounts.Any(a => a.ExternalId == acc.ExternalId && a.Protocol == acc.Protocol));
if (acc.IsUser == null)
Console.WriteLine($"we asked rememberer to search for acc's user. {acc.IsUser?.Id}");
if (acc.IsUser != null)
{
acc.IsUser = new vassago.Models.User() { Accounts = new List<Account>() { acc } };
_db.Users.Add(acc.IsUser);
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 = _db.Channels.FirstOrDefault(ci => ci.ExternalId == channelName && ci.Protocol == PROTOCOL);
Channel c = Rememberer.SearchChannel(ci => ci.ExternalId == channelName
&& ci.Protocol == PROTOCOL);
if (c == null)
{
c = new Channel();
_db.Channels.Add(c);
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.Messages ?? new List<Message>();
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.ToString());
if(selfAccountInChannel == null)
{
selfAccountInChannel = UpsertAccount(selfAccountInProtocol.Username, c);
}
return c;
}
private Channel UpsertDMChannel(string whisperWith)
{
Channel c = _db.Channels.FirstOrDefault(ci => ci.ExternalId == $"w_{whisperWith}" && ci.Protocol == PROTOCOL);
Channel c = Rememberer.SearchChannel(ci => ci.ExternalId == $"w_{whisperWith}"
&& ci.Protocol == PROTOCOL);
if (c == null)
{
c = new Channel();
_db.Channels.Add(c);
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.Messages ?? new List<Message>();
c.Messages ??= [];
c.Protocol = PROTOCOL;
c.ParentChannel = protocolAsChannel;
c.SubChannels = c.SubChannels ?? new List<Channel>();
@ -229,50 +245,59 @@ public class TwitchInterface
});
};
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.ToString());
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 = _db.Messages.FirstOrDefault(mi => mi.ExternalId == chatMessage.Id);
if (m == null)
var m = Rememberer.SearchMessage(mi => mi.ExternalId == chatMessage.Id && mi.Protocol == PROTOCOL)
?? new()
{
m = new Message();
m.Protocol = PROTOCOL;
_db.Messages.Add(m);
m.Timestamp = (DateTimeOffset)DateTime.SpecifyKind(DateTime.UtcNow, DateTimeKind.Utc);
}
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.Id);
m.Author.SeenInChannel = m.Channel;
m.Author = UpsertAccount(chatMessage.Username, m.Channel); //TODO: m.channel, instead, for consistency
m.Author.SeenInChannel = m.Channel;//TODO: should be handled in UpsertAccount
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)
{
var m = _db.Messages.FirstOrDefault(mi => mi.ExternalId == whisperMessage.MessageId);
if (m == null)
//WhisperMessage.Id corresponds to chatMessage.Id. \*eye twitch*
var m = Rememberer.SearchMessage(mi => mi.ExternalId == whisperMessage.MessageId && mi.Protocol == PROTOCOL)
?? new()
{
m = new Message();
m.Protocol = PROTOCOL;
_db.Messages.Add(m);
m.Timestamp = (DateTimeOffset)DateTime.SpecifyKind(DateTime.UtcNow, DateTimeKind.Utc);
}
Protocol = PROTOCOL,
Timestamp = (DateTimeOffset)DateTime.SpecifyKind(DateTime.UtcNow, DateTimeKind.Utc)
};
m.Content = whisperMessage.Message;
m.ExternalId = whisperMessage.MessageId;
m.Channel = UpsertDMChannel(whisperMessage.Username);
m.Channel.ChannelType = vassago.Models.Enumerations.ChannelType.DM;
m.Author = UpsertAccount(whisperMessage.Username, m.Channel.Id);
m.Author.SeenInChannel = m.Channel;
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;
}

View File

@ -6,105 +6,160 @@ using Microsoft.EntityFrameworkCore;
public static class Rememberer
{
private static readonly SemaphoreSlim dbAccessSemaphore = new(1, 1);
private static readonly ChattingContext db = new();
public static Account SearchAccount(Expression<Func<Account, bool>> predicate)
{
return db.Accounts.Include(a => a.IsUser).FirstOrDefault(predicate);
Account toReturn;
dbAccessSemaphore.Wait();
toReturn = db.Accounts.Include(a => a.IsUser).FirstOrDefault(predicate);
dbAccessSemaphore.Release();
return toReturn;
}
public static List<Account> SearchAccounts(Expression<Func<Account, bool>> predicate)
{
return db.Accounts.Where(predicate).ToList();
List<Account> toReturn;
dbAccessSemaphore.Wait();
toReturn = db.Accounts.Where(predicate).ToList();
dbAccessSemaphore.Release();
return toReturn;
}
public static Attachment SearchAttachment(Expression<Func<Attachment, bool>> predicate)
{
return db.Attachments.FirstOrDefault(predicate);
Attachment toReturn;
dbAccessSemaphore.Wait();
toReturn = db.Attachments.FirstOrDefault(predicate);
dbAccessSemaphore.Release();
return toReturn;
}
public static Channel SearchChannel(Expression<Func<Channel, bool>> predicate)
{
return db.Channels.FirstOrDefault(predicate);
Channel toReturn;
dbAccessSemaphore.Wait();
toReturn = db.Channels.FirstOrDefault(predicate);
dbAccessSemaphore.Release();
return toReturn;
}
public static Message SearchMessage(Expression<Func<Message, bool>> predicate)
{
return db.Messages.FirstOrDefault(predicate);
Message toReturn;
dbAccessSemaphore.Wait();
toReturn = db.Messages.FirstOrDefault(predicate);
dbAccessSemaphore.Release();
return toReturn;
}
public static User SearchUser(Expression<Func<User, bool>> predicate)
{
return db.Users.Include(u => u.Accounts).FirstOrDefault(predicate);
User toReturn;
dbAccessSemaphore.Wait();
toReturn = db.Users.Where(predicate).Include(u => u.Accounts).FirstOrDefault(predicate);
dbAccessSemaphore.Release();
return toReturn;
}
public static void RememberAccount(Account toRemember)
{
toRemember.IsUser ??= new User{ Accounts = [toRemember]};
dbAccessSemaphore.Wait();
toRemember.IsUser ??= new User { Accounts = [toRemember] };
db.Update(toRemember.IsUser);
db.SaveChanges();
dbAccessSemaphore.Release();
}
public static void RememberAttachment(Attachment toRemember)
{
toRemember.Message ??= new Message() { Attachments = [toRemember]};
dbAccessSemaphore.Wait();
toRemember.Message ??= new Message() { Attachments = [toRemember] };
db.Update(toRemember.Message);
db.SaveChanges();
dbAccessSemaphore.Release();
}
public static Channel RememberChannel(Channel toRemember)
{
dbAccessSemaphore.Wait();
db.Update(toRemember);
db.SaveChanges();
dbAccessSemaphore.Release();
return toRemember;
}
public static void RememberMessage(Message toRemember)
{
toRemember.Channel ??= new (){ Messages = [toRemember] };
dbAccessSemaphore.Wait();
toRemember.Channel ??= new() { Messages = [toRemember] };
db.Update(toRemember.Channel);
db.SaveChanges();
dbAccessSemaphore.Release();
}
public static void RememberUser(User toRemember)
{
dbAccessSemaphore.Wait();
db.Users.Update(toRemember);
db.SaveChanges();
dbAccessSemaphore.Release();
}
public static void ForgetAccount(Account toForget)
{
var user = toForget.IsUser;
var usersOnlyAccount = user.Accounts?.Count == 1;
if(usersOnlyAccount)
if (usersOnlyAccount)
{
Rememberer.ForgetUser(user);
}
else
{
dbAccessSemaphore.Wait();
db.Accounts.Remove(toForget);
db.SaveChanges();
dbAccessSemaphore.Release();
}
}
public static void ForgetChannel(Channel toForget)
{
dbAccessSemaphore.Wait();
db.Channels.Remove(toForget);
db.SaveChanges();
dbAccessSemaphore.Release();
}
public static void ForgetUser(User toForget)
{
dbAccessSemaphore.Wait();
db.Users.Remove(toForget);
db.SaveChanges();
dbAccessSemaphore.Release();
}
public static List<Account> AccountsOverview()
{
return [..db.Accounts];
List<Account> toReturn;
dbAccessSemaphore.Wait();
toReturn = [.. db.Accounts];
dbAccessSemaphore.Release();
return toReturn;
}
public static List<Channel> ChannelsOverview()
{
return [..db.Channels.Include(u => u.SubChannels).Include(c => c.ParentChannel)];
List<Channel> toReturn;
dbAccessSemaphore.Wait();
toReturn = [.. db.Channels.Include(u => u.SubChannels).Include(c => c.ParentChannel)];
dbAccessSemaphore.Release();
return toReturn;
}
public static Channel ChannelDetail(Guid Id)
{
return db.Channels.Find(Id);
Channel toReturn;
dbAccessSemaphore.Wait();
toReturn = db.Channels.Find(Id);
dbAccessSemaphore.Release();
return toReturn;
// .Include(u => u.SubChannels)
// .Include(u => u.Users)
// .Include(u => u.ParentChannel);
}
public static List<User> UsersOverview()
{
return db.Users.ToList();
List<User> toReturn;
dbAccessSemaphore.Wait();
toReturn = db.Users.ToList();
dbAccessSemaphore.Release();
return toReturn;
}
}

View File

@ -2,7 +2,6 @@ using System.Diagnostics;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using vassago.Models;
using vassago.ProtocolInterfaces.DiscordInterface;
namespace vassago.Controllers.api;