Compare commits

..

30 Commits
1.0 ... master

Author SHA1 Message Date
ab16600463 inerited channel stats works better
Some checks failed
gitea/vassago/pipeline/head There was a failure building this commit
gitea.arg.rip/vassago/pipeline/head There was a failure building this commit
before it would include 1 level, then assume that's the top. so 2 degrees of inheritance would confuse it
2024-06-29 16:45:07 -04:00
af4d68caa1 ca2254 is moronic. maybe if it wasn't filed under "code quality" and instead was filed under "you didn't include a workaround for the weaknesses of other external junk" i'd be kinder to it ;) 2024-06-23 20:35:00 -04:00
0ac28c35fb the api to commit web interface changes to a channel works 2024-06-23 20:31:09 -04:00
942b11fcce we have a dropdown that fires an onchange to post the new value.
although, errors ensue
2024-06-16 18:10:49 -04:00
4bd51721b6 more details, more links, fixed a bug where it wouldn't give itself a seen-in-channel 2024-06-10 16:24:30 -04:00
e364b47c0f details pages for channels added 2024-06-09 17:37:09 -04:00
c6ea3ef790 integrate swagger 2024-06-02 18:37:04 -04:00
2f7bc2c0ea get to channel details 2024-06-02 17:13:15 -04:00
43eaa5ad0d users in treeview 2024-05-26 20:00:46 -04:00
b4b5544ec4 channels and accounts organized into a treeview
even if every account is orphaned, this isn't how it should be
2024-05-26 19:43:17 -04:00
c446521977 fixed a bug where we'd immediately crash on any message 2024-05-10 17:07:50 -04:00
54414b8748 db migration: no featurepermissions, and channel permissions embedded in channel 2024-05-10 16:41:00 -04:00
8cb4005192 update links to guy who forked it 2024-05-10 10:54:45 -04:00
cd2fa384d5 bootstrap, fontawesome, and bootstrap treeview 2024-05-05 16:55:00 -04:00
f4bed1e6cb contain web interface stuff 2024-04-08 15:00:55 -04:00
88ca468708 get rid of feature permissions
Some checks failed
gitea/vassago/pipeline/head There was a failure building this commit
2024-04-06 00:01:31 -04:00
464b6a90e4 channel permissions are just part of channel 2024-04-05 23:59:39 -04:00
581fddf6f9 t h e p l a n 2024-04-05 23:08:20 -04:00
bed8d3cbef move web interface around 2024-04-05 23:07:48 -04:00
2dd9e903db once i figure out jenkins secrets, this'll be useful 2024-04-05 23:02:15 -04:00
ef31418166 organize - behaver is not a behavior, only one use of connection string 2024-04-05 22:18:45 -04:00
6d181e2b68 mitigates #33
All checks were successful
gitea/vassago/pipeline/head This commit looks good
the actual problem though; it should realize thats itself
2024-01-26 17:55:09 -05:00
10167b597a disable twitchsummon
All checks were successful
gitea/vassago/pipeline/head This commit looks good
2024-01-24 21:16:56 -05:00
26d8373dc8 i don't know how I didn't trip over this one a million times before
read the help for MigrateAsync - "ensure created" does something different, and worse, which prevents future migrations!
2024-01-24 20:32:49 -05:00
a63a3fcb58 "just google it" general snark disabled
All checks were successful
gitea/vassago/pipeline/head This commit looks good
"temporarily"
2024-01-23 14:12:52 -05:00
b84e47344b db in the right place; fix QR code math
All checks were successful
gitea/vassago/pipeline/head This commit looks good
2024-01-10 21:21:31 -05:00
c5f9ae2c6b collapse users - extracted for LinkMe and my own self-registration
All checks were successful
gitea/vassago/pipeline/head This commit looks good
2024-01-05 22:12:57 -05:00
efb4ab00d2 IsSelf fix for Definitely snarkiness
notes to self. 1) trust in upsert. an account has an external ID, a channel has an external ID w.r.t. its protocol. 2) as long as you can collapse a User, collapse Self.
2024-01-05 21:39:31 -05:00
451ace753d how about don't clear out the aliases? now I can configure more aliases
All checks were successful
gitea/vassago/pipeline/head This commit looks good
2023-12-05 23:57:21 -05:00
894b536c04 smoot is defined as 5ft 7, don't use cm to approximate 2023-12-05 23:44:40 -05:00
67 changed files with 1404 additions and 519 deletions

124
Behaver.cs Normal file
View File

@ -0,0 +1,124 @@
namespace vassago;
#pragma warning disable 4014 //the "not awaited" error
using vassago.Behavior;
using vassago.Models;
using System;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Collections.Generic;
public class Behaver
{
private List<Account> SelfAccounts { get; set; } = new List<Account>();
private User SelfUser { get; set; }
public static List<vassago.Behavior.Behavior> Behaviors { get; private set; } = new List<vassago.Behavior.Behavior>();
internal Behaver()
{
var subtypes = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(domainAssembly => domainAssembly.GetTypes())
.Where(type => type.IsSubclassOf(typeof(vassago.Behavior.Behavior)) && !type.IsAbstract &&
type.GetCustomAttributes(typeof(StaticPlzAttribute),false)?.Any() == true)
.ToList();
foreach (var subtype in subtypes)
{
Behaviors.Add((vassago.Behavior.Behavior)Activator.CreateInstance(subtype));
}
}
static Behaver() { }
private static readonly Behaver _instance = new Behaver();
public static Behaver Instance
{
get { return _instance; }
}
public async Task<bool> ActOn(Message message)
{
foreach (var behavior in Behaviors)
{
if (behavior.ShouldAct(message))
{
behavior.ActOn(message);
message.ActedOn = true;
Console.WriteLine("acted on, moving forward");
}
}
if (message.ActedOn == false && message.MentionsMe && message.Content.Contains('?') && !Behaver.Instance.SelfAccounts.Any(acc => acc.Id == message.Author.Id))
{
Console.WriteLine("providing bullshit nonanswer / admitting uselessness");
var responses = new List<string>(){
@"Well, that's a great question, and there are certainly many different possible answers. Ultimately, the decision will depend on a variety of factors, including your personal interests and goals, as well as any practical considerations (like the economy). I encourage you to do your research, speak with experts and educators, and explore your options before making a decision that's right for you.",
@"┐(゚ ~゚ )┌", @"¯\_(ツ)_/¯", @"╮ (. ❛ ᴗ ❛.) ╭", @"╮(╯ _╰ )╭"
};
await message.Channel.SendMessage(responses[Shared.r.Next(responses.Count)]);
message.ActedOn = true;
}
return message.ActedOn;
}
internal bool IsSelf(Guid AccountId)
{
var db = new ChattingContext();
var acc = db.Accounts.Find(AccountId);
return SelfAccounts.Any(acc => acc.Id == AccountId);
}
public void MarkSelf(Account selfAccount)
{
var db = new ChattingContext();
if(SelfUser == null)
{
SelfUser = selfAccount.IsUser;
}
else if (SelfUser != selfAccount.IsUser)
{
CollapseUsers(SelfUser, selfAccount.IsUser, db);
}
SelfAccounts = db.Accounts.Where(a => a.IsUser == SelfUser).ToList();
}
public bool CollapseUsers(User primary, User secondary, ChattingContext db)
{
Console.WriteLine($"{secondary.Id} is being consumed into {primary.Id}");
primary.Accounts.AddRange(secondary.Accounts);
foreach(var a in secondary.Accounts)
{
a.IsUser = primary;
}
secondary.Accounts.Clear();
Console.WriteLine("accounts transferred");
try
{
db.SaveChangesAsync().Wait();
}
catch(Exception e)
{
Console.WriteLine("First save exception.");
Console.Error.WriteLine(e);
return false;
}
Console.WriteLine("saved");
db.Users.Remove(secondary);
Console.WriteLine("old account cleaned up");
try
{
db.SaveChangesAsync().Wait();
}
catch(Exception e)
{
Console.WriteLine("Second save exception.");
Console.Error.WriteLine(e);
return false;
}
Console.WriteLine("saved, again, separately");
return true;
}
}
#pragma warning restore 4014 //the "async not awaited" error

View File

@ -1,64 +0,0 @@
namespace vassago.Behavior;
#pragma warning disable 4014 //the "not awaited" error
using vassago.Models;
using System;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Collections.Generic;
public class Behaver
{
private ChattingContext _db;
public List<Account> Selves { get; internal set; } = new List<Account>();
public static List<Behavior> Behaviors { get; private set; } = new List<Behavior>();
internal Behaver()
{
_db = new ChattingContext();
var subtypes = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(domainAssembly => domainAssembly.GetTypes())
.Where(type => type.IsSubclassOf(typeof(Behavior)) && !type.IsAbstract &&
type.GetCustomAttributes(typeof(StaticPlzAttribute),false)?.Any() == true)
.ToList();
foreach (var subtype in subtypes)
{
Behaviors.Add((Behavior)Activator.CreateInstance(subtype));
}
}
static Behaver() { }
private static readonly Behaver _instance = new Behaver();
public static Behaver Instance
{
get { return _instance; }
}
public async Task<bool> ActOn(Message message)
{
foreach (var behavior in Behaviors)
{
if (behavior.ShouldAct(message))
{
behavior.ActOn(message);
message.ActedOn = true;
Console.WriteLine("acted on, moving forward");
}
}
if (message.ActedOn == false && message.MentionsMe && message.Content.Contains('?') && !Behaver.Instance.Selves.Any(acc => acc.Id == message.Author.Id))
{
Console.WriteLine("providing bullshit nonanswer / admitting uselessness");
var responses = new List<string>(){
@"Well, that's a great question, and there are certainly many different possible answers. Ultimately, the decision will depend on a variety of factors, including your personal interests and goals, as well as any practical considerations (like the economy). I encourage you to do your research, speak with experts and educators, and explore your options before making a decision that's right for you.",
@"┐(゚ ~゚ )┌", @"¯\_(ツ)_/¯", @"╮ (. ❛ ᴗ ❛.) ╭", @"╮(╯ _╰ )╭"
};
await message.Channel.SendMessage(responses[Shared.r.Next(responses.Count)]);
message.ActedOn = true;
}
return message.ActedOn;
}
}
#pragma warning restore 4014 //the "async not awaited" error

View File

@ -14,7 +14,7 @@ public abstract class Behavior
public virtual bool ShouldAct(Message message) public virtual bool ShouldAct(Message message)
{ {
if(Behaver.Instance.Selves.Any(acc => acc.Id == message.Author.Id)) if(Behaver.Instance.IsSelf(message.Author.Id))
return false; return false;
return Regex.IsMatch(message.Content, $"{Trigger}\\b", RegexOptions.IgnoreCase); return Regex.IsMatch(message.Content, $"{Trigger}\\b", RegexOptions.IgnoreCase);
} }

View File

