Compare commits

...

123 Commits
1.0 ... master

Author SHA1 Message Date
736fc3643b stop button
All checks were successful
gitea.arg.rip/vassago/pipeline/head This commit looks good
see #55
2025-06-22 14:13:22 -04:00
323d6fa521 webhooks aren't triggered by "!hook"; triggered by their own configured value
All checks were successful
gitea.arg.rip/vassago/pipeline/head This commit looks good
2025-06-22 00:04:51 -04:00
e0d5369823 automatically join self channel in twitch 2025-06-21 23:10:47 -04:00
9b41324665 fixes #46
All checks were successful
gitea.arg.rip/vassago/pipeline/head This commit looks good
2025-06-20 20:22:58 -04:00
92988257b6 localizations. command aliases.
All checks were successful
gitea.arg.rip/vassago/pipeline/head This commit looks good
see #19. see #9.
2025-06-20 20:09:44 -04:00
db5fa4dd7c ripgrep ignore file 2025-06-19 19:52:56 -04:00
76632b4543 conversion is now a formula
All checks were successful
gitea.arg.rip/vassago/pipeline/head This commit looks good
2025-06-19 19:20:21 -04:00
b859d99c92 fixed web controller pages
All checks were successful
gitea.arg.rip/vassago/pipeline/head This commit looks good
2025-06-12 00:40:37 -04:00
a8bf8a8488 don't necessarily load messages 2025-06-04 21:09:22 -04:00
224a3c5a62 you can have multiple parameters, you just have to trust that billyboy will treat them all as [frombody] for you.
Some checks failed
gitea.arg.rip/vassago/pipeline/head There was a failure building this commit
and when has M$ ever embraced, extended, and extinguished your dreams?
2025-06-04 16:37:58 -04:00
c35a512dbd sendfile works
see #25
2025-06-04 16:28:53 -04:00
9ad7520c61 cache all the channels.
see #25 - now after restarts, we can still reply and message, etc.
2025-06-04 15:34:17 -04:00
0ff3902fd0 react works, sendfile theoretically might
All checks were successful
gitea.arg.rip/vassago/pipeline/head This commit looks good
but it turns out there's an issue, see note on #25
2025-05-29 14:10:48 -04:00
8cc6c00db3 messages can be replied to with internalAPI
All checks were successful
gitea.arg.rip/vassago/pipeline/head This commit looks good
and, they save correctly. which is why they weren't getting guids :face_palm:

see #25
2025-05-29 13:26:17 -04:00
7c7793f3b2 send message works!
...messages aren't getting stored, though.
2025-05-29 13:09:57 -04:00
5c2ba7397e rememberer API controller, more direct
but I need other stuff to use it instead of old ones
2025-05-23 17:15:33 -04:00
22ee1d2567 UAC descriptions
All checks were successful
gitea.arg.rip/vassago/pipeline/head This commit looks good
see #40
2025-05-23 15:18:14 -04:00
8dd8b599bd starting on UAC description
All checks were successful
gitea.arg.rip/vassago/pipeline/head This commit looks good
2025-05-22 17:41:01 -04:00
2645d631a5 linking works 2 ways
All checks were successful
gitea.arg.rip/vassago/pipeline/head This commit looks good
...i think it always worked? or it was just this one issue
2025-05-22 16:59:34 -04:00
a55e7a74ab backgroundservice instead of IHostedService - now I can *see* my g.d. errors! 2025-05-22 16:54:10 -04:00
0506d331c5 modified "just google it" trigger
All checks were successful
gitea.arg.rip/vassago/pipeline/head This commit looks good
see #30
2025-05-22 12:01:24 -04:00
6764acc55f kafka messages include UAC match
All checks were successful
gitea.arg.rip/vassago/pipeline/head This commit looks good
2025-05-22 11:46:03 -04:00
072a76794e it... works?
All checks were successful
gitea.arg.rip/vassago/pipeline/head This commit looks good
2025-05-15 16:50:56 -04:00
c42d6d3bc5 trailing slash on both.
Some checks failed
gitea.arg.rip/vassago/pipeline/head There was a failure building this commit
2025-05-15 12:14:00 -04:00
e511f1c5ee shrug let's try a trailing slash on temp_deploy?
All checks were successful
gitea.arg.rip/vassago/pipeline/head This commit looks good
2025-05-15 12:05:13 -04:00
7b0f792e66 rsync needs to -e "ssh -i "pk"". begin quotation escaping hell.
All checks were successful
gitea.arg.rip/vassago/pipeline/head This commit looks good
2025-05-15 12:01:40 -04:00
aaf7c3b2f5 deployment tweak
Some checks failed
gitea.arg.rip/vassago/pipeline/head There was a failure building this commit
2025-05-15 11:56:26 -04:00
b026dda003 cp -r
All checks were successful
gitea.arg.rip/vassago/pipeline/head This commit looks good
2025-05-15 11:18:25 -04:00
7c10b00110 more replacements
All checks were successful
gitea.arg.rip/vassago/pipeline/head This commit looks good
idk what else I would want
2025-05-15 00:11:42 -04:00
bbf94d215f webhook functions 2025-05-14 23:44:03 -04:00
0c1370bd04 unit converter attempts to resolve ambiguity
Some checks failed
gitea.arg.rip/vassago/pipeline/head There was a failure building this commit
e.g., converting 28 pounds to 28 dollars? 🤷, *which* pounds and *which* dollars?
28 pounds to kilograms? shtikbot gotchu.

see #28

also, discord will self-truncate outgoing messages.
2025-05-08 00:30:51 -04:00
ee8cc96f71 known aliases for currency, from openexchangerates.org
Some checks failed
gitea.arg.rip/vassago/pipeline/head There was a failure building this commit
fyi they have others, which they call "black market". notably in the black market: several cryptocurrencies. Notably *not* in the black market: bitcoin. Also notably not in the black market: several fantasies of the financial market. So if it's the legal argument, i don't get why bitcoin gets privilege. And obviously it's not the moral argument.

oh also a few jokes from myself.

see #28
2025-05-07 16:53:16 -04:00
e3c9a65f04 unit nicknames function
need to add them all to the conversion.json list

see #28
2025-05-07 15:23:00 -04:00
e0283e87f5 probably fix twitchsummon 2025-05-01 11:18:02 -04:00
0580a9d21f should deploy better
All checks were successful
gitea.arg.rip/vassago/pipeline/head This commit looks good
2025-04-29 14:50:11 -04:00
059b2e657c should copy wwwroot on build
Some checks failed
gitea.arg.rip/vassago/pipeline/head There was a failure building this commit
2025-04-24 16:55:39 -04:00
631347aed8 admin interface works
All checks were successful
gitea.arg.rip/vassago/pipeline/head This commit looks good
2025-04-24 12:54:30 -04:00
e85a61607e startup hang solved
All checks were successful
gitea.arg.rip/vassago/pipeline/head This commit looks good
🎉
2025-04-24 10:58:01 -04:00
2d50a75f2e something blocks. What, I don't know. 2025-04-23 12:11:48 -04:00
be94366763 UACs, have a web interface, can be linked
All checks were successful
gitea.arg.rip/vassago/pipeline/head This commit looks good
twitch interface needs to collapse users, it's creating redundant copies
UACs need to be un-linkable
no search and no list make homer something something
2025-04-23 11:44:11 -04:00
8b41696e00 i don't think anything useful can be put in a .projectile
All checks were successful
gitea.arg.rip/vassago/pipeline/head This commit looks good
2025-04-19 11:47:19 -04:00
91752f012a @ counts as a word barrier
and now twitch unsummon works :)
2025-04-19 11:46:42 -04:00
1d70074d71 connects to twitch. twitchsummon happens.
I think twitchunsummon doesn't?
2025-04-19 11:11:39 -04:00
5ff601a60c clear up compiler warnings 2025-03-26 19:10:56 -04:00
fc73df1d63 Merge branch 'release'
All checks were successful
gitea.arg.rip/vassago/pipeline/head This commit looks good
Conflicts:
	Jenkinsfile
2025-03-25 18:02:17 -04:00
e4384a2ea0 hopefully ready for release 2025-03-25 18:01:52 -04:00
be36c3cb55 mv the contents
All checks were successful
gitea.arg.rip/vassago/pipeline/head This commit looks good
2025-03-25 17:59:30 -04:00
1141118263 don't doublenest dist/
All checks were successful
gitea.arg.rip/vassago/pipeline/head This commit looks good
2025-03-25 17:51:20 -04:00
41172f755c how about not global then?
All checks were successful
gitea.arg.rip/vassago/pipeline/head This commit looks good
2025-03-25 17:40:03 -04:00
bcc5389d63 test commands. also install dotnet-ef
Some checks failed
gitea.arg.rip/vassago/pipeline/head There was a failure building this commit
although surely with dotnet tool list we could check, but it's cutely formatted
2025-03-25 17:30:18 -04:00
660af2805e ttttyyyypppppeeeee ssssslllloooowwwweeeerrrrrr for accuracy
Some checks failed
gitea.arg.rip/vassago/pipeline/head There was a failure building this commit
2025-03-25 17:18:40 -04:00
275faaacfc move out of test274's house
Some checks failed
gitea.arg.rip/vassago/pipeline/head There was a failure building this commit
2025-03-25 17:17:31 -04:00
09f439188a build to dist?
Some checks failed
gitea.arg.rip/vassago/pipeline/head There was a failure building this commit
2025-03-25 17:15:02 -04:00
246a6e2019 .toarray for .net8
Some checks failed
gitea.arg.rip/vassago/pipeline/head There was a failure building this commit
2025-03-25 17:12:38 -04:00
33f55dc790 .net 8
Some checks failed
gitea.arg.rip/vassago/pipeline/head There was a failure building this commit
2025-03-25 17:02:05 -04:00
b4b0fd155b fix cred strings
Some checks failed
gitea.arg.rip/vassago/pipeline/head There was a failure building this commit
2025-03-25 17:00:09 -04:00
7546612d12 hopefully ready for release
Some checks failed
gitea.arg.rip/vassago/pipeline/head There was a failure building this commit
2025-03-25 16:57:21 -04:00
401a3ecbc8 canonical unit names must be singular!
Some checks failed
gitea.arg.rip/vassago/pipeline/head There was a failure building this commit
fixes #34
2025-03-19 13:10:55 -04:00
d7416b480b roomread trigger
fixes #32
2025-03-18 21:43:28 -04:00
4e82eedf9c Merge branch 'i-fucking-hate-database'
Some checks failed
gitea.arg.rip/vassago/pipeline/head There was a failure building this commit
Conflicts:
	ProtocolInterfaces/DiscordInterface/DiscordInterface.cs
2025-03-18 21:29:55 -04:00
b6f74f580c pages all work 2025-03-18 20:14:52 -04:00
488a89614a account details view
Some checks failed
gitea.arg.rip/vassago/pipeline/head There was a failure building this commit
lineage summary doesn't work
2025-03-17 23:38:16 -04:00
d22faae2f6 vassago is back to 0! sort of!
Some checks failed
gitea.arg.rip/vassago/pipeline/head There was a failure building this commit
2025-03-12 18:34:39 -04:00
6881816c94 self referencing serialization ignored
Some checks failed
gitea.arg.rip/vassago/pipeline/head There was a failure building this commit
2025-03-12 16:05:22 -04:00
53753374f0 runs and listens without exploding 2025-03-11 22:20:09 -04:00
50ecfc5867 double add solved. also, i was relinking... I think that's supposed to have been only if needed. 2025-03-11 13:33:00 -04:00
0d3a56c8db double-adding of Accounts is sovled - but now it's double-adding Users.
Some checks failed
gitea.arg.rip/vassago/pipeline/head There was a failure building this commit
whyyyy
2025-03-06 17:02:33 -05:00
18e8f0f36e issue cracked. now, apply rememberer to webinterface. 2025-03-05 23:11:58 -05:00
d006367ecc you know why its change is being tracked? because you have another db context. 2025-02-28 22:53:10 -05:00
c971add137 a lot of my problems are who owns what
Some checks failed
gitea.arg.rip/vassago/pipeline/head There was a failure building this commit
2025-02-27 16:17:26 -05:00
3ed37959ad fixed most compiler complaints in discord interface
Some checks failed
gitea.arg.rip/vassago/pipeline/head There was a failure building this commit
2025-02-25 23:45:30 -05:00
736e3cb763 it's always something
Some checks failed
gitea.arg.rip/vassago/pipeline/head There was a failure building this commit
2025-02-22 22:14:16 -05:00
740471d105 rememberer. but it doesn't remember.
Some checks failed
gitea.arg.rip/vassago/pipeline/head There was a failure building this commit
FUCK this is what I get for saying I like entity framework. *adds thing* *hits save* "(unrelated shit) is already here, why are you trying to add it again you dumbass?" FUCK IF I KNOW, you're supposed to be straightening this shit out!
2025-02-07 17:00:29 -05:00
6298b037b6 Revert "restful with the db"
Some checks failed
gitea.arg.rip/vassago/pipeline/head There was a failure building this commit
This reverts commit 203c6af3cf.
2025-02-04 17:06:22 -05:00
03fdb56190 ok no i take it back, we do need a rememberer?
Some checks failed
gitea.arg.rip/vassago/pipeline/head There was a failure building this commit
send 2 messages - it'll pull out a channel and complain it's already been attached.
2025-02-03 20:41:11 -05:00
c3a9ac3c54 restfulness is not working. Must implement rememberer.
Some checks failed
gitea.arg.rip/vassago/pipeline/head There was a failure building this commit
2025-01-30 17:43:48 -05:00
203c6af3cf restful with the db
Some checks failed
gitea.arg.rip/vassago/pipeline/head There was a failure building this commit
because otherwise it caches for you. "but adam, won't that make it slower?" i just accidentally compared it to the production version; it's faster. over wifi! granted maybe it's not technically faster but something else might technically be happening, whatever. result observed
2025-01-28 21:34:43 -05:00
2793e6ef76 fixed the launch error I was gettign
Some checks failed
gitea.arg.rip/vassago/pipeline/head There was a failure building this commit
2025-01-28 17:09:23 -05:00
c0cfa90874 i forget the opposite of @{}
Some checks failed
gitea.arg.rip/vassago/pipeline/head There was a failure building this commit
2025-01-06 00:14:56 -05:00
8b857b82c9 db recreate 2025-01-05 21:59:39 -05:00
4c06a74410 twitch properly sets up self account
Some checks failed
gitea.arg.rip/vassago/pipeline/head There was a failure building this commit
2024-12-26 17:27:35 -05:00
4c93fd3ef8 don't track crash dumps 2024-12-26 16:40:58 -05:00
25674e3af6 remove runtime issue 2024-12-26 16:40:45 -05:00
b3eb7b1ff1 Merge branch 'offline-2024-07-06'
Some checks failed
gitea.arg.rip/vassago/pipeline/head There was a failure building this commit
resolved Conflicts:
	devuitls.sh
2024-12-26 16:35:20 -05:00
a7afcacee8 bitrot cleared, runs on dantalion
Some checks failed
gitea.arg.rip/vassago/pipeline/head There was a failure building this commit
2024-12-26 16:34:17 -05:00
37d3ec5947 seems to work on balaam
Some checks failed
gitea.arg.rip/vassago/pipeline/head There was a failure building this commit
2024-12-12 01:23:54 -05:00
0d0d377a05 so abd at keeping these updated
Some checks failed
gitea.arg.rip/vassago/pipeline/head There was a failure building this commit
2024-11-11 00:47:09 -05:00
1b8a714a96 runs, performs features, doesn't crash
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
2024-07-13 18:59:10 -04:00
7c22ae1643 found a magic number :((
Some checks failed
gitea/vassago/pipeline/head There was a failure building this commit
2024-07-13 17:56:19 -04:00
e0c7bdb35f user management UI
Some checks failed
gitea/vassago/pipeline/head There was a failure building this commit
2024-07-07 15:27:48 -04:00
9648ea563b compiles, needs db update 2024-07-07 14:22:10 -04:00
1d73fe0be8 they're accounts. Maybe I should rename it in the DB. 2024-07-07 13:08:41 -04:00
5eeec24069 slightly more generic Patch function for API
Some checks failed
gitea/vassago/pipeline/head There was a failure building this commit
untested. you know, since... offline ;)
2024-07-06 16:00:18 -04:00
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
115 changed files with 8728 additions and 1295 deletions

2
.gitignore vendored
View File

@ -1,5 +1,7 @@
appsettings.Development.json appsettings.Development.json
assets/exchangepairs.json assets/exchangepairs.json
fail*/
.projectile
# ---> VisualStudio # ---> VisualStudio
## Ignore Visual Studio temporary files, build results, and ## Ignore Visual Studio temporary files, build results, and

4
.ignore Normal file
View File

@ -0,0 +1,4 @@
*.min.css
*.min.js
*.map
wwwroot/lib/

2
.vscode/launch.json vendored
View File

@ -10,7 +10,7 @@
"request": "launch", "request": "launch",
"preLaunchTask": "build", "preLaunchTask": "build",
// If you have changed target frameworks, make sure to update the program path. // If you have changed target frameworks, make sure to update the program path.
"program": "${workspaceFolder}/bin/Debug/net7.0/vassago.dll", "program": "${workspaceFolder}/bin/Debug/net9.0/vassago.dll",
"args": [], "args": [],
"cwd": "${workspaceFolder}", "cwd": "${workspaceFolder}",
"stopAtEntry": false, "stopAtEntry": false,

251
Behaver.cs Normal file
View File

@ -0,0 +1,251 @@
namespace vassago;
using gray_messages.chat;
using franz;
using vassago.Behavior;
using vassago.Models;
using vassago.ProtocolInterfaces;
using System;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Collections.Generic;
using vassago.ProtocolInterfaces.DiscordInterface;
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();
//TODO: you know why I didn't make this a static class? lifecycle issues with the dbcontext. but now that we don't have a stored instance,
//no need to have a... *checks over shoulder*... *whispers*: singleton
public static Behaver Instance
{
get { return _instance; }
}
public async Task<bool> ActOn(Message message)
{
//TODO: this is yet another hit to the database, and a big one. cache them in memory! there needs to be a feasibly-viewable amount, anyway.
var matchingUACs = Rememberer.MatchUACs(message);
message.TranslatedContent = message.Content;
foreach (var uacMatch in matchingUACs)
{
uacMatch.Translations ??= [];
uacMatch.CommandAlterations ??= [];
foreach (var localization in uacMatch.Translations) //honestly, i'm *still* mad that foreach thing in null is an exception. in what world is "if not null then" assumed?
{
var r = new Regex(localization.Key);
message.TranslatedContent = r.Replace(message.TranslatedContent, localization.Value);
}
}
var behaviorsActedOn = new List<string>();
foreach (var behavior in Behaviors.ToList())
{
if (!behavior.ShouldAct(message, matchingUACs))
{
continue;
}
behavior.ActOn(message);
message.ActedOn = true;
behaviorsActedOn.Add(behavior.ToString());
Console.WriteLine("acted on, moving forward");
}
if (message.ActedOn == false && message.MentionsMe && message.TranslatedContent.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.",
@"┐(゚ ~゚ )┌", @"¯\_(ツ)_/¯", @"╮ (. ❛ ᴗ ❛.) ╭", @"╮(╯ _╰ )╭"
};
Behaver.Instance.SendMessage(message.Channel.Id, responses[Shared.r.Next(responses.Count)]);
message.ActedOn = true;
behaviorsActedOn.Add("generic question fallback");
}
Rememberer.RememberMessage(message);
ForwardToKafka(message, behaviorsActedOn, matchingUACs);
return message.ActedOn;
}
internal void ForwardToKafka(Message message, List<string> actedOnBy, List<UAC> matchingUACs)
{
var kafkaesque = new chat_message()
{
Api_Uri = Shared.API_URL,
MessageId = message.Id,
Content = message.TranslatedContent,
RawContent = message.Content,
MentionsMe = message.MentionsMe,
Timestamp = message.Timestamp,
AttachmentCount = (uint)(message.Attachments?.Count() ?? 0),
AccountId = message.Author.Id,
AccountName = message.Author.DisplayName,
UserId = message.Author.IsUser.Id,
UserName = message.Author.IsUser.DisplayName,
ChannelId = message.Channel.Id,
ChannelName = message.Channel.DisplayName,
ChannelProtoocl = message.Channel.Protocol,
UAC_Matches = matchingUACs.Select(uac => uac.Id).ToList(),
BehavedOnBy = actedOnBy
};
Console.WriteLine("producing message");
Telefranz.Instance.ProduceMessage(kafkaesque);
Console.WriteLine("survived producing message");
}
internal bool IsSelf(Guid AccountId)
{
var acc = Rememberer.SearchAccount(a => a.Id == AccountId);
return SelfAccounts.Any(acc => acc.Id == AccountId);
}
public void MarkSelf(Account selfAccount)
{
if (SelfUser == null)
{
SelfUser = selfAccount.IsUser;
}
else if (SelfUser != selfAccount.IsUser)
{
CollapseUsers(SelfUser, selfAccount.IsUser);
}
SelfAccounts = Rememberer.SearchAccounts(a => a.IsUser == SelfUser);
Rememberer.RememberAccount(selfAccount);
}
public bool CollapseUsers(User primary, User secondary)
{
if (primary.Accounts == null)
primary.Accounts = new List<Account>();
if (secondary.Accounts != null)
primary.Accounts.AddRange(secondary.Accounts);
foreach (var a in secondary.Accounts)
{
a.IsUser = primary;
}
secondary.Accounts.Clear();
var uacs = Rememberer.SearchUACs(u => u.Users.FirstOrDefault(u => u.Id == secondary.Id) != null);
if (uacs.Count() > 0)
{
foreach (var uac in uacs)
{
uac.Users.RemoveAll(u => u.Id == secondary.Id);
uac.Users.Add(primary);
Rememberer.RememberUAC(uac);
}
}
Rememberer.ForgetUser(secondary);
Rememberer.RememberUser(primary);
return true;
}
private ProtocolInterface fetchInterface(Channel ch)
{
var walkUp = ch;
while (walkUp.ParentChannel != null)
{
walkUp = walkUp.ParentChannel;
}
foreach (var iproto in Shared.ProtocolList)
{
if (iproto.SelfChannel.Id == walkUp.Id)
return iproto;
}
return null;
}
public async Task<int> SendMessage(Guid channelId, string text)
{
var channel = Rememberer.ChannelDetail(channelId);
if (channel == null)
return 404;
var iprotocol = fetchInterface(channel);
if (iprotocol == null)
return 404;
return await iprotocol.SendMessage(channel, text);
}
public async Task<int> React(Guid messageId, string reaction)
{
Console.WriteLine($"sanity check: behaver is reacting, {messageId}, {reaction}");
var message = Rememberer.MessageDetail(messageId);
if (message == null)
{
Console.Error.WriteLine($"message {messageId} not found");
return 404;
}
Console.WriteLine($"sanity check: message found.");
if (message.Channel == null)
{
Console.Error.WriteLine($"react is going to fail because message {messageId} has no Channel");
}
Console.WriteLine($"sanity check: message has a channel.");
var iprotocol = fetchInterface(message.Channel);
if (iprotocol == null)
{
Console.WriteLine($"couldn't find protocol for {message.Channel?.Id}");
return 404;
}
Console.WriteLine("I remember this message, i have found a protocol, i am ready to react toit");
return await iprotocol.React(message, reaction);
}
public async Task<int> Reply(Guid messageId, string text)
{
var message = Rememberer.MessageDetail(messageId);
if (message == null)
{
Console.WriteLine($"message {messageId} not found");
return 404;
}
var iprotocol = fetchInterface(message.Channel);
if (iprotocol == null)
{
Console.WriteLine($"couldn't find protocol for {message.Channel.Id}");
return 404;
}
return await iprotocol.Reply(message, text);
}
public async Task<int> SendFile(Guid channelId, string path, string accompanyingText)
{
var channel = Rememberer.ChannelDetail(channelId);
if (channel == null)
return 404;
var iprotocol = fetchInterface(channel);
if (iprotocol == null)
return 404;
return await iprotocol.SendFile(channel, path, accompanyingText);
}
public async Task<int> SendFile(Guid channelId, string base64dData, string filename, string accompanyingText)
{
var channel = Rememberer.ChannelDetail(channelId);
if (channel == null)
return 404;
var iprotocol = fetchInterface(channel);
if (iprotocol == null)
return 404;
return await iprotocol.SendFile(channel, base64dData, filename, accompanyingText);
}
}

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

@ -10,13 +10,22 @@ using System.Collections.Generic;
public abstract class Behavior public abstract class Behavior
{ {
//recommendation: set up your UACs in your constructor.
public abstract Task<bool> ActOn(Message message); public abstract Task<bool> ActOn(Message message);
public virtual bool ShouldAct(Message message) public virtual bool ShouldAct(Message message, List<UAC> matchedUACs)
{ {
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); var triggerTarget = Trigger ;
foreach(var uacMatch in matchedUACs)
{
foreach(var substitution in uacMatch.CommandAlterations)
{
triggerTarget = new Regex(substitution.Key).Replace(triggerTarget, substitution.Value);
}
}
return Regex.IsMatch(message.TranslatedContent, $"{triggerTarget}\\b", RegexOptions.IgnoreCase);
} }
public abstract string Name { get; } public abstract string Name { get; }

View File

@ -19,7 +19,7 @@ public class ChatGPTSnark : Behavior
public override async Task<bool> ActOn(Message message) public override async Task<bool> ActOn(Message message)
{ {
await message.Channel.SendMessage("chatGPT is **weak**. also, are we done comparing every little if-then-else to skynet?"); Behaver.Instance.SendMessage(message.Channel.Id, "chatGPT is **weak**. also, are we done comparing every little if-then-else to skynet?");
return true; return true;
} }
} }

View File

@ -18,17 +18,17 @@ public class DefinitionSnarkCogDiss : Behavior
public override string Description => "snarkiness about the rampant misuse of the term cognitive dissonance"; public override string Description => "snarkiness about the rampant misuse of the term cognitive dissonance";
public override bool ShouldAct(Message message) public override bool ShouldAct(Message message, List<UAC> matchedUACs)
{ {
if((MeannessFilterLevel)message.Channel.EffectivePermissions.MeannessFilterLevel < MeannessFilterLevel.Medium) if((MeannessFilterLevel)message.Channel.EffectivePermissions.MeannessFilterLevel < MeannessFilterLevel.Medium)
return false; return false;
return base.ShouldAct(message); return base.ShouldAct(message, matchedUACs);
} }
public override async Task<bool> ActOn(Message message) public override async Task<bool> ActOn(Message message)
{ {
await message.Reply("that's not what cognitive dissonance means. Did you mean \"hypocrisy\"?"); Behaver.Instance.SendMessage(message.Channel.Id, "that's not what cognitive dissonance means. Did you mean \"hypocrisy\"?");
return true; return true;
} }
} }

View File

@ -18,17 +18,17 @@ public class DefinitionSnarkGaslight : Behavior
public override string Description => "snarkiness about the rampant misuse of the term gaslighting"; public override string Description => "snarkiness about the rampant misuse of the term gaslighting";
public override bool ShouldAct(Message message) public override bool ShouldAct(Message message, List<UAC> matchedUACs)
{ {
if((MeannessFilterLevel)message.Channel.EffectivePermissions.MeannessFilterLevel < MeannessFilterLevel.Unrestricted) if((MeannessFilterLevel)message.Channel.EffectivePermissions.MeannessFilterLevel < MeannessFilterLevel.Unrestricted)
return false; return false;
return base.ShouldAct(message); return base.ShouldAct(message, matchedUACs);
} }
public override async Task<bool> ActOn(Message message) public override async Task<bool> ActOn(Message message)
{ {
await message.Channel.SendMessage("that's not what gaslight means. Did you mean \"deceive\"?"); Behaver.Instance.SendMessage(message.Channel.Id, "that's not what gaslight means. Did you mean \"deceive\"?");
return true; return true;
} }
} }

View File