@ -26,10 +26,11 @@ public class Detiktokify : Behavior
} }
public override bool ShouldAct(Message message) public override bool ShouldAct(Message message)
{ {
if(message.Channel.EffectivePermissions.MaxAttachmentBytes == 0)
if(Behaver.Instance.IsSelf(message.Author.Id))
return false; return false;
if(Behaver.Instance.Selves.Any(acc => acc.Id == message.Author.Id)) if(message.Channel.EffectivePermissions.MaxAttachmentBytes == 0)
return false; return false;
var wordLikes = message.Content.Split(' ', StringSplitOptions.TrimEntries); var wordLikes = message.Content.Split(' ', StringSplitOptions.TrimEntries);
@ -66,7 +67,7 @@ public class Detiktokify : Behavior
{ {
Console.Error.WriteLine("tried to dl, failed. \n" + string.Join('\n', res.ErrorOutput)); Console.Error.WriteLine("tried to dl, failed. \n" + string.Join('\n', res.ErrorOutput));
await message.React("problemon"); await message.React("problemon");
await message.Channel.SendMessage("tried to dl, failed. \n" + string.Join('\n', res.ErrorOutput)); await message.Channel.SendMessage("tried to dl, failed. \n");
} }
else else
{ {

View File

@ -21,8 +21,9 @@ public class FiximageHeic : Behavior
private List<Attachment> heics = new List<Attachment>(); private List<Attachment> heics = new List<Attachment>();
public override bool ShouldAct(Message message) public override bool ShouldAct(Message message)
{ {
if(Behaver.Instance.Selves.Any(acc => acc.Id == message.Author.Id)) if(Behaver.Instance.IsSelf(message.Author.Id))
return false; return false;
if (message.Attachments?.Count() > 0) if (message.Attachments?.Count() > 0)
{ {
foreach (var att in message.Attachments) foreach (var att in message.Attachments)

View File

@ -19,7 +19,7 @@ public class GeneralSnarkCloudNative : Behavior
public override string Trigger => "certain tech buzzwords that no human uses in normal conversation"; public override string Trigger => "certain tech buzzwords that no human uses in normal conversation";
public override bool ShouldAct(Message message) public override bool ShouldAct(Message message)
{ {
if(Behaver.Instance.Selves.Any(acc => acc.Id == message.Author.Id)) if(Behaver.Instance.IsSelf(message.Author.Id))
return false; return false;
if(message.Channel.EffectivePermissions.ReactionsPossible) if(message.Channel.EffectivePermissions.ReactionsPossible)

View File

@ -20,10 +20,15 @@ public class GeneralSnarkGooglit : Behavior
public override bool ShouldAct(Message message) public override bool ShouldAct(Message message)
{ {
if(Behaver.Instance.Selves.Any(acc => acc.Id == message.Author.Id))
return false; return false;
return Regex.IsMatch(message.Content, $"(just )?google( (it|that|things|before))?\\b", RegexOptions.IgnoreCase);
} }
// public override bool ShouldAct(Message message)
// {
// if(Behaver.Instance.IsSelf(message.Author.Id))
// return false;
// return Regex.IsMatch(message.Content, $"(just )?google( (it|that|things|before))?\\b", RegexOptions.IgnoreCase);
// }
public override async Task<bool> ActOn(Message message) public override async Task<bool> ActOn(Message message)
{ {

View File

@ -0,0 +1,62 @@
namespace vassago.Behavior;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using vassago.Models;
using static vassago.Models.Enumerations;
[StaticPlz]
public class GeneralSnarkMisspellDefinitely : Behavior
{
public override string Name => "Snarkiness: misspell definitely";
public override string Trigger => "definitely but not";
public override string Description => "https://xkcd.com/2871/";
private Dictionary<string, string> snarkmap = new Dictionary<string, string>()
{
{"definetly", "*almost* definitely"},
{"definately", "probably"},
{"definatly", "probably not"},
{"defenitely", "not telling (it's a surprise)"},
{"defintely", "per the propheecy"},
{"definetely", "definitely, maybe"},
{"definantly", "to be decided by coin toss"},
{"defanitely", "in one universe out of 14 million"},
{"defineatly", "only the gods know"},
{"definitly", "unless someone cute shows up"}
};
public override bool ShouldAct(Message message)
{
if(Behaver.Instance.IsSelf(message.Author.Id))
return false;
// if((MeannessFilterLevel)message.Channel.EffectivePermissions.MeannessFilterLevel < MeannessFilterLevel.Medium)
// return false;
foreach(var k in snarkmap.Keys)
{
if( Regex.IsMatch(message.Content, "\\b"+k+"\\b", RegexOptions.IgnoreCase))
return true;
}
return false;
}
public override async Task<bool> ActOn(Message message)
{
foreach(var k in snarkmap.Keys)
{
if( Regex.IsMatch(message.Content, "\\b"+k+"\\b", RegexOptions.IgnoreCase))
{
await message.Reply(k + "? so... " + snarkmap[k] + "?");
return true;
}
}
return true;
}
}

View File

@ -21,7 +21,7 @@ public class GeneralSnarkPlaying : Behavior
public override bool ShouldAct(Message message) public override bool ShouldAct(Message message)
{ {
if(Behaver.Instance.Selves.Any(acc => acc.Id == message.Author.Id)) if(Behaver.Instance.IsSelf(message.Author.Id))
return false; return false;
if((MeannessFilterLevel)message.Channel.EffectivePermissions.MeannessFilterLevel < MeannessFilterLevel.Medium || if((MeannessFilterLevel)message.Channel.EffectivePermissions.MeannessFilterLevel < MeannessFilterLevel.Medium ||

View File

@ -19,6 +19,10 @@ public class GeneralSnarkSkynet : Behavior
public override async Task<bool> ActOn(Message message) public override async Task<bool> ActOn(Message message)
{ {
if(Behaver.Instance.IsSelf(message.Author.Id))
return false;
switch (Shared.r.Next(5)) switch (Shared.r.Next(5))
{ {
default: default:

View File

@ -18,8 +18,9 @@ public class Gratitude : Behavior
public override bool ShouldAct(Message message) public override bool ShouldAct(Message message)
{ {
if(Behaver.Instance.Selves.Any(acc => acc.Id == message.Author.Id)) if(Behaver.Instance.IsSelf(message.Author.Id))
return false; return false;
return Regex.IsMatch(message.Content, "\\bthank (yo)?u\\b", RegexOptions.IgnoreCase) && message.MentionsMe; return Regex.IsMatch(message.Content, "\\bthank (yo)?u\\b", RegexOptions.IgnoreCase) && message.MentionsMe;
} }
public override async Task<bool> ActOn(Message message) public override async Task<bool> ActOn(Message message)

View File

@ -69,9 +69,12 @@ public class LaughAtOwnJoke : Behavior
} }
public override bool ShouldAct(Message message) public override bool ShouldAct(Message message)
{ {
if(Behaver.Instance.IsSelf(message.Author.Id))
return false;
Console.WriteLine($"{message.Content} == {_punchline}"); Console.WriteLine($"{message.Content} == {_punchline}");
return message.Content == _punchline return message.Content == _punchline
&& Behaver.Instance.Selves.Any(acc => acc.Id == message.Author.Id); && Behaver.Instance.IsSelf(message.Author.Id);
} }
public override async Task<bool> ActOn(Message message) public override async Task<bool> ActOn(Message message)

View File

@ -41,13 +41,11 @@ public class LinkClose : Behavior
public override string Description => "the second half of LinkMe - this is confirmation that you are the other one"; public override string Description => "the second half of LinkMe - this is confirmation that you are the other one";
private ChattingContext _db;
private string _pw; private string _pw;
private Account _primary; private Account _primary;
public LinkClose(string pw, Account primary) public LinkClose(string pw, Account primary)
{ {
_db = new ChattingContext();
_pw = pw; _pw = pw;
_primary = primary; _primary = primary;
} }
@ -59,6 +57,9 @@ public class LinkClose : Behavior
public override async Task<bool> ActOn(Message message) public override async Task<bool> ActOn(Message message)
{ {
if(Behaver.Instance.IsSelf(message.Author.Id))
return false;
var secondary = message.Author.IsUser; var secondary = message.Author.IsUser;
if(_primary.IsUser.Id == secondary.Id) if(_primary.IsUser.Id == secondary.Id)
{ {
@ -71,44 +72,14 @@ public class LinkClose : Behavior
return true; return true;
} }
Console.WriteLine($"{secondary.Id} is being consumed into {_primary.IsUser.Id}"); if(Behaver.Instance.CollapseUsers(_primary.IsUser, secondary, new ChattingContext()))
_primary.IsUser.Accounts.AddRange(secondary.Accounts);
foreach(var a in secondary.Accounts)
{ {
a.IsUser = _primary.IsUser;
}
secondary.Accounts.Clear();
Console.WriteLine("accounts transferred");
try
{
await _db.SaveChangesAsync();
}
catch(Exception e)
{
message.Channel.SendMessage("error in first save");
Console.WriteLine("fucks sake if I don't catch Exception it *mysteriously vanishes*");
Console.Error.WriteLine(e);
return false;
}
Console.WriteLine("saved");
_db.Users.Remove(secondary);
Console.WriteLine("old account cleaned up");
try
{
await _db.SaveChangesAsync();
}
catch(Exception e)
{
message.Channel.SendMessage("error in second save");
Console.WriteLine("fucks sake if I don't catch Exception it *mysteriously vanishes*");
Console.Error.WriteLine(e);
return false;
}
Console.WriteLine("saved, again, separately");
await message.Channel.SendMessage("done :)"); await message.Channel.SendMessage("done :)");
}
else
{
await message.Channel.SendMessage("failed :(");
}
return true; return true;
} }

View File

@ -15,12 +15,13 @@ public class PepTalk : Behavior
{ {
public override string Name => "PepTalk"; public override string Name => "PepTalk";
public override string Trigger => "i need (an? )?(peptalk|inspiration|ego-?boost)"; public override string Trigger => "\\bneeds? (an? )?(peptalk|inspiration|ego-?boost)";
public override string Description => "assembles a pep talk from a few pieces"; public override string Description => "assembles a pep talk from a few pieces";
public override async Task<bool> ActOn(Message message) public override async Task<bool> ActOn(Message message)
{var piece1 = new List<string>{ {
var piece1 = new List<string>{
"Champ, ", "Champ, ",
"Fact: ", "Fact: ",
"Everybody says ", "Everybody says ",

View File

@ -42,10 +42,10 @@ public class QRify : Behavior
File.WriteAllText($"tmp/qr{todaysnumber}.svg", qrCodeAsSvg); File.WriteAllText($"tmp/qr{todaysnumber}.svg", qrCodeAsSvg);
if (ExternalProcess.GoPlz("convert", $"tmp/qr{todaysnumber}.svg tmp/qr{todaysnumber}.png")) if (ExternalProcess.GoPlz("convert", $"tmp/qr{todaysnumber}.svg tmp/qr{todaysnumber}.png"))
{ {
if(message.Channel.EffectivePermissions.MaxAttachmentBytes < (ulong)(new System.IO.FileInfo($"tmp/qr{todaysnumber}.png").Length)) if(message.Channel.EffectivePermissions.MaxAttachmentBytes >= (ulong)(new System.IO.FileInfo($"tmp/qr{todaysnumber}.png").Length))
await message.Channel.SendFile($"tmp/qr{todaysnumber}.png", null); await message.Channel.SendFile($"tmp/qr{todaysnumber}.png", null);
else else
await message.Channel.SendMessage("resulting qr image 2 big 4 here"); await message.Channel.SendMessage($"resulting qr image 2 big 4 here ({(ulong)(new System.IO.FileInfo($"tmp/qr{todaysnumber}.png").Length)} / {message.Channel.EffectivePermissions.MaxAttachmentBytes})");
File.Delete($"tmp/qr{todaysnumber}.svg"); File.Delete($"tmp/qr{todaysnumber}.svg");
File.Delete($"tmp/qr{todaysnumber}.png"); File.Delete($"tmp/qr{todaysnumber}.png");
} }

View File

@ -16,6 +16,11 @@ public class TwitchSummon : Behavior
//HOWEVER, if not-the-broadcaster summons it, 1) all channel permissions to strict and 2) auto-disconnect on stream end //HOWEVER, if not-the-broadcaster summons it, 1) all channel permissions to strict and 2) auto-disconnect on stream end
//i don't know if the twitch *chat* interface has knowledge of if the stream ends. maybe auto-disconnect after like 2 hours? //i don't know if the twitch *chat* interface has knowledge of if the stream ends. maybe auto-disconnect after like 2 hours?
public override bool ShouldAct(Message message)
{
return false;
}
public override async Task<bool> ActOn(Message message) public override async Task<bool> ActOn(Message message)
{ {
var ti = ProtocolInterfaces.ProtocolList.twitchs.FirstOrDefault(); var ti = ProtocolInterfaces.ProtocolList.twitchs.FirstOrDefault();

View File

@ -22,8 +22,7 @@ namespace vassago
public async Task StartAsync(CancellationToken cancellationToken) public async Task StartAsync(CancellationToken cancellationToken)
{ {
var dbc = new ChattingContext(); var dbc = new ChattingContext();
dbc.Database.EnsureCreated(); await dbc.Database.MigrateAsync();
dbc.Database.Migrate();
if (DiscordTokens?.Any() ?? false) if (DiscordTokens?.Any() ?? false)
foreach (var dt in DiscordTokens) foreach (var dt in DiscordTokens)
@ -40,11 +39,12 @@ namespace vassago
await t.Init(tc); await t.Init(tc);
ProtocolInterfaces.ProtocolList.twitchs.Add(t); ProtocolInterfaces.ProtocolList.twitchs.Add(t);
} }
Console.WriteLine("survived initting");
} }
public Task StopAsync(CancellationToken cancellationToken) public Task StopAsync(CancellationToken cancellationToken)
{ {
throw new NotImplementedException(); return null;
} }
} }
} }

View File

@ -1,37 +0,0 @@
using System.Diagnostics;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using vassago.Models;
namespace vassago.Controllers;
public class ChannelsController : Controller
{
private readonly ILogger<ChannelsController> _logger;
private readonly ChattingContext _db;
public ChannelsController(ILogger<ChannelsController> logger, ChattingContext db)
{
_logger = logger;
_db = db;
}
public async Task<IActionResult> Index(string searchString)
{
return _db.Channels != null ?
View(_db.Channels.Include(u => u.ParentChannel).ToList().OrderBy(c => c.LineageSummary)) :
Problem("Entity set '_db.Channels' is null.");
}
public async Task<IActionResult> Details(Guid id)
{
return _db.Channels != null ?
View(await _db.Channels.Include(u => u.ParentChannel).FirstAsync(u => u.Id == id)) :
Problem("Entity set '_db.Channels' is null.");
}
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
return View(new ErrorPageViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
}

View File

@ -1,26 +0,0 @@
using System.Diagnostics;
using Microsoft.AspNetCore.Mvc;
using vassago.Models;
namespace vassago.Controllers;
public class HomeController : Controller
{
private readonly ILogger<HomeController> _logger;
public HomeController(ILogger<HomeController> logger)
{
_logger = logger;
}
public IActionResult Index()
{
return View();
}
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
return View(new ErrorPageViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
}

View File

@ -57,18 +57,21 @@ namespace vassago.Conversion
if(currencyConf != null) if(currencyConf != null)
{ {
knownConversions.RemoveAll(kc => kc.Item1 == currencyConf.Base); knownConversions.RemoveAll(kc => kc.Item1 == currencyConf.Base);
knownAliases.Remove(knownAliases.FirstOrDefault(kvp => kvp.Value == currencyConf.Base).Key);
foreach (var rate in currencyConf.rates)
knownAliases.Remove(knownAliases.FirstOrDefault(kvp => kvp.Value == rate.Key).Key);
} }
if (File.Exists(currencyPath)) if (File.Exists(currencyPath))
{ {
currencyConf = JsonConvert.DeserializeObject<ExchangePairs>(File.ReadAllText(currencyPath)); currencyConf = JsonConvert.DeserializeObject<ExchangePairs>(File.ReadAllText(currencyPath));
if(!knownAliases.ContainsValue(currencyConf.Base))
{
knownAliases.Add(new List<string>() { currencyConf.Base.ToLower() }, currencyConf.Base); knownAliases.Add(new List<string>() { currencyConf.Base.ToLower() }, currencyConf.Base);
}
foreach (var rate in currencyConf.rates) foreach (var rate in currencyConf.rates)
{
if(!knownAliases.ContainsValue(rate.Key))
{ {
knownAliases.Add(new List<string>() { rate.Key.ToLower() }, rate.Key); knownAliases.Add(new List<string>() { rate.Key.ToLower() }, rate.Key);
}
AddLinearPair(currencyConf.Base, rate.Key, rate.Value); AddLinearPair(currencyConf.Base, rate.Key, rate.Value);
Console.WriteLine($"{rate.Key.ToLower()} alias of {rate.Key}"); Console.WriteLine($"{rate.Key.ToLower()} alias of {rate.Key}");
} }

8
Jenkinsfile vendored
View File

@ -13,5 +13,13 @@ pipeline {
archiveArtifacts artifacts: 'bin/Release/net7.0/linux-x64/publish/*' archiveArtifacts artifacts: 'bin/Release/net7.0/linux-x64/publish/*'
} }
} }
stage('Deploy'){
when{
branch "release"
}
steps{
}
}
} }
} }

View File

@ -0,0 +1,266 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using vassago.Models;
#nullable disable
namespace vassago.Migrations
{
[DbContext(typeof(ChattingContext))]
[Migration("20240510202057_channelpermissions_partofchannel")]
partial class channelpermissions_partofchannel
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "7.0.5")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("vassago.Models.Account", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("DisplayName")
.HasColumnType("text");
b.Property<string>("ExternalId")
.HasColumnType("text");
b.Property<bool>("IsBot")
.HasColumnType("boolean");
b.Property<Guid?>("IsUserId")
.HasColumnType("uuid");
b.Property<string>("Protocol")
.HasColumnType("text");
b.Property<Guid?>("SeenInChannelId")
.HasColumnType("uuid");
b.Property<string>("Username")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("IsUserId");
b.HasIndex("SeenInChannelId");
b.ToTable("Accounts");
});
modelBuilder.Entity("vassago.Models.Attachment", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<byte[]>("Content")
.HasColumnType("bytea");
b.Property<string>("ContentType")
.HasColumnType("text");
b.Property<string>("Description")
.HasColumnType("text");
b.Property<decimal?>("ExternalId")
.HasColumnType("numeric(20,0)");
b.Property<string>("Filename")
.HasColumnType("text");
b.Property<Guid?>("MessageId")
.HasColumnType("uuid");
b.Property<int>("Size")
.HasColumnType("integer");
b.Property<string>("Source")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("MessageId");
b.ToTable("Attachments");
});
modelBuilder.Entity("vassago.Models.Channel", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<int>("ChannelType")
.HasColumnType("integer");
b.Property<string>("DisplayName")
.HasColumnType("text");
b.Property<string>("ExternalId")
.HasColumnType("text");
b.Property<int?>("LewdnessFilterLevel")
.HasColumnType("integer");
b.Property<bool?>("LinksAllowed")
.HasColumnType("boolean");
b.Property<decimal?>("MaxAttachmentBytes")
.HasColumnType("numeric(20,0)");
b.Property<long?>("MaxTextChars")
.HasColumnType("bigint");
b.Property<int?>("MeannessFilterLevel")
.HasColumnType("integer");
b.Property<Guid?>("ParentChannelId")
.HasColumnType("uuid");
b.Property<string>("Protocol")
.HasColumnType("text");
b.Property<bool?>("ReactionsPossible")
.HasColumnType("boolean");
b.HasKey("Id");
b.HasIndex("ParentChannelId");
b.ToTable("Channels");
});
modelBuilder.Entity("vassago.Models.Message", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<bool>("ActedOn")
.HasColumnType("boolean");
b.Property<Guid?>("AuthorId")
.HasColumnType("uuid");
b.Property<Guid?>("ChannelId")
.HasColumnType("uuid");
b.Property<string>("Content")
.HasColumnType("text");
b.Property<string>("ExternalId")
.HasColumnType("text");
b.Property<bool>("MentionsMe")
.HasColumnType("boolean");
b.Property<string>("Protocol")
.HasColumnType("text");
b.Property<DateTimeOffset>("Timestamp")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("AuthorId");
b.HasIndex("ChannelId");
b.ToTable("Messages");
});
modelBuilder.Entity("vassago.Models.User", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.HasKey("Id");
b.ToTable("Users");
});
modelBuilder.Entity("vassago.Models.Account", b =>
{
b.HasOne("vassago.Models.User", "IsUser")
.WithMany("Accounts")
.HasForeignKey("IsUserId");
b.HasOne("vassago.Models.Channel", "SeenInChannel")
.WithMany("Users")
.HasForeignKey("SeenInChannelId");
b.Navigation("IsUser");
b.Navigation("SeenInChannel");
});
modelBuilder.Entity("vassago.Models.Attachment", b =>
{
b.HasOne("vassago.Models.Message", "Message")
.WithMany("Attachments")
.HasForeignKey("MessageId");
b.Navigation("Message");
});
modelBuilder.Entity("vassago.Models.Channel", b =>
{
b.HasOne("vassago.Models.Channel", "ParentChannel")
.WithMany("SubChannels")
.HasForeignKey("ParentChannelId");
b.Navigation("ParentChannel");
});
modelBuilder.Entity("vassago.Models.Message", b =>
{
b.HasOne("vassago.Models.Account", "Author")
.WithMany()
.HasForeignKey("AuthorId");
b.HasOne("vassago.Models.Channel", "Channel")
.WithMany("Messages")
.HasForeignKey("ChannelId");
b.Navigation("Author");
b.Navigation("Channel");
});
modelBuilder.Entity("vassago.Models.Channel", b =>
{
b.Navigation("Messages");
b.Navigation("SubChannels");
b.Navigation("Users");
});
modelBuilder.Entity("vassago.Models.Message", b =>
{
b.Navigation("Attachments");
});
modelBuilder.Entity("vassago.Models.User", b =>
{
b.Navigation("Accounts");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,228 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace vassago.Migrations
{
/// <inheritdoc />
public partial class channelpermissions_partofchannel : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Accounts_FeaturePermissions_FeaturePermissionId",
table: "Accounts");
migrationBuilder.DropForeignKey(
name: "FK_Channels_ChannelPermissions_PermissionsId",
table: "Channels");
migrationBuilder.DropForeignKey(
name: "FK_Channels_FeaturePermissions_FeaturePermissionId",
table: "Channels");
migrationBuilder.DropForeignKey(
name: "FK_Users_FeaturePermissions_FeaturePermissionId",
table: "Users");
migrationBuilder.DropTable(
name: "ChannelPermissions");
migrationBuilder.DropTable(
name: "FeaturePermissions");
migrationBuilder.DropIndex(
name: "IX_Users_FeaturePermissionId",
table: "Users");
migrationBuilder.DropIndex(
name: "IX_Channels_FeaturePermissionId",
table: "Channels");
migrationBuilder.DropIndex(
name: "IX_Channels_PermissionsId",
table: "Channels");
migrationBuilder.DropIndex(
name: "IX_Accounts_FeaturePermissionId",
table: "Accounts");
migrationBuilder.DropColumn(
name: "FeaturePermissionId",
table: "Users");
migrationBuilder.DropColumn(
name: "FeaturePermissionId",
table: "Channels");
migrationBuilder.DropColumn(
name: "FeaturePermissionId",
table: "Accounts");
migrationBuilder.RenameColumn(
name: "PermissionsId",
table: "Channels",
newName: "MeannessFilterLevel");
migrationBuilder.AddColumn<int>(
name: "LewdnessFilterLevel",
table: "Channels",
type: "integer",
nullable: true);
migrationBuilder.AddColumn<bool>(
name: "LinksAllowed",
table: "Channels",
type: "boolean",
nullable: true);
migrationBuilder.AddColumn<decimal>(
name: "MaxAttachmentBytes",
table: "Channels",
type: "numeric(20,0)",
nullable: true);
migrationBuilder.AddColumn<long>(
name: "MaxTextChars",
table: "Channels",
type: "bigint",
nullable: true);
migrationBuilder.AddColumn<bool>(
name: "ReactionsPossible",
table: "Channels",
type: "boolean",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "LewdnessFilterLevel",
table: "Channels");
migrationBuilder.DropColumn(
name: "LinksAllowed",
table: "Channels");
migrationBuilder.DropColumn(
name: "MaxAttachmentBytes",
table: "Channels");
migrationBuilder.DropColumn(
name: "MaxTextChars",
table: "Channels");
migrationBuilder.DropColumn(
name: "ReactionsPossible",
table: "Channels");
migrationBuilder.RenameColumn(
name: "MeannessFilterLevel",
table: "Channels",
newName: "PermissionsId");
migrationBuilder.AddColumn<Guid>(
name: "FeaturePermissionId",
table: "Users",
type: "uuid",
nullable: true);
migrationBuilder.AddColumn<Guid>(
name: "FeaturePermissionId",
table: "Channels",
type: "uuid",
nullable: true);
migrationBuilder.AddColumn<Guid>(
name: "FeaturePermissionId",
table: "Accounts",
type: "uuid",
nullable: true);
migrationBuilder.CreateTable(
name: "ChannelPermissions",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
LewdnessFilterLevel = table.Column<int>(type: "integer", nullable: true),
LinksAllowed = table.Column<bool>(type: "boolean", nullable: true),
MaxAttachmentBytes = table.Column<decimal>(type: "numeric(20,0)", nullable: true),
MaxTextChars = table.Column<long>(type: "bigint", nullable: true),
MeannessFilterLevel = table.Column<int>(type: "integer", nullable: true),
ReactionsPossible = table.Column<bool>(type: "boolean", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_ChannelPermissions", x => x.Id);
});
migrationBuilder.CreateTable(
name: "FeaturePermissions",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
Inheritable = table.Column<bool>(type: "boolean", nullable: false),
InternalName = table.Column<string>(type: "text", nullable: true),
InternalTag = table.Column<int>(type: "integer", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_FeaturePermissions", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_Users_FeaturePermissionId",
table: "Users",
column: "FeaturePermissionId");
migrationBuilder.CreateIndex(
name: "IX_Channels_FeaturePermissionId",
table: "Channels",
column: "FeaturePermissionId");
migrationBuilder.CreateIndex(
name: "IX_Channels_PermissionsId",
table: "Channels",
column: "PermissionsId");
migrationBuilder.CreateIndex(
name: "IX_Accounts_FeaturePermissionId",
table: "Accounts",
column: "FeaturePermissionId");
migrationBuilder.AddForeignKey(
name: "FK_Accounts_FeaturePermissions_FeaturePermissionId",
table: "Accounts",
column: "FeaturePermissionId",
principalTable: "FeaturePermissions",
principalColumn: "Id");
migrationBuilder.AddForeignKey(
name: "FK_Channels_ChannelPermissions_PermissionsId",
table: "Channels",
column: "PermissionsId",
principalTable: "ChannelPermissions",
principalColumn: "Id");
migrationBuilder.AddForeignKey(
name: "FK_Channels_FeaturePermissions_FeaturePermissionId",
table: "Channels",
column: "FeaturePermissionId",
principalTable: "FeaturePermissions",
principalColumn: "Id");
migrationBuilder.AddForeignKey(
name: "FK_Users_FeaturePermissions_FeaturePermissionId",
table: "Users",
column: "FeaturePermissionId",
principalTable: "FeaturePermissions",
principalColumn: "Id");
}
}
}

View File

@ -34,9 +34,6 @@ namespace vassago.Migrations
b.Property<string>("ExternalId") b.Property<string>("ExternalId")
.HasColumnType("text"); .HasColumnType("text");
b.Property<Guid?>("FeaturePermissionId")
.HasColumnType("uuid");
b.Property<bool>("IsBot") b.Property<bool>("IsBot")
.HasColumnType("boolean"); .HasColumnType("boolean");
@ -54,8 +51,6 @@ namespace vassago.Migrations
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("FeaturePermissionId");
b.HasIndex("IsUserId"); b.HasIndex("IsUserId");
b.HasIndex("SeenInChannelId"); b.HasIndex("SeenInChannelId");
@ -115,37 +110,6 @@ namespace vassago.Migrations
b.Property<string>("ExternalId") b.Property<string>("ExternalId")
.HasColumnType("text"); .HasColumnType("text");
b.Property<Guid?>("FeaturePermissionId")
.HasColumnType("uuid");
b.Property<Guid?>("ParentChannelId")
.HasColumnType("uuid");
b.Property<int?>("PermissionsId")
.HasColumnType("integer");
b.Property<string>("Protocol")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("FeaturePermissionId");
b.HasIndex("ParentChannelId");
b.HasIndex("PermissionsId");
b.ToTable("Channels");
});
modelBuilder.Entity("vassago.Models.ChannelPermissions", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<int?>("LewdnessFilterLevel") b.Property<int?>("LewdnessFilterLevel")
.HasColumnType("integer"); .HasColumnType("integer");
@ -161,32 +125,20 @@ namespace vassago.Migrations
b.Property<int?>("MeannessFilterLevel") b.Property<int?>("MeannessFilterLevel")
.HasColumnType("integer"); .HasColumnType("integer");
b.Property<Guid?>("ParentChannelId")
.HasColumnType("uuid");
b.Property<string>("Protocol")
.HasColumnType("text");
b.Property<bool?>("ReactionsPossible") b.Property<bool?>("ReactionsPossible")
.HasColumnType("boolean"); .HasColumnType("boolean");
b.HasKey("Id"); b.HasKey("Id");
b.ToTable("ChannelPermissions"); b.HasIndex("ParentChannelId");
});
modelBuilder.Entity("vassago.Models.FeaturePermission", b => b.ToTable("Channels");
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<bool>("Inheritable")
.HasColumnType("boolean");
b.Property<string>("InternalName")
.HasColumnType("text");
b.Property<int?>("InternalTag")
.HasColumnType("integer");
b.HasKey("Id");
b.ToTable("FeaturePermissions");
}); });
modelBuilder.Entity("vassago.Models.Message", b => modelBuilder.Entity("vassago.Models.Message", b =>
@ -234,22 +186,13 @@ namespace vassago.Migrations
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("uuid"); .HasColumnType("uuid");
b.Property<Guid?>("FeaturePermissionId")
.HasColumnType("uuid");
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("FeaturePermissionId");
b.ToTable("Users"); b.ToTable("Users");
}); });
modelBuilder.Entity("vassago.Models.Account", b => modelBuilder.Entity("vassago.Models.Account", b =>
{ {
b.HasOne("vassago.Models.FeaturePermission", null)
.WithMany("RestrictedToAccounts")
.HasForeignKey("FeaturePermissionId");
b.HasOne("vassago.Models.User", "IsUser") b.HasOne("vassago.Models.User", "IsUser")
.WithMany("Accounts") .WithMany("Accounts")
.HasForeignKey("IsUserId"); .HasForeignKey("IsUserId");
@ -274,21 +217,11 @@ namespace vassago.Migrations
modelBuilder.Entity("vassago.Models.Channel", b => modelBuilder.Entity("vassago.Models.Channel", b =>
{ {
b.HasOne("vassago.Models.FeaturePermission", null)
.WithMany("RestrictedToChannels")
.HasForeignKey("FeaturePermissionId");
b.HasOne("vassago.Models.Channel", "ParentChannel") b.HasOne("vassago.Models.Channel", "ParentChannel")
.WithMany("SubChannels") .WithMany("SubChannels")
.HasForeignKey("ParentChannelId"); .HasForeignKey("ParentChannelId");
b.HasOne("vassago.Models.ChannelPermissions", "Permissions")
.WithMany()
.HasForeignKey("PermissionsId");
b.Navigation("ParentChannel"); b.Navigation("ParentChannel");
b.Navigation("Permissions");
}); });
modelBuilder.Entity("vassago.Models.Message", b => modelBuilder.Entity("vassago.Models.Message", b =>
@ -306,13 +239,6 @@ namespace vassago.Migrations
b.Navigation("Channel"); b.Navigation("Channel");
}); });
modelBuilder.Entity("vassago.Models.User", b =>
{
b.HasOne("vassago.Models.FeaturePermission", null)
.WithMany("RestrictedToUsers")
.HasForeignKey("FeaturePermissionId");
});
modelBuilder.Entity("vassago.Models.Channel", b => modelBuilder.Entity("vassago.Models.Channel", b =>
{ {
b.Navigation("Messages"); b.Navigation("Messages");
@ -322,15 +248,6 @@ namespace vassago.Migrations
b.Navigation("Users"); b.Navigation("Users");
}); });
modelBuilder.Entity("vassago.Models.FeaturePermission", b =>
{
b.Navigation("RestrictedToAccounts");
b.Navigation("RestrictedToChannels");
b.Navigation("RestrictedToUsers");
});
modelBuilder.Entity("vassago.Models.Message", b => modelBuilder.Entity("vassago.Models.Message", b =>
{ {
b.Navigation("Attachments"); b.Navigation("Attachments");

View File

@ -5,6 +5,7 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
using System.Reflection; using System.Reflection;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Web;
using static vassago.Models.Enumerations; using static vassago.Models.Enumerations;
public class Channel public class Channel
@ -13,14 +14,20 @@ public class Channel
public Guid Id { get; set; } public Guid Id { get; set; }
public string ExternalId { get; set; } public string ExternalId { get; set; }
public string DisplayName { get; set; } public string DisplayName { get; set; }
public ChannelPermissions Permissions { get; set; }
public List<Channel> SubChannels { get; set; } public List<Channel> SubChannels { get; set; }
public Channel ParentChannel { get; set; } public Channel ParentChannel { get; set; }
public string Protocol { get; set; } public string Protocol { get; set; }
public List<Message> Messages { get; set; } public List<Message> Messages { get; set; }
public List<Account> Users { get; set; } public List<Account> Users { get; set; }
public ChannelType ChannelType {get; set; } public ChannelType ChannelType {get; set; }
//public Dictionary<string, string> EmoteOverrides{get;set;}
//Permissions
public ulong? MaxAttachmentBytes { get; set; }
public uint? MaxTextChars { get; set; }
public bool? LinksAllowed { get; set; }
public bool? ReactionsPossible { get; set; }
public Enumerations.LewdnessFilterLevel? LewdnessFilterLevel { get; set; }
public Enumerations.MeannessFilterLevel? MeannessFilterLevel { get; set; }
[NonSerialized] [NonSerialized]
public Func<string, string, Task> SendFile; public Func<string, string, Task> SendFile;
@ -33,33 +40,28 @@ public class Channel
{ {
get get
{ {
ChannelPermissions toReturn = Permissions ?? new ChannelPermissions(); var path = new Stack<Channel>(); //omg i actually get to use a data structure from university
return GetEffectivePermissions(ref toReturn).Definite(); var walker = this;
} path.Push(this);
} while(walker.ParentChannel != null)
private ChannelPermissions GetEffectivePermissions(ref ChannelPermissions settings)
{ {
if(settings == null) throw new ArgumentNullException(); walker = walker.ParentChannel;
settings.LewdnessFilterLevel = settings.LewdnessFilterLevel ?? Permissions?.LewdnessFilterLevel; path.Push(walker);
settings.MeannessFilterLevel = settings.MeannessFilterLevel ?? Permissions?.MeannessFilterLevel; }
settings.LinksAllowed = settings.LinksAllowed ?? Permissions?.LinksAllowed; DefinitePermissionSettings toReturn = new DefinitePermissionSettings();
settings.MaxAttachmentBytes = settings.MaxAttachmentBytes ?? Permissions?.MaxAttachmentBytes;
settings.MaxTextChars = settings.MaxTextChars ?? Permissions?.MaxTextChars;
settings.ReactionsPossible = settings.ReactionsPossible ?? Permissions?.ReactionsPossible;
if(this.ParentChannel != null && while(path.Count > 0)
(settings.LewdnessFilterLevel == null ||
settings.MeannessFilterLevel == null ||
settings.LinksAllowed == null ||
settings.MaxAttachmentBytes == null ||
settings.MaxTextChars == null ||
settings.ReactionsPossible == null))
{ {
return this.ParentChannel.GetEffectivePermissions(ref settings); walker = path.Pop();
toReturn.LewdnessFilterLevel = walker.LewdnessFilterLevel ?? toReturn.LewdnessFilterLevel;
toReturn.MeannessFilterLevel = walker.MeannessFilterLevel ?? toReturn.MeannessFilterLevel;
toReturn.LinksAllowed = walker.LinksAllowed ?? toReturn.LinksAllowed;
toReturn.MaxAttachmentBytes = walker.MaxAttachmentBytes ?? toReturn.MaxAttachmentBytes;
toReturn.MaxTextChars = walker.MaxTextChars ?? toReturn.MaxTextChars;
toReturn.ReactionsPossible = walker.ReactionsPossible ?? toReturn.ReactionsPossible;
} }
else
{ return toReturn;
return settings;
} }
} }
public string LineageSummary public string LineageSummary
@ -77,3 +79,13 @@ public class Channel
} }
} }
} }
public class DefinitePermissionSettings
{
public ulong MaxAttachmentBytes { get; set; }
public uint MaxTextChars { get; set; }
public bool LinksAllowed { get; set; }
public bool ReactionsPossible { get; set; }
public Enumerations.LewdnessFilterLevel LewdnessFilterLevel { get; set; }
public Enumerations.MeannessFilterLevel MeannessFilterLevel { get; set; }
}

View File

@ -1,38 +0,0 @@
namespace vassago.Models;
using System;
using System.ComponentModel.DataAnnotations.Schema;
public class ChannelPermissions
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public ulong? MaxAttachmentBytes { get; set; }
public uint? MaxTextChars { get; set; }
public bool? LinksAllowed { get; set; }
public bool? ReactionsPossible { get; set; }
public Enumerations.LewdnessFilterLevel? LewdnessFilterLevel { get; set; }
public Enumerations.MeannessFilterLevel? MeannessFilterLevel { get; set; }
internal DefinitePermissionSettings Definite()
{
return new DefinitePermissionSettings()
{
MaxAttachmentBytes = this.MaxAttachmentBytes ?? 0,
MaxTextChars = this.MaxTextChars ?? 0,
LinksAllowed = this.LinksAllowed ?? false,
LewdnessFilterLevel = this.LewdnessFilterLevel ?? Enumerations.LewdnessFilterLevel.G,
MeannessFilterLevel = this.MeannessFilterLevel ?? Enumerations.MeannessFilterLevel.Strict,
ReactionsPossible = this.ReactionsPossible ?? false
};
}
}
public class DefinitePermissionSettings
{
public ulong MaxAttachmentBytes { get; set; }
public uint MaxTextChars { get; set; }
public bool LinksAllowed { get; set; }
public bool ReactionsPossible { get; set; }
public Enumerations.LewdnessFilterLevel LewdnessFilterLevel { get; set; }
public Enumerations.MeannessFilterLevel MeannessFilterLevel { get; set; }
}

View File

@ -9,14 +9,14 @@ public class ChattingContext : DbContext
public DbSet<Channel> Channels { get; set; } public DbSet<Channel> Channels { get; set; }
//public DbSet<Emoji> Emoji {get;set;} //public DbSet<Emoji> Emoji {get;set;}
public DbSet<Message> Messages { get; set; } public DbSet<Message> Messages { get; set; }
public DbSet<ChannelPermissions> ChannelPermissions{get;set;}
public DbSet<FeaturePermission> FeaturePermissions{get;set;}
public DbSet<Account> Accounts { get; set; } public DbSet<Account> Accounts { get; set; }
public DbSet<User> Users { get; set; } public DbSet<User> Users { get; set; }
public ChattingContext(DbContextOptions<ChattingContext> options) : base(options) { } public ChattingContext(DbContextOptions<ChattingContext> options) : base(options) { }
public ChattingContext() : base() { } public ChattingContext() : base() { }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.UseNpgsql(Shared.DBConnectionString) {
optionsBuilder.UseNpgsql(Shared.DBConnectionString)
.EnableSensitiveDataLogging(true); //who the fuck is looking at log output but not allowed to see it? this should be on by default. .EnableSensitiveDataLogging(true); //who the fuck is looking at log output but not allowed to see it? this should be on by default.
} }
}

View File

@ -45,7 +45,7 @@ public static class Enumerations
Type type = enumerationValue.GetType(); Type type = enumerationValue.GetType();
if (!type.IsEnum) if (!type.IsEnum)
{ {
throw new ArgumentException("EnumerationValue must be of Enum type", "enumerationValue"); throw new ArgumentException("EnumerationValue must be of Enum type", nameof(enumerationValue));
} }
//Tries to find a DescriptionAttribute for a potential friendly name //Tries to find a DescriptionAttribute for a potential friendly name

View File

@ -1,109 +0,0 @@
namespace vassago.Models;
using System;
using System.Linq;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Reflection;
using System.Threading.Tasks;
using Discord.WebSocket;
using static vassago.Models.Enumerations;
public enum WellknownPermissions
{
Administrator,
TwitchSummon,
}
public class FeaturePermission
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid Id { get; set; }
public string InternalName { get; set; }
public WellknownPermissions? InternalTag { get; set; }
//a permissions-needing-feature can determine how to use these, but a default "matches" is provided
//for a message to "match", it must match in every category for which there are candidates.
//e.g., Administrator is going to be restricted to Users only, and that'll be me
//e.g., my future Torrent feature would be restricted to accounts and channels.
//hmmm, what would be inheritable and what wouldn't?
public IEnumerable<User> RestrictedToUsers { get; set; }
public IEnumerable<Account> RestrictedToAccounts { get; set; }
public IEnumerable<Channel> RestrictedToChannels { get; set; }
public bool Inheritable { get; set; } = true;
public bool Matches(Message message)
{
if(RestrictedToUsers?.Count() > 0)
{
if(RestrictedToUsers.FirstOrDefault(u => u.Id == message.Author.IsUser.Id) == null)
{
return false;
}
}
if(RestrictedToChannels?.Count() > 0)
{
if(Inheritable)
{
var found = false;
var walker = message.Channel;
if (RestrictedToChannels.FirstOrDefault(c => c.Id == walker.Id) != null)
{
found = true;
}
else
{
while (walker.ParentChannel != null)
{
walker = walker.ParentChannel;
if(walker.Users.FirstOrDefault(a => a.ExternalId == message.Author.ExternalId) == null)
{
//the chain is broken; I don't exist in this channel
break;
}
if (RestrictedToChannels.FirstOrDefault(c => c.Id == walker.Id) != null)
{
found = true;
break;
}
}
}
if (found)
{
if(RestrictedToAccounts?.Count() > 0)
{
//walker is the "actual" restricted-to channel, but we're inheriting
if(walker.Users.FirstOrDefault(a => a.Id == message.Author.Id) == null)
{
return false;
}
}
}
else
{
return false;
}
}
else
{
if(RestrictedToChannels.FirstOrDefault(c => c.Id == message.Channel.Id) == null)
{
return false;
}
}
}
if(RestrictedToAccounts?.Count() > 0)
{
if(RestrictedToAccounts.FirstOrDefault(a => a.Id == message.Author.Id) == null)
{
return false;
}
}
//if I got all the way down here, I must be good
return true;
}
}