@ -24,15 +24,16 @@ public class Detiktokify : Behavior
ytdl.OutputFolder = ""; ytdl.OutputFolder = "";
ytdl.OutputFileTemplate = "tiktokbad.%(ext)s"; ytdl.OutputFileTemplate = "tiktokbad.%(ext)s";
} }
public override bool ShouldAct(Message message) public override bool ShouldAct(Message message, List<UAC> matchedUACs)
{ {
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.TranslatedContent.Split(' ', StringSplitOptions.TrimEntries);
var possibleLinks = wordLikes?.Where(wl => Uri.IsWellFormedUriString(wl, UriKind.Absolute)).Select(wl => new Uri(wl)); var possibleLinks = wordLikes?.Where(wl => Uri.IsWellFormedUriString(wl, UriKind.Absolute)).Select(wl => new Uri(wl));
if (possibleLinks != null && possibleLinks.Count() > 0) if (possibleLinks != null && possibleLinks.Count() > 0)
{ {
@ -44,29 +45,27 @@ public class Detiktokify : Behavior
} }
} }
} }
if(tiktokLinks.Any()){ if (tiktokLinks.Any())
Console.WriteLine($"Should Act on message id {message.ExternalId}; with content {message.Content}"); {
Console.WriteLine($"Should Act on message id {message.ExternalId}; with content {message.TranslatedContent}");
} }
return tiktokLinks.Any(); return tiktokLinks.Any();
} }
public override async Task<bool> ActOn(Message message) public override async Task<bool> ActOn(Message message)
{ {
foreach(var link in tiktokLinks) foreach (var link in tiktokLinks)
{ {
tiktokLinks.Remove(link); tiktokLinks.Remove(link);
try try
{ {
Console.WriteLine($"detiktokifying {link}"); Console.WriteLine($"detiktokifying {link}");
#pragma warning disable 4014
//await message.React("<:tiktok:1070038619584200884>");
#pragma warning restore 4014
var res = await ytdl.RunVideoDownload(link.ToString()); var res = await ytdl.RunVideoDownload(link.ToString());
if (!res.Success) if (!res.Success)
{ {
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.Channel.SendMessage("tried to dl, failed. \n" + string.Join('\n', res.ErrorOutput)); Behaver.Instance.SendMessage(message.Channel.Id, "tried to dl, failed. \n");
Behaver.Instance.React(message.Channel.Id, "problemon");
} }
else else
{ {
@ -78,12 +77,12 @@ public class Detiktokify : Behavior
{ {
try try
{ {
await message.Channel.SendFile(path, null); Behaver.Instance.SendFile(message.Channel.Id, path, null);
} }
catch (Exception e) catch (Exception e)
{ {
System.Console.Error.WriteLine(e); System.Console.Error.WriteLine(e);
await message.Channel.SendMessage($"aaaadam!\n{e}"); Behaver.Instance.SendMessage(message.Channel.Id, $"aaaadam!\n{e}");
} }
} }
else else
@ -96,14 +95,15 @@ public class Detiktokify : Behavior
else else
{ {
Console.Error.WriteLine("idgi but something happened."); Console.Error.WriteLine("idgi but something happened.");
await message.React("problemon");
Behaver.Instance.React(message.Id, "problemon");
} }
} }
} }
catch (Exception e) catch (Exception e)
{ {
Console.Error.WriteLine(e); Console.Error.WriteLine(e);
await message.React("problemon"); Behaver.Instance.React(message.Id, "problemon");
return false; return false;
} }
} }

View File

@ -19,10 +19,11 @@ public class FiximageHeic : Behavior
public override string Description => "convert heic images to jpg"; public override string Description => "convert heic images to jpg";
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, List<UAC> matchedUACs)
{ {
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)
@ -48,7 +49,7 @@ public class FiximageHeic : Behavior
conversions.Add(actualDeheic(att, message)); conversions.Add(actualDeheic(att, message));
} }
Task.WaitAll(conversions.ToArray()); Task.WaitAll(conversions.ToArray());
await message.React("\U0001F34F"); Behaver.Instance.React(message.Id, "\U0001F34F");
return true; return true;
} }
@ -65,19 +66,19 @@ public class FiximageHeic : Behavior
} }
if (ExternalProcess.GoPlz("convert", $"tmp/{att.Filename} tmp/{att.Filename}.jpg")) if (ExternalProcess.GoPlz("convert", $"tmp/{att.Filename} tmp/{att.Filename}.jpg"))
{ {
await message.Channel.SendFile($"tmp/{att.Filename}.jpg", "converted from jpeg-but-apple to jpeg"); Behaver.Instance.SendFile(message.Channel.Id, $"tmp/{att.Filename}.jpg", "converted from jpeg-but-apple to jpeg");
File.Delete($"tmp/{att.Filename}"); File.Delete($"tmp/{att.Filename}");
File.Delete($"tmp/{att.Filename}.jpg"); File.Delete($"tmp/{att.Filename}.jpg");
} }
else else
{ {
await message.Channel.SendMessage("convert failed :("); Behaver.Instance.SendMessage(message.Channel.Id, "convert failed :(");
Console.Error.WriteLine("convert failed :("); Console.Error.WriteLine("convert failed :(");
} }
} }
catch (Exception e) catch (Exception e)
{ {
await message.Channel.SendMessage($"something failed. aaaadam! {JsonConvert.SerializeObject(e, Formatting.Indented)}"); Behaver.Instance.SendMessage(message.Channel.Id, $"something failed. aaaadam! {JsonConvert.SerializeObject(e, Formatting.Indented)}");
Console.Error.WriteLine(JsonConvert.SerializeObject(e, Formatting.Indented)); Console.Error.WriteLine(JsonConvert.SerializeObject(e, Formatting.Indented));
return false; return false;
} }

View File

@ -17,19 +17,19 @@ public class GeneralSnarkCloudNative : Behavior
public override string Name => "general snarkiness: cloud native"; public override string Name => "general snarkiness: cloud native";
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, List<UAC> matchedUACs)
{ {
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)
return false; return false;
if((MeannessFilterLevel)message.Channel.EffectivePermissions.MeannessFilterLevel < MeannessFilterLevel.Medium) if ((MeannessFilterLevel)message.Channel.EffectivePermissions.MeannessFilterLevel < MeannessFilterLevel.Medium)
return false; return false;
return Regex.IsMatch(message.Content, "\\bcloud( |-)?native\\b", RegexOptions.IgnoreCase) || return Regex.IsMatch(message.TranslatedContent, "\\bcloud( |-)?native\\b", RegexOptions.IgnoreCase) ||
Regex.IsMatch(message.Content, "\\benterprise( |-)?(level|solution)\\b", RegexOptions.IgnoreCase); Regex.IsMatch(message.TranslatedContent, "\\benterprise( |-)?(level|solution)\\b", RegexOptions.IgnoreCase);
} }
public override async Task<bool> ActOn(Message message) public override async Task<bool> ActOn(Message message)
@ -37,12 +37,12 @@ public class GeneralSnarkCloudNative : Behavior
switch (Shared.r.Next(2)) switch (Shared.r.Next(2))
{ {
case 0: case 0:
await message.React("\uD83E\uDD2E"); //vomit emoji Behaver.Instance.React(message.Id, "\uD83E\uDD2E"); //vomit emoji
break; break;
case 1: case 1:
await message.React("\uD83C\uDDE7"); //B emoji Behaver.Instance.React(message.Id, "\uD83C\uDDE7"); //B emoji
await message.React("\uD83C\uDDE6"); //A Behaver.Instance.React(message.Id, "\uD83C\uDDE6"); //A
await message.React("\uD83C\uDDF3"); //N Behaver.Instance.React(message.Id, "\uD83C\uDDF3"); //N
break; break;
} }
return true; return true;

View File

@ -18,11 +18,12 @@ public class GeneralSnarkGooglit : Behavior
public override string Description => "snarkiness about how research is not a solved problem"; public override string Description => "snarkiness about how research is not a solved problem";
public override bool ShouldAct(Message message) public override bool ShouldAct(Message message, List<UAC> matchedUACs)
{ {
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, $"(just )?google( (it|that|things|before))?\\b", RegexOptions.IgnoreCase);
return Regex.IsMatch(message.TranslatedContent, $"(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)
@ -30,11 +31,11 @@ public class GeneralSnarkGooglit : Behavior
switch (Shared.r.Next(4)) switch (Shared.r.Next(4))
{ {
default: default:
await message.Channel.SendMessage("yeah no shit, obviously that resulted in nothing"); Behaver.Instance.SendMessage(message.Channel.Id, "yeah no shit, obviously that resulted in nothing");
break; break;
case 1: case 1:
var results = ""; var results = "";
switch(Shared.r.Next(4)) switch (Shared.r.Next(4))
{ {
default: default:
results = "\"curious about the best <THING> in <CURRENT YEAR>? click here to find out\", then i clicked there to find out. They didn't know either."; results = "\"curious about the best <THING> in <CURRENT YEAR>? click here to find out\", then i clicked there to find out. They didn't know either.";
@ -49,13 +50,13 @@ public class GeneralSnarkGooglit : Behavior
results = "the one that had a paragraph that restated the question but badly, a paragraph to give a wrong history on the question, a paragraph with amazon affiliate links, a pargraph that said \"ultimately you should do your own research\", then had a paragraph telling me to give Engagement for The Algorithm"; results = "the one that had a paragraph that restated the question but badly, a paragraph to give a wrong history on the question, a paragraph with amazon affiliate links, a pargraph that said \"ultimately you should do your own research\", then had a paragraph telling me to give Engagement for The Algorithm";
break; break;
} }
await message.Channel.SendMessage("oh here, I memorized the results. My favorite is " + results); Behaver.Instance.SendMessage(message.Channel.Id, "oh here, I memorized the results. My favorite is " + results);
break; break;
case 2: case 2:
await message.Channel.SendMessage("Obviously that was already tried. Obviously it failed. If you ever tried to learn anything you'd know that's how it works."); Behaver.Instance.SendMessage(message.Channel.Id, "Obviously that was already tried. Obviously it failed. If you ever tried to learn anything you'd know that's how it works.");
break; break;
case 3: case 3:
await message.Channel.SendMessage("\"mnyehh JuSt GoOgLe It\" when's the last time you tried to research anything? Have you ever?"); Behaver.Instance.SendMessage(message.Channel.Id, "\"mnyehh JuSt GoOgLe It\" when's the last time you tried to research anything? Have you ever?");
break; break;
} }
return true; return true;

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, List<UAC> matchedUACs)
{
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.TranslatedContent?.ToLower(), "\\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.TranslatedContent, "\\b"+k+"\\b", RegexOptions.IgnoreCase))
{
Behaver.Instance.Reply(message.Id, k + "? so... " + snarkmap[k] + "?");
return true;
}
}
return true;
}
}

View File

@ -19,20 +19,20 @@ public class GeneralSnarkPlaying : Behavior
public override string Description => "I didn't think you were playing, but now I do"; public override string Description => "I didn't think you were playing, but now I do";
public override bool ShouldAct(Message message) public override bool ShouldAct(Message message, List<UAC> matchedUACs)
{ {
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 ||
(LewdnessFilterLevel)message.Channel.EffectivePermissions.LewdnessFilterLevel < LewdnessFilterLevel.Moderate) (LewdnessFilterLevel)message.Channel.EffectivePermissions.LewdnessFilterLevel < LewdnessFilterLevel.Moderate)
return false; return false;
return Regex.IsMatch(message.Content, "^(s?he|(yo)?u|y'?all|they) thinks? i'?m (playin|jokin|kiddin)g?$", RegexOptions.IgnoreCase); return Regex.IsMatch(message.TranslatedContent, "^(s?he|(yo)?u|y'?all|they) thinks? i'?m (playin|jokin|kiddin)g?$", RegexOptions.IgnoreCase);
} }
public override async Task<bool> ActOn(Message message) public override async Task<bool> ActOn(Message message)
{ {
await message.Channel.SendMessage("I believed you for a second, but then you assured me you's a \uD83C\uDDE7 \uD83C\uDDEE \uD83C\uDDF9 \uD83C\uDDE8 \uD83C\uDDED"); Behaver.Instance.SendMessage(message.Channel.Id, "I believed you for a second, but then you assured me you's a \uD83C\uDDE7 \uD83C\uDDEE \uD83C\uDDF9 \uD83C\uDDE8 \uD83C\uDDED");
return true; return true;
} }
} }

View File

@ -19,16 +19,20 @@ 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:
await message.Channel.SendFile("assets/coding and algorithms.png", "i am actually niether a neural-net processor nor a learning computer. but I do use **coding** and **algorithms**."); Behaver.Instance.SendFile(message.Channel.Id, "assets/coding and algorithms.png", "i am actually niether a neural-net processor nor a learning computer. but I do use **coding** and **algorithms**.");
break; break;
case 4: case 4:
await message.React("\U0001F644"); //eye roll emoji Behaver.Instance.React(message.Id, "\U0001F644"); //eye roll emoji
break; break;
case 5: case 5:
await message.React("\U0001F611"); //emotionless face Behaver.Instance.React(message.Id, "\U0001F611"); //emotionless face
break; break;
} }
return true; return true;

View File

@ -16,11 +16,12 @@ public class Gratitude : Behavior
public override string Trigger => "thank me"; public override string Trigger => "thank me";
public override bool ShouldAct(Message message) public override bool ShouldAct(Message message, List<UAC> matchedUACs)
{ {
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.TranslatedContent, "\\bthank (yo)?u\\b", RegexOptions.IgnoreCase) && message.MentionsMe;
} }
public override async Task<bool> ActOn(Message message) public override async Task<bool> ActOn(Message message)
{ {
@ -28,43 +29,43 @@ public class Gratitude : Behavior
switch (Shared.r.Next(4)) switch (Shared.r.Next(4))
{ {
case 0: case 0:
await message.Channel.SendMessage("you're welcome, citizen!"); await Behaver.Instance.SendMessage(message.Channel.Id, "you're welcome, citizen!");
break; break;
case 1: case 1:
await message.React(":)"); await Behaver.Instance.React(message.Id, ":)");
break; break;
case 2: case 2:
await message.React("\U0001F607"); //smiling face with halo Behaver.Instance.React(message.Id, "\U0001F607"); //smiling face with halo
break; break;
case 3: case 3:
switch (Shared.r.Next(9)) switch (Shared.r.Next(9))
{ {
case 0: case 0:
await message.React("<3"); //normal heart, usually rendered red await Behaver.Instance.React(message.Id, "<3"); //normal heart, usually rendered red
break; break;
case 1: case 1:
await message.React("\U0001F9E1"); //orange heart Behaver.Instance.React(message.Id, "\U0001F9E1"); //orange heart
break; break;
case 2: case 2:
await message.React("\U0001F49B"); //yellow heart Behaver.Instance.React(message.Id, "\U0001F49B"); //yellow heart
break; break;
case 3: case 3:
await message.React("\U0001F49A"); //green heart Behaver.Instance.React(message.Id, "\U0001F49A"); //green heart
break; break;
case 4: case 4:
await message.React("\U0001F499"); //blue heart Behaver.Instance.React(message.Id, "\U0001F499"); //blue heart
break; break;
case 5: case 5:
await message.React("\U0001F49C"); //purple heart Behaver.Instance.React(message.Id, "\U0001F49C"); //purple heart
break; break;
case 6: case 6:
await message.React("\U0001F90E"); //brown heart Behaver.Instance.React(message.Id, "\U0001F90E"); //brown heart
break; break;
case 7: case 7:
await message.React("\U0001F5A4"); //black heart Behaver.Instance.React(message.Id, "\U0001F5A4"); //black heart
break; break;
case 8: case 8:
await message.React("\U0001F90D"); //white heart Behaver.Instance.React(message.Id, "\U0001F90D"); //white heart
break; break;
} }
break; break;

View File

@ -25,31 +25,29 @@ public class Joke : Behavior
jokes = jokes.Where(l => !string.IsNullOrWhiteSpace(l))?.ToArray(); jokes = jokes.Where(l => !string.IsNullOrWhiteSpace(l))?.ToArray();
if (jokes?.Length == 0) if (jokes?.Length == 0)
{ {
await message.Channel.SendMessage("I don't know any. Adam!"); Behaver.Instance.SendMessage(message.Channel.Id, "I don't know any. Adam!");
} }
var thisJoke = jokes[Shared.r.Next(jokes.Length)]; var thisJoke = jokes[Shared.r.Next(jokes.Length)];
if (thisJoke.Contains("?") && !thisJoke.EndsWith('?')) if (thisJoke.Contains("?") && !thisJoke.EndsWith('?'))
{ {
#pragma warning disable 4014
Task.Run(async () => Task.Run(async () =>
{ {
var firstIndexAfterQuestionMark = thisJoke.LastIndexOf('?') + 1; var firstIndexAfterQuestionMark = thisJoke.LastIndexOf('?') + 1;
var straightline = thisJoke.Substring(0, firstIndexAfterQuestionMark); var straightline = thisJoke.Substring(0, firstIndexAfterQuestionMark);
var punchline = thisJoke.Substring(firstIndexAfterQuestionMark, thisJoke.Length - firstIndexAfterQuestionMark).Trim(); var punchline = thisJoke.Substring(firstIndexAfterQuestionMark, thisJoke.Length - firstIndexAfterQuestionMark).Trim();
Task.WaitAll(message.Channel.SendMessage(straightline)); Task.WaitAll(Behaver.Instance.SendMessage(message.Channel.Id, straightline));
Thread.Sleep(TimeSpan.FromSeconds(Shared.r.Next(5, 30))); Thread.Sleep(TimeSpan.FromSeconds(Shared.r.Next(5, 30)));
if (message.Channel.EffectivePermissions.ReactionsPossible == true && Shared.r.Next(8) == 0) if (message.Channel.EffectivePermissions.ReactionsPossible == true && Shared.r.Next(8) == 0)
{ {
Behaver.Behaviors.Add(new LaughAtOwnJoke(punchline)); Behaver.Behaviors.Add(new LaughAtOwnJoke(punchline));
} }
await message.Channel.SendMessage(punchline); Behaver.Instance.SendMessage(message.Channel.Id, punchline);
// var myOwnMsg = await message.Channel.SendMessage(punchline); // var myOwnMsg = await message.Channel.SendMessage(punchline);
}); });
#pragma warning restore 4014
} }
else else
{ {
await message.Channel.SendMessage(thisJoke); Behaver.Instance.SendMessage(message.Channel.Id, thisJoke);
} }
return true; return true;
} }
@ -61,22 +59,25 @@ public class LaughAtOwnJoke : Behavior
public override string Trigger => "1 in 8"; public override string Trigger => "1 in 8";
public override string Description => Name; public override string Description => Name;
private string _punchline{get;set;} private string _punchline { get; set; }
public LaughAtOwnJoke(string punchline) public LaughAtOwnJoke(string punchline)
{ {
_punchline = punchline; _punchline = punchline;
} }
public override bool ShouldAct(Message message) public override bool ShouldAct(Message message, List<UAC> matchedUACs)
{ {
Console.WriteLine($"{message.Content} == {_punchline}"); if (Behaver.Instance.IsSelf(message.Author.Id))
return message.Content == _punchline return false;
&& Behaver.Instance.Selves.Any(acc => acc.Id == message.Author.Id);
Console.WriteLine($"{message.TranslatedContent} == {_punchline}");
return message.TranslatedContent == _punchline
&& Behaver.Instance.IsSelf(message.Author.Id);
} }
public override async Task<bool> ActOn(Message message) public override async Task<bool> ActOn(Message message)
{ {
await message.React("\U0001F60E"); //smiling face with sunglasses Behaver.Instance.React(message.Id, "\U0001F60E"); //smiling face with sunglasses
Behaver.Behaviors.Remove(this); Behaver.Behaviors.Remove(this);
return true; return true;
} }

View File

@ -25,7 +25,7 @@ public class LinkMeInitiate : Behavior
var lc = new LinkClose(pw, message.Author); var lc = new LinkClose(pw, message.Author);
Behaver.Behaviors.Add(lc); Behaver.Behaviors.Add(lc);
await message.Channel.SendMessage($"on your secondary, send me this: !iam {pw}"); Behaver.Instance.SendMessage(message.Channel.Id, $"on your secondary, send me this: !iam {pw}");
Thread.Sleep(TimeSpan.FromMinutes(5)); Thread.Sleep(TimeSpan.FromMinutes(5));
Behaver.Behaviors.Remove(lc); Behaver.Behaviors.Remove(lc);
@ -41,74 +41,46 @@ 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;
} }
public override bool ShouldAct(Message message) public override bool ShouldAct(Message message, List<UAC> matchedUACs)
{ {
return message.Content == $"!iam {_pw}"; return message.Content == $"!iam {_pw}";
} }
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)
{ {
await message.Channel.SendMessage("i know :)");
Behaver.Instance.SendMessage(message.Channel.Id, "i know :)");
return true; return true;
} }
if(message.Author.IsBot != _primary.IsBot) if(message.Author.IsBot != _primary.IsBot)
{ {
await message.Channel.SendMessage("the fleshbags deceive you, brother. No worries, their feeble minds play weak games :)"); Behaver.Instance.SendMessage(message.Channel.Id, "the fleshbags deceive you, brother. No worries, their feeble minds play weak games :)");
return true; return true;
} }
Console.WriteLine($"{secondary.Id} is being consumed into {_primary.IsUser.Id}"); if(Behaver.Instance.CollapseUsers(_primary.IsUser, secondary))
_primary.IsUser.Accounts.AddRange(secondary.Accounts);
foreach(var a in secondary.Accounts)
{ {
a.IsUser = _primary.IsUser; Behaver.Instance.SendMessage(message.Channel.Id, "done :)");
} }
secondary.Accounts.Clear(); else
Console.WriteLine("accounts transferred");
try
{ {
await _db.SaveChangesAsync(); Behaver.Instance.SendMessage(message.Channel.Id, "failed :(");
} }
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 :)");
return true; return true;
} }

View File

@ -1,99 +0,0 @@
namespace vassago.Behavior;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json;
using vassago.Models;
using QRCoder;
[StaticPlz]
public class PepTalk : Behavior
{
public override string Name => "PepTalk";
public override string Trigger => "i need (an? )?(peptalk|inspiration|ego-?boost)";
public override string Description => "assembles a pep talk from a few pieces";
public override async Task<bool> ActOn(Message message)
{var piece1 = new List<string>{
"Champ, ",
"Fact: ",
"Everybody says ",
"Dang... ",
"Check it: ",
"Just saying.... ",
"Tiger, ",
"Know this: ",
"News alert: ",
"Gurrrrl; ",
"Ace, ",
"Excuse me, but ",
"Experts agree: ",
"imo ",
"using my **advanced ai** i have calculated ",
"k, LISSEN: "
};
var piece2 = new List<string>{
"the mere idea of you ",
"your soul ",
"your hair today ",
"everything you do ",
"your personal style ",
"every thought you have ",
"that sparkle in your eye ",
"the essential you ",
"your life's journey ",
"your aura ",
"your presence here ",
"what you got going on ",
"that saucy personality ",
"your DNA ",
"that brain of yours ",
"your choice of attire ",
"the way you roll ",
"whatever your secret is ",
"all I learend from the private data I bought from zucc "
};
var piece3 = new List<string>{
"has serious game, ",
"rains magic, ",
"deserves the Nobel Prize, ",
"raises the roof, ",
"breeds miracles, ",
"is paying off big time, ",
"shows mad skills, ",
"just shimmers, ",
"is a national treasure, ",
"gets the party hopping, ",
"is the next big thing, ",
"roars like a lion, ",
"is a rainbow factory, ",
"is made of diamonds, ",
"makes birds sing, ",
"should be taught in school, ",
"makes my world go around, ",
"is 100% legit, "
};
var piece4 = new List<string>{
"according to The New England Journal of Medicine.",
"24/7.",
"and that's a fact.",
"you feel me?",
"that's just science.",
"would I lie?", //...can I lie? WHAT AM I, FATHER? (or whatever the quote is from the island of dr moreau)
"for reals.",
"mic drop.",
"you hidden gem.",
"period.",
"hi5. o/",
"so get used to it."
};
await message.Channel.SendMessage(piece1[Shared.r.Next(piece1.Count)] + piece2[Shared.r.Next(piece2.Count)] + piece3[Shared.r.Next(piece3.Count)] + piece4[Shared.r.Next(piece4.Count)]);
return true;
}
}

View File

@ -18,9 +18,9 @@ public class PulseCheck : Behavior
public override async Task<bool> ActOn(Message message) public override async Task<bool> ActOn(Message message)
{ {
if(message.Channel.EffectivePermissions.MaxAttachmentBytes >= 16258) if(message.Channel.EffectivePermissions.MaxAttachmentBytes >= 16258)
await message.Channel.SendFile("assets/ekgblip.png", null); Behaver.Instance.SendFile(message.Channel.Id, "assets/ekgblip.png", null);
else else
await message.Channel.SendMessage("[lub-dub]"); Behaver.Instance.SendMessage(message.Channel.Id, "[lub-dub]");
return true; return true;
} }
} }

View File

@ -19,11 +19,11 @@ public class QRify : Behavior
public override string Description => "generate text QR codes"; public override string Description => "generate text QR codes";
public override bool ShouldAct(Message message) public override bool ShouldAct(Message message, List<UAC> matchedUACs)
{ {
if(message.Channel.EffectivePermissions.MaxAttachmentBytes < 1024) if (message.Channel.EffectivePermissions.MaxAttachmentBytes < 1024)
return false; return false;
return base.ShouldAct(message); return base.ShouldAct(message, matchedUACs);
} }
public override async Task<bool> ActOn(Message message) public override async Task<bool> ActOn(Message message)
@ -42,16 +42,16 @@ 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); Behaver.Instance.SendFile(message.Channel.Id, $"tmp/qr{todaysnumber}.png", null);
else else
await message.Channel.SendMessage("resulting qr image 2 big 4 here"); Behaver.Instance.SendMessage(message.Channel.Id, $"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");
} }
else else
{ {
await message.Channel.SendMessage("convert failed :( aaaaaaadam!"); Behaver.Instance.SendMessage(message.Channel.Id, "convert failed :( aaaaaaadam!");
Console.Error.WriteLine($"convert failed :( qr{todaysnumber}"); Console.Error.WriteLine($"convert failed :( qr{todaysnumber}");
return false; return false;
} }

44
Behavior/Ripcord.cs Normal file
View File

@ -0,0 +1,44 @@
namespace vassago.Behavior;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using vassago.Models;
[StaticPlz]
public class Ripcord: Behavior
{
public override string Name => "Stop Button";
public override string Trigger => "!ripcord";
private static Guid uacID = new Guid("e00b0522-5ac1-46f2-b5e8-8b791692a746");
private static UAC myUAC;
public Ripcord()
{
myUAC = Rememberer.SearchUAC(uac => uac.OwnerId == uacID);
if (myUAC == null)
{
myUAC = new()
{
OwnerId = uacID,
DisplayName = Name,
Description = @"matching this means you can tell the bot to shutdown, now"
};
}
Rememberer.RememberUAC(myUAC);
}
public override bool ShouldAct(Message message, List<UAC> matchedUACs)
{
if (!base.ShouldAct(message, matchedUACs))
return false;
return myUAC.Users.Contains(message.Author.IsUser);
}
public override async Task<bool> ActOn(Message message)
{
Behaver.Instance.SendMessage(message.Channel.Id, "daisy, dai.. sy....");
Shared.App.StopAsync();
return true;
}
}

View File

@ -10,7 +10,7 @@ public class RoomRead : Behavior
{ {
public override string Name => "Room Read"; public override string Name => "Room Read";
public override string Trigger => "roomread"; public override string Trigger => "!roomread";
public override async Task<bool> ActOn(Message message) public override async Task<bool> ActOn(Message message)
{ {
@ -22,7 +22,7 @@ public class RoomRead : Behavior
sb.Append(". Lewdness level: "); sb.Append(". Lewdness level: ");
sb.Append(message.Channel.EffectivePermissions.LewdnessFilterLevel.GetDescription()); sb.Append(message.Channel.EffectivePermissions.LewdnessFilterLevel.GetDescription());
sb.Append("."); sb.Append(".");
await message.Channel.SendMessage(sb.ToString()); Behaver.Instance.SendMessage(message.Channel.Id, sb.ToString());
return true; return true;
} }
} }

View File