View File

@ -1,13 +1,24 @@
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Mvc.NewtonsoftJson;
using vassago.Models; using vassago.Models;
#pragma warning disable CA2254
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
// Add services to the container. // Add services to the container.
builder.Services.AddControllersWithViews(); builder.Services.AddControllersWithViews();
builder.Services.AddSingleton<IHostedService, vassago.ConsoleService>(); builder.Services.AddSingleton<IHostedService, vassago.ConsoleService>();
builder.Services.AddDbContext<ChattingContext>(options => builder.Services.AddDbContext<ChattingContext>();
options.UseNpgsql(builder.Configuration.GetConnectionString("ChattingContext"))); builder.Services.AddControllers().AddNewtonsoftJson();
builder.Services.AddProblemDetails();
builder.Services.Configure<RazorViewEngineOptions>(o => {
o.ViewLocationFormats.Clear();
o.ViewLocationFormats.Add("/WebInterface/Views/{1}/{0}" + RazorViewEngine.ViewExtension);
o.ViewLocationFormats.Add("/WebInterface/Views/Shared/{0}" + RazorViewEngine.ViewExtension);
});
builder.Services.AddSwaggerGen();
var app = builder.Build(); var app = builder.Build();
@ -24,4 +35,19 @@ app.MapControllerRoute(
name: "default", name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}"); pattern: "{controller=Home}/{action=Index}/{id?}");
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "api");
});
app.UseExceptionHandler();
app.UseStatusCodePages();
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.Run(); app.Run();