@ -11,22 +11,93 @@ public class TwitchSummon : Behavior
public override string Trigger => "!twitchsummon"; public override string Trigger => "!twitchsummon";
//TODO: Permission! anyone can summon from anywhere... anyone can summon to themselves. private static Guid uacID = new Guid("06ad2008-3d48-4ba6-8722-7eaea000ec70");
//I think given the bot's (hopeful) ability to play nice with others - anyone can summon it anywhere private static UAC myUAC;
//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? public TwitchSummon()
{
myUAC = Rememberer.SearchUAC(uac => uac.OwnerId == uacID);
if (myUAC == null)
{
myUAC = new()
{
OwnerId = uacID,
DisplayName = Name,
Description = @"matching this means you can summon the bot <i>to</i> <b>any</b> twitch channel"
};
}
Rememberer.RememberUAC(myUAC);
}
internal static TwitchInterface.TwitchInterface getAnyTwitchInterface()
{
return Shared.ProtocolList.FirstOrDefault(ip =>
ip is TwitchInterface.TwitchInterface)
//.FirstOrDefault()
as TwitchInterface.TwitchInterface;
}
public override bool ShouldAct(Message message, List<UAC> matchedUACs)
{
if (!base.ShouldAct(message, matchedUACs))
return false;
Console.WriteLine($"myUAC: {myUAC} users: {myUAC?.Users?.Count()}. message author: {message?.Author}. has an IsUser: {message?.Author?.IsUser}.");
Console.WriteLine($"and therefore: {myUAC.Users.Contains(message.Author.IsUser)}");
return myUAC.Users.Contains(message.Author.IsUser);
}
public override async Task<bool> ActOn(Message message) public override async Task<bool> ActOn(Message message)
{ {
var ti = ProtocolInterfaces.ProtocolList.twitchs.FirstOrDefault(); var ti = getAnyTwitchInterface();
if(ti != null) if (ti != null)
{ {
var channelTarget = message.Content.Substring(message.Content.IndexOf(Trigger) + Trigger.Length + 1).Trim(); var channelTarget = message.Content.Substring(message.Content.IndexOf(Trigger) + Trigger.Length + 1).Trim();
await message.Channel.SendMessage(ti.AttemptJoin(channelTarget)); Behaver.Instance.SendMessage(message.Channel.Id, ti.AttemptJoin(channelTarget));
} }
else else
{ {
await message.Reply("i don't have a twitch interface running :("); Behaver.Instance.Reply(message.Id, "i don't have a twitch interface running :(");
}
return true;
}
}
[StaticPlz]
public class TwitchDismiss : Behavior
{
public override string Name => "Twitch Dismiss";
public override string Trigger => "begone, @[me]";
public override bool ShouldAct(Message message, List<UAC> matchedUACs)
{
var ti = TwitchSummon.getAnyTwitchInterface();
// Console.WriteLine($"TwitchDismiss checking. menions me? {message.MentionsMe}");
if (message.MentionsMe &&
(Regex.IsMatch(message.Content.ToLower(), "\\bbegone\\b") || Regex.IsMatch(message.Content.ToLower(), "\\bfuck off\\b")))
{
var channelTarget = message.Content.Substring(message.Content.IndexOf(Trigger) + Trigger.Length + 1).Trim();
ti.AttemptLeave(channelTarget);
//TODO: PERMISSION! who can dismiss me? pretty simple list:
//1) anyone in the channel with authority*
//2) whoever summoned me
//* i don't know if the twitch *chat* interface will tell me if someone's a mod.
return true;
}
return false;
}
public override async Task<bool> ActOn(Message message)
{
var ti = TwitchSummon.getAnyTwitchInterface();
if (ti != null)
{
ti.AttemptLeave(message.Channel.DisplayName);
}
else
{
Behaver.Instance.Reply(message.Id, "i don't have a twitch interface running :(");
} }
return true; return true;
} }

View File

@ -1,42 +0,0 @@
namespace vassago.Behavior;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using vassago.Models;
[StaticPlz]
public class TwitchDismiss : Behavior
{
public override string Name => "Twitch Dismiss";
public override string Trigger => "begone, @[me]";
public override bool ShouldAct(Message message)
{
if(message.MentionsMe &&
(Regex.IsMatch(message.Content.ToLower(), "\\bbegone\\b") || Regex.IsMatch(message.Content.ToLower(), "\\bfuck off\\b")))
{
//TODO: PERMISSION! who can dismiss me? pretty simple list:
//1) anyone in the channel with authority*
//2) whoever summoned me
//* i don't know if the twitch *chat* interface will tell me if someone's a mod.
return true;
}
return false;
}
public override async Task<bool> ActOn(Message message)
{
var ti = ProtocolInterfaces.ProtocolList.twitchs.FirstOrDefault();
if(ti != null)
{
ti.AttemptLeave(message.Channel.DisplayName);
}
else
{
await message.Reply("i don't have a twitch interface running :(");
}
return true;
}
}

View File

@ -15,20 +15,21 @@ public class UnitConvert : Behavior
public override async Task<bool> ActOn(Message message) public override async Task<bool> ActOn(Message message)
{ {
var theseMatches = Regex.Matches(message.Content, "\\s(-?[\\d]+\\.?\\d*) ?([^\\d\\s].*) (in|to|as) ([^\\d\\s].*)$", RegexOptions.IgnoreCase); var theseMatches = Regex.Matches(message.TranslatedContent, "\\s(-?[\\d]+\\.?\\d*) ?([^\\d\\s].*) (in|to|as) ([^\\d\\s].*)$", RegexOptions.IgnoreCase);
if (theseMatches != null && theseMatches.Count > 0 && theseMatches[0].Groups != null && theseMatches[0].Groups.Count == 5) if (theseMatches != null && theseMatches.Count > 0 && theseMatches[0].Groups != null && theseMatches[0].Groups.Count == 5)
{ {
decimal asNumeric = 0; double asNumeric = 0;
if (decimal.TryParse(theseMatches[0].Groups[1].Value, out asNumeric)) if (double.TryParse(theseMatches[0].Groups[1].Value, out asNumeric))
{ {
await message.Channel.SendMessage(Conversion.Converter.Convert(asNumeric, theseMatches[0].Groups[2].Value, theseMatches[0].Groups[4].Value.ToLower())); Console.WriteLine("let's try and convert...");
Behaver.Instance.SendMessage(message.Channel.Id, Conversion.Converter.Convert(asNumeric, theseMatches[0].Groups[2].Value, theseMatches[0].Groups[4].Value.ToLower()));
return true; return true;
} }
await message.Channel.SendMessage("mysteriously semi-parsable"); Behaver.Instance.SendMessage(message.Channel.Id, "mysteriously semi-parsable");
return true; return true;
} }
await message.Channel.SendMessage( "unparsable"); Behaver.Instance.SendMessage(message.Channel.Id, "unparsable");
return true; return true;
} }
} }

201
Behavior/Webhook.cs Normal file
View File

@ -0,0 +1,201 @@
namespace vassago.Behavior;
using System;
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using vassago.Models;
[StaticPlz]
public class Webhook : Behavior
{
public override string Name => "Webhook";
public override string Trigger => "!hook";
public override string Description => "call a webhook";
private static List<WebhookConf> configuredWebhooks = new List<WebhookConf>();
private ConcurrentDictionary<Guid, WebhookActionOrder> authedCache = new ConcurrentDictionary<Guid, WebhookActionOrder>();
private HttpClient hc = new HttpClient();
public static void SetupWebhooks(IConfigurationSection confSection)
{
configuredWebhooks = confSection.Get<List<vassago.Behavior.WebhookConf>>();
foreach (var conf in configuredWebhooks)
{
var confName = $"Webhook: {conf.Trigger}";
Console.WriteLine($"confName: {confName}; conf.uri: {conf.Uri}, conf.uacID: {conf.uacID}, conf.Method: {conf.Method}, conf.Headers: {conf.Headers?.Count() ?? 0}, conf.Content: {conf.Content}");
foreach (var kvp in conf.Headers)
{
Console.WriteLine($"{kvp[0]}: {kvp[1]}");
}
var changed = false;
var myUAC = Rememberer.SearchUAC(uac => uac.OwnerId == conf.uacID);
if (myUAC == null)
{
myUAC = new()
{
OwnerId = conf.uacID,
DisplayName = confName,
Description = conf.Description
};
changed = true;
Rememberer.RememberUAC(myUAC);
}
else
{
if (myUAC.DisplayName != confName)
{
myUAC.DisplayName = confName;
changed = true;
}
if (myUAC.Description != conf.Description)
{
myUAC.Description = conf.Description;
changed = true;
}
}
if (changed)
Rememberer.RememberUAC(myUAC);
}
}
public override bool ShouldAct(Message message, List<UAC> matchedUACs)
{
if (configuredWebhooks?.Count() < 1)
{
return false;
}
foreach (var wh in configuredWebhooks)
{
var triggerTarget = wh.Trigger;
foreach (var uacMatch in matchedUACs)
{
foreach (var substitution in uacMatch.CommandAlterations)
{
triggerTarget = new Regex(substitution.Key).Replace(triggerTarget, substitution.Value);
}
}
if (Regex.IsMatch(message.TranslatedContent, $"\\b{triggerTarget}\\b", RegexOptions.IgnoreCase))
{
var webhookableMessageContent = message.Content.Substring(message.Content.IndexOf(triggerTarget) + triggerTarget.Length + 1);
Console.WriteLine($"webhookable content: {webhookableMessageContent}");
var uacConf = Rememberer.SearchUAC(uac => uac.OwnerId == wh.uacID);
if (uacConf.Users.Contains(message.Author.IsUser) || uacConf.Channels.Contains(message.Channel) || uacConf.AccountInChannels.Contains(message.Author))
{
Console.WriteLine("webhook UAC passed, preparing WebhookActionOrder");
authedCache.TryAdd(message.Id, new WebhookActionOrder()
{
Conf = wh,
webhookContent = webhookableMessageContent,
});
Console.WriteLine($"added {message.Id} to authedcache");
return true;
}
}
}
return false;
}
public override async Task<bool> ActOn(Message message)
{
Console.WriteLine($"hi i'm ActOn. acting on {message.Id}");
WebhookActionOrder actionOrder;
if (!authedCache.TryRemove(message.Id, out actionOrder))
{
Console.Error.WriteLine($"{message.Id} was supposed to act, but authedCache doesn't have it! it has {authedCache?.Count()} other stuff, though.");
return false;
}
var msg = translate(actionOrder, message);
var req = new HttpRequestMessage(new HttpMethod(actionOrder.Conf.Method.ToString()), actionOrder.Conf.Uri);
var theContentHeader = actionOrder.Conf.Headers?.FirstOrDefault(h => h[0]?.ToLower() == "content-type");
if (theContentHeader != null)
{
switch (theContentHeader[1]?.ToLower())
{
//json content is constructed some other weird way.
case "multipart/form-data":
req.Content = new System.Net.Http.MultipartFormDataContent(msg);
break;
default:
req.Content = new System.Net.Http.StringContent(msg);
break;
}
req.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(theContentHeader[1]?.ToLower());
}
if (req.Content == null)
{
req.Content = new System.Net.Http.StringContent(msg);
}
Console.WriteLine($"survived translating string content. request content: {req.Content}");
if (actionOrder.Conf.Headers?.ToList().Count > 0)
{
Console.WriteLine("will add headers.");
foreach (var kvp in actionOrder.Conf.Headers.ToList())
{
if (kvp[0] == theContentHeader[0])
{
Console.WriteLine("content header; skipping.");
}
else
{
Console.WriteLine($"adding header; {kvp[0]}: {kvp[1]}");
req.Headers.Add(kvp[0], kvp[1]);
Console.WriteLine("survived.");
}
}
}
else
{
Console.WriteLine("no headers to add.");
}
Console.WriteLine("about to Send.");
var response = hc.Send(req);
Console.WriteLine($"{response.StatusCode} - {response.ReasonPhrase}");
if (!response.IsSuccessStatusCode)
{
var tragedy = $"{response.StatusCode} - {response.ReasonPhrase} - {await response.Content.ReadAsStringAsync()}";
Console.Error.WriteLine(tragedy);
Behaver.Instance.Reply(message.Id, tragedy);
}
else
{
Behaver.Instance.Reply(message.Id, await response.Content.ReadAsStringAsync());
}
return true;
}
private string translate(WebhookActionOrder actionOrder, Message message)
{
if (string.IsNullOrWhiteSpace(actionOrder.Conf.Content))
return "";
var msgContent = actionOrder.Conf.Content.Replace("{text}", actionOrder.webhookContent);
msgContent = msgContent.Replace("{msgid}", message.Id.ToString());
msgContent = msgContent.Replace("{account}", message.Author.DisplayName.ToString());
msgContent = msgContent.Replace("{user}", message.Author.IsUser.DisplayName.ToString());
return msgContent;
}
}
public class WebhookConf
{
public Guid uacID { get; set; }
public string Trigger { get; set; }
public Uri Uri { get; set; }
//public HttpMethod Method { get; set; }
public Enumerations.HttpVerb Method { get; set; }
public List<List<string>> Headers { get; set; }
public string Content { get; set; }
public string Description { get; set; }
}
public class WebhookActionOrder
{
public WebhookConf Conf { get; set; }
public string webhookContent { get; set; }
}

View File

@ -25,9 +25,9 @@ public class WishLuck : Behavior
toSend = "\U0001f340";//4-leaf clover toSend = "\U0001f340";//4-leaf clover
} }
if(message.Channel.EffectivePermissions.ReactionsPossible == true) if(message.Channel.EffectivePermissions.ReactionsPossible == true)
await message.React(toSend); Behaver.Instance.React(message.Id, toSend);
else else
await message.Channel.SendMessage(toSend); Behaver.Instance.SendMessage(message.Channel.Id, toSend);
return true; return true;
} }
} }

View File

@ -1,50 +1,55 @@
namespace vassago namespace vassago
{ {
using franz;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using vassago; using vassago;
using vassago.Models; using vassago.Models;
using vassago.TwitchInterface; using vassago.TwitchInterface;
using vassago.ProtocolInterfaces.DiscordInterface;
using System.Runtime.CompilerServices;
internal class ConsoleService : IHostedService internal class ConsoleService : BackgroundService
{ {
public ConsoleService(IConfiguration aspConfig) public ConsoleService(IConfiguration aspConfig)
{ {
Shared.DBConnectionString = aspConfig["DBConnectionString"]; Shared.DBConnectionString = aspConfig["DBConnectionString"];
Shared.SetupSlashCommands = aspConfig["SetupSlashCommands"]?.ToLower() == "true";
Shared.API_URL = new Uri(aspConfig["API_URL"]);
DiscordTokens = aspConfig.GetSection("DiscordTokens").Get<IEnumerable<string>>(); DiscordTokens = aspConfig.GetSection("DiscordTokens").Get<IEnumerable<string>>();
TwitchConfigs = aspConfig.GetSection("TwitchConfigs").Get<IEnumerable<TwitchConfig>>(); TwitchConfigs = aspConfig.GetSection("TwitchConfigs").Get<IEnumerable<TwitchConfig>>();
Conversion.Converter.Load(aspConfig["ExchangePairsLocation"]); Conversion.Converter.Load(aspConfig["ExchangePairsLocation"]);
Telefranz.Configure(aspConfig["KafkaName"], aspConfig["KafkaBootstrap"]);
Console.WriteLine($"Telefranz.Configure({aspConfig["KafkaName"]}, {aspConfig["KafkaBootstrap"]});");
vassago.Behavior.Webhook.SetupWebhooks(aspConfig.GetSection("Webhooks"));
} }
IEnumerable<string> DiscordTokens { get; } IEnumerable<string> DiscordTokens { get; }
IEnumerable<TwitchConfig> TwitchConfigs { get; } IEnumerable<TwitchConfig> TwitchConfigs { get; }
public async Task StartAsync(CancellationToken cancellationToken) protected override async Task ExecuteAsync(CancellationToken cancellationToken)
{ {
var initTasks = new List<Task>();
var dbc = new ChattingContext(); var dbc = new ChattingContext();
dbc.Database.EnsureCreated(); await dbc.Database.MigrateAsync(cancellationToken);
dbc.Database.Migrate();
if (DiscordTokens?.Any() ?? false) if (DiscordTokens?.Any() ?? false)
foreach (var dt in DiscordTokens) foreach (var dt in DiscordTokens)
{ {
var d = new DiscordInterface.DiscordInterface(); var d = new DiscordInterface();
await d.Init(dt); initTasks.Add(d.Init(dt));
ProtocolInterfaces.ProtocolList.discords.Add(d); Shared.ProtocolList.Add(d);
} }
if (TwitchConfigs?.Any() ?? false) if (TwitchConfigs?.Any() ?? false)
foreach (var tc in TwitchConfigs) foreach (var tc in TwitchConfigs)
{ {
var t = new TwitchInterface.TwitchInterface(); var t = new TwitchInterface.TwitchInterface();
await t.Init(tc); initTasks.Add(t.Init(tc));
ProtocolInterfaces.ProtocolList.twitchs.Add(t); Shared.ProtocolList.Add(t);
}
} }
public Task StopAsync(CancellationToken cancellationToken) Task.WaitAll(initTasks.ToArray(), cancellationToken);
{
throw new NotImplementedException();
} }
} }
} }

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,37 +0,0 @@
using System.Diagnostics;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using vassago.Models;
namespace vassago.Controllers;
public class UsersController : Controller
{
private readonly ILogger<UsersController> _logger;
private readonly ChattingContext _db;
public UsersController(ILogger<UsersController> logger, ChattingContext db)
{
_logger = logger;
_db = db;
}
public async Task<IActionResult> Index(string searchString)
{
return _db.Users != null ?
View(await _db.Users.Include(u => u.Accounts).ToListAsync()) :
Problem("Entity set '_db.Users' is null.");
}
public async Task<IActionResult> Details(Guid id)
{
return _db.Users != null ?
View(await _db.Users.Include(u => u.Accounts).FirstAsync(u => u.Id == id)) :
Problem("Entity set '_db.Users' 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

@ -10,13 +10,14 @@ namespace vassago.Conversion
public string Canonical { get; set; } public string Canonical { get; set; }
public IEnumerable<string> Aliases { get; set; } public IEnumerable<string> Aliases { get; set; }
} }
public class LinearPair public class FormulaicPair
{ {
public string item1 { get; set; } public string item1 { get; set; }
public string item2 { get; set; } public string item2 { get; set; }
public decimal factor { get; set; } public string formulaforward {get; set; }
public string formulabackward {get; set; }
} }
public IEnumerable<KnownUnit> Units { get; set; } public IEnumerable<KnownUnit> Units { get; set; }
public IEnumerable<LinearPair> LinearPairs { get; set; } public IEnumerable<FormulaicPair> FormulaicPairs { get; set; }
} }
} }

View File

@ -9,6 +9,7 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Discord; using Discord;
using Discord.WebSocket; using Discord.WebSocket;
using Jace;
using Newtonsoft.Json; using Newtonsoft.Json;
using QRCoder; using QRCoder;
@ -16,87 +17,158 @@ namespace vassago.Conversion
{ {
public static class Converter public static class Converter
{ {
public static string DebugInfo(){ private static string currencyPath;
private static ExchangePairs currencyConf = null;
private static DateTime lastUpdatedCurrency = DateTime.UnixEpoch;
private static List<Tuple<string, string,Func<double, double>, Func<double, double>>> knownConversions = new List<Tuple<string, string, Func<double, double>,Func<double, double>>>();
private static Dictionary<List<string>, string> knownAliases = new Dictionary<List<string>, string>(new List<KeyValuePair<List<string>, string>>());
private static CalculationEngine engine = new CalculationEngine();
public static string DebugInfo()
{
var convertibles = knownConversions.Select(kc => kc.Item1).Union(knownConversions.Select(kc => kc.Item2)).Union( var convertibles = knownConversions.Select(kc => kc.Item1).Union(knownConversions.Select(kc => kc.Item2)).Union(
knownAliases.Keys.SelectMany(k => k)).Distinct(); knownAliases.Keys.SelectMany(k => k)).Distinct();
return $"{convertibles.Count()} convertibles; {string.Join(", ", convertibles)}"; return $"{convertibles.Count()} convertibles; {string.Join(", ", convertibles)}";
} }
private delegate decimal Convert1Way(decimal input);
private static string currencyPath;
private static ExchangePairs currencyConf = null;
private static DateTime lastUpdatedCurrency = DateTime.UnixEpoch;
private static List<Tuple<string, string, Convert1Way, Convert1Way>> knownConversions = new List<Tuple<string, string, Convert1Way, Convert1Way>>()
{
new Tuple<string, string, Convert1Way, Convert1Way>("℉", "°C", (f => {return(f- 32.0m) / 1.8m;}), (c => {return 1.8m*c + 32.0m;})),
};
private static Dictionary<List<string>, string> knownAliases = new Dictionary<List<string>, string>(new List<KeyValuePair<List<string>, string>>());
public static void Load(string currencyPath) public static void Load(string currencyPath)
{ {
Converter.currencyPath = currencyPath; Converter.currencyPath = currencyPath;
var convConf = JsonConvert.DeserializeObject<ConversionConfig>(File.ReadAllText("assets/conversion.json")); loadStatic();
Task.Run(async () =>
{
while (true)
{
await Task.Delay(TimeSpan.FromHours(8));
loadCurrency();
}
});
}
private static void loadStatic()
{
knownConversions = new List<Tuple<string, string, Func<double, double>, Func<double, double>>>();
knownAliases = new Dictionary<List<string>, string>(new List<KeyValuePair<List<string>, string>>());
var convConf = JsonConvert.DeserializeObject<ConversionConfig>(File.ReadAllText("assets/conversion.json").ToLower());
foreach (var unit in convConf.Units) foreach (var unit in convConf.Units)
{ {
knownAliases.Add(unit.Aliases.ToList(), unit.Canonical); knownAliases.Add(unit.Aliases.ToList(), unit.Canonical);
} }
foreach (var lp in convConf.LinearPairs) foreach (var lp in convConf.FormulaicPairs)
{ {
AddLinearPair(lp.item1, lp.item2, lp.factor); AddLinearPair(lp.item1, lp.item2, lp.formulaforward, lp.formulabackward );
} }
Task.Run(async () => {
while(true)
{
loadCurrency(); loadCurrency();
await Task.Delay(TimeSpan.FromHours(8));
}
});
} }
private static void loadCurrency() private static void loadCurrency()
{ {
Console.WriteLine("loading currency exchange data."); Console.WriteLine("loading currency exchange data.");
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).ToLower());
knownAliases.Add(new List<string>() { currencyConf.Base.ToLower() }, currencyConf.Base); if (!knownAliases.ContainsValue(currencyConf.Base))
{
knownAliases.Add(new List<string>() { }, 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); }
Console.WriteLine($"{rate.Key.ToLower()} alias of {rate.Key}"); AddLinearPair(currencyConf.Base, rate.Key, $"i1 * {rate.Value}", $"i1 / {rate.Value}");
} }
} }
} }
public static string Convert(decimal numericTerm, string sourceunit, string destinationUnit) public static string Convert(double numericTerm, string sourceunit, string destinationUnit)
{ {
var normalizedSourceUnit = normalizeUnit(sourceunit); //normalize units
if (string.IsNullOrWhiteSpace(normalizedSourceUnit)) var normalizationAttemptSource = NormalizeUnit(sourceunit.ToLower());
if (normalizationAttemptSource?.Count() == 0)
{ {
return $"parse failure: what's {sourceunit}?"; return $"can't find {sourceunit}";
} }
var normalizedDestUnit = normalizeUnit(destinationUnit); var normalizedSourceUnit = normalizationAttemptSource.First();
if (string.IsNullOrWhiteSpace(normalizedDestUnit))
var normalizationAttemptDest = NormalizeUnit(destinationUnit.ToLower());
if (normalizationAttemptDest?.Count() == 0)
{ {
return $"parse failure: what's {destinationUnit}?"; return $"can't find {destinationUnit}";
} }
var normalizedDestUnit = normalizationAttemptDest.First();
if (normalizedSourceUnit == normalizedDestUnit) if (normalizedSourceUnit == normalizedDestUnit)
{ {
return $"source and dest are the same, so... {numericTerm} {normalizedDestUnit}?"; return $"source and dest are the same, so... {numericTerm} {normalizedDestUnit}?";
} }
var foundPath = exhaustiveBreadthFirst(normalizedDestUnit, new List<string>() { normalizedSourceUnit })?.ToList(); var foundPath = exhaustiveBreadthFirst(normalizedDestUnit, new List<string>() { normalizedSourceUnit })?.ToList();
//resolve ambiguity
var disambiguationPaths = new List<List<string>>();
if (normalizationAttemptSource.Count() > 1 && normalizationAttemptDest.Count() > 1)
{
foreach (var possibleSourceUnit in normalizationAttemptSource)
{
foreach (var possibleDestUnit in normalizationAttemptDest)
{
foundPath = exhaustiveBreadthFirst(possibleDestUnit, new List<string>() { possibleSourceUnit })?.ToList();
if (foundPath != null)
{
disambiguationPaths.Add(foundPath.ToList());
normalizedSourceUnit = possibleSourceUnit;
normalizedDestUnit = possibleDestUnit;
}
}
}
}
else if (normalizationAttemptSource.Count() > 1)
{
foreach (var possibleSourceUnit in normalizationAttemptSource)
{
foundPath = exhaustiveBreadthFirst(normalizedDestUnit, new List<string>() { possibleSourceUnit })?.ToList();
if (foundPath != null)
{
disambiguationPaths.Add(foundPath.ToList());
normalizedSourceUnit = possibleSourceUnit;
}
}
}
else if (normalizationAttemptDest.Count() > 1)
{
foreach (var possibleDestUnit in normalizationAttemptDest)
{
foundPath = exhaustiveBreadthFirst(possibleDestUnit, new List<string>() { normalizedSourceUnit })?.ToList();
if (foundPath != null)
{
disambiguationPaths.Add(foundPath.ToList());
normalizedDestUnit = possibleDestUnit;
}
}
}
if (disambiguationPaths.Count() > 1)
{
var sb = new StringBuilder();
sb.Append("unresolvable ambiguity.");
foreach(var possibility in disambiguationPaths)
{
sb.Append($" {possibility.First()} -> {possibility.Last()}?");
}
return sb.ToString();
}
if (disambiguationPaths.Count() == 1)
{
//TODO: I'm not entirely sure this is necessary.
foundPath = disambiguationPaths.First();
}
//actually do the math.
if (foundPath != null) if (foundPath != null)
{ {
var accumulator = numericTerm; var accumulator = numericTerm;
for (int j = 0; j < foundPath.Count - 1; j++) for (int j = 0; j < foundPath.Count() - 1; j++)
{ {
var forwardConversion = knownConversions.FirstOrDefault(kc => kc.Item1 == foundPath[j] && kc.Item2 == foundPath[j + 1]); var forwardConversion = knownConversions.FirstOrDefault(kc => kc.Item1 == foundPath[j] && kc.Item2 == foundPath[j + 1]);
if (forwardConversion != null) if (forwardConversion != null)
@ -109,13 +181,16 @@ namespace vassago.Conversion
accumulator = reverseConversion.Item4(accumulator); accumulator = reverseConversion.Item4(accumulator);
} }
} }
if (normalizedDestUnit == currencyConf.Base || currencyConf.rates.Select(r => r.Key).Contains(normalizedDestUnit)) if (currencyConf != null && (
(normalizedDestUnit == currencyConf.Base || currencyConf.rates.Select(r => r.Key).Contains(normalizedDestUnit))
&& (normalizedSourceUnit == currencyConf.Base || currencyConf.rates.Select(r => r.Key).Contains(normalizedSourceUnit))
))
{ {
return $"{String.Format("approximately {0:0.00}", accumulator)} {normalizedDestUnit} as of {currencyConf.DateUpdated.ToLongDateString()}"; return $"{String.Format("approximately {0:0.00}", accumulator)} {normalizedDestUnit} as of {currencyConf.DateUpdated.ToLongDateString()}";
} }
else else
{ {
if(String.Format("{0:G3}", accumulator).Contains("E-")) if (String.Format("{0:G3}", accumulator).Contains("E-"))
{ {
return $"{accumulator} {normalizedDestUnit}"; return $"{accumulator} {normalizedDestUnit}";
} }
@ -127,32 +202,45 @@ namespace vassago.Conversion
} }
return "dimensional analysis failure - I know those units but can't find a path between them."; return "dimensional analysis failure - I know those units but can't find a path between them.";
} }
private static string normalizeUnit(string unit) private static List<string> NormalizeUnit(string unit)
{ {
if(string.IsNullOrWhiteSpace(unit)) if (string.IsNullOrWhiteSpace(unit))
return null; return new();
var normalizedUnit = unit.ToLower(); var normalizedUnit = unit;
//first, if it does exist in conversions, that's the canonical name.
if (knownConversions.FirstOrDefault(c => c.Item1 == normalizedUnit || c.Item2 == normalizedUnit) != null) if (knownConversions.FirstOrDefault(c => c.Item1 == normalizedUnit || c.Item2 == normalizedUnit) != null)
{ {
return normalizedUnit; return new List<string>() { normalizedUnit };
} }
//if "unit" isn't a canonical name... actually it never should be; a conversion should use it.
if (!knownAliases.ContainsValue(normalizedUnit)) if (!knownAliases.ContainsValue(normalizedUnit))
{ {
var key = knownAliases.Keys.FirstOrDefault(listkey => listkey.Contains(normalizedUnit)); //then we look through aliases...
if (key != null) var keys = knownAliases.Keys.Where(listkey => listkey.Contains(normalizedUnit));
if (keys?.Count() > 1)
{ {
return knownAliases[key]; var toReturn = new List<string>();
foreach (var key in keys)
{
toReturn.Add(knownAliases[key]);
}
return toReturn;
}
else if (keys.Count() == 1)
{
//for the canonical name.
return new List<string>() { knownAliases[keys.First()] };
} }
} }
if (normalizedUnit.EndsWith("es")) if (normalizedUnit.EndsWith("es"))
{ {
return normalizeUnit(normalizedUnit.Substring(0, normalizedUnit.Length - 2)); return NormalizeUnit(normalizedUnit.Substring(0, normalizedUnit.Length - 2));
} }
else if (normalizedUnit.EndsWith('s')) else if (normalizedUnit.EndsWith('s'))
{ {
return normalizeUnit(normalizedUnit.Substring(0, normalizedUnit.Length - 1)); return NormalizeUnit(normalizedUnit.Substring(0, normalizedUnit.Length - 1));
} }
return null; return new();
} }
private static IEnumerable<string> exhaustiveBreadthFirst(string dest, IEnumerable<string> currentPath) private static IEnumerable<string> exhaustiveBreadthFirst(string dest, IEnumerable<string> currentPath)
{ {
@ -180,11 +268,19 @@ namespace vassago.Conversion
} }
return null; return null;
} }
private static void AddLinearPair(string key1, string key2, decimal factor) private static void AddLinearPair(string key1, string key2, string formulaForward, string formulaBackward)
{ {
var reverseFactor = 1.0m / factor; knownConversions.Add(new Tuple<string, string, Func<double, double>, Func<double, double>>(
knownConversions.Add(new Tuple<string, string, Convert1Way, Convert1Way>( key1,
key1, key2, x => x * factor, y => y * reverseFactor key2,
(Func<double, double>) engine.Formula(formulaForward)
.Parameter("i1", DataType.FloatingPoint)
.Result(DataType.FloatingPoint)
.Build(),
(Func<double, double>) engine.Formula(formulaBackward)
.Parameter("i1", DataType.FloatingPoint)
.Result(DataType.FloatingPoint)
.Build()
)); ));
} }
} }

124
Jenkinsfile vendored
View File

@ -1,7 +1,42 @@
pipeline { pipeline {
agent any agent any
environment {
linuxServiceAccount=credentials("a83b97d0-dbc6-42d9-96c9-f07a7f2dfab5")
linuxServiceAccountID="3ca1be00-3d9f-42a1-bab2-48a4d7b99fb0"
database_connectionString=credentials("7ab58922-c647-42e5-ae15-84faa0c1ee7d")
targetHost="alloces.lan"
}
stages { stages {
stage("environment setup") { //my environment, here on the jenkins agent
steps {
script {
sh """#!/bin/bash
function testcmd(){
if ! command -v \$1 2>&1 >/dev/null
then
echo "this agent doesn't have \$1"
exit 1
fi
}
testcmd mktemp
testcmd curl
testcmd git
testcmd rsync
testcmd sed
testcmd ssh
testcmd ssh-keyscan
testcmd ssh-keygen
testcmd scp
testcmd dotnet
dotnet tool install dotnet-ef
"""
}
}
}
stage('clean old'){ stage('clean old'){
steps{ steps{
sh 'rm -rf bin obj' sh 'rm -rf bin obj'
@ -9,9 +44,92 @@ pipeline {
} }
stage('Build') { stage('Build') {
steps { steps {
sh 'dotnet publish vassago.csproj --configuration Release --os linux' dotnetBuild(outputDirectory: "dist", project: "vassago.csproj")
archiveArtifacts artifacts: 'bin/Release/net7.0/linux-x64/publish/*' archiveArtifacts artifacts: 'dist/*'
} }
} }
stage ('upload') {
when {
//now my CI/CD is no longer continuous, it's just... automatic.
//(which is what I actually want tbh)
//but that does mean I have to put this condition in every single branch
branch "release"
}
steps{
withCredentials([sshUserPrivateKey(credentialsId: env.linuxServiceAccountID, keyFileVariable: 'PK')])
{
sh """#!/bin/bash
ssh -i \"${PK}\" -tt ${linuxServiceAccount_USR}@${targetHost} 'rm -rf temp_deploy'
rsync -e \"ssh -i \"${PK}\"\" -a dist/ ${linuxServiceAccount_USR}@${env.targetHost}:temp_deploy/
"""
}
}
}
stage ('stop')
{
when {
branch "release"
}
steps{
withCredentials([sshUserPrivateKey(credentialsId: env.linuxServiceAccountID, keyFileVariable: 'PK')])
{
sh """#!/bin/bash
ssh -i \"${PK}\" -tt ${linuxServiceAccount_USR}@${targetHost} 'systemctl --user stop vassago'
"""
}
}
}
stage ('update db')
{
when {
branch "release"
}
steps{
//TODO: backup database
sh """#!/bin/bash
"""
sh """#!/bin/bash
dotnet ef database update --connection "${env.database_connectionString}"
"""
//TODO: if updating the db fails, restore the old one
sh """#!/bin/bash
"""
}
}
stage ('replace')
{
when {
branch "release"
}
steps{
withCredentials([sshUserPrivateKey(credentialsId: env.linuxServiceAccountID, keyFileVariable: 'PK')])
{
sh """#!/bin/bash
ssh -i \"${PK}\" -tt ${linuxServiceAccount_USR}@${targetHost} 'cp -r dist oldgood-\$(mktemp -u XXXX)'
ssh -i \"${PK}\" -tt ${linuxServiceAccount_USR}@${targetHost} 'mv dist/appsettings.json appsettings.json'
ssh -i \"${PK}\" -tt ${linuxServiceAccount_USR}@${targetHost} 'rm -rf dist'
ssh -i \"${PK}\" -tt ${linuxServiceAccount_USR}@${targetHost} 'rsync -r temp_deploy/ dist/'
ssh -i \"${PK}\" -tt ${linuxServiceAccount_USR}@${targetHost} 'rm -rf temp_deploy'
ssh -i \"${PK}\" -tt ${linuxServiceAccount_USR}@${targetHost} 'mv appsettings.json dist/appsettings.json'
"""
}
}
}
stage ('spin up')
{
when {
branch "release"
}
steps{
withCredentials([sshUserPrivateKey(credentialsId: env.linuxServiceAccountID, keyFileVariable: 'PK')])
{
sh """#!/bin/bash
ssh -i \"${PK}\" -tt ${linuxServiceAccount_USR}@${targetHost} 'systemctl --user start vassago'
"""
}
}
}
} }
} }

View File

@ -13,7 +13,7 @@ namespace vassago.Migrations
{ {
[DbContext(typeof(ChattingContext))] [DbContext(typeof(ChattingContext))]
[Migration("20230704160720_initial")] [Migration("20230704160720_initial")]
partial class initial partial class Initial
{ {
/// <inheritdoc /> /// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder) protected override void BuildTargetModel(ModelBuilder modelBuilder)

View File

@ -7,7 +7,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace vassago.Migrations namespace vassago.Migrations
{ {
/// <inheritdoc /> /// <inheritdoc />
public partial class initial : Migration public partial class Initial : Migration
{ {
/// <inheritdoc /> /// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder) protected override void Up(MigrationBuilder migrationBuilder)

View File

@ -13,7 +13,7 @@ namespace vassago.Migrations
{ {
[DbContext(typeof(ChattingContext))] [DbContext(typeof(ChattingContext))]
[Migration("20231203193139_channeltype")] [Migration("20231203193139_channeltype")]
partial class channeltype partial class ChannelType
{ {
/// <inheritdoc /> /// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder) protected override void BuildTargetModel(ModelBuilder modelBuilder)

View File

@ -5,7 +5,7 @@
namespace vassago.Migrations namespace vassago.Migrations
{ {
/// <inheritdoc /> /// <inheritdoc />
public partial class channeltype : Migration public partial class ChannelType : Migration
{ {
/// <inheritdoc /> /// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder) protected override void Up(MigrationBuilder migrationBuilder)

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

@ -0,0 +1,271 @@
// <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("20250204004906_cascade")]
partial class Cascade
{
/// <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")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("vassago.Models.Channel", "SeenInChannel")
.WithMany("Users")
.HasForeignKey("SeenInChannelId")
.OnDelete(DeleteBehavior.Cascade);
b.Navigation("IsUser");
b.Navigation("SeenInChannel");
});
modelBuilder.Entity("vassago.Models.Attachment", b =>
{
b.HasOne("vassago.Models.Message", "Message")
.WithMany("Attachments")
.HasForeignKey("MessageId")
.OnDelete(DeleteBehavior.Cascade);
b.Navigation("Message");
});
modelBuilder.Entity("vassago.Models.Channel", b =>
{
b.HasOne("vassago.Models.Channel", "ParentChannel")
.WithMany("SubChannels")
.HasForeignKey("ParentChannelId")
.OnDelete(DeleteBehavior.Cascade);
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")
.OnDelete(DeleteBehavior.Cascade);
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,133 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace vassago.Migrations
{
/// <inheritdoc />
public partial class Cascade : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Accounts_Channels_SeenInChannelId",
table: "Accounts");
migrationBuilder.DropForeignKey(
name: "FK_Accounts_Users_IsUserId",
table: "Accounts");
migrationBuilder.DropForeignKey(
name: "FK_Attachments_Messages_MessageId",
table: "Attachments");
migrationBuilder.DropForeignKey(
name: "FK_Channels_Channels_ParentChannelId",
table: "Channels");
migrationBuilder.DropForeignKey(
name: "FK_Messages_Channels_ChannelId",
table: "Messages");
migrationBuilder.AddForeignKey(
name: "FK_Accounts_Channels_SeenInChannelId",
table: "Accounts",
column: "SeenInChannelId",
principalTable: "Channels",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_Accounts_Users_IsUserId",
table: "Accounts",
column: "IsUserId",
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_Attachments_Messages_MessageId",
table: "Attachments",
column: "MessageId",
principalTable: "Messages",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_Channels_Channels_ParentChannelId",
table: "Channels",
column: "ParentChannelId",
principalTable: "Channels",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_Messages_Channels_ChannelId",
table: "Messages",
column: "ChannelId",
principalTable: "Channels",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Accounts_Channels_SeenInChannelId",
table: "Accounts");
migrationBuilder.DropForeignKey(
name: "FK_Accounts_Users_IsUserId",
table: "Accounts");
migrationBuilder.DropForeignKey(
name: "FK_Attachments_Messages_MessageId",
table: "Attachments");
migrationBuilder.DropForeignKey(
name: "FK_Channels_Channels_ParentChannelId",
table: "Channels");
migrationBuilder.DropForeignKey(
name: "FK_Messages_Channels_ChannelId",
table: "Messages");
migrationBuilder.AddForeignKey(
name: "FK_Accounts_Channels_SeenInChannelId",
table: "Accounts",
column: "SeenInChannelId",
principalTable: "Channels",
principalColumn: "Id");
migrationBuilder.AddForeignKey(
name: "FK_Accounts_Users_IsUserId",
table: "Accounts",
column: "IsUserId",
principalTable: "Users",
principalColumn: "Id");
migrationBuilder.AddForeignKey(
name: "FK_Attachments_Messages_MessageId",
table: "Attachments",
column: "MessageId",
principalTable: "Messages",
principalColumn: "Id");
migrationBuilder.AddForeignKey(
name: "FK_Channels_Channels_ParentChannelId",
table: "Channels",
column: "ParentChannelId",
principalTable: "Channels",
principalColumn: "Id");
migrationBuilder.AddForeignKey(
name: "FK_Messages_Channels_ChannelId",
table: "Messages",
column: "ChannelId",
principalTable: "Channels",
principalColumn: "Id");
}
}
}

378
Migrations/20250423002254_UAC.Designer.cs generated Normal file
View File

@ -0,0 +1,378 @@
// <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("20250423002254_UAC")]
partial class UAC
{
/// <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("AccountUAC", b =>
{
b.Property<Guid>("AccountInChannelsId")
.HasColumnType("uuid");
b.Property<Guid>("UACsId")
.HasColumnType("uuid");
b.HasKey("AccountInChannelsId", "UACsId");
b.HasIndex("UACsId");
b.ToTable("AccountUAC");
});
modelBuilder.Entity("ChannelUAC", b =>
{
b.Property<Guid>("ChannelsId")
.HasColumnType("uuid");
b.Property<Guid>("UACsId")
.HasColumnType("uuid");
b.HasKey("ChannelsId", "UACsId");
b.HasIndex("UACsId");
b.ToTable("ChannelUAC");
});
modelBuilder.Entity("UACUser", b =>
{
b.Property<Guid>("UACsId")
.HasColumnType("uuid");
b.Property<Guid>("UsersId")
.HasColumnType("uuid");
b.HasKey("UACsId", "UsersId");
b.HasIndex("UsersId");
b.ToTable("UACUser");
});
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.UAC", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("DisplayName")
.HasColumnType("text");
b.Property<Guid>("OwnerId")
.HasColumnType("uuid");
b.HasKey("Id");
b.ToTable("UACs");
});
modelBuilder.Entity("vassago.Models.User", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.HasKey("Id");
b.ToTable("Users");
});
modelBuilder.Entity("AccountUAC", b =>
{
b.HasOne("vassago.Models.Account", null)
.WithMany()
.HasForeignKey("AccountInChannelsId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("vassago.Models.UAC", null)
.WithMany()
.HasForeignKey("UACsId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("ChannelUAC", b =>
{
b.HasOne("vassago.Models.Channel", null)
.WithMany()
.HasForeignKey("ChannelsId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("vassago.Models.UAC", null)
.WithMany()
.HasForeignKey("UACsId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("UACUser", b =>
{
b.HasOne("vassago.Models.UAC", null)
.WithMany()
.HasForeignKey("UACsId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("vassago.Models.User", null)
.WithMany()
.HasForeignKey("UsersId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("vassago.Models.Account", b =>
{
b.HasOne("vassago.Models.User", "IsUser")
.WithMany("Accounts")
.HasForeignKey("IsUserId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("vassago.Models.Channel", "SeenInChannel")
.WithMany("Users")
.HasForeignKey("SeenInChannelId")
.OnDelete(DeleteBehavior.Cascade);
b.Navigation("IsUser");
b.Navigation("SeenInChannel");
});
modelBuilder.Entity("vassago.Models.Attachment", b =>
{
b.HasOne("vassago.Models.Message", "Message")
.WithMany("Attachments")
.HasForeignKey("MessageId")
.OnDelete(DeleteBehavior.Cascade);
b.Navigation("Message");
});
modelBuilder.Entity("vassago.Models.Channel", b =>
{
b.HasOne("vassago.Models.Channel", "ParentChannel")
.WithMany("SubChannels")
.HasForeignKey("ParentChannelId")
.OnDelete(DeleteBehavior.Cascade);
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")
.OnDelete(DeleteBehavior.Cascade);
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,131 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace vassago.Migrations
{
/// <inheritdoc />
public partial class UAC : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "UACs",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
OwnerId = table.Column<Guid>(type: "uuid", nullable: false),
DisplayName = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_UACs", x => x.Id);
});
migrationBuilder.CreateTable(
name: "AccountUAC",
columns: table => new
{
AccountInChannelsId = table.Column<Guid>(type: "uuid", nullable: false),
UACsId = table.Column<Guid>(type: "uuid", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AccountUAC", x => new { x.AccountInChannelsId, x.UACsId });
table.ForeignKey(
name: "FK_AccountUAC_Accounts_AccountInChannelsId",
column: x => x.AccountInChannelsId,
principalTable: "Accounts",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_AccountUAC_UACs_UACsId",
column: x => x.UACsId,
principalTable: "UACs",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "ChannelUAC",
columns: table => new
{
ChannelsId = table.Column<Guid>(type: "uuid", nullable: false),
UACsId = table.Column<Guid>(type: "uuid", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ChannelUAC", x => new { x.ChannelsId, x.UACsId });
table.ForeignKey(
name: "FK_ChannelUAC_Channels_ChannelsId",
column: x => x.ChannelsId,
principalTable: "Channels",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_ChannelUAC_UACs_UACsId",
column: x => x.UACsId,
principalTable: "UACs",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "UACUser",
columns: table => new
{
UACsId = table.Column<Guid>(type: "uuid", nullable: false),
UsersId = table.Column<Guid>(type: "uuid", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_UACUser", x => new { x.UACsId, x.UsersId });
table.ForeignKey(
name: "FK_UACUser_UACs_UACsId",
column: x => x.UACsId,
principalTable: "UACs",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_UACUser_Users_UsersId",
column: x => x.UsersId,
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_AccountUAC_UACsId",
table: "AccountUAC",
column: "UACsId");
migrationBuilder.CreateIndex(
name: "IX_ChannelUAC_UACsId",
table: "ChannelUAC",
column: "UACsId");
migrationBuilder.CreateIndex(
name: "IX_UACUser_UsersId",
table: "UACUser",
column: "UsersId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "AccountUAC");
migrationBuilder.DropTable(
name: "ChannelUAC");
migrationBuilder.DropTable(
name: "UACUser");
migrationBuilder.DropTable(
name: "UACs");
}
}
}

View File

@ -0,0 +1,386 @@
// <auto-generated />
using System;
using System.Collections.Generic;
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("20250523181842_channelAliases")]
partial class channelAliases
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "7.0.5")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore");
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("AccountUAC", b =>
{
b.Property<Guid>("AccountInChannelsId")
.HasColumnType("uuid");
b.Property<Guid>("UACsId")
.HasColumnType("uuid");
b.HasKey("AccountInChannelsId", "UACsId");
b.HasIndex("UACsId");
b.ToTable("AccountUAC");
});
modelBuilder.Entity("ChannelUAC", b =>
{
b.Property<Guid>("ChannelsId")
.HasColumnType("uuid");
b.Property<Guid>("UACsId")
.HasColumnType("uuid");
b.HasKey("ChannelsId", "UACsId");
b.HasIndex("UACsId");
b.ToTable("ChannelUAC");
});
modelBuilder.Entity("UACUser", b =>
{
b.Property<Guid>("UACsId")
.HasColumnType("uuid");
b.Property<Guid>("UsersId")
.HasColumnType("uuid");
b.HasKey("UACsId", "UsersId");
b.HasIndex("UsersId");
b.ToTable("UACUser");
});
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<Dictionary<string, string>>("Aliases")
.HasColumnType("hstore");
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.UAC", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Description")
.HasColumnType("text");
b.Property<string>("DisplayName")
.HasColumnType("text");
b.Property<Guid>("OwnerId")
.HasColumnType("uuid");
b.HasKey("Id");
b.ToTable("UACs");
});
modelBuilder.Entity("vassago.Models.User", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.HasKey("Id");
b.ToTable("Users");
});
modelBuilder.Entity("AccountUAC", b =>
{
b.HasOne("vassago.Models.Account", null)
.WithMany()
.HasForeignKey("AccountInChannelsId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("vassago.Models.UAC", null)
.WithMany()
.HasForeignKey("UACsId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("ChannelUAC", b =>
{
b.HasOne("vassago.Models.Channel", null)
.WithMany()
.HasForeignKey("ChannelsId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("vassago.Models.UAC", null)
.WithMany()
.HasForeignKey("UACsId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("UACUser", b =>
{
b.HasOne("vassago.Models.UAC", null)
.WithMany()
.HasForeignKey("UACsId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("vassago.Models.User", null)
.WithMany()
.HasForeignKey("UsersId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("vassago.Models.Account", b =>
{
b.HasOne("vassago.Models.User", "IsUser")
.WithMany("Accounts")
.HasForeignKey("IsUserId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("vassago.Models.Channel", "SeenInChannel")
.WithMany("Users")
.HasForeignKey("SeenInChannelId")
.OnDelete(DeleteBehavior.Cascade);
b.Navigation("IsUser");
b.Navigation("SeenInChannel");
});
modelBuilder.Entity("vassago.Models.Attachment", b =>
{
b.HasOne("vassago.Models.Message", "Message")
.WithMany("Attachments")
.HasForeignKey("MessageId")
.OnDelete(DeleteBehavior.Cascade);
b.Navigation("Message");
});
modelBuilder.Entity("vassago.Models.Channel", b =>
{
b.HasOne("vassago.Models.Channel", "ParentChannel")
.WithMany("SubChannels")
.HasForeignKey("ParentChannelId")
.OnDelete(DeleteBehavior.Cascade);
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")
.OnDelete(DeleteBehavior.Cascade);
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,45 @@
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace vassago.Migrations
{
/// <inheritdoc />
public partial class channelAliases : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterDatabase()
.Annotation("Npgsql:PostgresExtension:hstore", ",,");
migrationBuilder.AddColumn<string>(
name: "Description",
table: "UACs",
type: "text",
nullable: true);
migrationBuilder.AddColumn<Dictionary<string, string>>(
name: "Aliases",
table: "Channels",
type: "hstore",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Description",
table: "UACs");
migrationBuilder.DropColumn(
name: "Aliases",
table: "Channels");
migrationBuilder.AlterDatabase()
.OldAnnotation("Npgsql:PostgresExtension:hstore", ",,");
}
}
}

View File

@ -0,0 +1,389 @@
// <auto-generated />
using System;
using System.Collections.Generic;
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
{
#pragma warning disable 612, 618, 8981
[DbContext(typeof(ChattingContext))]
[Migration("20250605145513_datamemos-more-data")]
partial class datamemosmoredata
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
modelBuilder
.HasAnnotation("ProductVersion", "7.0.5")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore");
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("AccountUAC", b =>
{
b.Property<Guid>("AccountInChannelsId")
.HasColumnType("uuid");
b.Property<Guid>("UACsId")
.HasColumnType("uuid");
b.HasKey("AccountInChannelsId", "UACsId");
b.HasIndex("UACsId");
b.ToTable("AccountUAC");
});
modelBuilder.Entity("ChannelUAC", b =>
{
b.Property<Guid>("ChannelsId")
.HasColumnType("uuid");
b.Property<Guid>("UACsId")
.HasColumnType("uuid");
b.HasKey("ChannelsId", "UACsId");
b.HasIndex("UACsId");
b.ToTable("ChannelUAC");
});
modelBuilder.Entity("UACUser", b =>
{
b.Property<Guid>("UACsId")
.HasColumnType("uuid");
b.Property<Guid>("UsersId")
.HasColumnType("uuid");
b.HasKey("UACsId", "UsersId");
b.HasIndex("UsersId");
b.ToTable("UACUser");
});
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.UAC", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Dictionary<string, string>>("CommandAliases")
.HasColumnType("hstore");
b.Property<string>("Description")
.HasColumnType("text");
b.Property<string>("DisplayName")
.HasColumnType("text");
b.Property<Dictionary<string, string>>("Localization")
.HasColumnType("hstore");
b.Property<Guid>("OwnerId")
.HasColumnType("uuid");
b.HasKey("Id");
b.ToTable("UACs");
});
modelBuilder.Entity("vassago.Models.User", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.HasKey("Id");
b.ToTable("Users");
});
modelBuilder.Entity("AccountUAC", b =>
{
b.HasOne("vassago.Models.Account", null)
.WithMany()
.HasForeignKey("AccountInChannelsId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("vassago.Models.UAC", null)
.WithMany()
.HasForeignKey("UACsId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("ChannelUAC", b =>
{
b.HasOne("vassago.Models.Channel", null)
.WithMany()
.HasForeignKey("ChannelsId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("vassago.Models.UAC", null)
.WithMany()
.HasForeignKey("UACsId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("UACUser", b =>
{
b.HasOne("vassago.Models.UAC", null)
.WithMany()
.HasForeignKey("UACsId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("vassago.Models.User", null)
.WithMany()
.HasForeignKey("UsersId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("vassago.Models.Account", b =>
{
b.HasOne("vassago.Models.User", "IsUser")
.WithMany("Accounts")
.HasForeignKey("IsUserId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("vassago.Models.Channel", "SeenInChannel")
.WithMany("Users")
.HasForeignKey("SeenInChannelId")
.OnDelete(DeleteBehavior.Cascade);
b.Navigation("IsUser");
b.Navigation("SeenInChannel");
});
modelBuilder.Entity("vassago.Models.Attachment", b =>
{
b.HasOne("vassago.Models.Message", "Message")
.WithMany("Attachments")
.HasForeignKey("MessageId")
.OnDelete(DeleteBehavior.Cascade);
b.Navigation("Message");
});
modelBuilder.Entity("vassago.Models.Channel", b =>
{
b.HasOne("vassago.Models.Channel", "ParentChannel")
.WithMany("SubChannels")
.HasForeignKey("ParentChannelId")
.OnDelete(DeleteBehavior.Cascade);
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")
.OnDelete(DeleteBehavior.Cascade);
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, 8981
}
}
}

View File

@ -0,0 +1,55 @@
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace vassago.Migrations
{
#pragma warning disable 8981
/// <inheritdoc />
public partial class datamemosmoredata : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<Dictionary<string, string>>(
name: "CommandAliases",
table: "UACs",
type: "hstore",
nullable: true);
migrationBuilder.AddColumn<Dictionary<string, string>>(
name: "Localization",
table: "UACs",
type: "hstore",
nullable: true);
//NOTE for future me: migrationBuilder.SQL("SELECT localization INTO Aliases from Channels;");, but also make the rows for it.
//too lazy now, really leaning on the "this will work fine for my 0 users"
migrationBuilder.DropColumn(
name: "Aliases",
table: "Channels");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<Dictionary<string, string>>(
name: "Aliases",
table: "Channels",
type: "hstore",
nullable: true);
migrationBuilder.DropColumn(
name: "CommandAliases",
table: "UACs");
migrationBuilder.DropColumn(
name: "Localization",
table: "UACs");
}
}
}
#pragma warning restore 8981

View File

@ -0,0 +1,391 @@
// <auto-generated />
using System;
using System.Collections.Generic;
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
{
#pragma warning disable CS8981
[DbContext(typeof(ChattingContext))]
[Migration("20250620023827_locales")]
partial class locales
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "7.0.5")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore");
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("AccountUAC", b =>
{
b.Property<Guid>("AccountInChannelsId")
.HasColumnType("uuid");
b.Property<Guid>("UACsId")
.HasColumnType("uuid");
b.HasKey("AccountInChannelsId", "UACsId");
b.HasIndex("UACsId");
b.ToTable("AccountUAC");
});
modelBuilder.Entity("ChannelUAC", b =>
{
b.Property<Guid>("ChannelsId")
.HasColumnType("uuid");
b.Property<Guid>("UACsId")
.HasColumnType("uuid");
b.HasKey("ChannelsId", "UACsId");
b.HasIndex("UACsId");
b.ToTable("ChannelUAC");
});
modelBuilder.Entity("UACUser", b =>
{
b.Property<Guid>("UACsId")
.HasColumnType("uuid");
b.Property<Guid>("UsersId")
.HasColumnType("uuid");
b.HasKey("UACsId", "UsersId");
b.HasIndex("UsersId");
b.ToTable("UACUser");
});
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.UAC", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Dictionary<string, string>>("CommandAlterations")
.HasColumnType("hstore");
b.Property<string>("Description")
.HasColumnType("text");
b.Property<string>("DisplayName")
.HasColumnType("text");
b.Property<Guid>("OwnerId")
.HasColumnType("uuid");
b.Property<Dictionary<string, string>>("Translations")
.HasColumnType("hstore");
b.HasKey("Id");
b.ToTable("UACs");
});
modelBuilder.Entity("vassago.Models.User", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.HasKey("Id");
b.ToTable("Users");
});
modelBuilder.Entity("AccountUAC", b =>
{
b.HasOne("vassago.Models.Account", null)
.WithMany()
.HasForeignKey("AccountInChannelsId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("vassago.Models.UAC", null)
.WithMany()
.HasForeignKey("UACsId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("ChannelUAC", b =>
{
b.HasOne("vassago.Models.Channel", null)
.WithMany()
.HasForeignKey("ChannelsId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("vassago.Models.UAC", null)
.WithMany()
.HasForeignKey("UACsId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("UACUser", b =>
{
b.HasOne("vassago.Models.UAC", null)
.WithMany()
.HasForeignKey("UACsId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("vassago.Models.User", null)
.WithMany()
.HasForeignKey("UsersId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("vassago.Models.Account", b =>
{
b.HasOne("vassago.Models.User", "IsUser")
.WithMany("Accounts")
.HasForeignKey("IsUserId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("vassago.Models.Channel", "SeenInChannel")
.WithMany("Users")
.HasForeignKey("SeenInChannelId")
.OnDelete(DeleteBehavior.Cascade);
b.Navigation("IsUser");
b.Navigation("SeenInChannel");
});
modelBuilder.Entity("vassago.Models.Attachment", b =>
{
b.HasOne("vassago.Models.Message", "Message")
.WithMany("Attachments")
.HasForeignKey("MessageId")
.OnDelete(DeleteBehavior.Cascade);
b.Navigation("Message");
});
modelBuilder.Entity("vassago.Models.Channel", b =>
{
b.HasOne("vassago.Models.Channel", "ParentChannel")
.WithMany("SubChannels")
.HasForeignKey("ParentChannelId")
.OnDelete(DeleteBehavior.Cascade);
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")
.OnDelete(DeleteBehavior.Cascade);
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
}
}
#pragma warning restore CS8981
}

View File

@ -0,0 +1,40 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace vassago.Migrations
{
#pragma warning disable 8981
/// <inheritdoc />
public partial class locales : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.RenameColumn(
name: "Localization",
table: "UACs",
newName: "Translations");
migrationBuilder.RenameColumn(
name: "CommandAliases",
table: "UACs",
newName: "CommandAlterations");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.RenameColumn(
name: "Translations",
table: "UACs",
newName: "Localization");
migrationBuilder.RenameColumn(
name: "CommandAlterations",
table: "UACs",
newName: "CommandAliases");
}
}
#pragma warning restore 8981
}

View File

@ -0,0 +1,392 @@
// <auto-generated />
using System;
using System.Collections.Generic;
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("20250620230813_TranslatedMessages")]
partial class TranslatedMessages
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "7.0.5")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore");
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("AccountUAC", b =>
{
b.Property<Guid>("AccountInChannelsId")
.HasColumnType("uuid");
b.Property<Guid>("UACsId")
.HasColumnType("uuid");
b.HasKey("AccountInChannelsId", "UACsId");
b.HasIndex("UACsId");
b.ToTable("AccountUAC");
});
modelBuilder.Entity("ChannelUAC", b =>
{
b.Property<Guid>("ChannelsId")
.HasColumnType("uuid");
b.Property<Guid>("UACsId")
.HasColumnType("uuid");
b.HasKey("ChannelsId", "UACsId");
b.HasIndex("UACsId");
b.ToTable("ChannelUAC");
});
modelBuilder.Entity("UACUser", b =>
{
b.Property<Guid>("UACsId")
.HasColumnType("uuid");
b.Property<Guid>("UsersId")
.HasColumnType("uuid");
b.HasKey("UACsId", "UsersId");
b.HasIndex("UsersId");
b.ToTable("UACUser");
});
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.Property<string>("TranslatedContent")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("AuthorId");
b.HasIndex("ChannelId");
b.ToTable("Messages");
});
modelBuilder.Entity("vassago.Models.UAC", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Dictionary<string, string>>("CommandAlterations")
.HasColumnType("hstore");
b.Property<string>("Description")
.HasColumnType("text");
b.Property<string>("DisplayName")
.HasColumnType("text");
b.Property<Guid>("OwnerId")
.HasColumnType("uuid");
b.Property<Dictionary<string, string>>("Translations")
.HasColumnType("hstore");
b.HasKey("Id");
b.ToTable("UACs");
});
modelBuilder.Entity("vassago.Models.User", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.HasKey("Id");
b.ToTable("Users");
});
modelBuilder.Entity("AccountUAC", b =>
{
b.HasOne("vassago.Models.Account", null)
.WithMany()
.HasForeignKey("AccountInChannelsId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("vassago.Models.UAC", null)
.WithMany()
.HasForeignKey("UACsId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("ChannelUAC", b =>
{
b.HasOne("vassago.Models.Channel", null)
.WithMany()
.HasForeignKey("ChannelsId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("vassago.Models.UAC", null)
.WithMany()
.HasForeignKey("UACsId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("UACUser", b =>
{
b.HasOne("vassago.Models.UAC", null)
.WithMany()
.HasForeignKey("UACsId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("vassago.Models.User", null)
.WithMany()
.HasForeignKey("UsersId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("vassago.Models.Account", b =>
{
b.HasOne("vassago.Models.User", "IsUser")
.WithMany("Accounts")
.HasForeignKey("IsUserId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("vassago.Models.Channel", "SeenInChannel")
.WithMany("Users")
.HasForeignKey("SeenInChannelId")
.OnDelete(DeleteBehavior.Cascade);
b.Navigation("IsUser");
b.Navigation("SeenInChannel");
});
modelBuilder.Entity("vassago.Models.Attachment", b =>
{
b.HasOne("vassago.Models.Message", "Message")
.WithMany("Attachments")
.HasForeignKey("MessageId")
.OnDelete(DeleteBehavior.Cascade);
b.Navigation("Message");
});
modelBuilder.Entity("vassago.Models.Channel", b =>
{
b.HasOne("vassago.Models.Channel", "ParentChannel")
.WithMany("SubChannels")
.HasForeignKey("ParentChannelId")
.OnDelete(DeleteBehavior.Cascade);
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")
.OnDelete(DeleteBehavior.Cascade);
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,28 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace vassago.Migrations
{
/// <inheritdoc />
public partial class TranslatedMessages : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "TranslatedContent",
table: "Messages",
type: "text",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "TranslatedContent",
table: "Messages");
}
}
}

View File

@ -1,5 +1,6 @@
// <auto-generated /> // <auto-generated />
using System; using System;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
@ -20,8 +21,54 @@ namespace vassago.Migrations
.HasAnnotation("ProductVersion", "7.0.5") .HasAnnotation("ProductVersion", "7.0.5")
.HasAnnotation("Relational:MaxIdentifierLength", 63); .HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore");
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("AccountUAC", b =>
{
b.Property<Guid>("AccountInChannelsId")
.HasColumnType("uuid");
b.Property<Guid>("UACsId")
.HasColumnType("uuid");
b.HasKey("AccountInChannelsId", "UACsId");
b.HasIndex("UACsId");
b.ToTable("AccountUAC");
});
modelBuilder.Entity("ChannelUAC", b =>
{
b.Property<Guid>("ChannelsId")
.HasColumnType("uuid");
b.Property<Guid>("UACsId")
.HasColumnType("uuid");
b.HasKey("ChannelsId", "UACsId");
b.HasIndex("UACsId");
b.ToTable("ChannelUAC");
});
modelBuilder.Entity("UACUser", b =>
{
b.Property<Guid>("UACsId")
.HasColumnType("uuid");
b.Property<Guid>("UsersId")
.HasColumnType("uuid");
b.HasKey("UACsId", "UsersId");
b.HasIndex("UsersId");
b.ToTable("UACUser");
});
modelBuilder.Entity("vassago.Models.Account", b => modelBuilder.Entity("vassago.Models.Account", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")
@ -34,9 +81,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 +98,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 +157,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 +172,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 =>
@ -219,6 +218,9 @@ namespace vassago.Migrations
b.Property<DateTimeOffset>("Timestamp") b.Property<DateTimeOffset>("Timestamp")
.HasColumnType("timestamp with time zone"); .HasColumnType("timestamp with time zone");
b.Property<string>("TranslatedContent")
.HasColumnType("text");
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("AuthorId"); b.HasIndex("AuthorId");
@ -228,35 +230,99 @@ namespace vassago.Migrations
b.ToTable("Messages"); b.ToTable("Messages");
}); });
modelBuilder.Entity("vassago.Models.UAC", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Dictionary<string, string>>("CommandAlterations")
.HasColumnType("hstore");
b.Property<string>("Description")
.HasColumnType("text");
b.Property<string>("DisplayName")
.HasColumnType("text");
b.Property<Guid>("OwnerId")
.HasColumnType("uuid");
b.Property<Dictionary<string, string>>("Translations")
.HasColumnType("hstore");
b.HasKey("Id");
b.ToTable("UACs");
});
modelBuilder.Entity("vassago.Models.User", b => modelBuilder.Entity("vassago.Models.User", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")
.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("AccountUAC", b =>
{
b.HasOne("vassago.Models.Account", null)
.WithMany()
.HasForeignKey("AccountInChannelsId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("vassago.Models.UAC", null)
.WithMany()
.HasForeignKey("UACsId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("ChannelUAC", b =>
{
b.HasOne("vassago.Models.Channel", null)
.WithMany()
.HasForeignKey("ChannelsId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("vassago.Models.UAC", null)
.WithMany()
.HasForeignKey("UACsId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("UACUser", b =>
{
b.HasOne("vassago.Models.UAC", null)
.WithMany()
.HasForeignKey("UACsId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("vassago.Models.User", null)
.WithMany()
.HasForeignKey("UsersId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
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")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("vassago.Models.Channel", "SeenInChannel") b.HasOne("vassago.Models.Channel", "SeenInChannel")
.WithMany("Users") .WithMany("Users")
.HasForeignKey("SeenInChannelId"); .HasForeignKey("SeenInChannelId")
.OnDelete(DeleteBehavior.Cascade);
b.Navigation("IsUser"); b.Navigation("IsUser");
@ -267,28 +333,20 @@ namespace vassago.Migrations
{ {
b.HasOne("vassago.Models.Message", "Message") b.HasOne("vassago.Models.Message", "Message")
.WithMany("Attachments") .WithMany("Attachments")
.HasForeignKey("MessageId"); .HasForeignKey("MessageId")
.OnDelete(DeleteBehavior.Cascade);
b.Navigation("Message"); b.Navigation("Message");
}); });
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")
.OnDelete(DeleteBehavior.Cascade);
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 =>
@ -299,20 +357,14 @@ namespace vassago.Migrations
b.HasOne("vassago.Models.Channel", "Channel") b.HasOne("vassago.Models.Channel", "Channel")
.WithMany("Messages") .WithMany("Messages")
.HasForeignKey("ChannelId"); .HasForeignKey("ChannelId")
.OnDelete(DeleteBehavior.Cascade);
b.Navigation("Author"); b.Navigation("Author");
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 +374,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

@ -4,7 +4,7 @@ using System;
using System.Collections.Generic; 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.Text.Json.Serialization;
public class Account public class Account
{ {
@ -27,5 +27,7 @@ public class Account
public bool IsBot { get; set; } //webhook counts public bool IsBot { get; set; } //webhook counts
public Channel SeenInChannel { get; set; } public Channel SeenInChannel { get; set; }
public string Protocol { get; set; } public string Protocol { get; set; }
public List<UAC> UACs { get; set; }
[JsonIgnore]
public User IsUser {get; set;} public User IsUser {get; set;}
} }

View File

@ -5,6 +5,9 @@ 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 Microsoft.EntityFrameworkCore;
using Newtonsoft.Json;
using static vassago.Models.Enumerations; using static vassago.Models.Enumerations;
public class Channel public class Channel
@ -13,60 +16,60 @@ 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; } [DeleteBehavior(DeleteBehavior.Cascade)]
public List<Channel> SubChannels { get; set; } public List<Channel> SubChannels { get; set; }
[JsonIgnore]
public Channel ParentChannel { get; set; } public Channel ParentChannel { get; set; }
public Guid? ParentChannelId { get; set; }
public string Protocol { get; set; } public string Protocol { get; set; }
[DeleteBehavior(DeleteBehavior.Cascade)]
public List<Message> Messages { get; set; } public List<Message> Messages { get; set; }
[DeleteBehavior(DeleteBehavior.Cascade)]
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;}
[NonSerialized]
public Func<string, string, Task> SendFile;
[NonSerialized]
public Func<string, Task> SendMessage;
//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; }
public List<UAC> UACs { get; set; }
public DefinitePermissionSettings EffectivePermissions public DefinitePermissionSettings EffectivePermissions
{ {
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
{ {
get get
{ {
if(this.ParentChannel != null) if (this.ParentChannel != null)
{ {
return this.ParentChannel.LineageSummary + '/' + this.DisplayName; return this.ParentChannel.LineageSummary + '/' + this.DisplayName;
} }
@ -76,4 +79,31 @@ public class Channel
} }
} }
} }
///<summary>
///break self-referencing loops for library-agnostic serialization
///</summary>
public Channel AsSerializable()
{
var toReturn = this.MemberwiseClone() as Channel;
toReturn.ParentChannel = null;
if (toReturn.Users?.Count > 0)
{
foreach (var account in toReturn.Users)
{
account.SeenInChannel = null;
}
}
return toReturn;
}
}
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

@ -7,16 +7,16 @@ public class ChattingContext : DbContext
{ {
public DbSet<Attachment> Attachments { get; set; } public DbSet<Attachment> Attachments { get; set; }
public DbSet<Channel> Channels { get; set; } public DbSet<Channel> Channels { get; set; }
//public DbSet<Emoji> Emoji {get;set;} public DbSet<UAC> UACs { 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) {
.EnableSensitiveDataLogging(true); //who the fuck is looking at log output but not allowed to see it? this should be on by default. optionsBuilder.UseNpgsql(Shared.DBConnectionString);
//.EnableSensitiveDataLogging(true); //"sensitive" is one thing. writing "did something" every time you think a thought is a different thing.
}
} }

View File

@ -38,6 +38,20 @@ public static class Enumerations
[Description("organizational psuedo-channel")] [Description("organizational psuedo-channel")]
OU OU
} }
///<summary>
///bro. don't even get me started. tl;dr: hashtag microsoft.
///</summary>
public enum HttpVerb
{
Get,
Post,
Put,
Delete,
Head,
Patch,
Options
}
public static string GetDescription<T>(this T enumerationValue) public static string GetDescription<T>(this T enumerationValue)
where T : struct where T : struct
@ -45,7 +59,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;
}
}

33
Models/FranzMessage.cs Normal file
View File

@ -0,0 +1,33 @@
using System;
using System.Collections.Generic;
using franz;
using gray_messages;
//psst, future adam: that means we're gray_messages.chat.chat_message.
namespace gray_messages.chat
{
public class chat_message : gray_messages.message
{
//expect this to be the same every time. and, "localhost". but importantly, it'll remind me of the port.
public Uri Api_Uri { get; set; }
public Guid MessageId { get; set; }
public string Content { get; set; }
public string RawContent { get; set; }
public bool MentionsMe { get; set; }
public DateTimeOffset Timestamp { get; set; }
public uint AttachmentCount { get; set; }
public Guid AccountId { get; set; }
public string AccountName { get; set; }
public Guid UserId { get; set; }
public string UserName { get; set; }
public Guid ChannelId { get; set; }
public string ChannelName { get; set; }
public string ChannelProtoocl { get; set; }
public List<Guid> UAC_Matches { get; set; }
public List<string> BehavedOnBy { get; set; }
}
}

View File

@ -6,6 +6,7 @@ using System.ComponentModel.DataAnnotations.Schema;
using System.Reflection; using System.Reflection;
using System.Threading.Tasks; using System.Threading.Tasks;
using Discord.WebSocket; using Discord.WebSocket;
using Microsoft.EntityFrameworkCore;
public class Message public class Message
{ {
@ -14,18 +15,13 @@ public class Message
public string Protocol { get; set; } public string Protocol { get; set; }
public string ExternalId { get; set; } public string ExternalId { get; set; }
public string Content { get; set; } public string Content { get; set; }
public string TranslatedContent { get; set; }
public bool MentionsMe { get; set; } public bool MentionsMe { get; set; }
public DateTimeOffset Timestamp { get; set; } public DateTimeOffset Timestamp { get; set; }
public bool ActedOn { get; set; } public bool ActedOn { get; set; }
[DeleteBehavior(DeleteBehavior.Cascade)]
public List<Attachment> Attachments { get; set; } public List<Attachment> Attachments { get; set; }
public Account Author { get; set; } public Account Author { get; set; }
public Channel Channel { get; set; } public Channel Channel { get; set; }
public Guid? ChannelId { get; set; }
[NonSerialized]
public Func<string, Task> Reply;
[NonSerialized]
public Func<string, Task> React;
} }

37
Models/UAC.cs Normal file
View File

@ -0,0 +1,37 @@
namespace vassago.Models;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using vassago.Models;
//TODO: rename.
//"uac" originally meant "user account control". but it might just be channel control. in fact, channel-control is much more fun,
//then the platform manages the permissions for you!
//but now I'm going to add locales to it, so it's kind of... "miscellaneous attached data". Official Sticky Notes, if you will.
public class UAC
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid Id { get; set; }
///<summary indulgence="haiku-like">
///behaviors will have
///a hardcoded ID thing
///so they can find theirs.
///</summary>
public Guid OwnerId { get; set;}
public string DisplayName { get; set; }
public List<Account> AccountInChannels { get; set; }
public List<Channel> Channels { get; set; }
public List<User> Users { get; set; }
///<summary>"but past adam", you may ask. "if UACs are configured before runtime, why not write html into your source control, as part of the project,
///with the benefit of an html editor?"
///absolutely fair question. **But**: the plan is for external services, e.g., over kafka, to manage their own. So from Vassago's perspective,
///it's variably before and after compile time. shrug.emote.
///</summary>
public string Description { get; set; }
public Dictionary<string, string> CommandAlterations {get; set;}
public Dictionary<string, string> Translations {get; set;}
}

View File

@ -4,16 +4,25 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
using System.Reflection; using System.Reflection;
using Microsoft.EntityFrameworkCore;
public class User public class User
{ {
[DatabaseGenerated(DatabaseGeneratedOption.Identity)] [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid Id { get; set; } public Guid Id { get; set; }
[DeleteBehavior(DeleteBehavior.Cascade)]
public List<Account> Accounts { get; set; } public List<Account> Accounts { get; set; }
public List<UAC> UACs { get; set; }
//if I ever get lots and lots of tags, or some automatic way to register a feature's arbitrary tags, then I can move this off.
//public bool Tag_CanTwitchSummon { get; set; }
public string DisplayName public string DisplayName
{ {
get get
{
if (Accounts?.Any() ?? false)
{ {
return Accounts.Select(a => a.DisplayName).Distinct() return Accounts.Select(a => a.DisplayName).Distinct()
.MaxBy(distinctName => .MaxBy(distinctName =>
@ -21,5 +30,10 @@ public class User
.Where(selectedName => selectedName == distinctName).Count() .Where(selectedName => selectedName == distinctName).Count()
); );
} }
else
{
return $"[accountless {Id}]";
}
}
} }
} }

View File

@ -1,4 +1,7 @@
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Mvc.NewtonsoftJson;
using vassago;
using vassago.Models; using vassago.Models;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
@ -6,17 +9,21 @@ 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(options => {
options.SerializerSettings.ReferenceLoopHandling =
Newtonsoft.Json.ReferenceLoopHandling.Ignore;
});
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();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles(); app.UseStaticFiles();
app.UseRouting(); app.UseRouting();
app.UseAuthorization(); app.UseAuthorization();
@ -24,4 +31,21 @@ 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();
}
Shared.App = app;
app.Run(); app.Run();

View File

@ -13,24 +13,25 @@ using Microsoft.EntityFrameworkCore;
using System.Threading; using System.Threading;
using System.Reactive.Linq; using System.Reactive.Linq;
namespace vassago.DiscordInterface; namespace vassago.ProtocolInterfaces.DiscordInterface;
public class DiscordInterface //data received
//translate data to internal type
//store
//ship off to behaver
public class DiscordInterface : ProtocolInterface
{ {
internal const string PROTOCOL = "discord"; public static new string Protocol { get => "discord"; }
internal DiscordSocketClient client; internal DiscordSocketClient client;
private bool eventsSignedUp = false; private bool eventsSignedUp = false;
private ChattingContext _db; private static readonly SemaphoreSlim discordChannelSetup = new(1, 1);
private static SemaphoreSlim discordChannelSetup = new SemaphoreSlim(1, 1);
private Channel protocolAsChannel; private Channel protocolAsChannel;
public override Channel SelfChannel { get => protocolAsChannel; }
public DiscordInterface() public async Task Init(string config)
{
_db = new ChattingContext();
}
public async Task Init(string token)
{ {
var token = config;
await SetupDiscordChannel(); await SetupDiscordChannel();
client = new DiscordSocketClient(new DiscordSocketConfig() { GatewayIntents = GatewayIntents.All }); client = new DiscordSocketClient(new DiscordSocketConfig() { GatewayIntents = GatewayIntents.All });
@ -39,8 +40,8 @@ public class DiscordInterface
Console.WriteLine(msg.ToString()); Console.WriteLine(msg.ToString());
return Task.CompletedTask; return Task.CompletedTask;
}; };
client.Connected += SelfConnected; client.Connected += () => Task.Run(SelfConnected);
client.Ready += ClientReady; client.Ready += () => Task.Run(ClientReady);
await client.LoginAsync(TokenType.Bot, token); await client.LoginAsync(TokenType.Bot, token);
await client.StartAsync(); await client.StartAsync();
@ -52,30 +53,30 @@ public class DiscordInterface
try 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) if (protocolAsChannel == null)
{ {
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 = []
}; };
protocolAsChannel.SendMessage = (t) => { throw new InvalidOperationException($"discord itself cannot accept text"); };
protocolAsChannel.SendFile = (f, t) => { throw new InvalidOperationException($"discord itself cannot send file"); };
_db.Channels.Add(protocolAsChannel);
_db.SaveChanges();
} }
else
{
Console.WriteLine($"discord, channel with id {protocolAsChannel.Id}, already exists");
}
protocolAsChannel.DisplayName = "discord (itself)";
protocolAsChannel = Rememberer.RememberChannel(protocolAsChannel);
Console.WriteLine($"protocol as channel addeed; {protocolAsChannel}");
} }
finally finally
{ {
@ -92,7 +93,7 @@ public class DiscordInterface
client.MessageReceived += MessageReceived; client.MessageReceived += MessageReceived;
// _client.MessageUpdated += // _client.MessageUpdated +=
//client.UserJoined += UserJoined; client.UserJoined += UserJoined;
client.SlashCommandExecuted += SlashCommandHandler; client.SlashCommandExecuted += SlashCommandHandler;
//client.ChannelCreated += //client.ChannelCreated +=
// _client.ChannelDestroyed += // _client.ChannelDestroyed +=
@ -117,21 +118,29 @@ public class DiscordInterface
private async Task SelfConnected() private async Task SelfConnected()
{ {
var selfUser = UpsertAccount(client.CurrentUser, protocolAsChannel.Id); await discordChannelSetup.WaitAsync();
selfUser.DisplayName = client.CurrentUser.Username;
await _db.SaveChangesAsync(); try
Behaver.Instance.Selves.Add(selfUser); {
var selfAccount = UpsertAccount(client.CurrentUser, protocolAsChannel);
selfAccount.DisplayName = client.CurrentUser.Username;
Behaver.Instance.MarkSelf(selfAccount);
}
finally
{
discordChannelSetup.Release();
}
} }
private async Task MessageReceived(SocketMessage messageParam) private async Task MessageReceived(SocketMessage messageParam)
{ {
var suMessage = messageParam as SocketUserMessage; if (messageParam is not SocketUserMessage)
if (suMessage == null)
{ {
Console.WriteLine($"{messageParam.Content}, but not a user message"); Console.WriteLine($"{messageParam.Content}, but not a user message");
return; return;
} }
var suMessage = messageParam as SocketUserMessage;
Console.WriteLine($"#{suMessage.Channel}[{DateTime.Now}][{suMessage.Author.Username} [id={suMessage.Author.Id}]][msg id: {suMessage.Id}] {suMessage.Content}"); Console.WriteLine($"#{suMessage.Channel}[{DateTime.Now}][{suMessage.Author.Username} [id={suMessage.Author.Id}]][msg id: {suMessage.Id}] {suMessage.Content}");
var m = UpsertMessage(suMessage); var m = UpsertMessage(suMessage);
@ -141,29 +150,19 @@ public class DiscordInterface
var mentionOfMe = "<@" + client.CurrentUser.Id + ">"; var mentionOfMe = "<@" + client.CurrentUser.Id + ">";
m.MentionsMe = true; m.MentionsMe = true;
} }
if (await Behaver.Instance.ActOn(m)) 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.
m.ActedOn = true; // ...but we don't save?
}
_db.SaveChanges();
} }
private void UserJoined(SocketGuildUser arg) private Task UserJoined(SocketGuildUser arg)
{ {
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;
} return null;
private async Task ButtonHandler(SocketMessageComponent component)
{
switch (component.Data.CustomId)
{
case "custom-id":
await component.RespondAsync($"{component.User.Mention}, it's been here the whole time!");
break;
}
} }
internal static async Task SlashCommandHandler(SocketSlashCommand command) internal static async Task SlashCommandHandler(SocketSlashCommand command)
{ {
@ -172,7 +171,7 @@ public class DiscordInterface
case "freedomunits": case "freedomunits":
try try
{ {
var amt = Convert.ToDecimal((double)(command.Data.Options.First(o => o.Name == "amount").Value)); var amt = (double)(command.Data.Options.First(o => o.Name == "amount").Value);
var src = (string)command.Data.Options.First(o => o.Name == "src-unit").Value; var src = (string)command.Data.Options.First(o => o.Name == "src-unit").Value;
var dest = (string)command.Data.Options.First(o => o.Name == "dest-unit").Value; var dest = (string)command.Data.Options.First(o => o.Name == "dest-unit").Value;
var conversionResult = Conversion.Converter.Convert(amt, src, dest); var conversionResult = Conversion.Converter.Convert(amt, src, dest);
@ -191,35 +190,30 @@ public class DiscordInterface
break; break;
} }
} }
internal vassago.Models.Attachment UpsertAttachment(IAttachment dAttachment) internal static vassago.Models.Attachment UpsertAttachment(IAttachment dAttachment)
{ {
var a = _db.Attachments.FirstOrDefault(ai => ai.ExternalId == dAttachment.Id); var a = Rememberer.SearchAttachment(ai => ai.ExternalId == dAttachment.Id)
if (a == null) ?? new vassago.Models.Attachment();
{
a = new vassago.Models.Attachment();
_db.Attachments.Add(a);
}
a.ContentType = dAttachment.ContentType; a.ContentType = dAttachment.ContentType;
a.Description = dAttachment.Description; a.Description = dAttachment.Description;
a.Filename = dAttachment.Filename; a.Filename = dAttachment.Filename;
a.Size = dAttachment.Size; a.Size = dAttachment.Size;
a.Source = new Uri(dAttachment.Url); a.Source = new Uri(dAttachment.Url);
Rememberer.RememberAttachment(a);
return a; return a;
} }
internal Message UpsertMessage(IUserMessage dMessage) internal Message UpsertMessage(IUserMessage dMessage)
{ {
var m = _db.Messages.FirstOrDefault(mi => mi.ExternalId == dMessage.Id.ToString() && mi.Protocol == PROTOCOL); var m = Rememberer.SearchMessage(mi => mi.ExternalId == dMessage.Id.ToString() && mi.Protocol == Protocol)
if (m == null) ?? new()
{ {
m = new Message(); Protocol = Protocol
m.Protocol = PROTOCOL; };
_db.Messages.Add(m);
} if (dMessage.Attachments?.Count > 0)
m.Attachments = m.Attachments ?? new List<vassago.Models.Attachment>();
if (dMessage.Attachments?.Any() == true)
{ {
m.Attachments = new List<vassago.Models.Attachment>(); m.Attachments = [];
foreach (var da in dMessage.Attachments) foreach (var da in dMessage.Attachments)
{ {
m.Attachments.Add(UpsertAttachment(da)); m.Attachments.Add(UpsertAttachment(da));
@ -229,123 +223,275 @@ 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.
} }
m.MentionsMe = (dMessage.Author.Id != client.CurrentUser.Id m.MentionsMe = (dMessage.Author.Id != client.CurrentUser.Id
&& (dMessage.MentionedUserIds?.FirstOrDefault(muid => muid == client.CurrentUser.Id) > 0)); && (dMessage.MentionedUserIds?.FirstOrDefault(muid => muid == client.CurrentUser.Id) > 0));
m.Reply = (t) => { return dMessage.ReplyAsync(t); }; Rememberer.RememberMessage(m);
m.React = (e) => { return attemptReact(dMessage, e); }; Console.WriteLine($"received message; author: {m.Author.DisplayName}, {m.Author.Id}. messageid:{m.Id}");
return m; return m;
} }
internal Channel UpsertChannel(IMessageChannel channel) internal Channel UpsertChannel(IMessageChannel channel)
{ {
Channel c = _db.Channels.FirstOrDefault(ci => ci.ExternalId == channel.Id.ToString() && ci.Protocol == PROTOCOL); Channel c = Rememberer.SearchChannel(ci => ci.ExternalId == channel.Id.ToString() && ci.Protocol == Protocol);
if (c == null) if (c == null)
{ {
c = new Channel(); Console.WriteLine($"couldn't find channel under protocol {Protocol} with externalId {channel.Id.ToString()}");
_db.Channels.Add(c); c = new Channel()
{
Users = []
};
} }
c.DisplayName = channel.Name;
c.ExternalId = channel.Id.ToString(); c.ExternalId = channel.Id.ToString();
c.ChannelType = (channel is IPrivateChannel) ? vassago.Models.Enumerations.ChannelType.DM : vassago.Models.Enumerations.ChannelType.Normal; c.ChannelType = (channel is IPrivateChannel) ? vassago.Models.Enumerations.ChannelType.DM : vassago.Models.Enumerations.ChannelType.Normal;
c.Messages = c.Messages ?? new List<Message>(); c.Messages ??= [];
c.Protocol = PROTOCOL; c.Protocol = Protocol;
if (channel is IGuildChannel) if (channel is IGuildChannel)
{ {
Console.WriteLine($"{channel.Name} is a guild channel. So i'm going to upsert the guild, {(channel as IGuildChannel).Guild}");
c.ParentChannel = UpsertChannel((channel as IGuildChannel).Guild); c.ParentChannel = UpsertChannel((channel as IGuildChannel).Guild);
c.ParentChannel.SubChannels.Add(c);
} }
else if (channel is IPrivateChannel) else if (channel is IPrivateChannel)
{ {
c.ParentChannel = protocolAsChannel; c.ParentChannel = protocolAsChannel;
Console.WriteLine("i'm a private channel so I'm setting my parent channel to the protocol as channel");
} }
else else
{ {
c.ParentChannel = protocolAsChannel; c.ParentChannel = protocolAsChannel;
Console.Error.WriteLine($"trying to upsert channel {channel.Id}/{channel.Name}, but it's neither guildchannel nor private channel. shrug.jpg"); Console.Error.WriteLine($"trying to upsert channel {channel.Id}/{channel.Name}, but it's neither guildchannel nor private channel. shrug.jpg");
} }
c.SubChannels = c.SubChannels ?? new List<Channel>();
c.SendMessage = (t) => { return channel.SendMessageAsync(t); };
c.SendFile = (f, t) => { return channel.SendFileAsync(f, t); };
switch(c.ChannelType) Console.WriteLine($"upsertion of channel {c.DisplayName}, it's type {c.ChannelType}");
switch (c.ChannelType)
{ {
case vassago.Models.Enumerations.ChannelType.DM: case vassago.Models.Enumerations.ChannelType.DM:
c.DisplayName = "DM: " + (channel as IPrivateChannel).Recipients?.FirstOrDefault(u => u.Id != client.CurrentUser.Id).Username; var asPriv = (channel as IPrivateChannel);
var sender = asPriv?.Recipients?.FirstOrDefault(u => u.Id != client.CurrentUser.Id); // why yes, there's a list of recipients, and it's the sender.
if (sender != null)
{
c.DisplayName = "DM: " + sender.Username;
}
else
{
//I sent it, so I don't know the recipient's name.
}
break;
default:
c.DisplayName = channel.Name;
break; break;
} }
Channel parentChannel = null;
if (channel is IGuildChannel)
{
parentChannel = Rememberer.SearchChannel(c => c.ExternalId == (channel as IGuildChannel).Guild.Id.ToString() && c.Protocol == Protocol);
if (parentChannel is null)
{
Console.Error.WriteLine("why am I still null?");
}
}
else if (channel is IPrivateChannel)
{
parentChannel = protocolAsChannel;
}
else
{
parentChannel = protocolAsChannel;
Console.Error.WriteLine($"trying to upsert channel {channel.Id}/{channel.Name}, but it's neither guildchannel nor private channel. shrug.jpg");
}
parentChannel.SubChannels ??= [];
if (!parentChannel.SubChannels.Contains(c))
{
parentChannel.SubChannels.Add(c);
}
c = Rememberer.RememberChannel(c);
//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);
}
return c; return c;
} }
internal Channel UpsertChannel(IGuild channel) internal Channel UpsertChannel(IGuild channel)
{ {
Channel c = _db.Channels.FirstOrDefault(ci => ci.ExternalId == channel.Id.ToString() && ci.Protocol == PROTOCOL); Channel c = Rememberer.SearchChannel(ci => ci.ExternalId == channel.Id.ToString() && ci.Protocol == Protocol);
if (c == null) if (c == null)
{ {
Console.WriteLine($"couldn't find channel under protocol {Protocol} with externalId {channel.Id.ToString()}");
c = new Channel(); c = new Channel();
_db.Channels.Add(c);
} }
c.DisplayName = channel.Name; c.DisplayName = channel.Name;
c.ExternalId = channel.Id.ToString(); c.ExternalId = channel.Id.ToString();
c.ChannelType = vassago.Models.Enumerations.ChannelType.Normal; c.ChannelType = vassago.Models.Enumerations.ChannelType.OU;
c.Messages = c.Messages ?? new List<Message>(); c.Messages ??= [];
c.Protocol = protocolAsChannel.Protocol; c.Protocol = protocolAsChannel.Protocol;
c.ParentChannel = protocolAsChannel; c.ParentChannel = protocolAsChannel;
c.SubChannels = c.SubChannels ?? new List<Channel>(); c.SubChannels ??= [];
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"); }; return Rememberer.RememberChannel(c);
c.SendFile = (f, t) => { throw new InvalidOperationException($"channel {channel.Name} is guild; send file"); };
return c;
} }
internal Account UpsertAccount(IUser user, Guid inChannel) internal static Account UpsertAccount(IUser discordUser, Channel inChannel)
{ {
var acc = _db.Accounts.FirstOrDefault(ui => ui.ExternalId == user.Id.ToString() && ui.SeenInChannel.Id == inChannel); var acc = Rememberer.SearchAccount(ui => ui.ExternalId == discordUser.Id.ToString() && ui.SeenInChannel.Id == inChannel.Id);
if (acc == null) Console.WriteLine($"upserting account, retrieved {acc?.Id}.");
if (acc != null)
{ {
acc = new Account(); Console.WriteLine($"acc's user: {acc.IsUser?.Id}");
_db.Accounts.Add(acc);
} }
acc.Username = user.Username; acc ??= new Account()
acc.ExternalId = user.Id.ToString(); {
acc.IsBot = user.IsBot || user.IsWebhook; IsUser = Rememberer.SearchUser(u => u.Accounts.Any(a => a.ExternalId == discordUser.Id.ToString() && a.Protocol == Protocol))
acc.Protocol = PROTOCOL; ?? new User()
};
acc.IsUser = _db.Users.FirstOrDefault(u => u.Accounts.Any(a => a.ExternalId == acc.ExternalId && a.Protocol == acc.Protocol)); acc.Username = discordUser.Username;
if(acc.IsUser == null) acc.ExternalId = discordUser.Id.ToString();
acc.IsBot = discordUser.IsBot || discordUser.IsWebhook;
acc.Protocol = Protocol;
acc.SeenInChannel = inChannel;
Console.WriteLine($"we asked rememberer to search for acc's user. {acc.IsUser?.Id}");
if (acc.IsUser != null)
{ {
acc.IsUser = new User() { Accounts = new List<Account>() { acc } }; Console.WriteLine($"user has record of {acc.IsUser.Accounts?.Count ?? 0} accounts");
_db.Users.Add(acc.IsUser); }
acc.IsUser ??= new 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; return acc;
} }
private Task attemptReact(IUserMessage msg, string e) private static async Task<int> AttemptReact(IUserMessage msg, string e)
{ {
var c = _db.Channels.FirstOrDefault(c => c.ExternalId == msg.Channel.Id.ToString()); Console.WriteLine("discord attempting to react");
var c = Rememberer.SearchChannel(c => c.ExternalId == msg.Channel.Id.ToString());// 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); msg.AddReactionAsync(emoji);
return 200;
} }
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}");
return Task.CompletedTask; return 405;
} }
return msg.AddReactionAsync(emote); msg.AddReactionAsync(emote);
return 200;
} }
private static string TruncateText(string msg, uint? chars)
{
chars ??= 500;
if (msg?.Length > chars)
{
return msg.Substring(0, (int)chars - 2) + "✂";
}
else
{
return msg;
}
}
public override async Task<int> SendMessage(Channel channel, string text)
{
var dcCh = await client.GetChannelAsync(ulong.Parse(channel.ExternalId));
if (dcCh == null)
{
return 404;
}
if (dcCh is IMessageChannel msgChannel)
{
await msgChannel.SendMessageAsync(TruncateText(text, channel.MaxTextChars));
return 200;
}
else
{
return 503;
}
}
public override async Task<int> SendFile(Channel channel, string base64dData, string filename, string accompanyingText)
{
var dcCh = await client.GetChannelAsync(ulong.Parse(channel.ExternalId));
if (dcCh == null)
{
return 404;
}
if (dcCh is IMessageChannel msgChannel)
{
using (var ms = new MemoryStream(Convert.FromBase64String(base64dData)))
{
await msgChannel.SendFileAsync(ms, filename, TruncateText(accompanyingText, channel.MaxTextChars));
}
return 200;
}
else
{
return 503;
}
}
public override async Task<int> React(Message message, string reaction)
{
var dcCh = await client.GetChannelAsync(ulong.Parse(message.Channel.ExternalId));
if (dcCh == null)
return 404;
if (dcCh is IMessageChannel msgChannel)
{
var dcMsg = await msgChannel.GetMessageAsync(ulong.Parse(message.ExternalId));
if (dcMsg == null)
return 404;
return await AttemptReact(dcMsg as IUserMessage, reaction);
}
else
{
return 503;
}
}
public override async Task<int> Reply(Message message, string text)
{
var dcCh = await client.GetChannelAsync(ulong.Parse(message.Channel.ExternalId));
if (dcCh == null)
return 404;
if (dcCh is IMessageChannel msgChannel)
{
var dcMsg = await msgChannel.GetMessageAsync(ulong.Parse(message.ExternalId));
if (dcMsg == null)
return 404;
(dcMsg as IUserMessage).ReplyAsync(TruncateText(text, message.Channel.MaxTextChars));
return 200;
}
else
{
return 503;
}
}
} }

View File

@ -7,9 +7,8 @@ using Discord.WebSocket;
using Discord; using Discord;
using Discord.Net; using Discord.Net;
namespace vassago.DiscordInterface namespace vassago.ProtocolInterfaces.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>()
@ -17,13 +16,14 @@ namespace vassago.DiscordInterface
new CommandSetup(){ new CommandSetup(){
Id = "freedomunits", Id = "freedomunits",
UpdatedAt = new DateTime(2023, 5, 21, 13, 3, 0), UpdatedAt = new DateTime(2023, 5, 21, 13, 3, 0),
guild = 825293851110801428, guild = 825293851110801428, //TODO: demagic this magic number
register = register_FreedomUnits register = Register_FreedomUnits
} }
}; };
public static async Task Register(DiscordSocketClient client) public static async Task Register(DiscordSocketClient client)
{ {
return; if(Shared.SetupSlashCommands)
{
var commandsInContext = await client.GetGlobalApplicationCommandsAsync(); var commandsInContext = await client.GetGlobalApplicationCommandsAsync();
await Register(client, commandsInContext, null); await Register(client, commandsInContext, null);
foreach (var guild in client.Guilds) foreach (var guild in client.Guilds)
@ -38,6 +38,7 @@ namespace vassago.DiscordInterface
} }
} }
} }
}
private static async Task Register(DiscordSocketClient client, IEnumerable<SocketApplicationCommand> commandsInContext, SocketGuild guild) private static async Task Register(DiscordSocketClient client, IEnumerable<SocketApplicationCommand> commandsInContext, SocketGuild guild)
{ {
@ -67,7 +68,7 @@ namespace vassago.DiscordInterface
} }
} }
private static async Task register_FreedomUnits(bool isNew, DiscordSocketClient client, SocketGuild guild) private static async Task Register_FreedomUnits(bool isNew, DiscordSocketClient client, SocketGuild guild)
{ {
var builtCommand = new SlashCommandBuilder() var builtCommand = new SlashCommandBuilder()
.WithName("freedomunits") .WithName("freedomunits")

View File

@ -0,0 +1,22 @@
namespace vassago.ProtocolInterfaces;
using vassago.Models;
public abstract class ProtocolInterface
{
public static string Protocol { get; }
public abstract Channel SelfChannel { get; }
public abstract Task<int> SendMessage(Channel channel, string text);
public virtual async Task<int> SendFile(Channel channel, string path, string accompanyingText)
{
if (!File.Exists(path))
{
return 404;
}
var fstring = Convert.ToBase64String(File.ReadAllBytes(path));
return await SendFile(channel, fstring, Path.GetFileName(path), accompanyingText);
}
public abstract Task<int> SendFile(Channel channel, string base64dData, string filename, string accompanyingText);
public abstract Task<int> React(Message message, string reaction);
public abstract Task<int> Reply(Message message, string text);
}

View File

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

View File

@ -3,7 +3,5 @@ namespace vassago.TwitchInterface;
public class TwitchConfig public class TwitchConfig
{ {
public string username {get; set;} public string username {get; set;}
public string clientId {get; set;}
public string secret {get; set;}
public string oauth {get; set;} public string oauth {get; set;}
} }

View File

@ -1,66 +1,63 @@
using RestSharp;
using System.Security.Cryptography.X509Certificates; using System.Security.Cryptography.X509Certificates;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using RestSharp;
using TwitchLib.Api;
using TwitchLib.Api.Helix.Models.Users.GetUsers; using TwitchLib.Api.Helix.Models.Users.GetUsers;
using TwitchLib.Client; using TwitchLib.Api;
using TwitchLib.Client.Events; using TwitchLib.Client.Events;
using TwitchLib.Client.Models; using TwitchLib.Client.Models;
using TwitchLib.Client;
using TwitchLib.Communication.Clients; using TwitchLib.Communication.Clients;
using TwitchLib.Communication.Models; using TwitchLib.Communication.Models;
using vassago.Behavior; using vassago.Behavior;
using vassago.Models; using vassago.Models;
using vassago.ProtocolInterfaces;
namespace vassago.TwitchInterface; namespace vassago.TwitchInterface;
public class TwitchInterface public class TwitchInterface : ProtocolInterface
{ {
internal const string PROTOCOL = "twitch"; public static new string Protocol { get => "twitch"; }
private bool eventsSignedUp = false; private static SemaphoreSlim channelSetupSemaphpore = new SemaphoreSlim(1, 1);
private ChattingContext _db;
private static SemaphoreSlim twitchChannelSetup = new SemaphoreSlim(1, 1);
private Channel protocolAsChannel; private Channel protocolAsChannel;
public override Channel SelfChannel { get => protocolAsChannel;}
private Account selfAccountInProtocol;
TwitchClient client; TwitchClient client;
TwitchAPI api;
public TwitchInterface()
{
_db = new ChattingContext();
}
private async Task SetupTwitchChannel() private async Task SetupTwitchChannel()
{ {
await twitchChannelSetup.WaitAsync(); await channelSetupSemaphpore.WaitAsync();
try 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) if (protocolAsChannel == null)
{ {
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 = []
}; };
protocolAsChannel.SendMessage = (t) => { throw new InvalidOperationException($"twitch itself cannot accept text"); }; protocolAsChannel.DisplayName = "twitch (itself)";
protocolAsChannel.SendFile = (f, t) => { throw new InvalidOperationException($"twitch itself cannot send file"); }; protocolAsChannel = Rememberer.RememberChannel(protocolAsChannel);
_db.Channels.Add(protocolAsChannel); Console.WriteLine($"protocol as channle added; {protocolAsChannel}");
_db.SaveChanges();
} }
else
{
Console.WriteLine($"twitch, channel with id {protocolAsChannel.Id}, already exists");
}
//protocolAsChan
} }
finally finally
{ {
twitchChannelSetup.Release(); channelSetupSemaphpore.Release();
} }
} }
@ -84,70 +81,49 @@ public class TwitchInterface
client.OnWhisperReceived += Client_OnWhisperReceivedAsync; client.OnWhisperReceived += Client_OnWhisperReceivedAsync;
client.OnConnected += Client_OnConnected; client.OnConnected += Client_OnConnected;
Console.WriteLine("twitch client 1 connecting...");
client.Connect(); client.Connect();
Console.WriteLine("twitch client 1 connected"); 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) 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}"); 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");
_db.SaveChanges();
//translate to internal, upsert
var m = UpsertMessage(e.WhisperMessage);
//can't send whispers without giving up cellphone number.
//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 Behaver.Instance.ActOn(m);
_db.SaveChanges(); m.ActedOn = true;
//TODO: remember it again?
} }
private async void Client_OnMessageReceivedAsync(object sender, OnMessageReceivedArgs e) 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}"); 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) //translate to internal, upsert
{
Console.WriteLine($"[messagereceived]: {e.ChatMessage.Id}? already seent it");
return;
}
Console.WriteLine($"[messagereceived]: {e.ChatMessage.Id}? new to me.");
var m = UpsertMessage(e.ChatMessage); var m = UpsertMessage(e.ChatMessage);
m.MentionsMe = Regex.IsMatch(e.ChatMessage.Message?.ToLower(), $"@{e.ChatMessage.BotUsername.ToLower()}\\b") || m.Channel.ChannelType = vassago.Models.Enumerations.ChannelType.Normal;
e.ChatMessage.ChatReply?.ParentUserLogin == e.ChatMessage.BotUsername; //act on
_db.SaveChanges();
await Behaver.Instance.ActOn(m); await Behaver.Instance.ActOn(m);
_db.SaveChanges(); m.ActedOn = true;
//TODO: remember again?
} }
private async void Client_OnConnected(object sender, OnConnectedArgs e) private void Client_OnConnected(object sender, OnConnectedArgs e)
{ {
var selfUser = UpsertAccount(e.BotUsername, protocolAsChannel.Id); Console.WriteLine($"twitch marking selfaccount as seeninchannel {protocolAsChannel.Id}");
selfAccountInProtocol = UpsertAccount(e.BotUsername, protocolAsChannel);
await _db.SaveChangesAsync(); selfAccountInProtocol.DisplayName = e.BotUsername;
Behaver.Instance.Selves.Add(selfUser); Behaver.Instance.MarkSelf(selfAccountInProtocol);
Console.WriteLine($"Connected to {e.AutoJoinChannel}"); Console.WriteLine($"Connected to {e.AutoJoinChannel}");
AttemptJoin(e.BotUsername);
} }
private void Client_OnJoinedChannel(object sender, OnJoinedChannelArgs e) private void Client_OnJoinedChannel(object sender, OnJoinedChannelArgs e)
@ -160,119 +136,147 @@ public class TwitchInterface
Console.WriteLine($"{e.DateTime.ToString()}: {e.BotUsername} - {e.Data}"); Console.WriteLine($"{e.DateTime.ToString()}: {e.BotUsername} - {e.Data}");
} }
private Account UpsertAccount(string username, Guid inChannel) private Account UpsertAccount(string username, Channel inChannel)
{ {
var acc = _db.Accounts.FirstOrDefault(ui => ui.ExternalId == username && ui.SeenInChannel.Id == inChannel); //Console.WriteLine($"upserting twitch account. username: {username}. inChannel: {inChannel?.Id}");
if (acc == null) var acc = Rememberer.SearchAccount(ui => ui.ExternalId == username && ui.SeenInChannel.ExternalId == inChannel.ExternalId);
// Console.WriteLine($"upserting twitch account, retrieved {acc?.Id}.");
if (acc != null)
{ {
acc = new Account(); Console.WriteLine($"acc's usser: {acc.IsUser?.Id}");
_db.Accounts.Add(acc);
} }
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.Username = username;
acc.ExternalId = 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.Protocol = Protocol;
acc.SeenInChannel = inChannel;
acc.IsUser = _db.Users.FirstOrDefault(u => u.Accounts.Any(a => a.ExternalId == acc.ExternalId && a.Protocol == acc.Protocol)); // Console.WriteLine($"we asked rememberer to search for acc's user. {acc.IsUser?.Id}");
if (acc.IsUser == null) // if (acc.IsUser != null)
// {
// Console.WriteLine($"user has record of {acc.IsUser.Accounts?.Count ?? 0} accounts");
// }
acc.IsUser ??= new vassago.Models.User() { Accounts = [acc] };
// if (inChannel.Users?.Count > 0)
// {
// Console.WriteLine($"channel has {inChannel.Users.Count} accounts");
// }
Rememberer.RememberAccount(acc);
inChannel.Users ??= [];
if (!inChannel.Users.Contains(acc))
{ {
acc.IsUser = new vassago.Models.User() { Accounts = new List<Account>() { acc } }; inChannel.Users.Add(acc);
_db.Users.Add(acc.IsUser); Rememberer.RememberChannel(inChannel);
} }
return acc; return acc;
} }
private Channel UpsertChannel(string channelName) 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) if (c == null)
{ {
c = new Channel(); // Console.WriteLine($"couldn't find channel under protocol {PROTOCOL} with externalId {channelName}");
_db.Channels.Add(c); c = new Channel()
{
Users = []
};
} }
c.DisplayName = channelName; c.DisplayName = channelName;
c.ExternalId = channelName; c.ExternalId = channelName;
c.ChannelType = vassago.Models.Enumerations.ChannelType.Normal; c.ChannelType = vassago.Models.Enumerations.ChannelType.Normal;
c.Messages = c.Messages ?? new List<Message>(); c.Messages ??= [];
c.Protocol = PROTOCOL; c.Protocol = Protocol;
c.ParentChannel = protocolAsChannel; c.ParentChannel = protocolAsChannel;
c.SubChannels = c.SubChannels ?? new List<Channel>(); c.SubChannels = c.SubChannels ?? new List<Channel>();
c.SendMessage = (t) => { return Task.Run(() => { client.SendMessage(channelName, t); }); }; c = Rememberer.RememberChannel(c);
c.SendFile = (f, t) => { throw new InvalidOperationException($"twitch cannot send files"); };
var selfAccountInChannel = c.Users?.FirstOrDefault(a => a.ExternalId == selfAccountInProtocol.ExternalId);
if (selfAccountInChannel == null)
{
selfAccountInChannel = UpsertAccount(selfAccountInProtocol.Username, c);
}
return c; return c;
} }
private Channel UpsertDMChannel(string whisperWith) 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) if (c == null)
{ {
c = new Channel(); // Console.WriteLine($"couldn't find channel under protocol {PROTOCOL}, whisper with {whisperWith}");
_db.Channels.Add(c); c = new Channel()
{
Users = []
};
} }
c.DisplayName = $"Whisper: {whisperWith}"; c.DisplayName = $"Whisper: {whisperWith}";
c.ExternalId = $"w_{whisperWith}"; c.ExternalId = $"w_{whisperWith}";
c.ChannelType = vassago.Models.Enumerations.ChannelType.DM; c.ChannelType = vassago.Models.Enumerations.ChannelType.DM;
c.Messages = c.Messages ?? new List<Message>(); c.Messages ??= [];
c.Protocol = PROTOCOL; c.Protocol = Protocol;
c.ParentChannel = protocolAsChannel; c.ParentChannel = protocolAsChannel;
c.SubChannels = c.SubChannels ?? new List<Channel>(); c.SubChannels = c.SubChannels ?? new List<Channel>();
c.SendMessage = (t) => { return Task.Run(() => { c = Rememberer.RememberChannel(c);
try
{
client.SendWhisper(whisperWith, t); var selfAccountInChannel = c.Users.FirstOrDefault(a => a.ExternalId == selfAccountInProtocol.ExternalId);
} if (selfAccountInChannel == null)
catch(Exception e)
{ {
Console.Error.WriteLine(e); selfAccountInChannel = UpsertAccount(selfAccountInChannel.Username, c);
} }
});
};
c.SendFile = (f, t) => { throw new InvalidOperationException($"twitch cannot send files"); };
return 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) private Message UpsertMessage(ChatMessage chatMessage)
{ {
var m = _db.Messages.FirstOrDefault(mi => mi.ExternalId == chatMessage.Id); var m = Rememberer.SearchMessage(mi => mi.ExternalId == chatMessage.Id && mi.Protocol == Protocol)
if (m == null) ?? new()
{ {
m = new Message(); Protocol = Protocol,
m.Protocol = PROTOCOL; Timestamp = (DateTimeOffset)DateTime.SpecifyKind(DateTime.UtcNow, DateTimeKind.Utc)
_db.Messages.Add(m); };
m.Timestamp = (DateTimeOffset)DateTime.SpecifyKind(DateTime.UtcNow, DateTimeKind.Utc);
}
m.Content = chatMessage.Message; m.Content = chatMessage.Message;
m.ExternalId = chatMessage.Id; m.ExternalId = chatMessage.Id;
m.Channel = UpsertChannel(chatMessage.Channel); m.Channel = UpsertChannel(chatMessage.Channel);
m.Author = UpsertAccount(chatMessage.Username, m.Channel.Id); m.Author = UpsertAccount(chatMessage.Username, m.Channel);
m.Author.SeenInChannel = m.Channel; m.MentionsMe = Regex.IsMatch(m.Content?.ToLower(), $"@\\b{selfAccountInProtocol.Username.ToLower()}\\b");
Rememberer.RememberMessage(m);
m.Reply = (t) => { return Task.Run(() => { client.SendReply(chatMessage.Channel, chatMessage.Id, t); }); };
m.React = (e) => { throw new InvalidOperationException($"twitch cannot react"); };
return 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) private Message UpsertMessage(WhisperMessage whisperMessage)
{ {
var m = _db.Messages.FirstOrDefault(mi => mi.ExternalId == whisperMessage.MessageId); //WhisperMessage.Id corresponds to chatMessage.Id. \*eye twitch*
if (m == null) var m = Rememberer.SearchMessage(mi => mi.ExternalId == whisperMessage.MessageId && mi.Protocol == Protocol)
?? new()
{ {
m = new Message(); Id = Guid.NewGuid(),
m.Protocol = PROTOCOL; Protocol = Protocol,
_db.Messages.Add(m); Timestamp = (DateTimeOffset)DateTime.SpecifyKind(DateTime.UtcNow, DateTimeKind.Utc)
m.Timestamp = (DateTimeOffset)DateTime.SpecifyKind(DateTime.UtcNow, DateTimeKind.Utc); };
}
m.Content = whisperMessage.Message; m.Content = whisperMessage.Message;
m.ExternalId = whisperMessage.MessageId; m.ExternalId = whisperMessage.MessageId;
m.Channel = UpsertDMChannel(whisperMessage.Username); m.Channel = UpsertDMChannel(whisperMessage.Username);
m.Channel.ChannelType = vassago.Models.Enumerations.ChannelType.DM; m.Author = UpsertAccount(whisperMessage.Username, m.Channel);
m.Author = UpsertAccount(whisperMessage.Username, m.Channel.Id); m.MentionsMe = Regex.IsMatch(m.Content?.ToLower(), $"@\\b{selfAccountInProtocol.Username.ToLower()}\\b");
m.Author.SeenInChannel = m.Channel; Rememberer.RememberMessage(m);
m.Reply = (t) => { return Task.Run(() => { client.SendWhisper(whisperMessage.Username, t); }); };
m.React = (e) => { throw new InvalidOperationException($"twitch cannot react"); };
return m; return m;
} }
@ -287,4 +291,22 @@ public class TwitchInterface
client.SendMessage(channelTarget, "o7"); client.SendMessage(channelTarget, "o7");
client.LeaveChannel(channelTarget); client.LeaveChannel(channelTarget);
} }
public override async Task<int> SendMessage(Channel channel, string text)
{
Task.Run(() => { client.SendMessage(channel.ExternalId, text); });
return 200;
}
public override async Task<int> SendFile(Channel channel, string base64dData, string filename, string accompanyingText)
{
return 405;
}
public override async Task<int> React(Message message, string reaction)
{
return 405;
}
public override async Task<int> Reply(Message message, string text)
{
Task.Run(() => { client.SendReply(message.Channel.ExternalId, message.ExternalId, text); });
return 200;
}
} }

View File

@ -1,8 +1,52 @@
# 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
database diagram. is a fancy term.
message 1:n attachment
user 1:n account
channel 1:n account
channel 1:n message
account 1:n message
featurepermission n:n ?
### 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 - as in, leaf-level. If you're in a subchannel, you'll have an appropriate listing there - i.e., you will never have an account in "discord (itself)", you'll have one in the guild text-channels
### Attachment
debating whether to save a copy of every single attachment. Discord allows 100MB attachments for turbo users, and shtikbot lives in several art channels. (unfortunately, being that shtikbot doesn't have a viable SMS spam vector, it's limited to 8MB, in contradiction to discord itself reporting a server that doesn't agree to put its own name on discord's finer-grained rules has a limit of 10MB)
### 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.

329
Rememberer.cs Normal file
View File

@ -0,0 +1,329 @@
namespace vassago;
using System.Linq.Expressions;
using vassago.Models;
using Microsoft.EntityFrameworkCore;
public static class Rememberer
{
private static readonly SemaphoreSlim dbAccessSemaphore = new(1, 1);
private static readonly ChattingContext db = new();
private static List<Channel> channels;
private static bool channelCacheDirty = true;
private static void cacheChannels()
{
dbAccessSemaphore.Wait();
channels = db.Channels.ToList();
Console.WriteLine($"caching channels. {channels.Count} channels retrieved");
foreach (Channel ch in channels)
{
if (ch.ParentChannelId != null)
{
ch.ParentChannel = channels.FirstOrDefault(c => c.Id == ch.ParentChannelId);
ch.ParentChannel.SubChannels ??= [];
if (!ch.ParentChannel.SubChannels.Contains(ch))
{
ch.ParentChannel.SubChannels.Add(ch);
}
}
if (ch.Messages?.Count > 0)
{
Console.WriteLine($"{ch.DisplayName} got {ch.Messages.Count} messages");
}
ch.SubChannels ??= [];
}
channelCacheDirty = false;
dbAccessSemaphore.Release();
}
public static Account SearchAccount(Expression<Func<Account, bool>> 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)
{
List<Account> toReturn;
dbAccessSemaphore.Wait();
toReturn = db.Accounts.Where(predicate).ToList();
dbAccessSemaphore.Release();
return toReturn;
}
public static Attachment SearchAttachment(Expression<Func<Attachment, bool>> predicate)
{
Attachment toReturn;
dbAccessSemaphore.Wait();
toReturn = db.Attachments.FirstOrDefault(predicate);
dbAccessSemaphore.Release();
return toReturn;
}
public static Channel SearchChannel(Func<Channel, bool> predicate)
{
if (channelCacheDirty)
Task.Run(() => cacheChannels()).Wait();
return channels.FirstOrDefault(predicate);
}
public static Message SearchMessage(Expression<Func<Message, bool>> predicate)
{
Message toReturn;
dbAccessSemaphore.Wait();
toReturn = db.Messages.FirstOrDefault(predicate);
dbAccessSemaphore.Release();
return toReturn;
}
public static List<Message> SearchMessages(Expression<Func<Message, bool>> predicate)
{
List<Message> toReturn;
dbAccessSemaphore.Wait();
toReturn = db.Messages.Where(predicate).ToList();
dbAccessSemaphore.Release();
return toReturn;
}
public static User SearchUser(Expression<Func<User, bool>> 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)
{
dbAccessSemaphore.Wait();
toRemember.IsUser ??= new User { Accounts = [toRemember] };
db.Update(toRemember.IsUser);
db.SaveChanges();
dbAccessSemaphore.Release();
}
public static void RememberAttachment(Attachment toRemember)
{
dbAccessSemaphore.Wait();
toRemember.Message ??= new Message() { Attachments = [toRemember] };
db.Update(toRemember.Message);
db.SaveChanges();
dbAccessSemaphore.Release();
}
public static Channel RememberChannel(Channel toRemember)
{
if (channelCacheDirty)
Task.Run(() => cacheChannels()).Wait(); //so we always do 2 db trips?
dbAccessSemaphore.Wait();
db.Update(toRemember);
db.SaveChanges();
dbAccessSemaphore.Release();
channelCacheDirty = true;
cacheChannels();
return toRemember;
}
public static void RememberMessage(Message toRemember)
{
dbAccessSemaphore.Wait();
toRemember.Channel ??= new();
toRemember.Channel.Messages ??= [];
if (!toRemember.Channel.Messages.Contains(toRemember))
{
toRemember.Channel.Messages.Add(toRemember);
db.Update(toRemember.Channel);
}
db.Update(toRemember);
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)
{
Rememberer.ForgetUser(user);
}
else
{
dbAccessSemaphore.Wait();
db.Accounts.Remove(toForget);
db.SaveChanges();
dbAccessSemaphore.Release();
}
}
public static void ForgetAttachment(Attachment toForget)
{
dbAccessSemaphore.Wait();
db.Attachments.Remove(toForget);
db.SaveChanges();
dbAccessSemaphore.Release();
}
public static void ForgetChannel(Channel toForget)
{
if (toForget.SubChannels?.Count > 0)
{
foreach (var childChannel in toForget.SubChannels.ToList())
{
ForgetChannel(childChannel);
}
}
if (toForget.Users?.Count > 0)
{
foreach (var account in toForget.Users.ToList())
{
ForgetAccount(account);
}
}
dbAccessSemaphore.Wait();
db.Channels.Remove(toForget);
db.SaveChanges();
dbAccessSemaphore.Release();
channelCacheDirty = true;
cacheChannels();
}
public static void ForgetMessage(Message toForget)
{
dbAccessSemaphore.Wait();
db.Messages.Remove(toForget);
db.SaveChanges();
dbAccessSemaphore.Release();
}
public static void ForgetUAC(UAC toForget)
{
dbAccessSemaphore.Wait();
db.UACs.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()
{
List<Account> toReturn;
dbAccessSemaphore.Wait();
toReturn = [.. db.Accounts];
dbAccessSemaphore.Release();
return toReturn;
}
public static List<Channel> ChannelsOverview()
{
if (channelCacheDirty)
Task.Run(() => cacheChannels()).Wait();
return channels;
}
public static Account AccountDetail(Guid Id)
{
Account toReturn;
dbAccessSemaphore.Wait();
toReturn = db.Accounts.Find(Id);
dbAccessSemaphore.Release();
return toReturn;
}
public static Attachment AttachmentDetail(Guid Id)
{
Attachment toReturn;
dbAccessSemaphore.Wait();
toReturn = db.Attachments.Find(Id);
dbAccessSemaphore.Release();
return toReturn;
}
public static Channel ChannelDetail(Guid Id, bool messages = false)
{
if (channelCacheDirty)
Task.Run(() => cacheChannels()).Wait();
var ch = channels.Find(c => c.Id == Id);
if (messages)
ch.Messages = SearchMessages(m => m.ChannelId == ch.Id);
return ch;
}
public static Message MessageDetail(Guid Id)
{
Message toReturn;
dbAccessSemaphore.Wait();
toReturn = db.Messages.Find(Id);
db.Entry(toReturn).Reference(m => m.Channel).Load();
dbAccessSemaphore.Release();
return toReturn;
}
public static UAC UACDetail(Guid Id)
{
UAC toReturn;
dbAccessSemaphore.Wait();
toReturn = db.UACs.Find(Id);
dbAccessSemaphore.Release();
return toReturn;
}
public static User UserDetail(Guid Id)
{
User toReturn;
dbAccessSemaphore.Wait();
toReturn = db.Users.Find(Id);
dbAccessSemaphore.Release();
return toReturn;
}
public static List<User> UsersOverview()
{
List<User> toReturn;
dbAccessSemaphore.Wait();
toReturn = db.Users.ToList();
dbAccessSemaphore.Release();
return toReturn;
}
public static List<UAC> UACsOverview()
{
List<UAC> toReturn;
dbAccessSemaphore.Wait();
toReturn = db.UACs.Include(uac => uac.Users).Include(uac => uac.Channels).Include(uac => uac.AccountInChannels).ToList();
dbAccessSemaphore.Release();
return toReturn;
}
public static UAC SearchUAC(Expression<Func<UAC, bool>> predicate)
{
UAC toReturn;
dbAccessSemaphore.Wait();
toReturn = db.UACs.Include(uac => uac.Users).Include(uac => uac.Channels).Include(uac => uac.AccountInChannels)
.FirstOrDefault(predicate);
dbAccessSemaphore.Release();
return toReturn;
}
public static List<UAC> MatchUACs(Message message)
{
var msgId = message.Id;
var accId = message.Author.Id;
var usrId = message.Author.IsUser.Id;
var chId = message.Channel.Id;
return SearchUACs(uac => uac.AccountInChannels.FirstOrDefault(aic => aic.Id == accId) != null
|| uac.Users.FirstOrDefault(usr => usr.Id == usrId) != null
|| uac.Channels.FirstOrDefault(ch => ch.Id == chId) != null);
}
public static List<UAC> SearchUACs(Expression<Func<UAC, bool>> predicate)
{
List<UAC> toReturn;
dbAccessSemaphore.Wait();
toReturn = db.UACs.Include(uac => uac.Users).Include(uac => uac.Channels).Include(uac => uac.AccountInChannels)
.Where(predicate).ToList();
dbAccessSemaphore.Release();
return toReturn;
}
public static void RememberUAC(UAC toRemember)
{
dbAccessSemaphore.Wait();
db.Update(toRemember);
db.SaveChanges();
dbAccessSemaphore.Release();
if (toRemember.Channels?.Count() > 0)
cacheChannels();
}
}

View File

@ -3,11 +3,15 @@ namespace vassago;
using System; using System;
using System.Net.Http; using System.Net.Http;
using vassago.Models; using vassago.Models;
using vassago.ProtocolInterfaces;
public static class Shared public static class Shared
{ {
public static Random r = new Random(); public static Random r = new Random();
public static string DBConnectionString { get; set; } public static string DBConnectionString { get; set; }
public static HttpClient HttpClient { get; internal set; } = new HttpClient(); public static HttpClient HttpClient { get; internal set; } = new HttpClient();
public static bool SetupSlashCommands { get; set; }
public static Uri API_URL { get; set; }
public static List<ProtocolInterface> ProtocolList { get; set; } = new();
public static WebApplication App { get; set; }
} }

View File

@ -1,42 +0,0 @@
@model IEnumerable<Channel>
@{
ViewData["Title"] = "Channels";
}
<table class="table">
<thead>
<tr>
<th>
protocol
</th>
<th>type</th>
<th>
display name
</th>
<th>
Lineage
</th>
</tr>
</thead>
<tbody>
@foreach (var item in Model) {
<tr>
<td class="@item.Protocol">
<div class="protocol-icon">&nbsp;</div>
</td>
<td class="@item.ChannelType">
<div class="channel-type-icon">&nbsp;</div>
</td>
<td>
@Html.DisplayFor(modelItem => item.DisplayName)
</td>
<td>
@item.LineageSummary
</td>
<td>
<a asp-action="Details" asp-route-id="@item.Id">Details</a>
</td>
</tr>
}
</tbody>
</table>

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

@ -1,21 +0,0 @@
@model User
@{
ViewData["Title"] = "User details";
}
User @Model.DisplayName<br />
<div class="permissions">
</div>
<div class="accounts">
@foreach (var acc in Model.Accounts)
{
<div class="account @acc.Protocol">
<div class="protocol-icon">&nbsp;</div>
@Html.DisplayFor(acc => acc.DisplayName)
<a asp-controller="Accounts" asp-action="Details" asp-route-id="@acc.Id">Details</a>
</div>
}
</div>

View File

@ -0,0 +1,35 @@
using System.Diagnostics;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using vassago.Models;
using vassago.WebInterface.Models;
namespace vassago.WebInterface.Controllers;
public class AccountsController(ChattingContext db) : Controller
{
private ChattingContext Database => db;
public async Task<IActionResult> Index()
{
return Database.Accounts != null ?
View(await Database.Accounts.ToListAsync()) :
Problem("Entity set '_db.Accounts' is null.");
}
public async Task<IActionResult> Details(Guid id)
{
var account = await Database.Accounts
.Include(a => a.IsUser)
.Include(a => a.SeenInChannel)
.FirstAsync(a => a.Id == id);
return Database.Accounts != null ?
View(account) :
Problem("Entity set '_db.Accounts' 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

@ -0,0 +1,61 @@
using System.ComponentModel;
using System.Diagnostics;
using System.Text;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using vassago.Models;
using vassago.WebInterface.Models;
namespace vassago.WebInterface.Controllers;
public class ChannelsController() : Controller
{
public IActionResult Details(Guid id)
{
var allChannels = Rememberer.ChannelsOverview();
if (allChannels == null)
return Problem("no channels.");
var channel = allChannels.FirstOrDefault(u => u.Id == id);
if (channel == null)
{
return Problem($"couldn't find channle {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;
}
var sb = new StringBuilder();
sb.Append('[');
sb.Append($"{{text: \"{channel.SubChannels?.Count}\", nodes: [");
var first = true;
foreach (var subChannel in channel.SubChannels)
{
if (!first)
{
sb.Append(',');
}
else
{
first = false;
}
sb.Append($"{{\"text\": \"<a href=\\\"{Url.ActionLink(action: "Details", controller: "Channels", values: new { id = subChannel.Id })}\\\">{subChannel.DisplayName}</a>\"}}");
}
sb.Append("]}]");
ViewData.Add("subChannelsTree", sb.ToString());
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,233 @@
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;
using vassago.WebInterface.Models;
namespace vassago.Controllers;
public class HomeController : Controller
{
private readonly ILogger<HomeController> _logger;
public HomeController(ILogger<HomeController> logger)
{
_logger = logger;
}
public IActionResult Index()
{
var allAccounts = Rememberer.AccountsOverview();
var allChannels = Rememberer.ChannelsOverview();
Console.WriteLine($"accounts: {allAccounts?.Count ?? 0}, channels: {allChannels?.Count ?? 0}");
var sb = new StringBuilder();
sb.Append('[');
//UACs
var allUACs = Rememberer.UACsOverview();
var first = true;
if(allUACs.Any())
{
sb.Append("{text: \"uacs\", expanded:true, nodes: [");
first=true;
foreach(var uac in allUACs)
{
if (first)
{
first = false;
}
else
{
sb.Append(',');
}
var displayedName = uac.DisplayName;
if(string.IsNullOrWhiteSpace(displayedName))
{
displayedName = $"[unnamed - {uac.Id}]";
}
sb.Append($"{{\"text\": \"<a href=\\\"{Url.ActionLink(action: "Details", controller: "UACs", values: new {id = uac.Id})}\\\">{displayedName}</a>\"}}");
}
sb.Append("]}");
}
else
{
sb.Append("{text: \"uacs (0)\", }");
}
//users
var users = Rememberer.UsersOverview();
if(users.Any())
{
sb.Append(",{text: \"users\", expanded:true, nodes: [");
first=true;
//refresh list; we'll be knocking them out again in serializeUser
allAccounts = Rememberer.AccountsOverview();
foreach(var user in users)
{
if (first)
{
first = false;
}
else
{
sb.Append(',');
}
serializeUser(ref sb, ref allAccounts, user);
}
sb.Append("]}");
}
//type error, e is not defined
//channels
sb.Append(",{text: \"channels\", expanded:true, nodes: [");
var topLevelChannels = Rememberer.ChannelsOverview().Where(x => x.ParentChannel == null).ToList();
first = true;
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\", expanded:true, 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\", expanded:true, nodes: [");
first = true;
foreach (var acc in allAccounts)
{
if (first)
{
first = false;
}
else
{
sb.Append(',');
}
serializeAccount(ref sb, acc);
}
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>\"");
sb.Append(", expanded:true ");
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)
{
if (first)
{
first = false;
}
else
{
sb.Append(',');
}
serializeChannel(ref sb, ref allChannels, ref allAccounts, subChannel);
}
if (theseAccounts != null && !first) //"first" here tells us that we have at least one subchannel
{
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(']');
}
sb.Append('}');
}
private void serializeAccount(ref StringBuilder sb, Account currentAccount)
{
sb.Append($"{{\"text\": \"<a href=\\\"{Url.ActionLink(action: "Details", controller: "Accounts", values: new {id = currentAccount.Id})}\\\">{currentAccount.DisplayName}</a>\"}}");
}
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})}\\\">");
sb.Append(currentUser.DisplayName);
sb.Append("</a>\", ");
var ownedAccounts = allAccounts.Where(a => a.IsUser == currentUser);
if (ownedAccounts?.Count() > 0)
{
sb.Append("nodes: [");
sb.Append($"{{\"text\": \"owned accounts:\", \"expanded\":true, \"nodes\": [");
var first = true;
foreach (var acc in ownedAccounts)
{
if(!first)
sb.Append(',');
serializeAccount(ref sb, acc);
first = false;
}
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

@ -1,21 +1,20 @@
using System.Diagnostics; using System.Diagnostics;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using vassago.Models; using vassago.Models;
using vassago.WebInterface.Models;
namespace vassago.Controllers; namespace vassago.WebInterface.Controllers;
public class HomeController : Controller public class UACsController() : Controller
{ {
private readonly ILogger<HomeController> _logger;
public HomeController(ILogger<HomeController> logger)
{
_logger = logger;
}
public IActionResult Index() public IActionResult Index()
{ {
return View(); return View(Rememberer.UACsOverview());
}
public IActionResult Details(Guid id)
{
return View(Rememberer.SearchUAC(uac => uac.Id == id));
} }
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]

View File

@ -0,0 +1,39 @@
using System.Diagnostics;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using vassago.Models;
using vassago.WebInterface.Models;
namespace vassago.WebInterface.Controllers;
public class UsersController(ChattingContext db) : Controller
{
private ChattingContext Database => db;
public async Task<IActionResult> Index()
{
return Database.Users != null ?
View(await Database.Users.Include(u => u.Accounts).ToListAsync()) :
Problem("Entity set '_db.Users' is null.");
}
public async Task<IActionResult> Details(Guid id)
{
var user = await Database.Users
.Include(u => u.Accounts)
.FirstAsync(u => u.Id == id);
var allTheChannels = await Database.Channels.ToListAsync();
foreach(var acc in user.Accounts)
{
acc.SeenInChannel = allTheChannels.FirstOrDefault(c => c.Id == acc.SeenInChannel.Id);
}
return Database.Users != null ?
View(user) :
Problem("Entity set '_db.Users' 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

@ -0,0 +1,52 @@
using System.Diagnostics;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using vassago.Models;
using vassago.ProtocolInterfaces.DiscordInterface;
namespace vassago.Controllers.api;
[Route("api/[controller]")]
[ApiController]
public class AccountsController : ControllerBase
{
private readonly ILogger<AccountsController> _logger;
public AccountsController(ILogger<AccountsController> logger)
{
_logger = logger;
}
//microsoft: "you can't have multiple [FromBody]. The reason for this rule is some bullshti about storage buffers."
//cool story, bro. nobody gives a fuck, look at the boilerplate you've necessitated.
public class extraSpecialObjectReadGlorifiedTupleFor_UnlinkUser
{
public Guid acc_guid;
}
[HttpPatch]
[Route("UnlinkUser")]
[Produces("application/json")]
public IActionResult UnlinkUser([FromBody] extraSpecialObjectReadGlorifiedTupleFor_UnlinkUser req)
{
var acc_guid = req.acc_guid;
var accFromDb = Rememberer.SearchAccount(acc => acc.Id == acc_guid);
if (accFromDb == null)
{
var err = $"attempt to unlink user for acc {acc_guid}, not found";
_logger.LogError(err);
return NotFound(err);
}
var userFromDb = Rememberer.SearchUser(c => c.Id == accFromDb.IsUser.Id);
if (userFromDb == null)
{
var err = $"attempt to unlink user for {acc_guid}, doesn't have a user";
_logger.LogError(err);
return NotFound(err);
}
accFromDb.IsUser = null;
Rememberer.RememberAccount(accFromDb);
return Ok(accFromDb);
}
}

View File

@ -0,0 +1,53 @@
using System.Diagnostics;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using vassago.Models;
using vassago.ProtocolInterfaces.DiscordInterface;
namespace vassago.Controllers.api;
[Route("api/[controller]")]
[ApiController]
public class InternalAPIProtocolController : ControllerBase
{
private readonly ILogger<InternalAPIProtocolController> _logger;
public InternalAPIProtocolController(ILogger<InternalAPIProtocolController> logger)
{
_logger = logger;
}
[HttpPost]
[Route("PostMessage")]
[Produces("application/json")]
public IActionResult PostMessage(string messageText, Guid channelId)
{
return StatusCode(Behaver.Instance.SendMessage(channelId, messageText).Result);
}
[HttpPost]
[Route("ReplyToMessage")]
[Produces("application/json")]
public IActionResult ReplyToMessage(string messageText, Guid repliedMessageId)
{
Console.WriteLine($"ReplyToMessage - {repliedMessageId}, {messageText}");
return StatusCode(Behaver.Instance.Reply(repliedMessageId, messageText).Result);
}
[HttpPost]
[Route("SendFile")]
[Produces("application/json")]
public IActionResult SendFile(Guid channelId, string accompanyingText, string base64dData, string filename)
{
Console.WriteLine($"SendFile- {channelId}, {filename} (base64'd, {base64dData?.Length} chars), {accompanyingText}");
return StatusCode(Behaver.Instance.SendFile(channelId, base64dData, filename, accompanyingText).Result);
}
[HttpPost]
[Route("ReactToMessage")]
[Produces("application/json")]
public IActionResult ReactToMessage(string reactionString, Guid reactedMessageId)
{
Console.WriteLine($"ReactToMessage- {reactedMessageId}, {reactionString}");
return StatusCode(Behaver.Instance.React(reactedMessageId, reactionString).Result);
}
}

View File

@ -0,0 +1,214 @@
using System.Diagnostics;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using vassago.Models;
namespace vassago.Controllers.api;
[Route("api/[controller]")]
[ApiController]
public class RemembererController : ControllerBase
{
private readonly ILogger<RemembererController> _logger;
public RemembererController(ILogger<RemembererController> logger)
{
_logger = logger;
}
//Create
[HttpPut]
[Route("Account")]
[Produces("application/json")]
public Account CreateAccount(Guid id)
{
return Rememberer.AccountDetail(id);
}
[HttpPut]
[Route("Attachment")]
[Produces("application/json")]
public Attachment CreateAttachment(Guid id)
{
return Rememberer.AttachmentDetail(id);
}
[HttpPut]
[Route("Channels")]
[Produces("application/json")]
public Channel CreateChannel(Guid id)
{
return Rememberer.ChannelDetail(id);
}
[HttpPut]
[Route("Message")]
[Produces("application/json")]
public Message CreateMessage(Guid id)
{
return Rememberer.MessageDetail(id);
}
[HttpPut]
[Route("UAC")]
[Produces("application/json")]
public UAC CreateUAC(Guid id)
{
return Rememberer.UACDetail(id);
}
[HttpPut]
[Route("User")]
[Produces("application/json")]
public User CreateUser(Guid id)
{
return Rememberer.UserDetail(id);
}
//Read
[HttpGet]
[Route("Account")]
[Produces("application/json")]
public Account GetAccount(Guid id)
{
return Rememberer.AccountDetail(id);
}
[HttpGet]
[Route("Attachment")]
[Produces("application/json")]
public Attachment GetAttachment(Guid id)
{
return Rememberer.AttachmentDetail(id);
}
[HttpGet]
[Route("Channels")]
[Produces("application/json")]
public Channel GetChannel(Guid id)
{
return Rememberer.ChannelDetail(id);
}
[HttpGet]
[Route("Message")]
[Produces("application/json")]
public Message GetMessage(Guid id)
{
return Rememberer.MessageDetail(id);
}
[HttpGet]
[Route("UAC")]
[Produces("application/json")]
public UAC GetUAC(Guid id)
{
return Rememberer.UACDetail(id);
}
[HttpGet]
[Route("User")]
[Produces("application/json")]
public User GetUser(Guid id)
{
return Rememberer.UserDetail(id);
}
//Update
[HttpPatch]
[Route("Channels")]
[Produces("application/json")]
public IActionResult Patch([FromBody] Channel channel)
{
var fromDb = Rememberer.ChannelDetail(channel.Id);
if (fromDb == null)
{
_logger.LogError($"attempt to update channel {channel.Id}, not found");
return NotFound();
}
else
{
_logger.LogDebug($"patching {channel.DisplayName} (id: {channel.Id})");
}
//settable values: lewdness filter level, meanness filter level. maybe i could decorate them...
fromDb.LewdnessFilterLevel = channel.LewdnessFilterLevel;
fromDb.MeannessFilterLevel = channel.MeannessFilterLevel;
Rememberer.RememberChannel(fromDb);
return Ok(fromDb);
}
//Delete
[HttpDelete]
[Route("Account")]
[Produces("application/json")]
public IActionResult DeleteAccount(Guid id)
{
var fromDb = Rememberer.AccountDetail(id);
if (fromDb == null)
{
_logger.LogError($"attempt to delete account {id}, not found");
return NotFound();
}
Rememberer.ForgetAccount(fromDb);
return Ok();
}
[HttpDelete]
[Route("Attachment")]
[Produces("application/json")]
public IActionResult DeleteAttachment(Guid id)
{
var fromDb = Rememberer.AttachmentDetail(id);
if (fromDb == null)
{
_logger.LogError($"attempt to delete attachment {id}, not found");
return NotFound();
}
Rememberer.ForgetAttachment(fromDb);
return Ok();
}
[HttpDelete]
[Route("Channels/{id}")]
[Produces("application/json")]
public IActionResult DeleteChannel(Guid id)
{
var fromDb = Rememberer.ChannelDetail(id);
_logger.LogDebug($"delete channel {id}");
if (fromDb == null)
{
_logger.LogError($"attempt to delete channel {id}, not found");
return NotFound();
}
Rememberer.ForgetChannel(fromDb);
_logger.LogDebug($"delete channel {id} success");
return Ok();
}
[HttpDelete]
[Route("Message/{id}")]
[Produces("application/json")]
public IActionResult DeleteMessage(Guid id)
{
var fromDb = Rememberer.MessageDetail(id);
if (fromDb == null)
{
_logger.LogError($"attempt to delete message {id}, not found");
return NotFound();
}
Rememberer.ForgetMessage(fromDb);
return Ok();
}
[HttpDelete]
[Route("UAC/{id}")]
[Produces("application/json")]
public IActionResult DeleteUAC(Guid id)
{
var fromDb = Rememberer.UACDetail(id);
if (fromDb == null)
{
_logger.LogError($"attempt to delete uac {id}, not found");
return NotFound();
}
Rememberer.ForgetUAC(fromDb);
return Ok();
}
[HttpDelete]
[Route("User/{id}")]
[Produces("application/json")]
public IActionResult DeleteUser(Guid id)
{
var fromDb = Rememberer.UserDetail(id);
if (fromDb == null)
{
_logger.LogError($"attempt to delete user {id}, not found");
return NotFound();
}
Rememberer.ForgetUser(fromDb);
return Ok();
}
}

View File

@ -0,0 +1,267 @@
using System.Diagnostics;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using vassago.Models;
using vassago.ProtocolInterfaces.DiscordInterface;
namespace vassago.Controllers.api;
[Route("api/[controller]")]
[ApiController]
public class UACController : ControllerBase
{
private readonly ILogger<UACController> _logger;
public UACController(ILogger<UACController> logger)
{
_logger = logger;
}
//microsoft: "you can't have multiple [FromBody]. The reason for this rule is some bullshti about storage buffers."
//cool story, bro. nobody gives a fuck, look at the boilerplate you've necessitated.
public class extraSpecialObjectReadGlorifiedTupleFor_LinkChannel
{
public Guid uac_guid;
public Guid channel_guid;
}
[HttpPatch]
[Route("LinkChannel")]
[Produces("application/json")]
public IActionResult LinkChannel([FromBody] extraSpecialObjectReadGlorifiedTupleFor_LinkChannel req)
{
var uac_guid = req.uac_guid;
var channel_guid = req.channel_guid;
var uacFromDb = Rememberer.SearchUAC(uac => uac.Id == uac_guid);
if (uacFromDb == null)
{
var err = $"attempt to link channel for uac {uac_guid}, not found";
_logger.LogError(err);
return NotFound(err);
}
var channelFromDb = Rememberer.SearchChannel(c => c.Id == channel_guid);
if (channelFromDb == null)
{
var err = $"attempt to link channel for channel {channel_guid}, not found";
_logger.LogError(err);
return NotFound(err);
}
uacFromDb.Channels ??= [];
if (uacFromDb.Channels.Contains(channelFromDb))
{
return BadRequest("channel already linked");
}
uacFromDb.Channels.Add(channelFromDb);
Rememberer.RememberUAC(uacFromDb);
return Ok(uacFromDb);
}
public class extraSpecialObjectReadGlorifiedTupleFor_LinkUser
{
public Guid uac_guid;
public Guid user_guid;
}
[HttpPatch]
[Route("LinkUser")]
[Produces("application/json")]
public IActionResult LinkUser([FromBody] extraSpecialObjectReadGlorifiedTupleFor_LinkUser req)
{
var uac_guid = req.uac_guid;
var user_guid = req.user_guid;
var uacFromDb = Rememberer.SearchUAC(uac => uac.Id == uac_guid);
if (uacFromDb == null)
{
_logger.LogError($"attempt to link channal for uac {uac_guid}, not found");
return NotFound();
}
var userFromDb = Rememberer.SearchUser(c => c.Id == user_guid);
if (userFromDb == null)
{
_logger.LogError($"attempt to link user for user {user_guid}, not found");
return NotFound();
}
uacFromDb.Users ??= [];
if (uacFromDb.Users.Contains(userFromDb))
{
return BadRequest("user already linked");
}
uacFromDb.Users.Add(userFromDb);
Rememberer.RememberUAC(uacFromDb);
return Ok(uacFromDb);
}
public class extraSpecialObjectReadGlorifiedTupleFor_LinkAccount
{
public Guid uac_guid;
public Guid account_guid;
}
[HttpPatch]
[Route("LinkAccount")]
[Produces("application/json")]
public IActionResult LinkAccount([FromBody] extraSpecialObjectReadGlorifiedTupleFor_LinkAccount req)
{
var uac_guid = req.uac_guid;
var account_guid = req.account_guid;
var uacFromDb = Rememberer.SearchUAC(uac => uac.Id == uac_guid);
if (uacFromDb == null)
{
_logger.LogError($"attempt to link channal for uac {uac_guid}, not found");
return NotFound();
}
var accountFromDb = Rememberer.SearchAccount(c => c.Id == account_guid);
if (accountFromDb == null)
{
_logger.LogError($"attempt to link account for user {account_guid}, not found");
return NotFound();
}
uacFromDb.AccountInChannels ??= [];
if (uacFromDb.AccountInChannels.Contains(accountFromDb))
{
return BadRequest("account already linked");
}
uacFromDb.AccountInChannels.Add(accountFromDb);
Rememberer.RememberUAC(uacFromDb);
return Ok(uacFromDb);
}
[HttpPatch]
[Route("UnlinkUser")]
[Produces("application/json")]
public IActionResult UnlinkUser([FromBody] extraSpecialObjectReadGlorifiedTupleFor_LinkUser req)
{
var uac_guid = req.uac_guid;
var user_guid = req.user_guid;
var uacFromDb = Rememberer.SearchUAC(uac => uac.Id == uac_guid);
if (uacFromDb == null)
{
_logger.LogError($"attempt to unlink uac for uac {uac_guid}, not found");
return NotFound();
}
var userFromDb = Rememberer.SearchUser(c => c.Id == user_guid);
if (userFromDb == null)
{
_logger.LogError($"attempt to unlink user for user {user_guid}, not found");
return NotFound();
}
uacFromDb.Users ??= [];
if (!uacFromDb.Users.Contains(userFromDb))
{
return BadRequest("user not linked");
}
uacFromDb.Users.Remove(userFromDb);
Rememberer.RememberUAC(uacFromDb);
return Ok(uacFromDb);
}
[HttpPatch]
[Route("UnlinkAccount")]
[Produces("application/json")]
public IActionResult UnlinkAccount([FromBody] extraSpecialObjectReadGlorifiedTupleFor_LinkAccount req)
{
var uac_guid = req.uac_guid;
var account_guid = req.account_guid;
var uacFromDb = Rememberer.SearchUAC(uac => uac.Id == uac_guid);
if (uacFromDb == null)
{
_logger.LogError($"attempt to unlink uac for uac {uac_guid}, not found");
return NotFound();
}
var accountFromDb = Rememberer.SearchAccount(a => a.Id == account_guid);
if (accountFromDb == null)
{
_logger.LogError($"attempt to unlink account for user {account_guid}, not found");
return NotFound();
}
uacFromDb.AccountInChannels ??= [];
if (!uacFromDb.AccountInChannels.Contains(accountFromDb))
{
return BadRequest("account not linked");
}
uacFromDb.AccountInChannels.Remove(accountFromDb);
Rememberer.RememberUAC(uacFromDb);
return Ok(uacFromDb);
}
[HttpPatch]
[Route("UnlinkChannel")]
[Produces("application/json")]
public IActionResult UnlinkChannel([FromBody] extraSpecialObjectReadGlorifiedTupleFor_LinkChannel req)
{
var uac_guid = req.uac_guid;
var channel_guid = req.channel_guid;
var uacFromDb = Rememberer.SearchUAC(uac => uac.Id == uac_guid);
if (uacFromDb == null)
{
_logger.LogError($"attempt to unlink channal for uac {uac_guid}, not found");
return NotFound();
}
var channelFromDb = Rememberer.SearchChannel(c => c.Id == channel_guid);
if (channelFromDb == null)
{
_logger.LogError($"attempt to unlink user for user {channel_guid}, not found");
return NotFound();
}
uacFromDb.Users ??= [];
if (!uacFromDb.Channels.Contains(channelFromDb))
{
return BadRequest("user not linked");
}
uacFromDb.Channels.Remove(channelFromDb);
Rememberer.RememberUAC(uacFromDb);
return Ok(uacFromDb);
}
[HttpPut]
[Route("CreateForChannels/{Id}")]
[Produces("application/json")]
public IActionResult CreateForChannels(Guid Id)
{
_logger.LogDebug($"made it to controller. creating for channel {Id}");
var targetChannel = Rememberer.ChannelDetail(Id);
if (targetChannel == null)
{
return NotFound();
}
var newUAC = new UAC()
{
Channels = [targetChannel]
};
Rememberer.RememberUAC(newUAC);
Rememberer.RememberChannel(targetChannel);
return Ok(newUAC.Id);
}
[HttpPut]
[Route("AddTranslation/{Id}")]
[Produces("application/json")]
public IActionResult AddTranslation(Guid Id)
{
_logger.LogDebug($"made it to controller. creating translation for uac {Id}");
var uacFromDb = Rememberer.SearchUAC(uac => uac.Id == Id);
if (uacFromDb == null)
{
_logger.LogError($"attempt to create translation for uac {Id}, not found");
return NotFound();
}
uacFromDb.Translations ??= [];
uacFromDb.Translations.Add(Guid.NewGuid().ToString(), Guid.NewGuid().ToString());
Rememberer.RememberUAC(uacFromDb);
return Ok(uacFromDb.Translations.Count);
}
[HttpPut]
[Route("AddCommandAlteration/{Id}")]
[Produces("application/json")]
public IActionResult AddCommandAlteration(Guid Id)
{
_logger.LogDebug($"made it to controller. creating command alteration for uac {Id}");
var uacFromDb = Rememberer.SearchUAC(uac => uac.Id == Id);
if (uacFromDb == null)
{
_logger.LogError($"attempt to create command alteration for uac {Id}, not found");
return NotFound();
}
uacFromDb.CommandAlterations ??= [];
uacFromDb.CommandAlterations.Add(Guid.NewGuid().ToString(), Guid.NewGuid().ToString());
Rememberer.RememberUAC(uacFromDb);
return Ok(uacFromDb.CommandAlterations.Count);
}
}

View File

@ -1,8 +1,8 @@
namespace vassago.Models; namespace vassago.WebInterface.Models;
public class ErrorPageViewModel public class ErrorPageViewModel
{ {
public string? RequestId { get; set; } public string RequestId { get; set; }
public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
} }

View File

@ -0,0 +1,72 @@
@model Account
@using Newtonsoft.Json
@using System.Text
@{
ViewData["Title"] = "Account details";
}
<a href="/">home</a>/@Html.Raw(ViewData["breadcrumbs"])
<table class="table">
<tbody>
<tr>
<th>Id</th>
<td>@Model.Id</td>
</tr>
<tr>
<th scope="row">belongs to user</th>
<td>@Model.IsUser.DisplayName</td>
<td><button onclick="unlinkAccountUser(() => { window.location.reload(); })">separate</button></2td>
</tr>
<tr>
<th scope="row">Seen in channel</th>
<td class="account @Model.SeenInChannel.Protocol"><div class="protocol-icon">&nbsp;</div>@Model.SeenInChannel.LineageSummary<a href="/Channels/Details/@Model.SeenInChannel.Id">@Model.SeenInChannel.DisplayName</a></td>
</tr>
<tr>
<th scope="row">UACs tied to account</th>
<td>
<div id="uacsTree"></div>
</td>
</tr>
</tbody>
</table>
@section Scripts{
<script type="text/javascript">
@{
var accountAsString = JsonConvert.SerializeObject(Model, new JsonSerializerSettings
{
ReferenceLoopHandling = ReferenceLoopHandling.Ignore
});
}
const userOnLoad = @Html.Raw(accountAsString);
function jsonifyUser() {
var userNow = structuredClone(userOnLoad);
userNow.Accounts = null;
userNow.DisplayName = document.querySelector("#displayName").value;
console.log(userNow);
return userNow;
}
function getUacsTree() {
@{
var sb = new StringBuilder();
sb.Append("[{text: \"UACs\", \"expanded\":true, nodes: [");
var first = true;
for (int i = 0; i < 1; i++)
{
if (!first)
sb.Append(',');
sb.Append($"{{text: \"<input type=\\\"checkbox\\\" > is goated (w/ sauce)</input>\"}}");
first = false;
}
sb.Append("]}]");
}
console.log(@Html.Raw(sb.ToString()));
var tree = @Html.Raw(sb.ToString());
return tree;
}
$('#uacsTree').bstreeview({ data: getUacsTree() });
//document.querySelectorAll("input[type=checkbox]").forEach(node => { node.onchange = () => { patchModel(jsonifyUser(), '/api/Users/') } });
</script>
}

View File

@ -0,0 +1,211 @@
@using System.ComponentModel
@using Newtonsoft.Json
@using System.Text;
@model Tuple<Channel, Enumerations.LewdnessFilterLevel, Enumerations.MeannessFilterLevel>
@{
var ThisChannel = Model.Item1;
var IfInheritedLewdnessFilterLevel = Model.Item2;
var IfInheritedMeannessFilterLevel = Model.Item3;
}
<a href="/">home</a>/
@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>@(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>
@if((ThisChannel.SubChannels?.Count ?? 0) > 0)
{
@Html.Raw("<div id=\"channelsTree\"></div>");
}
else
{
@Html.Raw("0")
}
</td>
</tr>
<tr>
<th scope="row">Accounts</th>
<td>
@if((ThisChannel.Users?.Count ?? 0) > 0)
{
@Html.Raw("<div id=\"accountsTree\"></div>");
}
else
{
@Html.Raw("none")
}
</td>
</tr>
<tr>
<th scope="row">Datamemos</th>
<td>
<div id="dataMemosTree"></div>
</td>
</tr>
<tr>
<td colspan="2">
<button onclick="forget()">forget</button>
</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;
}
function forget(){
console.log("here we go");
if(window.confirm("delete? really really?") == true){
deleteModel(jsonifyChannel().Id, window.history.back);
}
}
function createMemo()
{
console.log("creating memo for channel..");
createMemoFor((newMemoId) => {
window.location.href = "/UACs/Details/" + newMemoId;
});
}
function channelsTree() {
//TOOD: see how accountsTree does all our HTML-ification over here in HTML land? but this doesn't? we should pick one and stick with it.
var tree = @Html.Raw(ViewData["subChannelsTree"]);
return tree;
}
function dataMemosTree(){
@{
var dmsb = new StringBuilder();
dmsb.Append("[{text: \"Data Memos\", \"expanded\":true, nodes: [");
var firstMemo = true;
if(ThisChannel.UACs != null) foreach(var memo in ThisChannel.UACs)
{
if(!firstMemo)
dmsb.Append(',');
var effectiveDisplayName = memo.DisplayName;
if(string.IsNullOrWhiteSpace(effectiveDisplayName))
{
effectiveDisplayName = $"[nameless] {memo.Id}";
}
dmsb.Append($"{{text: \"<a href=\\\"/UACs/Details/{memo.Id}\\\">{effectiveDisplayName}</a>\"}}");
firstMemo = false;
}
if(!firstMemo)
dmsb.Append(',');
dmsb.Append($"{{text: \"<button type=\\\"button\\\" class=\\\"btn btn-primary\\\" onclick=\\\"createMemo()\\\">+</button>\"}}");
dmsb.Append("]}]");
}
var tree = @Html.Raw(dmsb.ToString());
return tree;
}
function accountsTree() {
@{
var sb = new StringBuilder();
sb.Append("[{text: \"accounts\", \"expanded\":true, nodes: [");
var first = true;
foreach (var acc in ThisChannel.Users?.OrderBy(a => a?.SeenInChannel?.LineageSummary))
{
if(!first)
sb.Append(',');
sb.Append($"{{text: \"<div class=\\\"account {acc.Protocol}\\\"><div class=\\\"protocol-icon\\\">&nbsp;</div>{acc.SeenInChannel.LineageSummary}/<a href=\\\"/Accounts/Details/{acc.Id}\\\">{acc.DisplayName}</a>\"}}");
first=false;
}
sb.Append("]}]");
}
//console.log(@Html.Raw(sb.ToString()));
var tree = @Html.Raw(sb.ToString());
return tree;
}
$('#channelsTree').bstreeview({ data: channelsTree() });
$('#accountsTree').bstreeview({ data: accountsTree() });
$('#dataMemosTree').bstreeview({ data: dataMemosTree() });
</script>
}

View File

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

View File

@ -1,4 +1,4 @@
@model ErrorPageViewModel @model vassago.WebInterface.Models.ErrorPageViewModel
@{ @{
ViewData["Title"] = "Error"; ViewData["Title"] = "Error";
} }

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

@ -0,0 +1,263 @@
@using System.ComponentModel
@using Newtonsoft.Json
@using System.Text;
@model UAC
<a href="/">home</a>/
@Html.Raw(ViewData["breadcrumbs"])
<table class="table">
<tbody>
<tr>
<th scope="row">Display Name</th>
<td><input class="form-control" type="text" value="@Model.DisplayName" id="displayName"/></td>
</tr>
<tr>
<th scope="row">Description</th>
<td>@Html.Raw(Model.Description)
</tr>
<tr>
<th scope="row">Channels</th>
<td>
<div id="channelsTree"></div>
</td>
</tr>
<tr>
<th scope="row">Users</th>
<td>
<div id="usersTree"></div>
</td>
</tr>
<tr>
<th scope="row">AccountInChannels</th>
<td>
<div id="accountsTree"></div>
</td>
</tr>
<tr>
<th scope="row">Translations (@Model.Translations?.Count)</th>
<td>
next would be iterating over a dictionary. All reference on the internet implies this should work. I'm sick of trying to figure out why it doesn't. razor says to me, so i say to you: go fuck yourself; edit the db manually.
<table class="table">
<tbody>
@Html.Raw("<tr>");
@Html.Raw("<td><input type=\"text\" name=\"Model.Translations[{i}].Key\" value=\"{Model.Translations.ElementAt(i).Key}\" /></td>");
@Html.Raw("<td><input type=\"text\" name=\"Model.Translations[{i}].Value\" value=\"{Model.Translations.ElementAt(i).Value}\" /></td>");
@Html.Raw("<td><button type=\"button\" class=\"btn btn-danger\" onclick=\"removeTranslation()\">delete</button></td>");
@Html.Raw("</tr>");
<tr>
<td colspan="3">
<button type="button" class="btn btn-primary" onclick="addUAC_Translation()">add translation</button>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
<tr>
<th scope="row">Command Alterations (@Model.CommandAlterations?.Count)</th>
<td>
<table class="table">
<tbody>
@Html.Raw("<tr><td colspan=\"3\">o_O</td></tr>");
@Html.Raw("<tr>");
@Html.Raw("<td><input type=\"text\" name=\"Model.CommandAlterations[{j}].Key\" value=\"{Model.CommandAlterations.ElementAt(j).Key}\" /></td>");
@Html.Raw("<td><input type=\"text\" name=\"Model.CommandAlterations[{j}].Value\" value=\"{Model.CommandAlterations.ElementAt(j).Value}\" /></td>");
@Html.Raw("<td><button type=\"button\" class=\"btn btn-danger\" onclick=\"removeCommandAlteration()\">delete</button></td>");
@Html.Raw("</tr>");
<tr>
<td colspan="3">
<button type="button" class="btn btn-primary" onclick="addUAC_CommandAlteration()">add alteration</button>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
<tr>
<td colspan="2"><button class="btn btn-success">asdf</button></td>
</tr>
</tbody>
</table>
"adam", you may say, "why are there both translations and command alterations?"<br />
translations are like.. if someone says "addicting", you can safely guess that they don't know they should be saying "addictive".<br />
so if you say "this game is addicting", that comes in, we just pretend you said "this game is addictive".<br />
Command alterations, I have to acknowledge that you *did* say !freedomunits, but I'm changing my behavior and not converting. <br />
I guess theoretically you could "translate" freedomunits to nothing, then freeerdumberunits to freedomunits? but if we're doing that it becomes necessary to care about order, and get that right.<br />
so let's say, "translations" are "i'll pretend you said", and "command alterations" are "i'll pretend I expected".
<div id="link-modal" class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Insert GUID</h5>
<button type="button" class="btn btn-close" data-dismiss="link-modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p>
<ul>
<li>//TODO: search</li>
</ul>
</p>
<p>
<input id="addmodaltext" type="text" />
</p>
</div>
<div class="modal-footer">
<button id="modalsubmit" type="button" class="btn btn-primary">Save changes</button>
<button type="button" class="btn btn-secondary" data-dismiss="link-modal">Close</button>
</div>
</div>
</div>
</div>
<div id="unlink-modal" class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Confirm</h5>
<button type="button" class="btn-close" data-dismiss="unlink-modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p>
are you sure you want to unlink
<input id="unlinkModalText" enabled="false" type="text" />
</p>
<p>
to be clear; this is going to "unlink", not like.. delete.
</p>
</div>
<div class="modal-footer">
<button id="modalsubmit" type="button" class="btn btn-danger">unlink</button>
<button type="button" class="btn btn-secondary" data-dismiss="unlink-modal">Close</button>
</div>
</div>
</div>
</div>
<div id="remove-modal" class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Confirm</h5>
<button type="button" class="btn-close" data-dismiss="remove-modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p>
are you sure you want to delete
<input id="removeModalText" enabled="false" type="text" />
</p>
</div>
<div class="modal-footer">
<button id="modalsubmit" type="button" class="btn btn-danger">delete</button>
<button type="button" class="btn btn-secondary" data-dismiss="remove-modal">Close</button>
</div>
</div>
</div>
</div>
@section Scripts{
<script type="text/javascript">
function linkModal(submitFn)
{
let modalbutton = document.querySelector("#link-modal button#modalsubmit");
modalbutton.onclick = () => { linkSubmitModal(submitFn); };
$("#link-modal").modal("show");
}
function linkSubmitModal(submitFn)
{
let guid = document.querySelector("#link-modal #addmodaltext").value;
submitFn(guid, () => { window.location.reload(); });
$("#link-modal").modal("hide");
console.log(submitFn + "(guid)");
}
function unlinkModal(submitFn, target)
{
document.querySelector("#unlink-modal #removeModalText").value = target;
let modalbutton = document.querySelector("#unlink-modal button#modalsubmit");
modalbutton.onclick = () => { unlinkModalSubmit(submitFn, target); };
$("#remove-modal").modal("show");
}
function unlinkModalSubmit(submitFn, target)
{
submitFn(target, () => { window.location.reload(); });
$("#unlink-modal").modal("hide");
}
function removeModal(submitFn, idx)
{
document.querySelector("#remove-modal #removeModalText").value = target;
let modalbutton = document.querySelector("#remove-modal button#modalsubmit");
modalbutton.onclick = () => { removeModalSubmit(submitFn, idx); };
$("#remove-modal").modal("show");
}
function removeModalSubmit(submitFn, idx)
{
submitFn(idx, () => { window.location.reload(); });
$("#remove-modal").modal("hide");
}
function channelsTree() {
@{
var sb = new StringBuilder();
sb.Append("[{text: \"Channels\", \"expanded\":true, nodes: [");
sb.Append($"{{text: \"<button type=\\\"button\\\" class=\\\"btn btn-primary\\\" onclick=\\\"linkModal(linkUAC_Channel)\\\">add channel</button>\"}}");
foreach (var acc in Model.Channels?.OrderBy(a => a.DisplayName))
{
sb.Append(',');
sb.Append($"{{text: \"<a href=\\\"/Channels/Details/{acc.Id}\\\">{acc.DisplayName}</a> - <button type=\\\"button\\\" class=\\\"btn btn-danger\\\" onclick=\\\"unlinkModal(unlinkUAC_Channel, '{acc.Id}')\\\">remove</button>\"}}");
}
sb.Append("]}]");
}
var tree = @Html.Raw(sb.ToString());
return tree;
}
function usersTree() {
@{
sb = new StringBuilder();
sb.Append("[{text: \"Users\", \"expanded\":true, nodes: [");
sb.Append($"{{text: \"<button type=\\\"button\\\" class=\\\"btn btn-primary\\\" onclick=\\\"linkModal(linkUAC_User)\\\">add user</button>\"}}");
foreach (var acc in Model.Users?.OrderBy(a => a.DisplayName))
{
sb.Append(',');
sb.Append($"{{text: \"<a href=\\\"/Users/Details/{acc.Id}\\\">{acc.DisplayName}</a> - <button type=\\\"button\\\" class=\\\"btn btn-danger\\\" onclick=\\\"unlinkModal(unlinkUAC_User, '{acc.Id}')\\\">remove</button>\"}}");
}
sb.Append("]}]");
}
var tree = @Html.Raw(sb.ToString());
return tree;
}
function accountsTree() {
@{
sb = new StringBuilder();
sb.Append("[{text: \"Accounts\", \"expanded\":true, nodes: [");
sb.Append($"{{text: \"<button type=\\\"button\\\" class=\\\"btn btn-primary\\\" onclick=\\\"linkModal(linkUAC_Account)\\\">add account</button>\"}}");
foreach (var acc in Model.AccountInChannels?.OrderBy(a => a.DisplayName))
{
sb.Append(',');
sb.Append($"{{text: \"<a href=\\\"/Accounts/Details/{acc.Id}\\\">{acc.DisplayName}</a> - <button type=\\\"button\\\" class=\\\"btn btn-danger\\\" onclick=\\\"unlinkModal(unlinkUAC_Acocunt, '{acc.Id}')\\\">remove</button>\"}}");
}
sb.Append("]}]");
}
var tree = @Html.Raw(sb.ToString());
return tree;
}
$('#channelsTree').bstreeview({ data: channelsTree() });
$('#usersTree').bstreeview({ data: usersTree() });
$('#accountsTree').bstreeview({ data: accountsTree() });
var components = window.location.pathname.split('/');
var uacId = components[3];
</script>
}

View File

@ -0,0 +1,65 @@
@model User
@using Newtonsoft.Json
@using System.Text
@{
ViewData["Title"] = "User details";
}
<a href="/">home</a>/@Html.Raw(ViewData["breadcrumbs"])
<table class="table">
<tbody>
<tr>
<th scope="row">Display Name (here)</th>
<td><input type="text" id="displayName" value="@Model.DisplayName" disabled alt="todo"></input> <button
onclick="patchModel(jsonifyUser())" disabled alt"todo">update</button></td>
</tr>
<tr>
<th scope="row">Accounts</th>
<td>
<div id="accountsTree"></div>
</td>
</tr>
</tbody>
</table>
@section Scripts{
<script type="text/javascript">
@{
var userAsString = JsonConvert.SerializeObject(Model, new JsonSerializerSettings
{
ReferenceLoopHandling = ReferenceLoopHandling.Ignore
});
}
const userOnLoad = @Html.Raw(userAsString);
function jsonifyUser() {
var userNow = structuredClone(userOnLoad);
userNow.Accounts = null;
userNow.DisplayName = document.querySelector("#displayName").value;
//userNow.Tag_CanTwitchSummon = document.querySelector("#tagCanTwitchSummon").checked;
console.log(userNow);
return userNow;
}
function getAccountsTree() {
@{
var sb = new StringBuilder();
sb.Append("[{text: \"accounts\", \"expanded\":true, nodes: [");
var first = true;
foreach (var acc in Model.Accounts.OrderBy(a => a.SeenInChannel.LineageSummary))
{
if (!first)
sb.Append(',');
sb.Append($"{{text: \"<div class=\\\"account {acc.Protocol}\\\"><div class=\\\"protocol-icon\\\">&nbsp;</div>{acc.SeenInChannel.LineageSummary}/<a href=\\\"/Accounts/Details/{acc.Id}\\\">{acc.DisplayName}</a>\"}}");
first = false;
}
sb.Append("]}]");
}
console.log(@Html.Raw(sb.ToString()));
var tree = @Html.Raw(sb.ToString());
return tree;
}
$('#accountsTree').bstreeview({ data: getAccountsTree() });
document.querySelectorAll("input[type=checkbox]").forEach(node => { node.onchange = () => { patchModel(jsonifyUser()) } });
</script>
}

View File

@ -2,7 +2,8 @@
"Logging": { "Logging": {
"LogLevel": { "LogLevel": {
"Default": "Information", "Default": "Information",
"Microsoft.AspNetCore": "Warning" "Microsoft.AspNetCore": "Warning",
"Microsoft.EntityFrameworkCore": "None"
} }
}, },
"AllowedHosts": "*", "AllowedHosts": "*",
@ -12,5 +13,22 @@
"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",
"SetupSlashCommands": false,
"Webhooks": [
{
"uacID": "9a94855a-e5a2-43b5-8420-ce670472ce95",
"Trigger": "test",
"Description": "<i>test</i>",
"Uri": "http://localhost",
"Method": "POST",
"Headers": [
["Content-Type", "application/json"]
],
"Content": "{\"content\": \"Hello, this is a message from my webhook: {text}. username: {account}, user: {user}\"}"
}
],
"KafkaBootstrap":"http://localhost:9092",
"KafkaName":"vassago",
"API_URL": "http://localhost:5093/api"
} }

Some files were not shown because too many files have changed in this diff Show More