View File

@ -58,15 +58,12 @@ public class DiscordInterface
protocolAsChannel = new Channel() protocolAsChannel = new Channel()
{ {
DisplayName = "discord (itself)", DisplayName = "discord (itself)",
Permissions = new Models.ChannelPermissions()
{
MeannessFilterLevel = Enumerations.MeannessFilterLevel.Strict, MeannessFilterLevel = Enumerations.MeannessFilterLevel.Strict,
LewdnessFilterLevel = Enumerations.LewdnessFilterLevel.Moderate, LewdnessFilterLevel = Enumerations.LewdnessFilterLevel.Moderate,
MaxTextChars = 2000, MaxTextChars = 2000,
MaxAttachmentBytes = 25 * 1024 * 1024, //allegedly it's 25, but I worry it's not actually. MaxAttachmentBytes = 25 * 1024 * 1024, //allegedly it's 25, but I worry it's not actually.
LinksAllowed = true, LinksAllowed = true,
ReactionsPossible = true ReactionsPossible = true,
},
ExternalId = null, ExternalId = null,
Protocol = PROTOCOL, Protocol = PROTOCOL,
SubChannels = new List<Channel>() SubChannels = new List<Channel>()
@ -117,11 +114,11 @@ public class DiscordInterface
private async Task SelfConnected() private async Task SelfConnected()
{ {
var selfUser = UpsertAccount(client.CurrentUser, protocolAsChannel.Id); var selfAccount = UpsertAccount(client.CurrentUser, protocolAsChannel);
selfUser.DisplayName = client.CurrentUser.Username; selfAccount.DisplayName = client.CurrentUser.Username;
await _db.SaveChangesAsync(); await _db.SaveChangesAsync();
Behaver.Instance.Selves.Add(selfUser);
Behaver.Instance.MarkSelf(selfAccount);
} }
private async Task MessageReceived(SocketMessage messageParam) private async Task MessageReceived(SocketMessage messageParam)
@ -153,7 +150,7 @@ public class DiscordInterface
var guild = UpsertChannel(arg.Guild); var guild = UpsertChannel(arg.Guild);
var defaultChannel = UpsertChannel(arg.Guild.DefaultChannel); var defaultChannel = UpsertChannel(arg.Guild.DefaultChannel);
defaultChannel.ParentChannel = guild; defaultChannel.ParentChannel = guild;
var u = UpsertAccount(arg, guild.Id); var u = UpsertAccount(arg, guild);
u.DisplayName = arg.DisplayName; u.DisplayName = arg.DisplayName;
} }
private async Task ButtonHandler(SocketMessageComponent component) private async Task ButtonHandler(SocketMessageComponent component)
@ -229,8 +226,7 @@ public class DiscordInterface
m.ExternalId = dMessage.Id.ToString(); m.ExternalId = dMessage.Id.ToString();
m.Timestamp = dMessage.EditedTimestamp ?? dMessage.CreatedAt; m.Timestamp = dMessage.EditedTimestamp ?? dMessage.CreatedAt;
m.Channel = UpsertChannel(dMessage.Channel); m.Channel = UpsertChannel(dMessage.Channel);
m.Author = UpsertAccount(dMessage.Author, m.Channel.Id); m.Author = UpsertAccount(dMessage.Author, m.Channel);
m.Author.SeenInChannel = m.Channel;
if(dMessage.Channel is IGuildChannel) if(dMessage.Channel is IGuildChannel)
{ {
m.Author.DisplayName = (dMessage.Author as IGuildUser).DisplayName;//discord forgot how display names work. m.Author.DisplayName = (dMessage.Author as IGuildUser).DisplayName;//discord forgot how display names work.
@ -298,16 +294,15 @@ public class DiscordInterface
c.Protocol = protocolAsChannel.Protocol; c.Protocol = protocolAsChannel.Protocol;
c.ParentChannel = protocolAsChannel; c.ParentChannel = protocolAsChannel;
c.SubChannels = c.SubChannels ?? new List<Channel>(); c.SubChannels = c.SubChannels ?? new List<Channel>();
c.Permissions = c.Permissions ?? new Models.ChannelPermissions(); c.MaxAttachmentBytes = channel.MaxUploadLimit;
c.Permissions.MaxAttachmentBytes = channel.MaxUploadLimit;
c.SendMessage = (t) => { throw new InvalidOperationException($"channel {channel.Name} is guild; cannot accept text"); }; c.SendMessage = (t) => { throw new InvalidOperationException($"channel {channel.Name} is guild; cannot accept text"); };
c.SendFile = (f, t) => { throw new InvalidOperationException($"channel {channel.Name} is guild; send file"); }; c.SendFile = (f, t) => { throw new InvalidOperationException($"channel {channel.Name} is guild; send file"); };
return c; return c;
} }
internal Account UpsertAccount(IUser user, Guid inChannel) internal Account UpsertAccount(IUser user, Channel inChannel)
{ {
var acc = _db.Accounts.FirstOrDefault(ui => ui.ExternalId == user.Id.ToString() && ui.SeenInChannel.Id == inChannel); var acc = _db.Accounts.FirstOrDefault(ui => ui.ExternalId == user.Id.ToString() && ui.SeenInChannel.Id == inChannel.Id);
if (acc == null) if (acc == null)
{ {
acc = new Account(); acc = new Account();
@ -317,6 +312,7 @@ public class DiscordInterface
acc.ExternalId = user.Id.ToString(); acc.ExternalId = user.Id.ToString();
acc.IsBot = user.IsBot || user.IsWebhook; acc.IsBot = user.IsBot || user.IsWebhook;
acc.Protocol = PROTOCOL; acc.Protocol = PROTOCOL;
acc.SeenInChannel = inChannel;
acc.IsUser = _db.Users.FirstOrDefault(u => u.Accounts.Any(a => a.ExternalId == acc.ExternalId && a.Protocol == acc.Protocol)); acc.IsUser = _db.Users.FirstOrDefault(u => u.Accounts.Any(a => a.ExternalId == acc.ExternalId && a.Protocol == acc.Protocol));
if(acc.IsUser == null) if(acc.IsUser == null)
@ -332,13 +328,11 @@ public class DiscordInterface
var c = _db.Channels.FirstOrDefault(c => c.ExternalId == msg.Channel.Id.ToString()); var c = _db.Channels.FirstOrDefault(c => c.ExternalId == msg.Channel.Id.ToString());
//var preferredEmote = c.EmoteOverrides?[e] ?? e; //TODO: emote overrides //var preferredEmote = c.EmoteOverrides?[e] ?? e; //TODO: emote overrides
var preferredEmote = e; var preferredEmote = e;
Emoji emoji; if (Emoji.TryParse(preferredEmote, out Emoji emoji))
if (Emoji.TryParse(preferredEmote, out emoji))
{ {
return msg.AddReactionAsync(emoji); return msg.AddReactionAsync(emoji);
} }
Emote emote; if (!Emote.TryParse(preferredEmote, out Emote emote))
if (!Emote.TryParse(preferredEmote, out emote))
{ {
if (preferredEmote == e) if (preferredEmote == e)
Console.Error.WriteLine($"never heard of emote {e}"); Console.Error.WriteLine($"never heard of emote {e}");

View File

@ -9,7 +9,6 @@ using Discord.Net;
namespace vassago.DiscordInterface namespace vassago.DiscordInterface
{ {
public static class SlashCommandsHelper public static class SlashCommandsHelper
{ {
private static List<CommandSetup> slashCommands = new List<CommandSetup>() private static List<CommandSetup> slashCommands = new List<CommandSetup>()

View File

@ -2,6 +2,6 @@ namespace vassago.ProtocolInterfaces;
public static class ProtocolList public static class ProtocolList
{ {
public static List<DiscordInterface.DiscordInterface> discords = new List<DiscordInterface.DiscordInterface>(); public static List<DiscordInterface.DiscordInterface> discords = new();
public static List<TwitchInterface.TwitchInterface> twitchs = new List<TwitchInterface.TwitchInterface>(); public static List<TwitchInterface.TwitchInterface> twitchs = new();
} }

View File

@ -39,15 +39,12 @@ public class TwitchInterface
protocolAsChannel = new Channel() protocolAsChannel = new Channel()
{ {
DisplayName = "twitch (itself)", DisplayName = "twitch (itself)",
Permissions = new ChannelPermissions()
{
MeannessFilterLevel = Enumerations.MeannessFilterLevel.Medium, MeannessFilterLevel = Enumerations.MeannessFilterLevel.Medium,
LewdnessFilterLevel = Enumerations.LewdnessFilterLevel.G, LewdnessFilterLevel = Enumerations.LewdnessFilterLevel.G,
MaxTextChars = 500, MaxTextChars = 500,
MaxAttachmentBytes = 0, MaxAttachmentBytes = 0,
LinksAllowed = false, LinksAllowed = false,
ReactionsPossible = false ReactionsPossible = false,
},
ExternalId = null, ExternalId = null,
Protocol = PROTOCOL, Protocol = PROTOCOL,
SubChannels = new List<Channel>() SubChannels = new List<Channel>()
@ -115,10 +112,10 @@ public class TwitchInterface
var m = UpsertMessage(e.WhisperMessage); var m = UpsertMessage(e.WhisperMessage);
m.Channel.ChannelType = vassago.Models.Enumerations.ChannelType.DM; m.Channel.ChannelType = vassago.Models.Enumerations.ChannelType.DM;
m.MentionsMe = Regex.IsMatch(e.WhisperMessage.Message?.ToLower(), $"\\b@{e.WhisperMessage.BotUsername.ToLower()}\\b"); m.MentionsMe = Regex.IsMatch(e.WhisperMessage.Message?.ToLower(), $"\\b@{e.WhisperMessage.BotUsername.ToLower()}\\b");
_db.SaveChanges(); await _db.SaveChangesAsync();
await Behaver.Instance.ActOn(m); await Behaver.Instance.ActOn(m);
_db.SaveChanges(); await _db.SaveChangesAsync();
} }
private async void Client_OnMessageReceivedAsync(object sender, OnMessageReceivedArgs e) private async void Client_OnMessageReceivedAsync(object sender, OnMessageReceivedArgs e)
@ -134,18 +131,18 @@ public class TwitchInterface
var m = UpsertMessage(e.ChatMessage); var m = UpsertMessage(e.ChatMessage);
m.MentionsMe = Regex.IsMatch(e.ChatMessage.Message?.ToLower(), $"@{e.ChatMessage.BotUsername.ToLower()}\\b") || m.MentionsMe = Regex.IsMatch(e.ChatMessage.Message?.ToLower(), $"@{e.ChatMessage.BotUsername.ToLower()}\\b") ||
e.ChatMessage.ChatReply?.ParentUserLogin == e.ChatMessage.BotUsername; e.ChatMessage.ChatReply?.ParentUserLogin == e.ChatMessage.BotUsername;
_db.SaveChanges(); await _db.SaveChangesAsync();
await Behaver.Instance.ActOn(m); await Behaver.Instance.ActOn(m);
_db.SaveChanges(); await _db.SaveChangesAsync();
} }
private async void Client_OnConnected(object sender, OnConnectedArgs e) private async void Client_OnConnected(object sender, OnConnectedArgs e)
{ {
var selfUser = UpsertAccount(e.BotUsername, protocolAsChannel.Id); var selfAccount = UpsertAccount(e.BotUsername, protocolAsChannel.Id);
await _db.SaveChangesAsync(); await _db.SaveChangesAsync();
Behaver.Instance.Selves.Add(selfUser); Behaver.Instance.MarkSelf(selfAccount);
Console.WriteLine($"Connected to {e.AutoJoinChannel}"); Console.WriteLine($"Connected to {e.AutoJoinChannel}");
} }

View File

@ -1,8 +1,42 @@
# discord-bot # discord-bot
copy appsettings.json and fill it in copy appsettings.json to appsettings.ENV.json and fill it in. dotnet seems to understand files called appsettings.json (and appsettings.xml?) and knows how to overwrite *specific values found within* the .[ENV].[extension] version
# auth link # auth link
https://discord.com/oauth2/authorize?client_id=913003037348491264&permissions=274877942784&scope=bot https://discord.com/oauth2/authorize?client_id=913003037348491264&permissions=274877942784&scope=bot
that's read messages/view channels, send messages, send messages in threads, and attach files. but not add reactions? that's read messages/view channels, send messages, send messages in threads, and attach files. but not add reactions?
# concepts
## Data Types
### Accounts
a `User` can have multiple `Account`s. e.g., @adam:greyn.club? that's an "account". I, however, am a `User`. An `Account` has references to the `Channels` its seen in.
### Attachment
debating whether to save a copy of every single attachment. Discord allows 25MB attachments, and shtikbot lives in several art channels.
### Channel
a place where communication can happen. any level of these can have any number of children. In matrix, everything is a "room" - even spaces and threads. Seems like a fine idea. So for vassago, a discord "channel" is a channel. a "thread" is a child of that channel. a "category" is a parent of that channel. A "server" (formerly "guild") is a parent of that channel. and fuck it, Discord itself is a "channel". Includes permissions vassago has for a channel; MaxAttachmentBytes, etc. go down the hierarchy until you find an override.
### FeaturePermission
the permissions of a feature. It can be restricted to accounts, to users, to channels. It has an internal name... and tag? and it can be (or not be) inheritable?
### Message
a message (duh). features bools for "mentions me", the external ID, the reference to the account, the channel.
### User
a person or program who operates an account. recognizing that 2 `Account`s belong to 1 `User` can be done by that user (using LinkMe). I should be able to collapse myself automatically.
## Behavior
both a "feature" and an "anti-feature". a channel might dictate something isn't allowed (lewdness in a g-rated channel). A person might not be allowed to do something - lots of me-only things like directing other bots (and the now rendered-moot Torrent feature). A behavior might need a command alias in a particular channel (freedomunits in jubel's)
so "behavior" might need to tag other data types? do I have it do a full select every time we get a message? ...no, only if the (other) triggering conditions are met. Then you can take your time.

View File

@ -1,12 +0,0 @@
@{
ViewData["Title"] = "Home Page";
}
<div>
<a href="Users">Users</a>
</div>
<div>
<a href="Accounts">Accounts</a>
</div>
<div>
<a href="Channels">Channels</a>
</div>

View File

@ -0,0 +1,58 @@
using System.ComponentModel;
using System.Diagnostics;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using vassago.Models;
namespace vassago.Controllers;
public class ChannelsController : Controller
{
private readonly ILogger<ChannelsController> _logger;
private readonly ChattingContext _db;
public ChannelsController(ILogger<ChannelsController> logger, ChattingContext db)
{
_logger = logger;
_db = db;
}
public async Task<IActionResult> Index(string searchString)
{
return _db.Channels != null ?
View(_db.Channels.Include(u => u.ParentChannel).ToList().OrderBy(c => c.LineageSummary)) :
Problem("Entity set '_db.Channels' is null.");
}
public async Task<IActionResult> Details(Guid id)
{
if(_db.Channels == null)
return Problem("Entity set '_db.Channels' is null.");
//"but adam", says the strawman, "why load *every* channel and walk your way up? surely there's a .Load command that works or something."
//eh. I checked. Not really. You could make an SQL view that recurses its way up, meh idk how. You could just eagerly load *every* related object...
//but that would take in all the messages.
//realistically I expect this will have less than 1MB of total "channels", and several GB of total messages per (text) channel.
var AllChannels = await _db.Channels
.Include(u => u.SubChannels)
.Include(u => u.Users)
.Include(u => u.ParentChannel)
.ToListAsync();
var channel = AllChannels.First(u => u.Id == id);
var walker = channel;
while(walker != null)
{
ViewData["breadcrumbs"] = $"<a href=\"{Url.ActionLink(action: "Details", controller: "Channels", values: new {id = walker.Id})}\">{walker.DisplayName}</a>/" +
ViewData["breadcrumbs"];
walker = walker.ParentChannel;
}
return View(
new Tuple<Channel, Enumerations.LewdnessFilterLevel, Enumerations.MeannessFilterLevel>(
channel, channel.EffectivePermissions.LewdnessFilterLevel, channel.EffectivePermissions.MeannessFilterLevel
));
}
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
return View(new ErrorPageViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
}

View File

@ -0,0 +1,193 @@
using System.Diagnostics;
using System.Text;
using System.Web;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.FileSystemGlobbing.Internal.PathSegments;
using vassago.Models;
namespace vassago.Controllers;
public class HomeController : Controller
{
private readonly ILogger<HomeController> _logger;
private readonly ChattingContext _db;
public HomeController(ILogger<HomeController> logger, ChattingContext db)
{
_logger = logger;
_db = db;
}
public IActionResult Index()
{
var allAccounts = _db.Accounts.ToList();
var allChannels = _db.Channels.Include(c => c.Users).ToList();
var sb = new StringBuilder();
sb.Append("[");
sb.Append("{text: \"channels\", nodes: [");
var first = true;
var topLevelChannels = _db.Channels.Where(x => x.ParentChannel == null);
foreach (var topLevelChannel in topLevelChannels)
{
if (first)
{
first = false;
}
else
{
sb.Append(',');
}
serializeChannel(ref sb, ref allChannels, ref allAccounts, topLevelChannel);
}
sb.Append("]}");
if (allChannels.Any())
{
sb.Append(",{text: \"orphaned channels\", nodes: [");
first = true;
while (true)
{
if (first)
{
first = false;
}
else
{
sb.Append(',');
}
serializeChannel(ref sb, ref allChannels, ref allAccounts, allChannels.First());
if (!allChannels.Any())
{
break;
}
}
sb.Append("]}");
}
if (allAccounts.Any())
{
sb.Append(",{text: \"channelless accounts\", nodes: [");
first = true;
foreach (var acc in allAccounts)
{
if (first)
{
first = false;
}
else
{
sb.Append(',');
}
serializeAccount(ref sb, acc);
}
sb.Append("]}");
}
var users = _db.Users.ToList();
if(users.Any())
{
sb.Append(",{text: \"users\", nodes: [");
first=true;
//refresh list; we'll be knocking them out again in serializeUser
allAccounts = _db.Accounts.ToList();
foreach(var user in users)
{
if (first)
{
first = false;
}
else
{
sb.Append(',');
}
serializeUser(ref sb, ref allAccounts, user);
}
sb.Append("]}");
}
sb.Append("]");
ViewData.Add("treeString", sb.ToString());
return View("Index");
}
private void serializeChannel(ref StringBuilder sb, ref List<Channel> allChannels, ref List<Account> allAccounts, Channel currentChannel)
{
allChannels.Remove(currentChannel);
//"but adam", you say, "there's an href attribute, why make a link?" because that makes the entire bar a link, and trying to expand the node will probably click the link
sb.Append($"{{\"text\": \"<a href=\\\"{Url.ActionLink(action: "Details", controller: "Channels", values: new {id = currentChannel.Id})}\\\">{currentChannel.DisplayName}</a>\"");
var theseAccounts = allAccounts.Where(a => a.SeenInChannel?.Id == currentChannel.Id).ToList();
allAccounts.RemoveAll(a => a.SeenInChannel?.Id == currentChannel.Id);
var first = true;
if (currentChannel.SubChannels != null || theseAccounts != null)
{
sb.Append(", \"nodes\": [");
}
if (currentChannel.SubChannels != null)
{
foreach (var subChannel in currentChannel.SubChannels ?? new List<Channel>())
{
if (first)
{
first = false;
}
else
{
sb.Append(',');
}
serializeChannel(ref sb, ref allChannels, ref allAccounts, subChannel);
}
if (theseAccounts != null)
{
sb.Append(',');
}
}
if (theseAccounts != null)
{
first = true;
sb.Append($"{{\"text\": \"(accounts: {theseAccounts.Count()})\", \"expanded\":true, nodes:[");
foreach (var account in theseAccounts)
{
if (first)
{
first = false;
}
else
{
sb.Append(',');
}
serializeAccount(ref sb, account);
}
sb.Append("]}");
}
sb.Append("]}");
}
private void serializeAccount(ref StringBuilder sb, Account currentAccount)
{
sb.Append($"{{\"text\": \"{currentAccount.DisplayName}\"}}");
}
private void serializeUser(ref StringBuilder sb, ref List<Account> allAccounts, User currentUser)
{
sb.Append($"{{\"text\": " +
$"\"<a href=\\\"{Url.ActionLink(action: "Details", controller: "Users", values: new {id = currentUser.Id})}\\\">"
+ currentUser.DisplayName +
"</a>\", ");
// \"{currentUser.DisplayName}\", ");
var ownedAccounts = allAccounts.Where(a => a.IsUser == currentUser);
sb.Append("nodes: [");
sb.Append($"{{\"text\": \"owned accounts:\", \"expanded\":true, \"nodes\": [");
if (ownedAccounts != null)
{
foreach (var acc in ownedAccounts)
{
serializeAccount(ref sb, acc);
sb.Append(',');
}
}
sb.Append("]}]}");
}
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
return View(new ErrorPageViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
}

View File

@ -0,0 +1,44 @@
using System.Diagnostics;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using vassago.Models;
namespace vassago.Controllers.api;
[Route("api/[controller]")]
[ApiController]
public class ChannelsController : ControllerBase
{
private readonly ILogger<ChannelsController> _logger;
private readonly ChattingContext _db;
public ChannelsController(ILogger<ChannelsController> logger, ChattingContext db)
{
_logger = logger;
_db = db;
}
[HttpGet("{id}")]
[Produces("application/json")]
public Channel Get(Guid id)
{
return _db.Find<Channel>(id);
}
[HttpPatch]
[Produces("application/json")]
public IActionResult Patch([FromBody] Channel channel)
{
var fromDb = _db.Channels.Find(channel.Id);
if (fromDb == null)
{
_logger.LogError($"attempt to update channel {channel.Id}, not found");
return NotFound();
}
//settable values: lewdness filter level, meanness filter level. maybe i could decorate them...
fromDb.LewdnessFilterLevel = channel.LewdnessFilterLevel;
fromDb.MeannessFilterLevel = channel.MeannessFilterLevel;
_db.SaveChanges();
return Ok(fromDb);
}
}

View File

@ -0,0 +1,113 @@
@using System.ComponentModel
@using Newtonsoft.Json
@model Tuple<Channel, Enumerations.LewdnessFilterLevel, Enumerations.MeannessFilterLevel>
@{
var ThisChannel = Model.Item1;
var IfInheritedLewdnessFilterLevel = Model.Item2;
var IfInheritedMeannessFilterLevel = Model.Item3;
}
@Html.Raw(ViewData["breadcrumbs"])
<table class="table">
<tbody>
<tr>
<th scope="row">Display Name</th>
<td>@ThisChannel.DisplayName</td>
</tr>
<tr>
<th scope="row">Channel type</th>
<td>@(ThisChannel.ChannelType != null ? Enumerations.GetDescription(ThisChannel.ChannelType) : "?")</td>
</tr>
<tr>
<th scope="row">Lewdness Filter Level</th>
<td>
<select name="LewdnessFilterLevel" id="LewdnessFilterLevel" onchange="patchModel(jsonifyChannel())">
<!option value="" @(ThisChannel.LewdnessFilterLevel == null ? "selected" : "")>⤵ inherited - @Enumerations.GetDescription(IfInheritedLewdnessFilterLevel)</!option>
@foreach (Enumerations.LewdnessFilterLevel enumVal in
Enum.GetValues(typeof(Enumerations.LewdnessFilterLevel)))
{
<!option value="@((int)enumVal)" @(ThisChannel.LewdnessFilterLevel == enumVal ? "selected" : "")>
@(Enumerations.GetDescription<Enumerations.LewdnessFilterLevel>(enumVal))</!option>
}
</select>
</td>
</tr>
<tr>
<th scope="row">Links Allowed</th>
<td>@(ThisChannel.LinksAllowed?.ToString() ?? "unknown")</td>
</tr>
<tr>
<th scope="row">Lineage summary</th>
<td>@ThisChannel.LineageSummary</td>
</tr>
<tr>
<th scope="row">max attachment bytes</th>
<td>@ThisChannel.MaxAttachmentBytes (i hear there's "ByteSize")</td>
</tr>
<tr>
<th scope="row">max message length</th>
<td>@(ThisChannel.MaxTextChars?.ToString() ?? "inherited")</td>
</tr>
<tr>
<th scope="row">Meanness Filter Level</th>
<td>
<select name="MeannessFilterLevel" id="MeannessFilterLevel" onchange="patchModel(jsonifyChannel())">
<!option value="" @(ThisChannel.MeannessFilterLevel == null ? "selected" : "")>⤵ inherited - @Enumerations.GetDescription(IfInheritedMeannessFilterLevel)</!option>
@foreach (Enumerations.MeannessFilterLevel enumVal in
Enum.GetValues(typeof(Enumerations.MeannessFilterLevel)))
{
<!option value="@((int)enumVal)" @(ThisChannel.MeannessFilterLevel == enumVal ? "selected" : "")>
@(Enumerations.GetDescription<Enumerations.MeannessFilterLevel>(enumVal))</!option>
}
</select>
</td>
</tr>
<tr>
<th scope="row">Messages (count)</th>
<td>@(ThisChannel.Messages?.Count ?? 0)</td>
</tr>
<tr>
<th scope="row">Protocol</th>
<td>@ThisChannel.Protocol</td>
</tr>
<tr>
<th scope="row">Reactions Possible</th>
<td>@(ThisChannel.ReactionsPossible?.ToString() ?? "inherited")</td>
</tr>
<tr>
<th scope="row">Sub Channels</th>
<td>@(ThisChannel.SubChannels?.Count ?? 0)</td>
</tr>
<tr>
<th scope="row">Users</th>
<td>@(ThisChannel.Users?.Count ?? 0)</td>
</tr>
</tbody>
</table>
@section Scripts{
<script type="text/javascript">
@{
var modelAsString = JsonConvert.SerializeObject(ThisChannel, new JsonSerializerSettings
{
ReferenceLoopHandling = ReferenceLoopHandling.Ignore
});
}
const channelOnLoad = @Html.Raw(modelAsString);
function jsonifyChannel() {
var channelNow = structuredClone(channelOnLoad);
channelNow.SubChannels = null;
channelNow.ParentChannel = null;
channelNow.Messages = null;
channelNow.Users = null;
channelNow.LewdnessFilterLevel = document.querySelector("#LewdnessFilterLevel").value;
channelNow.MeannessFilterLevel = document.querySelector("#MeannessFilterLevel").value;
console.log(channelNow);
return channelNow;
}
</script>
}

View File

@ -0,0 +1,17 @@
@{
ViewData["Title"] = "Home Page";
}
<div id="tree"></div>
tree above.
@section Scripts{
<script type="text/javascript">
function getTree() {
var tree = @Html.Raw(ViewData["treeString"]);
console.log(tree);
return tree;
}
$('#tree').bstreeview({ data: getTree() });
</script>
}

View File

@ -5,6 +5,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - vassago</title> <title>@ViewData["Title"] - vassago</title>
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" /> <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
<link rel="stylesheet" href="~/css/fontawesome.min.css" />
<link rel="stylesheet" href="~/css/bs.min.treeview.css" />
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" /> <link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
<link rel="stylesheet" href="~/vassago.styles.css" asp-append-version="true" /> <link rel="stylesheet" href="~/vassago.styles.css" asp-append-version="true" />
</head> </head>
@ -16,6 +18,7 @@
</div> </div>
<script src="~/lib/jquery/dist/jquery.min.js"></script> <script src="~/lib/jquery/dist/jquery.min.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script> <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
<script src="~/js/bstreeview.min.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script> <script src="~/js/site.js" asp-append-version="true"></script>
@await RenderSectionAsync("Scripts", required: false) @await RenderSectionAsync("Scripts", required: false)
</body> </body>

View File

@ -12,5 +12,5 @@
"TwitchConfigs": [ "TwitchConfigs": [
], ],
"exchangePairsLocation": "assets/exchangepairs.json", "exchangePairsLocation": "assets/exchangepairs.json",
"DBConnectionString": "Host=localhost;Database=db;Username=db;Password=db" "DBConnectionString": "Host=azure.club;Database=db;Username=user;Password=password"
} }

View File

@ -266,7 +266,7 @@
{"item1":"pc", "item2":"AU", "factor":206266.3}, {"item1":"pc", "item2":"AU", "factor":206266.3},
{"item1":"blue whale length", "item2": "m", "factor": 29.9}, {"item1":"blue whale length", "item2": "m", "factor": 29.9},
{"item1":"m", "item2": "ångström", "factor": 10000000000}, {"item1":"m", "item2": "ångström", "factor": 10000000000},
{"item1":"smoot", "item2": "cm", "factor": 170}, {"item1":"smoot", "item2": "ft", "factor": 5.583333333333},
{"item1":"floz", "item2":"mL", "factor":29.57344}, {"item1":"floz", "item2":"mL", "factor":29.57344},
{"item1":"L", "item2":"mL", "factor":1000}, {"item1":"L", "item2":"mL", "factor":1000},

View File

@ -3,11 +3,14 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net7.0</TargetFramework> <TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<NoWarn>$(NoWarn);CA2254</NoWarn>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="bootstrap" Version="5.3.3" />
<PackageReference Include="discord.net" Version="3.10.0" /> <PackageReference Include="discord.net" Version="3.10.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="7.0.20" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.5"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
@ -18,6 +21,9 @@
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="7.0.4" /> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="7.0.4" />
<PackageReference Include="QRCoder" Version="1.4.2" /> <PackageReference Include="QRCoder" Version="1.4.2" />
<PackageReference Include="RestSharp" Version="110.2.0" /> <PackageReference Include="RestSharp" Version="110.2.0" />
<PackageReference Include="Swashbuckle.AspNetCore.Swagger" Version="6.6.2" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="6.6.2" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="6.6.2" />
<PackageReference Include="TwitchLib" Version="3.5.3" /> <PackageReference Include="TwitchLib" Version="3.5.3" />
<PackageReference Include="youtubedlsharp" Version="0.3.1" /> <PackageReference Include="youtubedlsharp" Version="0.3.1" />
</ItemGroup> </ItemGroup>

10
wwwroot/css/bstreeview.min.css vendored Normal file
View File

@ -0,0 +1,10 @@
/*
@preserve
bstreeview.css
Version: 1.2.0
Authors: Sami CHNITER <sami.chniter@gmail.com>
Copyright 2020
License: Apache License 2.0
Project: https://github.com/nhmvienna/bs5treeview
*/
.bstreeview{position:relative;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:1px solid rgba(0,0,0,.125);border-radius:.25rem;padding:0;overflow:hidden}.bstreeview .list-group{margin-bottom:0}.bstreeview .list-group-item{border-radius:0;border-width:1px 0 0 0;padding-top:.5rem;padding-bottom:.5rem;cursor:pointer}.bstreeview .list-group-item:hover{background-color:#dee2e6}.bstreeview>.list-group-item:first-child{border-top-width:0}.bstreeview .state-icon{margin-right:8px}.bstreeview .item-icon{margin-right:5px}

9
wwwroot/css/fontawesome.min.css vendored Normal file

File diff suppressed because one or more lines are too long

10
wwwroot/js/bstreeview.min.js vendored Normal file
View File

@ -0,0 +1,10 @@
/*
@preserve
bstreeview.js
Version: 1.2.0
Authors: Sami CHNITER <sami.chniter@gmail.com>
Copyright 2020
License: Apache License 2.0
Project:https://github.com/nhmvienna/bs5treeview
*/
!function (t, e, i, s) { "use strict"; var n = { expandIcon: "fa fa-angle-down fa-fw", collapseIcon: "fa fa-angle-right fa-fw", expandClass: 'show', indent: 1.25, parentsMarginLeft: "1.25rem", openNodeLinkOnNewTab: !0 }, a = '<div role="treeitem" class="list-group-item" data-bs-toggle="collapse"></div>', d = '<div role="group" class="list-group collapse" id="itemid"></div>', o = '<i class="state-icon"></i>', r = '<i class="item-icon"></i>'; function l(e, i) { this.element = e, this.itemIdPrefix = e.id + "-item-", this.settings = t.extend({}, n, i), this.init() } t.extend(l.prototype, { init: function () { this.tree = [], this.nodes = [], this.settings.data && (this.settings.data.isPrototypeOf(String) && (this.settings.data = t.parseJSON(this.settings.data)), this.tree = t.extend(!0, [], this.settings.data), delete this.settings.data), t(this.element).addClass("bstreeview"), this.initData({ nodes: this.tree }); var i = this; this.build(t(this.element), this.tree, 0), t(this.element).on("click", ".list-group-item", function (s) { t(".state-icon", this).toggleClass(i.settings.expandIcon).toggleClass(i.settings.collapseIcon), s.target.hasAttribute("href") && (i.settings.openNodeLinkOnNewTab ? e.open(s.target.getAttribute("href"), "_blank") : e.location = s.target.getAttribute("href")) }) }, initData: function (e) { if (e.nodes) { var i = e, s = this; t.each(e.nodes, function (t, e) { e.nodeId = s.nodes.length, e.parentId = i.nodeId, s.nodes.push(e), e.nodes && s.initData(e) }) } }, build: function (e, i, s) { var n = this, l = n.settings.parentsMarginLeft; s > 0 && (l = (n.settings.indent + s * n.settings.indent).toString() + "rem;"), s += 1, t.each(i, function (i, g) { var h = t(a).attr("data-bs-target", "#" + n.itemIdPrefix + g.nodeId).attr("style", "padding-left:" + l).attr("aria-level", s); if (g.nodes) { var c = t(o).addClass((g.expanded)?n.settings.expandIcon:n.settings.collapseIcon); h.append(c) } if (g.icon) { var f = t(r).addClass(g.icon); h.append(f) } if (h.append(g.text), g.href && h.attr("href", g.href), g.class && h.addClass(g.class), g.id && h.attr("id", g.id), e.append(h), g.nodes) { var p = t(d).attr("id", n.itemIdPrefix + g.nodeId); e.append(p), n.build(p, g.nodes, s); if (g.expanded) p.addClass(n.settings.expandClass) } }) } }), t.fn.bstreeview = function (e) { return this.each(function () { t.data(this, "plugin_bstreeview") || t.data(this, "plugin_bstreeview", new l(this, e)) }) } }(jQuery, window, document);

View File

@ -2,3 +2,49 @@
// for details on configuring this project to bundle and minify static web assets. // for details on configuring this project to bundle and minify static web assets.
// Write your JavaScript code. // Write your JavaScript code.
function testfunct(caller){
console.log("[gibberish]");
console.log(caller);
}
function patchModel(model)
{
//structure the model your (dang) self into a nice object
console.log(model);
//i know the page url.
console.log(window.location.pathname);
var components = window.location.pathname.split('/');
if(components[2] !== "Details")
{
console.log("wtf are you doing? " + components[2] + " is something other than Details")
}
var type=components[1];
var id=components[3];
//todo: figure out what the URL actually needs to be, rather than assuming you get a whole-ass server to yourself.
//you selfish fuck. What are you, fox?
var apiUrl = "/api/Channels/"
console.log("dexter impression: I am now ready to post the following content:");
console.log(JSON.stringify(model));
fetch(apiUrl, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(model),
})
.then(response => {
if (!response.ok) {
throw new Error('Network response was not "ok". which is not ok.');
}
return response.json();
})
.then(returnedSuccessdata => {
// perhaps a success callback
console.log('returnedSuccessdata:', returnedSuccessdata);
})
.catch(error => {
console.error('Error:', error);
});
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.