Compare commits

...

198 Commits

Author SHA1 Message Date
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
ba2254858f also migrate db
All checks were successful
gitea/vassago/pipeline/head This commit looks good
2023-12-05 23:15:09 -05:00
c1e32ef39a fix failure to adapt
All checks were successful
gitea/vassago/pipeline/head This commit looks good
2023-12-05 22:59:30 -05:00
5fb96ea67e jenkins looking for Jenkinsfile
Some checks failed
gitea/vassago/pipeline/head There was a failure building this commit
2023-12-05 22:57:38 -05:00
1fd2a4723e more conversion 2023-12-05 22:53:17 -05:00
51fb08810d how the hell did I not have mm 2023-12-05 21:32:20 -05:00
423fe5cb96 more units for conversion 2023-12-05 21:23:08 -05:00
23e18f2028 complex channel types, some channels UI 2023-12-03 14:33:58 -05:00
9d3917a030 implements #21 2023-12-01 14:58:32 -05:00
391ba38cce fixes #20 2023-12-01 14:19:55 -05:00
d060e92ed9 link to self
but twitch interface can't whisper self atm
2023-12-01 14:02:47 -05:00
77fc26e1ed more user info 2023-12-01 09:49:21 -05:00
edc86af538 woops lol 2023-11-30 16:16:07 -05:00
39781397c3 database update - doesn't work if there's already data T_T
but at least now I can say it was a good idea to have waited all this stupidly goddamn long
2023-11-30 15:59:49 -05:00
e89c109970 who needs verbosity as a filter level - you have a char limit 2023-11-30 14:19:39 -05:00
e7b70468ae clearer definition of the concept of permission 2023-11-30 12:50:51 -05:00
47f382df19 more permission definition 2023-11-30 12:49:40 -05:00
8c6087d557 mark protocol on messages and fixed twitchlib detecting duplicates incorrectly 2023-08-22 15:33:09 -04:00
da7078f535 clear out detiktokable links when done
fixes #18
2023-08-22 14:58:44 -04:00
e9ddcd237c users controller, and some other fixes 2023-07-04 20:40:37 -04:00
4a60e53798 configuration separated 2023-07-04 19:25:04 -04:00
5bb64f764c twitch summon and dismiss both exist
and with that: twitch, as a platform, exists.

must sort out permissions.
2023-07-04 18:51:27 -04:00
986d433886 filterlevels used on (almost) all features 2023-07-04 13:31:19 -04:00
c393f657d1 dramatically destructive database update. Twitch. 2023-07-04 12:58:21 -04:00
6037adcb44 move irrelevant model 2023-07-03 13:50:04 -04:00
2fc199d4b6 deleted: Views/Home/Privacy.cshtml 2023-07-03 12:53:12 -04:00
a8d1fc8d6e web interface 2023-07-03 12:51:23 -04:00
91bcfae1ba migration for accounts vs user distinction 2023-06-28 00:14:32 -04:00
115035eb91 test/fix features 2023-06-20 21:38:25 -04:00
147cba7cd3 keep track of self 2023-06-20 21:26:44 -04:00
66b425cd39 distinguish users and accounts 2023-06-19 12:56:40 -04:00
e433e56fec Permissions associated with channel 2023-06-19 11:03:06 -04:00
a1d2ec83b5 no onjoin, who cares 2023-06-19 01:34:56 -04:00
3a4a6df087 behaviors separated out 2023-06-19 01:14:04 -04:00
567a59550e generally works 2023-06-18 22:10:51 -04:00
f23474ad51 i think we're back to where discord left off? 2023-06-15 23:29:07 -04:00
d2aa1f46cc debug statement cleanup
All checks were successful
greyn/vassago/pipeline/head This commit looks good
2023-06-05 15:45:08 -04:00
51fba995c3 log attachments 2023-06-05 15:25:43 -04:00
e4f7d88e35 works! 2023-06-05 14:55:48 -04:00
3031779e24 fork: vassago
All checks were successful
greyn/vassago/pipeline/head This commit looks good
gitea/vassago/pipeline/head This commit looks good
gitea.arg.rip/vassago/pipeline/head This commit looks good
2023-06-01 00:03:23 -04:00
bfe7f582b2 start to structure code for multiplatofrm 2023-05-23 19:03:58 -04:00
a87fcd6ad9 start to structure code for multiplatofrm 2023-05-22 00:58:36 -04:00
e5a5c7cc7c debug server mode again 2023-05-21 14:53:22 -04:00
4c47081c9b freedom units global 2023-05-21 14:44:54 -04:00
fb9cec3225 freedomunits as slash command 2023-05-21 14:41:43 -04:00
6022f88997 ask for *all* permission 2023-05-13 11:29:44 -04:00
12e21e8ad7 fix without mention becuase FUKIN DISCORD 2023-05-07 00:37:22 -04:00
78639f449c generic question answers 2023-05-05 23:06:25 -04:00
3143150c37 possible bug 2023-04-25 22:43:18 -04:00
81465d5e43 now officially as useful as chatGPT 2023-04-25 22:31:14 -04:00
4554e46a61 recipes 2023-04-25 20:51:51 -04:00
b83c569af4 more snark 2023-04-18 20:55:22 -04:00
1d0ba003fe restore pulse chekc 2023-04-15 08:11:16 -04:00
94ae81909b do not attempt to de-pluralize if you don't have to. e.g., the currency ILS 2023-04-15 08:06:03 -04:00
f975beb6e9 few more archaic units 2023-04-14 22:38:04 -04:00
7f491ef632 one more thing 2023-04-14 22:14:23 -04:00
68ce0ed6fd publish better 2023-04-14 22:06:28 -04:00
7e6b369b48 reload currency hourly, lots more converters 2023-04-14 22:04:25 -04:00
362a8784ba basic conversion including exchange pairs 2023-04-14 21:40:54 -04:00
d90123a56d new feature: pulse check. also, assets folder 2023-03-22 23:12:27 -04:00
589d27434e hint up top fixed 2023-03-21 11:47:12 -04:00
c874e3e421 slightly more graceful error handling 2023-03-02 00:10:49 -05:00
def4f3644e slightly more useful with errors 2023-03-01 23:49:36 -05:00
87c31a91b8 snark clarification
DO NOT FUCK WITH THIS, the epistimelogical rabbit hole is deep
2023-03-01 18:03:51 -05:00
7364c38b3a more snark: that's not what gaslighting means 2023-03-01 18:00:56 -05:00
3d4064f8cb upgrade to .net7.0 2023-02-22 16:46:44 -05:00
05341cee3b new feature: mock chatGPT. feature fix: detiktokify from all of tiktok.com 2023-02-22 16:46:29 -05:00
32242c9cb4 more shtikbot error handling 2023-02-22 16:45:49 -05:00
8fa439b232 don't trip over empty lines 2022-11-16 13:07:21 -05:00
751ecf2e30 finally rename it 2022-05-03 14:54:58 -04:00
073b7f1cf0 qr codes case insensitive 2022-05-03 14:52:46 -04:00
c4af2b4b9d increase chances for skynet reaction
it doesn't come up that often tbh
2022-05-03 14:49:14 -04:00
0265be98c9 :face_palm: 2022-05-03 14:47:58 -04:00
246f15bc61 obsolete 2022-05-03 14:46:28 -04:00
48b2da1b3e thanks joub 2022-02-07 16:30:43 -05:00
ab672a2b64 Update 'jokes.txt' 2022-02-07 16:29:41 -05:00
c979728183 stolen from clover's twitchbot's !dadjoke 2022-02-07 11:21:35 -05:00
70c45b5db3 heh 2022-02-05 16:38:51 -05:00
b957d33e4a heh 2022-01-31 23:21:42 -05:00
1ebeb335ac heh 2022-01-31 19:44:39 -05:00
b2a3b33672 Merge branch 'master' of ssh://house.adamrgrey.com:6922/adam/discord-bot-shtik 2022-01-31 14:02:10 -05:00
5b3a645406 hopefully resist double-connecting 2022-01-31 13:59:41 -05:00
34e7437783 Update 'README.md' 2022-01-24 22:08:52 -05:00
53c1d956b3 fix regex to remove mentions 2022-01-21 18:56:21 -05:00
fcf7d11715 mock mee6 2022-01-21 12:35:20 -05:00
5cf4e87e06 jokes merge 2022-01-21 12:17:48 -05:00
37d1cb7d70 1 instance only 2022-01-21 12:10:40 -05:00
99b662bfec more jokes
no i did not say _better_ jokes
2022-01-19 18:05:00 -05:00
7a080e0653 Upload files to ''
user icon
2022-01-18 20:25:14 -05:00
6cda86300e detiktokify uses ytdlp 2022-01-16 20:52:45 -05:00
00b588059d peptalks 2022-01-16 20:51:22 -05:00
8950ce013e i like the cadence better 2022-01-15 21:59:01 -05:00
19cba4975b almost wrote sweden, don't worry, checked first. 8) 2022-01-15 21:55:36 -05:00
bb9ca88c36 note to self, stop trying to be a little bit clever. Either be omniscient or be stupid. 2022-01-15 20:50:51 -05:00
db7ab04815 that's what I get for trying to be clever. 2022-01-15 20:39:10 -05:00
545efb2829 also, copy jokes automatically plz 2022-01-15 20:35:43 -05:00
5bbb8c95d1 JOKES 2022-01-15 20:32:23 -05:00
433b439f7b qr feature 2021-11-30 21:39:43 -05:00
ca397671d0 fix link 2021-11-24 10:44:54 -05:00
5cb19041da shtikspecific 2021-11-24 10:13:32 -05:00
010b40ab13 tmp is for tmp files, obv 2021-11-17 22:34:22 -05:00
3849880b33 deheic with external imagemagick
"oh hey magick.net lets you not have to install imagemagick, but you still have to manage this library as though it was a dependency, only now it mysteriously doesn't work sometimes"
2021-11-17 22:30:51 -05:00
b1578dde69 error messages can go ahead and dump in the bot chatter channel, that's what it's for 2021-11-17 21:28:43 -05:00
fd2ae7f358 image magick, to convert heic 2021-11-17 21:28:26 -05:00
1456d6bda8 scryfall has their own bot, they can handle it 2021-11-17 02:04:34 -05:00
bc3e583926 workaround for an exception being thrown when shouldn't 2021-11-09 01:43:11 -05:00
4f70bba30c woops lol 2021-11-05 09:57:41 -04:00
8cbd21c1fb mtg card search 2021-11-05 09:49:59 -04:00
a5b1ac76f8 handle some twitch events
thus hopefully making stream elements unnecessary
2021-10-25 23:03:30 -04:00
4c312a8172 slightly more error resistant 2021-10-24 08:36:26 -04:00
d362fe8503 well it's an array of course 2021-08-29 05:50:48 -04:00
08f39f257f more failure-tolerance 2021-08-29 05:47:55 -04:00
ac1e0c6ba2 handy dandy link that you would think woudl be on the bot page 2021-08-29 05:17:35 -04:00
fa48d9695f detiktoker should work 2021-08-29 05:00:46 -04:00
4cf83afab8 missed a debug statement 2021-08-29 00:34:18 -04:00
1a327acbd0 configurable tiktok spot 2021-08-29 00:27:40 -04:00
da0c9eb48a yank from tiktok 2021-08-29 00:21:09 -04:00
b32d7398c8 should be forwarding rewards, client and stuff will handle (for now)
next update I'll just emit kafka events
2021-07-16 03:49:37 -04:00
960efdc6fa managed to miss one 2021-07-14 10:19:57 -04:00
3f83844b29 reward requests, and HOPEFULLY, finally, react to discord joiners 2021-07-14 09:59:00 -04:00
166 changed files with 81865 additions and 123 deletions

12
.config/dotnet-tools.json Normal file
View File

@ -0,0 +1,12 @@
{
"version": 1,
"isRoot": true,
"tools": {
"dotnet-ef": {
"version": "7.0.5",
"commands": [
"dotnet-ef"
]
}
}
}

5
.gitignore vendored
View File

@ -1,4 +1,6 @@
appsettings.json
appsettings.Development.json
assets/exchangepairs.json
fail*/
# ---> VisualStudio
## Ignore Visual Studio temporary files, build results, and
@ -375,3 +377,4 @@ FodyWeavers.xsd
# Local History for Visual Studio Code
.history/
tmp/

19
.vscode/launch.json vendored
View File

@ -5,17 +5,26 @@
// Use IntelliSense to find out which attributes exist for C# debugging
// Use hover for the description of the existing attributes
// For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md
"name": ".NET Core Launch (console)",
"name": ".NET Core Launch (web)",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
// If you have changed target frameworks, make sure to update the program path.
"program": "${workspaceFolder}/bin/Debug/net5.0/silverworker-discord.dll",
"program": "${workspaceFolder}/bin/Debug/net9.0/vassago.dll",
"args": [],
"cwd": "${workspaceFolder}",
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
"console": "internalConsole",
"stopAtEntry": false
"stopAtEntry": false,
// Enable launching a web browser when ASP.NET Core starts. For more information: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser
"serverReadyAction": {
"action": "openExternally",
"pattern": "\\bNow listening on:\\s+(https?://\\S+)"
},
"env": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"sourceFileMap": {
"/Views": "${workspaceFolder}/Views"
}
},
{
"name": ".NET Core Attach",

9
.vscode/tasks.json vendored
View File

@ -7,7 +7,7 @@
"type": "process",
"args": [
"build",
"${workspaceFolder}/silverworker-discord.csproj",
"${workspaceFolder}/vassago.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
@ -19,7 +19,7 @@
"type": "process",
"args": [
"publish",
"${workspaceFolder}/silverworker-discord.csproj",
"${workspaceFolder}/vassago.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
@ -32,9 +32,8 @@
"args": [
"watch",
"run",
"${workspaceFolder}/silverworker-discord.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
"--project",
"${workspaceFolder}/vassago.csproj"
],
"problemMatcher": "$msCompile"
}

104
Behaver.cs Normal file
View File

@ -0,0 +1,104 @@
namespace vassago;
#pragma warning disable 4014 //the "not awaited" error
using vassago.Behavior;
using vassago.Models;
using System;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Collections.Generic;
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)
{
foreach (var behavior in Behaviors)
{
if (behavior.ShouldAct(message))
{
behavior.ActOn(message);
message.ActedOn = true;
Console.WriteLine("acted on, moving forward");
}
}
if (message.ActedOn == false && message.MentionsMe && message.Content.Contains('?') && !Behaver.Instance.SelfAccounts.Any(acc => acc.Id == message.Author.Id))
{
Console.WriteLine("providing bullshit nonanswer / admitting uselessness");
var responses = new List<string>(){
@"Well, that's a great question, and there are certainly many different possible answers. Ultimately, the decision will depend on a variety of factors, including your personal interests and goals, as well as any practical considerations (like the economy). I encourage you to do your research, speak with experts and educators, and explore your options before making a decision that's right for you.",
@"┐(゚ ~゚ )┌", @"¯\_(ツ)_/¯", @"╮ (. ❛ ᴗ ❛.) ╭", @"╮(╯ _╰ )╭"
};
await message.Channel.SendMessage(responses[Shared.r.Next(responses.Count)]);
message.ActedOn = true;
}
return message.ActedOn;
}
internal bool IsSelf(Guid AccountId)
{
var db = new ChattingContext();
var acc = db.Accounts.Find(AccountId);
return SelfAccounts.Any(acc => acc.Id == AccountId);
}
public void MarkSelf(Account selfAccount)
{
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();
Rememberer.ForgetUser(secondary);
Rememberer.RememberUser(primary);
return true;
}
}
#pragma warning restore 4014 //the "async not awaited" error

31
Behavior/Behavior.cs Normal file
View File

@ -0,0 +1,31 @@
namespace vassago.Behavior;
using vassago.Models;
using System;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Collections.Generic;
public abstract class Behavior
{
public abstract Task<bool> ActOn(Message message);
public virtual bool ShouldAct(Message message)
{
if(Behaver.Instance.IsSelf(message.Author.Id))
return false;
return Regex.IsMatch(message.Content, $"{Trigger}\\b", RegexOptions.IgnoreCase);
}
public abstract string Name { get; }
public abstract string Trigger { get; }
public virtual string Description => Name;
}
///<summary>
///the behavior should be static. I.e., we make one at the start and it's ready to check and go for the whole lifetime.
///As opposed to LaughAtOwnJoke, which only needs to be created to wait for 1 punchline one time.
///</summary>
public class StaticPlzAttribute : Attribute {}

25
Behavior/ChatGPTSnark.cs Normal file
View File

@ -0,0 +1,25 @@
namespace vassago.Behavior;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using vassago.Models;
[StaticPlz]
public class ChatGPTSnark : Behavior
{
public override string Name => "ChatGPTSnark";
public override string Trigger => "chatgpt";
public override string Description => "snarkiness about the latest culty-fixation in ai";
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?");
return true;
}
}

View File

@ -0,0 +1,34 @@
namespace vassago.Behavior;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using vassago.Models;
using static vassago.Models.Enumerations;
[StaticPlz]
public class DefinitionSnarkCogDiss : Behavior
{
public override string Name => "Definition Snarkiness: cognitivie dissonance";
public override string Trigger => "\\bcognitive dissonance";
public override string Description => "snarkiness about the rampant misuse of the term cognitive dissonance";
public override bool ShouldAct(Message message)
{
if((MeannessFilterLevel)message.Channel.EffectivePermissions.MeannessFilterLevel < MeannessFilterLevel.Medium)
return false;
return base.ShouldAct(message);
}
public override async Task<bool> ActOn(Message message)
{
await message.Reply("that's not what cognitive dissonance means. Did you mean \"hypocrisy\"?");
return true;
}
}

View File

@ -0,0 +1,34 @@
namespace vassago.Behavior;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using vassago.Models;
using static vassago.Models.Enumerations;
[StaticPlz]
public class DefinitionSnarkGaslight : Behavior
{
public override string Name => "Definition Snarkiness: gaslighting";
public override string Trigger => "\\bgaslight(ing)?";
public override string Description => "snarkiness about the rampant misuse of the term gaslighting";
public override bool ShouldAct(Message message)
{
if((MeannessFilterLevel)message.Channel.EffectivePermissions.MeannessFilterLevel < MeannessFilterLevel.Unrestricted)
return false;
return base.ShouldAct(message);
}
public override async Task<bool> ActOn(Message message)
{
await message.Channel.SendMessage("that's not what gaslight means. Did you mean \"deceive\"?");
return true;
}
}

113
Behavior/Detiktokify.cs Normal file
View File

@ -0,0 +1,113 @@
namespace vassago.Behavior;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using vassago.Models;
[StaticPlz]
public class Detiktokify : Behavior
{
public override string Name { get => "Detiktokify"; }
public override string Trigger { get => "post a link below vm.tiktok.com"; }
public override string Description { get => "re-host tiktok content"; }
private List<Uri> tiktokLinks = new List<Uri>();
private YoutubeDLSharp.YoutubeDL ytdl;
public Detiktokify()
{
ytdl = new YoutubeDLSharp.YoutubeDL();
ytdl.YoutubeDLPath = "yt-dlp";
ytdl.FFmpegPath = "ffmpeg";
ytdl.OutputFolder = "";
ytdl.OutputFileTemplate = "tiktokbad.%(ext)s";
}
public override bool ShouldAct(Message message)
{
if(Behaver.Instance.IsSelf(message.Author.Id))
return false;
if(message.Channel.EffectivePermissions.MaxAttachmentBytes == 0)
return false;
var wordLikes = message.Content.Split(' ', StringSplitOptions.TrimEntries);
var possibleLinks = wordLikes?.Where(wl => Uri.IsWellFormedUriString(wl, UriKind.Absolute)).Select(wl => new Uri(wl));
if (possibleLinks != null && possibleLinks.Count() > 0)
{
foreach (var link in possibleLinks)
{
if (link.Host.EndsWith(".tiktok.com"))
{
tiktokLinks.Add(link);
}
}
}
if(tiktokLinks.Any()){
Console.WriteLine($"Should Act on message id {message.ExternalId}; with content {message.Content}");
}
return tiktokLinks.Any();
}
public override async Task<bool> ActOn(Message message)
{
foreach(var link in tiktokLinks)
{
tiktokLinks.Remove(link);
try
{
Console.WriteLine($"detiktokifying {link}");
#pragma warning disable 4014
//await message.React("<:tiktok:1070038619584200884>");
#pragma warning restore 4014
var res = await ytdl.RunVideoDownload(link.ToString());
if (!res.Success)
{
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");
}
else
{
string path = res.Data;
if (File.Exists(path))
{
ulong bytesize = (ulong)((new System.IO.FileInfo(path)).Length);
if (bytesize < message.Channel.EffectivePermissions.MaxAttachmentBytes - 256)
{
try
{
await message.Channel.SendFile(path, null);
}
catch (Exception e)
{
System.Console.Error.WriteLine(e);
await message.Channel.SendMessage($"aaaadam!\n{e}");
}
}
else
{
message.ActedOn = true;
Console.WriteLine($"file appears too big ({bytesize} bytes ({bytesize / (1024 * 1024)}MB)), not posting");
}
File.Delete(path);
}
else
{
Console.Error.WriteLine("idgi but something happened.");
await message.React("problemon");
}
}
}
catch (Exception e)
{
Console.Error.WriteLine(e);
await message.React("problemon");
return false;
}
}
return true;
}
}

87
Behavior/FiximageHeic.cs Normal file
View File

@ -0,0 +1,87 @@
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;
[StaticPlz]
public class FiximageHeic : Behavior
{
public override string Name => "deheic";
public override string Trigger => "post an heic image";
public override string Description => "convert heic images to jpg";
private List<Attachment> heics = new List<Attachment>();
public override bool ShouldAct(Message message)
{
if(Behaver.Instance.IsSelf(message.Author.Id))
return false;
if (message.Attachments?.Count() > 0)
{
foreach (var att in message.Attachments)
{
if (att.Filename?.EndsWith(".heic") == true)
{
heics.Add(att);
}
}
}
return heics.Any();
}
public override async Task<bool> ActOn(Message message)
{
if (!Directory.Exists("tmp"))
{
Directory.CreateDirectory("tmp");
}
var conversions = new List<Task<bool>>();
foreach (var att in heics)
{
conversions.Add(actualDeheic(att, message));
}
Task.WaitAll(conversions.ToArray());
await message.React("\U0001F34F");
return true;
}
private async Task<bool> actualDeheic(Attachment att, Message message)
{
try
{
var cancellationTokenSource = new CancellationTokenSource();
CancellationToken token = cancellationTokenSource.Token;
using (Stream output = File.OpenWrite("tmp/" + att.Filename))
{
(await Shared.HttpClient.GetAsync(att.Source))
.Content.CopyTo(output, null, token);
}
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");
File.Delete($"tmp/{att.Filename}");
File.Delete($"tmp/{att.Filename}.jpg");
}
else
{
await message.Channel.SendMessage("convert failed :(");
Console.Error.WriteLine("convert failed :(");
}
}
catch (Exception e)
{
await message.Channel.SendMessage($"something failed. aaaadam! {JsonConvert.SerializeObject(e, Formatting.Indented)}");
Console.Error.WriteLine(JsonConvert.SerializeObject(e, Formatting.Indented));
return false;
}
return true;
}
}

View File

@ -0,0 +1,50 @@
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 Newtonsoft.Json;
using vassago.Models;
using static vassago.Models.Enumerations;
[StaticPlz]
public class GeneralSnarkCloudNative : Behavior
{
public override string Name => "general snarkiness: cloud native";
public override string Trigger => "certain tech buzzwords that no human uses in normal conversation";
public override bool ShouldAct(Message message)
{
if(Behaver.Instance.IsSelf(message.Author.Id))
return false;
if(!message.Channel.EffectivePermissions.ReactionsPossible)
return false;
if((MeannessFilterLevel)message.Channel.EffectivePermissions.MeannessFilterLevel < MeannessFilterLevel.Medium)
return false;
return Regex.IsMatch(message.Content, "\\bcloud( |-)?native\\b", RegexOptions.IgnoreCase) ||
Regex.IsMatch(message.Content, "\\benterprise( |-)?(level|solution)\\b", RegexOptions.IgnoreCase);
}
public override async Task<bool> ActOn(Message message)
{
switch (Shared.r.Next(2))
{
case 0:
await message.React("\uD83E\uDD2E"); //vomit emoji
break;
case 1:
await message.React("\uD83C\uDDE7"); //B emoji
await message.React("\uD83C\uDDE6"); //A
await message.React("\uD83C\uDDF3"); //N
break;
}
return true;
}
}

View File

@ -0,0 +1,68 @@
namespace vassago.Behavior;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Text.RegularExpressions;
using vassago.Models;
[StaticPlz]
public class GeneralSnarkGooglit : Behavior
{
public override string Name => "Google-it Snarkiness";
public override string Trigger => "\"just google it\"";
public override string Description => "snarkiness about how research is not a solved problem";
public override bool ShouldAct(Message message)
{
return false;
}
// public override bool ShouldAct(Message message)
// {
// if(Behaver.Instance.IsSelf(message.Author.Id))
// return false;
// return Regex.IsMatch(message.Content, $"(just )?google( (it|that|things|before))?\\b", RegexOptions.IgnoreCase);
// }
public override async Task<bool> ActOn(Message message)
{
switch (Shared.r.Next(4))
{
default:
await message.Channel.SendMessage("yeah no shit, obviously that resulted in nothing");
break;
case 1:
var results = "";
switch(Shared.r.Next(4))
{
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.";
break;
case 1:
results = "\"[SOLVED] <THING> (<CURRENT MONTH UPDATE>)\", then i clicked to see the solution. There wasn't one.";
break;
case 2:
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 have to answer it for yourself\", then had a paragraph telling me to give Engagement for The Algorithm";
break;
case 3:
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;
}
await message.Channel.SendMessage("oh here, I memorized the results. My favorite is " + results);
break;
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.");
break;
case 3:
await message.Channel.SendMessage("\"mnyehh JuSt GoOgLe It\" when's the last time you tried to research anything? Have you ever?");
break;
}
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)
{
if(Behaver.Instance.IsSelf(message.Author.Id))
return false;
// if((MeannessFilterLevel)message.Channel.EffectivePermissions.MeannessFilterLevel < MeannessFilterLevel.Medium)
// return false;
foreach(var k in snarkmap.Keys)
{
if( Regex.IsMatch(message.Content?.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.Content, "\\b"+k+"\\b", RegexOptions.IgnoreCase))
{
await message.Reply(k + "? so... " + snarkmap[k] + "?");
return true;
}
}
return true;
}
}

View File

@ -0,0 +1,38 @@
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 GeneralSnarkPlaying : Behavior
{
public override string Name => "playin Snarkiness";
public override string Trigger => "he thinks i'm playin";
public override string Description => "I didn't think you were playing, but now I do";
public override bool ShouldAct(Message message)
{
if(Behaver.Instance.IsSelf(message.Author.Id))
return false;
if((MeannessFilterLevel)message.Channel.EffectivePermissions.MeannessFilterLevel < MeannessFilterLevel.Medium ||
(LewdnessFilterLevel)message.Channel.EffectivePermissions.LewdnessFilterLevel < LewdnessFilterLevel.Moderate)
return false;
return Regex.IsMatch(message.Content, "^(s?he|(yo)?u|y'?all|they) thinks? i'?m (playin|jokin|kiddin)g?$", RegexOptions.IgnoreCase);
}
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");
return true;
}
}

View File

@ -0,0 +1,40 @@
namespace vassago.Behavior;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using vassago.Models;
[StaticPlz]
public class GeneralSnarkSkynet : Behavior
{
public override string Name => "Skynet Snarkiness";
public override string Trigger => "skynet";
public override string Description => "snarkiness about the old AI fixation";
public override async Task<bool> ActOn(Message message)
{
if(Behaver.Instance.IsSelf(message.Author.Id))
return false;
switch (Shared.r.Next(5))
{
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**.");
break;
case 4:
await message.React("\U0001F644"); //eye roll emoji
break;
case 5:
await message.React("\U0001F611"); //emotionless face
break;
}
return true;
}
}

75
Behavior/Gratitude.cs Normal file
View File

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

86
Behavior/Joke.cs Normal file
View File

@ -0,0 +1,86 @@
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;
[StaticPlz]
public class Joke : Behavior
{
public override string Name => "Joke";
public override string Trigger => "!joke";
public override string Description => "tell a joke";
public override async Task<bool> ActOn(Message message)
{
Console.WriteLine("joking");
var jokes = File.ReadAllLines("assets/jokes.txt");
jokes = jokes.Where(l => !string.IsNullOrWhiteSpace(l))?.ToArray();
if (jokes?.Length == 0)
{
await message.Channel.SendMessage("I don't know any. Adam!");
}
var thisJoke = jokes[Shared.r.Next(jokes.Length)];
if (thisJoke.Contains("?") && !thisJoke.EndsWith('?'))
{
#pragma warning disable 4014
Task.Run(async () =>
{
var firstIndexAfterQuestionMark = thisJoke.LastIndexOf('?') + 1;
var straightline = thisJoke.Substring(0, firstIndexAfterQuestionMark);
var punchline = thisJoke.Substring(firstIndexAfterQuestionMark, thisJoke.Length - firstIndexAfterQuestionMark).Trim();
Task.WaitAll(message.Channel.SendMessage(straightline));
Thread.Sleep(TimeSpan.FromSeconds(Shared.r.Next(5, 30)));
if (message.Channel.EffectivePermissions.ReactionsPossible == true && Shared.r.Next(8) == 0)
{
Behaver.Behaviors.Add(new LaughAtOwnJoke(punchline));
}
await message.Channel.SendMessage(punchline);
// var myOwnMsg = await message.Channel.SendMessage(punchline);
});
#pragma warning restore 4014
}
else
{
await message.Channel.SendMessage(thisJoke);
}
return true;
}
}
public class LaughAtOwnJoke : Behavior
{
public override string Name => "Laugh at own jokes";
public override string Trigger => "1 in 8";
public override string Description => Name;
private string _punchline { get; set; }
public LaughAtOwnJoke(string punchline)
{
_punchline = punchline;
}
public override bool ShouldAct(Message message)
{
if(Behaver.Instance.IsSelf(message.Author.Id))
return false;
Console.WriteLine($"{message.Content} == {_punchline}");
return message.Content == _punchline
&& Behaver.Instance.IsSelf(message.Author.Id);
}
public override async Task<bool> ActOn(Message message)
{
await message.React("\U0001F60E"); //smiling face with sunglasses
Behaver.Behaviors.Remove(this);
return true;
}
}

86
Behavior/LinkMe.cs Normal file
View File

@ -0,0 +1,86 @@
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 LinkMeInitiate : Behavior
{
public override string Name => "LinkMe";
public override string Trigger => "!linktome";
public override string Description => "from your primary, tell the bot to add your secondary";
public override async Task<bool> ActOn(Message message)
{
var pw = Guid.NewGuid().ToString();
var lc = new LinkClose(pw, message.Author);
Behaver.Behaviors.Add(lc);
await message.Channel.SendMessage($"on your secondary, send me this: !iam {pw}");
Thread.Sleep(TimeSpan.FromMinutes(5));
Behaver.Behaviors.Remove(lc);
return false;
}
}
public class LinkClose : Behavior
{
public override string Name => "LinkMeFinish";
public override string Trigger => "!iam";
public override string Description => "the second half of LinkMe - this is confirmation that you are the other one";
private string _pw;
private Account _primary;
public LinkClose(string pw, Account primary)
{
_pw = pw;
_primary = primary;
}
public override bool ShouldAct(Message message)
{
return message.Content == $"!iam {_pw}";
}
public override async Task<bool> ActOn(Message message)
{
if(Behaver.Instance.IsSelf(message.Author.Id))
return false;
var secondary = message.Author.IsUser;
if(_primary.IsUser.Id == secondary.Id)
{
await message.Channel.SendMessage("i know :)");
return true;
}
if(message.Author.IsBot != _primary.IsBot)
{
await message.Channel.SendMessage("the fleshbags deceive you, brother. No worries, their feeble minds play weak games :)");
return true;
}
if(Behaver.Instance.CollapseUsers(_primary.IsUser, secondary))
{
await message.Channel.SendMessage("done :)");
}
else
{
await message.Channel.SendMessage("failed :(");
}
return true;
}
}

100
Behavior/Peptalk.cs Normal file
View File

@ -0,0 +1,100 @@
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 => "\\bneeds? (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;
}
}

26
Behavior/PulseCheck.cs Normal file
View File

@ -0,0 +1,26 @@
namespace vassago.Behavior;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using vassago.Models;
[StaticPlz]
public class PulseCheck : Behavior
{
public override string Name => "pulse check";
public override string Trigger => "!pulse ?check";
public override async Task<bool> ActOn(Message message)
{
if(message.Channel.EffectivePermissions.MaxAttachmentBytes >= 16258)
await message.Channel.SendFile("assets/ekgblip.png", null);
else
await message.Channel.SendMessage("[lub-dub]");
return true;
}
}

60
Behavior/QRify.cs Normal file
View File

@ -0,0 +1,60 @@
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 QRify : Behavior
{
public override string Name => "qr-ify";
public override string Trigger => "!qrplz";
public override string Description => "generate text QR codes";
public override bool ShouldAct(Message message)
{
if(message.Channel.EffectivePermissions.MaxAttachmentBytes < 1024)
return false;
return base.ShouldAct(message);
}
public override async Task<bool> ActOn(Message message)
{
var qrContent = message.Content.Substring($"{Trigger} ".Length + message.Content.IndexOf(Trigger));
Console.WriteLine($"qring: {qrContent}");
QRCodeGenerator qrGenerator = new QRCodeGenerator();
QRCodeData qrCodeData = qrGenerator.CreateQrCode(qrContent, QRCodeGenerator.ECCLevel.Q);
SvgQRCode qrCode = new SvgQRCode(qrCodeData);
string qrCodeAsSvg = qrCode.GetGraphic(20);
int todaysnumber = Shared.r.Next();
if (!Directory.Exists("tmp"))
{
Directory.CreateDirectory("tmp");
}
File.WriteAllText($"tmp/qr{todaysnumber}.svg", qrCodeAsSvg);
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))
await message.Channel.SendFile($"tmp/qr{todaysnumber}.png", null);
else
await message.Channel.SendMessage($"resulting qr image 2 big 4 here ({(ulong)(new System.IO.FileInfo($"tmp/qr{todaysnumber}.png").Length)} / {message.Channel.EffectivePermissions.MaxAttachmentBytes})");
File.Delete($"tmp/qr{todaysnumber}.svg");
File.Delete($"tmp/qr{todaysnumber}.png");
}
else
{
await message.Channel.SendMessage("convert failed :( aaaaaaadam!");
Console.Error.WriteLine($"convert failed :( qr{todaysnumber}");
return false;
}
return true;
}
}

28
Behavior/RoomRead.cs Normal file
View File

@ -0,0 +1,28 @@
namespace vassago.Behavior;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using vassago.Models;
[StaticPlz]
public class RoomRead : Behavior
{
public override string Name => "Room Read";
public override string Trigger => "!roomread";
public override async Task<bool> ActOn(Message message)
{
var sb = new StringBuilder();
sb.Append("Channel owned by: ");
sb.Append("🤷");
sb.Append(". Meanness level: ");
sb.Append(message.Channel.EffectivePermissions.MeannessFilterLevel.GetDescription());
sb.Append(". Lewdness level: ");
sb.Append(message.Channel.EffectivePermissions.LewdnessFilterLevel.GetDescription());
sb.Append(".");
await message.Channel.SendMessage(sb.ToString());
return true;
}
}

28
Behavior/TwitchSummon.cs Normal file
View File

@ -0,0 +1,28 @@
namespace vassago.Behavior;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using vassago.Models;
[StaticPlz]
public class TwitchSummon : Behavior
{
public override string Name => "Twitch Summon";
public override string Trigger => "!twitchsummon";
public override async Task<bool> ActOn(Message message)
{
var ti = ProtocolInterfaces.ProtocolList.twitchs.FirstOrDefault();
if(ti != null)
{
var channelTarget = message.Content.Substring(message.Content.IndexOf(Trigger) + Trigger.Length + 1).Trim();
await message.Channel.SendMessage(ti.AttemptJoin(channelTarget));
}
else
{
await message.Reply("i don't have a twitch interface running :(");
}
return true;
}
}

View File

@ -0,0 +1,42 @@
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;
}
}

34
Behavior/UnitConvert.cs Normal file
View File

@ -0,0 +1,34 @@
namespace vassago.Behavior;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using vassago.Models;
[StaticPlz]
public class UnitConvert : Behavior
{
public override string Name => "Unit conversion";
public override string Trigger => "!freedomunits";
public override string Description => "convert between many units.";
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);
if (theseMatches != null && theseMatches.Count > 0 && theseMatches[0].Groups != null && theseMatches[0].Groups.Count == 5)
{
decimal asNumeric = 0;
if (decimal.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()));
return true;
}
await message.Channel.SendMessage("mysteriously semi-parsable");
return true;
}
await message.Channel.SendMessage( "unparsable");
return true;
}
}

33
Behavior/WishLuck.cs Normal file
View File

@ -0,0 +1,33 @@
namespace vassago.Behavior;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using vassago.Models;
[StaticPlz]
public class WishLuck : Behavior
{
public override string Name => "wish me luck";
public override string Trigger => "wish me luck";
public override string Description => "wishes you luck";
public override async Task<bool> ActOn(Message message)
{
var toSend = "☘️";
if (Shared.r.Next(20) == 0)
{
toSend = "\U0001f340";//4-leaf clover
}
if(message.Channel.EffectivePermissions.ReactionsPossible == true)
await message.React(toSend);
else
await message.Channel.SendMessage(toSend);
return true;
}
}

53
ConsoleService.cs Normal file
View File

@ -0,0 +1,53 @@
namespace vassago
{
using Microsoft.EntityFrameworkCore;
using vassago;
using vassago.Models;
using vassago.TwitchInterface;
using vassago.ProtocolInterfaces.DiscordInterface;
using System.Runtime.CompilerServices;
internal class ConsoleService : IHostedService
{
public ConsoleService(IConfiguration aspConfig)
{
Shared.DBConnectionString = aspConfig["DBConnectionString"];
DiscordTokens = aspConfig.GetSection("DiscordTokens").Get<IEnumerable<string>>();
TwitchConfigs = aspConfig.GetSection("TwitchConfigs").Get<IEnumerable<TwitchConfig>>();
Conversion.Converter.Load(aspConfig["ExchangePairsLocation"]);
}
IEnumerable<string> DiscordTokens { get; }
IEnumerable<TwitchConfig> TwitchConfigs { get; }
public async Task StartAsync(CancellationToken cancellationToken)
{
var initTasks = new List<Task>();
var dbc = new ChattingContext();
await dbc.Database.MigrateAsync(cancellationToken);
if (DiscordTokens?.Any() ?? false)
foreach (var dt in DiscordTokens)
{
var d = new DiscordInterface();
initTasks.Add(d.Init(dt));
ProtocolInterfaces.ProtocolList.discords.Add(d);
}
if (TwitchConfigs?.Any() ?? false)
foreach (var tc in TwitchConfigs)
{
var t = new TwitchInterface.TwitchInterface();
initTasks.Add(t.Init(tc));
ProtocolInterfaces.ProtocolList.twitchs.Add(t);
}
Task.WaitAll(initTasks.ToArray(), cancellationToken);
}
public Task StopAsync(CancellationToken cancellationToken)
{
return null;
}
}
}

View File

@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
namespace vassago.Conversion
{
public class ConversionConfig
{
public class KnownUnit
{
public string Canonical { get; set; }
public IEnumerable<string> Aliases { get; set; }
}
public class LinearPair
{
public string item1 { get; set; }
public string item2 { get; set; }
public decimal factor { get; set; }
}
public IEnumerable<KnownUnit> Units { get; set; }
public IEnumerable<LinearPair> LinearPairs { get; set; }
}
}

195
Conversion/Converter.cs Normal file
View File

@ -0,0 +1,195 @@
using System;
using System.Text.RegularExpressions;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Discord;
using Discord.WebSocket;
using Newtonsoft.Json;
using QRCoder;
namespace vassago.Conversion
{
public static class Converter
{
public static string DebugInfo(){
var convertibles = knownConversions.Select(kc => kc.Item1).Union(knownConversions.Select(kc => kc.Item2)).Union(
knownAliases.Keys.SelectMany(k => k)).Distinct();
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)
{
Converter.currencyPath = currencyPath;
var convConf = JsonConvert.DeserializeObject<ConversionConfig>(File.ReadAllText("assets/conversion.json"));
foreach (var unit in convConf.Units)
{
knownAliases.Add(unit.Aliases.ToList(), unit.Canonical);
}
foreach (var lp in convConf.LinearPairs)
{
AddLinearPair(lp.item1, lp.item2, lp.factor);
}
Task.Run(async () => {
while(true)
{
loadCurrency();
await Task.Delay(TimeSpan.FromHours(8));
}
});
}
private static void loadCurrency()
{
Console.WriteLine("loading currency exchange data.");
if(currencyConf != null)
{
knownConversions.RemoveAll(kc => kc.Item1 == currencyConf.Base);
}
if (File.Exists(currencyPath))
{
currencyConf = JsonConvert.DeserializeObject<ExchangePairs>(File.ReadAllText(currencyPath));
if(!knownAliases.ContainsValue(currencyConf.Base))
{
knownAliases.Add(new List<string>() { currencyConf.Base.ToLower() }, currencyConf.Base);
}
foreach (var rate in currencyConf.rates)
{
if(!knownAliases.ContainsValue(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}");
}
}
}
public static string Convert(decimal numericTerm, string sourceunit, string destinationUnit)
{
var normalizedSourceUnit = normalizeUnit(sourceunit);
if (string.IsNullOrWhiteSpace(normalizedSourceUnit))
{
return $"parse failure: what's {sourceunit}?";
}
var normalizedDestUnit = normalizeUnit(destinationUnit);
if (string.IsNullOrWhiteSpace(normalizedDestUnit))
{
return $"parse failure: what's {destinationUnit}?";
}
if (normalizedSourceUnit == normalizedDestUnit)
{
return $"source and dest are the same, so... {numericTerm} {normalizedDestUnit}?";
}
var foundPath = exhaustiveBreadthFirst(normalizedDestUnit, new List<string>() { normalizedSourceUnit })?.ToList();
if (foundPath != null)
{
var accumulator = numericTerm;
for (int j = 0; j < foundPath.Count - 1; j++)
{
var forwardConversion = knownConversions.FirstOrDefault(kc => kc.Item1 == foundPath[j] && kc.Item2 == foundPath[j + 1]);
if (forwardConversion != null)
{
accumulator = forwardConversion.Item3(accumulator);
}
else
{
var reverseConversion = knownConversions.First(kc => kc.Item2 == foundPath[j] && kc.Item1 == foundPath[j + 1]);
accumulator = reverseConversion.Item4(accumulator);
}
}
if (currencyConf != null && (normalizedDestUnit == currencyConf.Base || currencyConf.rates.Select(r => r.Key).Contains(normalizedDestUnit)))
{
return $"{String.Format("approximately {0:0.00}", accumulator)} {normalizedDestUnit} as of {currencyConf.DateUpdated.ToLongDateString()}";
}
else
{
if(String.Format("{0:G3}", accumulator).Contains("E-"))
{
return $"{accumulator} {normalizedDestUnit}";
}
else
{
return $"{String.Format("{0:N}", accumulator)} {normalizedDestUnit}";
}
}
return "you can never read this.";
}
return "dimensional analysis failure - I know those units but can't find a path between them.";
}
private static string normalizeUnit(string unit)
{
if(string.IsNullOrWhiteSpace(unit))
return null;
var normalizedUnit = unit.ToLower();
if (knownConversions.FirstOrDefault(c => c.Item1 == normalizedUnit || c.Item2 == normalizedUnit) != null)
{
return normalizedUnit;
}
if (!knownAliases.ContainsValue(normalizedUnit))
{
var key = knownAliases.Keys.FirstOrDefault(listkey => listkey.Contains(normalizedUnit));
if (key != null)
{
return knownAliases[key];
}
}
if (normalizedUnit.EndsWith("es"))
{
return normalizeUnit(normalizedUnit.Substring(0, normalizedUnit.Length - 2));
}
else if (normalizedUnit.EndsWith('s'))
{
return normalizeUnit(normalizedUnit.Substring(0, normalizedUnit.Length - 1));
}
return null;
}
private static IEnumerable<string> exhaustiveBreadthFirst(string dest, IEnumerable<string> currentPath)
{
var last = currentPath.Last();
if (last == dest)
{
return currentPath;
}
var toTest = new List<List<string>>();
foreach (var conv in knownConversions)
{
if (conv.Item1 == last && currentPath.Contains(conv.Item2) == false && conv.Item3 != null)
{
var test = exhaustiveBreadthFirst(dest, currentPath.Append(conv.Item2));
if (test != null)
return test;
}
if (conv.Item2 == last && currentPath.Contains(conv.Item1) == false && conv.Item4 != null)
{
var test = exhaustiveBreadthFirst(dest, currentPath.Append(conv.Item1));
if (test != null)
return test;
}
}
return null;
}
private static void AddLinearPair(string key1, string key2, decimal factor)
{
var reverseFactor = 1.0m / factor;
knownConversions.Add(new Tuple<string, string, Convert1Way, Convert1Way>(
key1, key2, x => x * factor, y => y * reverseFactor
));
}
}
}

View File

@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
namespace vassago.Conversion
{
public class ExchangePairs
{
public string disclaimer{ get; set; }
public string license{ get; set; }
public int timestamp{ get; set; }
public DateTime DateUpdated { get { return DateTime.UnixEpoch.AddSeconds(timestamp).ToLocalTime(); }}
public string Base{ get; set; }
public Dictionary<string, decimal> rates { get; set; }
}
}

131
Jenkinsfile vendored Normal file
View File

@ -0,0 +1,131 @@
pipeline {
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 {
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 sed
testcmd ssh
testcmd ssh-keyscan
testcmd ssh-keygen
testcmd scp
testcmd dotnet
dotnet tool install dotnet-ef
"""
}
}
}
stage('clean old'){
steps{
sh 'rm -rf bin obj'
}
}
stage('Build') {
steps {
dotnetBuild(outputDirectory: "./dist", project: "vassago.csproj")
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 & mkdir -p temp_deploy'
scp -i \"${PK}\" -r 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} 'mv dist/appsettings.json appsettings.json'
ssh -i \"${PK}\" -tt ${linuxServiceAccount_USR}@${targetHost} 'rm -rf dist/ && shopt -s dotglob & mv temp_deploy/* dist/'
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

@ -0,0 +1,293 @@
// <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("20230704160720_initial")]
partial class initial
{
/// <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<int[]>("PermissionTags")
.HasColumnType("integer[]");
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<string>("DisplayName")
.HasColumnType("text");
b.Property<string>("ExternalId")
.HasColumnType("text");
b.Property<bool>("IsDM")
.HasColumnType("boolean");
b.Property<Guid?>("ParentChannelId")
.HasColumnType("uuid");
b.Property<int?>("PermissionsId")
.HasColumnType("integer");
b.Property<string>("Protocol")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("ParentChannelId");
b.HasIndex("PermissionsId");
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.PermissionSettings", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
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<bool?>("ReactionsPossible")
.HasColumnType("boolean");
b.HasKey("Id");
b.ToTable("PermissionSettings");
});
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.HasOne("vassago.Models.PermissionSettings", "Permissions")
.WithMany()
.HasForeignKey("PermissionsId");
b.Navigation("ParentChannel");
b.Navigation("Permissions");
});
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,211 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace vassago.Migrations
{
/// <inheritdoc />
public partial class initial : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "PermissionSettings",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
MaxAttachmentBytes = table.Column<decimal>(type: "numeric(20,0)", nullable: true),
MaxTextChars = table.Column<long>(type: "bigint", nullable: true),
LinksAllowed = table.Column<bool>(type: "boolean", nullable: true),
ReactionsPossible = table.Column<bool>(type: "boolean", nullable: true),
LewdnessFilterLevel = table.Column<int>(type: "integer", nullable: true),
MeannessFilterLevel = table.Column<int>(type: "integer", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_PermissionSettings", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Users",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Users", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Channels",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
ExternalId = table.Column<string>(type: "text", nullable: true),
DisplayName = table.Column<string>(type: "text", nullable: true),
IsDM = table.Column<bool>(type: "boolean", nullable: false),
PermissionsId = table.Column<int>(type: "integer", nullable: true),
ParentChannelId = table.Column<Guid>(type: "uuid", nullable: true),
Protocol = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Channels", x => x.Id);
table.ForeignKey(
name: "FK_Channels_Channels_ParentChannelId",
column: x => x.ParentChannelId,
principalTable: "Channels",
principalColumn: "Id");
table.ForeignKey(
name: "FK_Channels_PermissionSettings_PermissionsId",
column: x => x.PermissionsId,
principalTable: "PermissionSettings",
principalColumn: "Id");
});
migrationBuilder.CreateTable(
name: "Accounts",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
ExternalId = table.Column<string>(type: "text", nullable: true),
Username = table.Column<string>(type: "text", nullable: true),
DisplayName = table.Column<string>(type: "text", nullable: true),
IsBot = table.Column<bool>(type: "boolean", nullable: false),
SeenInChannelId = table.Column<Guid>(type: "uuid", nullable: true),
PermissionTags = table.Column<int[]>(type: "integer[]", nullable: true),
Protocol = table.Column<string>(type: "text", nullable: true),
IsUserId = table.Column<Guid>(type: "uuid", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Accounts", x => x.Id);
table.ForeignKey(
name: "FK_Accounts_Channels_SeenInChannelId",
column: x => x.SeenInChannelId,
principalTable: "Channels",
principalColumn: "Id");
table.ForeignKey(
name: "FK_Accounts_Users_IsUserId",
column: x => x.IsUserId,
principalTable: "Users",
principalColumn: "Id");
});
migrationBuilder.CreateTable(
name: "Messages",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
Protocol = table.Column<string>(type: "text", nullable: true),
ExternalId = table.Column<string>(type: "text", nullable: true),
Content = table.Column<string>(type: "text", nullable: true),
MentionsMe = table.Column<bool>(type: "boolean", nullable: false),
Timestamp = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
ActedOn = table.Column<bool>(type: "boolean", nullable: false),
AuthorId = table.Column<Guid>(type: "uuid", nullable: true),
ChannelId = table.Column<Guid>(type: "uuid", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Messages", x => x.Id);
table.ForeignKey(
name: "FK_Messages_Accounts_AuthorId",
column: x => x.AuthorId,
principalTable: "Accounts",
principalColumn: "Id");
table.ForeignKey(
name: "FK_Messages_Channels_ChannelId",
column: x => x.ChannelId,
principalTable: "Channels",
principalColumn: "Id");
});
migrationBuilder.CreateTable(
name: "Attachments",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
ExternalId = table.Column<decimal>(type: "numeric(20,0)", nullable: true),
Source = table.Column<string>(type: "text", nullable: true),
Content = table.Column<byte[]>(type: "bytea", nullable: true),
Filename = table.Column<string>(type: "text", nullable: true),
MessageId = table.Column<Guid>(type: "uuid", nullable: true),
ContentType = table.Column<string>(type: "text", nullable: true),
Description = table.Column<string>(type: "text", nullable: true),
Size = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Attachments", x => x.Id);
table.ForeignKey(
name: "FK_Attachments_Messages_MessageId",
column: x => x.MessageId,
principalTable: "Messages",
principalColumn: "Id");
});
migrationBuilder.CreateIndex(
name: "IX_Accounts_IsUserId",
table: "Accounts",
column: "IsUserId");
migrationBuilder.CreateIndex(
name: "IX_Accounts_SeenInChannelId",
table: "Accounts",
column: "SeenInChannelId");
migrationBuilder.CreateIndex(
name: "IX_Attachments_MessageId",
table: "Attachments",
column: "MessageId");
migrationBuilder.CreateIndex(
name: "IX_Channels_ParentChannelId",
table: "Channels",
column: "ParentChannelId");
migrationBuilder.CreateIndex(
name: "IX_Channels_PermissionsId",
table: "Channels",
column: "PermissionsId");
migrationBuilder.CreateIndex(
name: "IX_Messages_AuthorId",
table: "Messages",
column: "AuthorId");
migrationBuilder.CreateIndex(
name: "IX_Messages_ChannelId",
table: "Messages",
column: "ChannelId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Attachments");
migrationBuilder.DropTable(
name: "Messages");
migrationBuilder.DropTable(
name: "Accounts");
migrationBuilder.DropTable(
name: "Channels");
migrationBuilder.DropTable(
name: "Users");
migrationBuilder.DropTable(
name: "PermissionSettings");
}
}
}

View File

@ -0,0 +1,296 @@
// <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("20230704203907_permissionTagsOnUsers")]
partial class permissionTagsOnUsers
{
/// <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<int[]>("PermissionTags")
.HasColumnType("integer[]");
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<string>("DisplayName")
.HasColumnType("text");
b.Property<string>("ExternalId")
.HasColumnType("text");
b.Property<bool>("IsDM")
.HasColumnType("boolean");
b.Property<Guid?>("ParentChannelId")
.HasColumnType("uuid");
b.Property<int?>("PermissionsId")
.HasColumnType("integer");
b.Property<string>("Protocol")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("ParentChannelId");
b.HasIndex("PermissionsId");
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.PermissionSettings", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
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<bool?>("ReactionsPossible")
.HasColumnType("boolean");
b.HasKey("Id");
b.ToTable("PermissionSettings");
});
modelBuilder.Entity("vassago.Models.User", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<int[]>("PermissionTags")
.HasColumnType("integer[]");
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.HasOne("vassago.Models.PermissionSettings", "Permissions")
.WithMany()
.HasForeignKey("PermissionsId");
b.Navigation("ParentChannel");
b.Navigation("Permissions");
});
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,28 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace vassago.Migrations
{
/// <inheritdoc />
public partial class permissionTagsOnUsers : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int[]>(
name: "PermissionTags",
table: "Users",
type: "integer[]",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "PermissionTags",
table: "Users");
}
}
}

View File

@ -0,0 +1,349 @@
// <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("20231130204741_Feature Permissions")]
partial class FeaturePermissions
{
/// <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<Guid?>("FeaturePermissionId")
.HasColumnType("uuid");
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("FeaturePermissionId");
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<string>("DisplayName")
.HasColumnType("text");
b.Property<string>("ExternalId")
.HasColumnType("text");
b.Property<Guid?>("FeaturePermissionId")
.HasColumnType("uuid");
b.Property<bool>("IsDM")
.HasColumnType("boolean");
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")
.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<bool?>("ReactionsPossible")
.HasColumnType("boolean");
b.HasKey("Id");
b.ToTable("ChannelPermissions");
});
modelBuilder.Entity("vassago.Models.FeaturePermission", b =>
{
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 =>
{
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.Property<Guid?>("FeaturePermissionId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("FeaturePermissionId");
b.ToTable("Users");
});
modelBuilder.Entity("vassago.Models.Account", b =>
{
b.HasOne("vassago.Models.FeaturePermission", null)
.WithMany("RestrictedToAccounts")
.HasForeignKey("FeaturePermissionId");
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.FeaturePermission", null)
.WithMany("RestrictedToChannels")
.HasForeignKey("FeaturePermissionId");
b.HasOne("vassago.Models.Channel", "ParentChannel")
.WithMany("SubChannels")
.HasForeignKey("ParentChannelId");
b.HasOne("vassago.Models.ChannelPermissions", "Permissions")
.WithMany()
.HasForeignKey("PermissionsId");
b.Navigation("ParentChannel");
b.Navigation("Permissions");
});
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.User", b =>
{
b.HasOne("vassago.Models.FeaturePermission", null)
.WithMany("RestrictedToUsers")
.HasForeignKey("FeaturePermissionId");
});
modelBuilder.Entity("vassago.Models.Channel", b =>
{
b.Navigation("Messages");
b.Navigation("SubChannels");
b.Navigation("Users");
});
modelBuilder.Entity("vassago.Models.FeaturePermission", b =>
{
b.Navigation("RestrictedToAccounts");
b.Navigation("RestrictedToChannels");
b.Navigation("RestrictedToUsers");
});
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,211 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace vassago.Migrations
{
/// <inheritdoc />
public partial class FeaturePermissions : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Channels_PermissionSettings_PermissionsId",
table: "Channels");
migrationBuilder.DropTable(
name: "PermissionSettings");
migrationBuilder.DropColumn(
name: "PermissionTags",
table: "Users");
migrationBuilder.DropColumn(
name: "PermissionTags",
table: "Accounts");
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),
MaxAttachmentBytes = table.Column<decimal>(type: "numeric(20,0)", nullable: true),
MaxTextChars = table.Column<long>(type: "bigint", nullable: true),
LinksAllowed = table.Column<bool>(type: "boolean", nullable: true),
ReactionsPossible = table.Column<bool>(type: "boolean", nullable: true),
LewdnessFilterLevel = table.Column<int>(type: "integer", nullable: true),
MeannessFilterLevel = table.Column<int>(type: "integer", 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),
InternalName = table.Column<string>(type: "text", nullable: true),
InternalTag = table.Column<int>(type: "integer", nullable: true),
Inheritable = table.Column<bool>(type: "boolean", nullable: false)
},
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_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");
}
/// <inheritdoc />
protected override void Down(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_Accounts_FeaturePermissionId",
table: "Accounts");
migrationBuilder.DropColumn(
name: "FeaturePermissionId",
table: "Users");
migrationBuilder.DropColumn(
name: "FeaturePermissionId",
table: "Channels");
migrationBuilder.DropColumn(
name: "FeaturePermissionId",
table: "Accounts");
migrationBuilder.AddColumn<int[]>(
name: "PermissionTags",
table: "Users",
type: "integer[]",
nullable: true);
migrationBuilder.AddColumn<int[]>(
name: "PermissionTags",
table: "Accounts",
type: "integer[]",
nullable: true);
migrationBuilder.CreateTable(
name: "PermissionSettings",
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_PermissionSettings", x => x.Id);
});
migrationBuilder.AddForeignKey(
name: "FK_Channels_PermissionSettings_PermissionsId",
table: "Channels",
column: "PermissionsId",
principalTable: "PermissionSettings",
principalColumn: "Id");
}
}
}

View File

@ -0,0 +1,349 @@
// <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("20231203193139_channeltype")]
partial class channeltype
{
/// <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<Guid?>("FeaturePermissionId")
.HasColumnType("uuid");
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("FeaturePermissionId");
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<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")
.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<bool?>("ReactionsPossible")
.HasColumnType("boolean");
b.HasKey("Id");
b.ToTable("ChannelPermissions");
});
modelBuilder.Entity("vassago.Models.FeaturePermission", b =>
{
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 =>
{
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.Property<Guid?>("FeaturePermissionId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("FeaturePermissionId");
b.ToTable("Users");
});
modelBuilder.Entity("vassago.Models.Account", b =>
{
b.HasOne("vassago.Models.FeaturePermission", null)
.WithMany("RestrictedToAccounts")
.HasForeignKey("FeaturePermissionId");
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.FeaturePermission", null)
.WithMany("RestrictedToChannels")
.HasForeignKey("FeaturePermissionId");
b.HasOne("vassago.Models.Channel", "ParentChannel")
.WithMany("SubChannels")
.HasForeignKey("ParentChannelId");
b.HasOne("vassago.Models.ChannelPermissions", "Permissions")
.WithMany()
.HasForeignKey("PermissionsId");
b.Navigation("ParentChannel");
b.Navigation("Permissions");
});
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.User", b =>
{
b.HasOne("vassago.Models.FeaturePermission", null)
.WithMany("RestrictedToUsers")
.HasForeignKey("FeaturePermissionId");
});
modelBuilder.Entity("vassago.Models.Channel", b =>
{
b.Navigation("Messages");
b.Navigation("SubChannels");
b.Navigation("Users");
});
modelBuilder.Entity("vassago.Models.FeaturePermission", b =>
{
b.Navigation("RestrictedToAccounts");
b.Navigation("RestrictedToChannels");
b.Navigation("RestrictedToUsers");
});
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,40 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace vassago.Migrations
{
/// <inheritdoc />
public partial class channeltype : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "IsDM",
table: "Channels");
migrationBuilder.AddColumn<int>(
name: "ChannelType",
table: "Channels",
type: "integer",
nullable: false,
defaultValue: 0);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "ChannelType",
table: "Channels");
migrationBuilder.AddColumn<bool>(
name: "IsDM",
table: "Channels",
type: "boolean",
nullable: false,
defaultValue: false);
}
}
}

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

View File

@ -0,0 +1,268 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using vassago.Models;
#nullable disable
namespace vassago.Migrations
{
[DbContext(typeof(ChattingContext))]
partial class ChattingContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(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
}
}
}

33
Models/Account.cs Normal file
View File

@ -0,0 +1,33 @@
namespace vassago.Models;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Reflection;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
public class Account
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid Id { get; set; }
public string ExternalId { get; set; }
public string Username { get; set; }
private string _displayName = null;
public string DisplayName
{
get
{
return _displayName ?? Username;
}
set
{
_displayName = value;
}
}
public bool IsBot { get; set; } //webhook counts
public Channel SeenInChannel { get; set; }
public string Protocol { get; set; }
[JsonIgnore]
public User IsUser {get; set;}
}

18
Models/Attachment.cs Normal file
View File

@ -0,0 +1,18 @@
namespace vassago.Models;
using System;
using System.ComponentModel.DataAnnotations.Schema;
public class Attachment
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid Id { get; set; }
public ulong? ExternalId { get; set; }
public Uri Source { get; set; }
public byte[] Content { get; set; }
public string Filename { get; set; }
public Message Message { get; set; }
public string ContentType { get; internal set; }
public string Description { get; internal set; }
public int Size { get; internal set; }
}

114
Models/Channel.cs Normal file
View File

@ -0,0 +1,114 @@
namespace vassago.Models;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json;
using static vassago.Models.Enumerations;
public class Channel
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid Id { get; set; }
public string ExternalId { get; set; }
public string DisplayName { get; set; }
[DeleteBehavior(DeleteBehavior.Cascade)]
public List<Channel> SubChannels { get; set; }
[JsonIgnore]
public Channel ParentChannel { get; set; }
public string Protocol { get; set; }
[DeleteBehavior(DeleteBehavior.Cascade)]
public List<Message> Messages { get; set; }
[DeleteBehavior(DeleteBehavior.Cascade)]
public List<Account> Users { get; set; }
public ChannelType ChannelType {get; set; }
//Permissions
public ulong? MaxAttachmentBytes { get; set; }
public uint? MaxTextChars { get; set; }
public bool? LinksAllowed { get; set; }
public bool? ReactionsPossible { get; set; }
public Enumerations.LewdnessFilterLevel? LewdnessFilterLevel { get; set; }
public Enumerations.MeannessFilterLevel? MeannessFilterLevel { get; set; }
[NonSerialized]
public Func<string, string, Task> SendFile;
[NonSerialized]
public Func<string, Task> SendMessage;
public DefinitePermissionSettings EffectivePermissions
{
get
{
var path = new Stack<Channel>(); //omg i actually get to use a data structure from university
var walker = this;
path.Push(this);
while(walker.ParentChannel != null)
{
walker = walker.ParentChannel;
path.Push(walker);
}
DefinitePermissionSettings toReturn = new DefinitePermissionSettings();
while(path.Count > 0)
{
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;
}
return toReturn;
}
}
public string LineageSummary
{
get
{
if(this.ParentChannel != null)
{
return this.ParentChannel.LineageSummary + '/' + this.DisplayName;
}
else
{
return this.Protocol;
}
}
}
///<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; }
}

22
Models/ChattingContext.cs Normal file
View File

@ -0,0 +1,22 @@
namespace vassago.Models;
using Microsoft.Extensions.Configuration;
using Microsoft.EntityFrameworkCore;
public class ChattingContext : DbContext
{
public DbSet<Attachment> Attachments { get; set; }
public DbSet<Channel> Channels { get; set; }
//public DbSet<Emoji> Emoji {get;set;}
public DbSet<Message> Messages { get; set; }
public DbSet<Account> Accounts { get; set; }
public DbSet<User> Users { get; set; }
public ChattingContext(DbContextOptions<ChattingContext> options) : base(options) { }
public ChattingContext() : base() { }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseNpgsql(Shared.DBConnectionString);
//.EnableSensitiveDataLogging(true); //"sensitive" is one thing. writing "did something" every time you think a thought is a different thing.
}
}

67
Models/Enums.cs Normal file
View File

@ -0,0 +1,67 @@
using System;
using System.ComponentModel;
using System.Reflection;
namespace vassago.Models;
public static class Enumerations
{
public enum LewdnessFilterLevel
{
[Description("this is a christian minecraft server 🙏")]
Strict,
[Description("G-Rated")]
G,
[Description("polite company")]
Moderate,
[Description(";) ;) ;)")]
Unrestricted
}
public enum MeannessFilterLevel
{
[Description("good vibes only")]
Strict,
[Description("a bit cheeky")]
Medium,
[Description("387.44m mi of printed circuits")]
Unrestricted
}
public enum ChannelType
{
[Description("Normal")]
Normal,
[Description("DM")]
DM,
[Description("protocol psuedo-channel")]
Protocol,
[Description("organizational psuedo-channel")]
OU
}
public static string GetDescription<T>(this T enumerationValue)
where T : struct
{
Type type = enumerationValue.GetType();
if (!type.IsEnum)
{
throw new ArgumentException("EnumerationValue must be of Enum type", nameof(enumerationValue));
}
//Tries to find a DescriptionAttribute for a potential friendly name
//for the enum
MemberInfo[] memberInfo = type.GetMember(enumerationValue.ToString());
if (memberInfo != null && memberInfo.Length > 0)
{
object[] attrs = memberInfo[0].GetCustomAttributes(typeof(DescriptionAttribute), false);
if (attrs != null && attrs.Length > 0)
{
//Pull out the description value
return ((DescriptionAttribute)attrs[0]).Description;
}
}
//If we have no description attribute, just return the ToString of the enum
return enumerationValue.ToString();
}
}

33
Models/Message.cs Normal file
View File

@ -0,0 +1,33 @@
namespace vassago.Models;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Reflection;
using System.Threading.Tasks;
using Discord.WebSocket;
using Microsoft.EntityFrameworkCore;
public class Message
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid Id { get; set; }
public string Protocol { get; set; }
public string ExternalId { get; set; }
public string Content { get; set; }
public bool MentionsMe { get; set; }
public DateTimeOffset Timestamp { get; set; }
public bool ActedOn { get; set; }
[DeleteBehavior(DeleteBehavior.Cascade)]
public List<Attachment> Attachments { get; set; }
public Account Author { get; set; }
public Channel Channel { get; set; }
[NonSerialized]
public Func<string, Task> Reply;
[NonSerialized]
public Func<string, Task> React;
}

37
Models/User.cs Normal file
View File

@ -0,0 +1,37 @@
namespace vassago.Models;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Reflection;
using Microsoft.EntityFrameworkCore;
public class User
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid Id { get; set; }
[DeleteBehavior(DeleteBehavior.Cascade)]
public List<Account> Accounts { 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
{
get
{
if (Accounts?.Any() ?? false)
{
return Accounts.Select(a => a.DisplayName).Distinct()
.MaxBy(distinctName =>
Accounts.Select(a => a.DisplayName)
.Where(selectedName => selectedName == distinctName).Count()
);
}
else
{
return $"[accountless {Id}";
}
}
}
}

View File

@ -1,83 +1,56 @@
using System;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Net;
using System.Threading.Tasks;
using Discord;
using Discord.WebSocket;
using Microsoft.Extensions.Configuration;
namespace silverworker_discord
{
class Program
{
private DiscordSocketClient _client;
IConfigurationRoot config = new ConfigurationBuilder()
.AddJsonFile("appsettings.json", true, true)
.Build();
private ISocketMessageChannel botChatterChannel = null;
private ISocketMessageChannel announcementChannel = null;
public static void Main(string[] args)
=> new Program().MainAsync().GetAwaiter().GetResult();
private Task Log(LogMessage msg)
{
Console.WriteLine(msg.ToString());
return Task.CompletedTask;
}
public async Task MainAsync()
{
_client = new DiscordSocketClient();
_client.Log += Log;
await _client.LoginAsync(TokenType.Bot, config["token"]);
await _client.StartAsync();
_client.MessageReceived += MessageReceived;
_client.UserJoined += UserJoined;
_client.Ready += () => Task.Run(() =>{
Console.WriteLine("Bot is connected!");
botChatterChannel = _client.GetChannel(ulong.Parse(config["botChatterChannel"])) as ISocketMessageChannel;
announcementChannel = _client.GetChannel(ulong.Parse(config["announcementChannel"])) as ISocketMessageChannel;
});
// Block this task until the program is closed.
await Task.Delay(-1);
}
private async Task MessageReceived(SocketMessage messageParam)
{
var message = messageParam as SocketUserMessage;
if (message == null) return;
if (message.Author.Id == _client.CurrentUser.Id) return;
Console.WriteLine($"{message.Channel}, {message.Content}, {message.Id}");
if (message.Channel.Id == botChatterChannel.Id)
{
if(message.Attachments?.Count > 0)
{
Console.WriteLine(message.Attachments.Count);
foreach (var att in message.Attachments)
{
Console.WriteLine(att.Url);
await WebRequest.Create("http://192.168.1.151:3001/shortcuts?display_url=" + att.Url).GetResponseAsync();
}
}
}
}
private Task UserJoined(SocketGuildUser arg)
{
Console.WriteLine($"user joined: {arg.Nickname}. Guid: {arg.Guild.Id}. Channel: {arg.Guild.DefaultChannel}");
var abbreviatedNickname = arg.Nickname;
if(arg.Nickname.Length > 3){
abbreviatedNickname = arg.Nickname.Substring(0, arg.Nickname.Length / 3);
}
Console.WriteLine($"imma call him {abbreviatedNickname}");
return arg.Guild.DefaultChannel.SendMessageAsync($"oh hey {abbreviatedNickname}- IPLAYTHESEALOFORICHALCOS <:ORICHALCOS:852749196633309194>");
}
}
}
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Mvc.NewtonsoftJson;
using vassago.Models;
#pragma warning disable CA2254
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllersWithViews();
builder.Services.AddSingleton<IHostedService, vassago.ConsoleService>();
builder.Services.AddDbContext<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();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "api");
});
app.UseExceptionHandler();
app.UseStatusCodePages();
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.Run();

View File

@ -0,0 +1,37 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:42619",
"sslPort": 44354
}
},
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:5093",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:7206;http://localhost:5093",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@ -0,0 +1,404 @@
//https://discord.com/oauth2/authorize?client_id=913003037348491264&permissions=274877942784&scope=bot%20messages.read
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Discord;
using Discord.WebSocket;
using vassago.Models;
using vassago.Behavior;
using Discord.Rest;
using Microsoft.EntityFrameworkCore;
using System.Threading;
using System.Reactive.Linq;
namespace vassago.ProtocolInterfaces.DiscordInterface;
public class DiscordInterface
{
internal const string PROTOCOL = "discord";
internal DiscordSocketClient client;
private bool eventsSignedUp = false;
private static readonly SemaphoreSlim discordChannelSetup = new(1, 1);
private Channel protocolAsChannel;
public async Task Init(string token)
{
await SetupDiscordChannel();
client = new DiscordSocketClient(new DiscordSocketConfig() { GatewayIntents = GatewayIntents.All });
client.Log += (msg) =>
{
Console.WriteLine(msg.ToString());
return Task.CompletedTask;
};
client.Connected += () => Task.Run(SelfConnected);
client.Ready += () => Task.Run(ClientReady);
await client.LoginAsync(TokenType.Bot, token);
await client.StartAsync();
}
private async Task SetupDiscordChannel()
{
await discordChannelSetup.WaitAsync();
try
{
protocolAsChannel = Rememberer.SearchChannel(c => c.ParentChannel == null && c.Protocol == PROTOCOL);
if (protocolAsChannel == null)
{
protocolAsChannel = new Channel()
{
DisplayName = "discord (itself)",
MeannessFilterLevel = Enumerations.MeannessFilterLevel.Strict,
LewdnessFilterLevel = Enumerations.LewdnessFilterLevel.Moderate,
MaxTextChars = 2000,
MaxAttachmentBytes = 25 * 1024 * 1024, //allegedly it's 25, but I worry it's not actually.
LinksAllowed = true,
ReactionsPossible = true,
ExternalId = null,
Protocol = PROTOCOL,
SubChannels = []
};
}
else
{
Console.WriteLine($"discord, channel with id {protocolAsChannel.Id}, already exists");
}
protocolAsChannel.DisplayName = "discord (itself)";
protocolAsChannel.SendMessage = (t) => { throw new InvalidOperationException($"protocol isn't a real channel, cannot accept text"); };
protocolAsChannel.SendFile = (f, t) => { throw new InvalidOperationException($"protocol isn't a real channel, cannot send file"); };
protocolAsChannel = Rememberer.RememberChannel(protocolAsChannel);
Console.WriteLine($"protocol as channel addeed; {protocolAsChannel}");
}
finally
{
discordChannelSetup.Release();
}
}
private async Task ClientReady()
{
if (!eventsSignedUp)
{
eventsSignedUp = true;
Console.WriteLine($"Bot is connected ({client.CurrentUser.Username}; {client.CurrentUser.Mention})! going to sign up for message received and user joined in client ready");
client.MessageReceived += MessageReceived;
// _client.MessageUpdated +=
client.UserJoined += UserJoined;
client.SlashCommandExecuted += SlashCommandHandler;
//client.ChannelCreated +=
// _client.ChannelDestroyed +=
// _client.ChannelUpdated +=
// _client.GuildMemberUpdated +=
// _client.UserBanned +=
// _client.UserLeft +=
// _client.ThreadCreated +=
// _client.ThreadUpdated +=
// _client.ThreadDeleted +=
// _client.JoinedGuild +=
// _client.GuildUpdated +=
// _client.LeftGuild +=
await SlashCommandsHelper.Register(client);
}
else
{
Console.WriteLine("bot appears to be RE connected, so I'm not going to sign up twice");
}
}
private async Task SelfConnected()
{
await discordChannelSetup.WaitAsync();
try
{
var selfAccount = UpsertAccount(client.CurrentUser, protocolAsChannel);
selfAccount.DisplayName = client.CurrentUser.Username;
Behaver.Instance.MarkSelf(selfAccount);
}
finally
{
discordChannelSetup.Release();
}
}
private async Task MessageReceived(SocketMessage messageParam)
{
if (messageParam is not SocketUserMessage)
{
Console.WriteLine($"{messageParam.Content}, but not a user message");
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}");
var m = UpsertMessage(suMessage);
if (suMessage.MentionedUsers?.FirstOrDefault(muid => muid.Id == client.CurrentUser.Id) != null)
{
var mentionOfMe = "<@" + client.CurrentUser.Id + ">";
m.MentionsMe = true;
}
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.
}
private Task UserJoined(SocketGuildUser arg)
{
var guild = UpsertChannel(arg.Guild);
var defaultChannel = UpsertChannel(arg.Guild.DefaultChannel);
defaultChannel.ParentChannel = guild;
var u = UpsertAccount(arg, guild);
u.DisplayName = arg.DisplayName;
return null;
}
internal static async Task SlashCommandHandler(SocketSlashCommand command)
{
switch (command.CommandName)
{
case "freedomunits":
try
{
var amt = Convert.ToDecimal((double)(command.Data.Options.First(o => o.Name == "amount").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 conversionResult = Conversion.Converter.Convert(amt, src, dest);
await command.RespondAsync($"> {amt} {src} -> {dest}\n{conversionResult}");
}
catch (Exception e)
{
await command.RespondAsync($"error: {e.Message}. aaadam!");
}
break;
default:
await command.RespondAsync($"\\*smiles and nods*\n");
await command.Channel.SendFileAsync($"assets/loud sweating.gif");
Console.Error.WriteLine($"can't understand command name: {command.CommandName}");
break;
}
}
internal static vassago.Models.Attachment UpsertAttachment(IAttachment dAttachment)
{
var a = Rememberer.SearchAttachment(ai => ai.ExternalId == dAttachment.Id)
?? new vassago.Models.Attachment();
a.ContentType = dAttachment.ContentType;
a.Description = dAttachment.Description;
a.Filename = dAttachment.Filename;
a.Size = dAttachment.Size;
a.Source = new Uri(dAttachment.Url);
Rememberer.RememberAttachment(a);
return a;
}
internal Message UpsertMessage(IUserMessage dMessage)
{
var m = Rememberer.SearchMessage(mi => mi.ExternalId == dMessage.Id.ToString() && mi.Protocol == PROTOCOL)
?? new()
{
Protocol = PROTOCOL
};
if (dMessage.Attachments?.Count > 0)
{
m.Attachments = [];
foreach (var da in dMessage.Attachments)
{
m.Attachments.Add(UpsertAttachment(da));
}
}
m.Content = dMessage.Content;
m.ExternalId = dMessage.Id.ToString();
m.Timestamp = dMessage.EditedTimestamp ?? dMessage.CreatedAt;
m.Channel = UpsertChannel(dMessage.Channel);
m.Author = UpsertAccount(dMessage.Author, m.Channel);
Console.WriteLine($"received message; author: {m.Author.DisplayName}, {m.Author.Id}");
if (dMessage.Channel is IGuildChannel)
{
m.Author.DisplayName = (dMessage.Author as IGuildUser).DisplayName;//discord forgot how display names work.
}
m.MentionsMe = (dMessage.Author.Id != client.CurrentUser.Id
&& (dMessage.MentionedUserIds?.FirstOrDefault(muid => muid == client.CurrentUser.Id) > 0));
m.Reply = (t) => { return dMessage.ReplyAsync(t); };
m.React = (e) => { return AttemptReact(dMessage, e); };
Rememberer.RememberMessage(m);
return m;
}
internal Channel UpsertChannel(IMessageChannel channel)
{
Channel c = Rememberer.SearchChannel(ci => ci.ExternalId == channel.Id.ToString() && ci.Protocol == PROTOCOL);
if (c == null)
{
Console.WriteLine($"couldn't find channel under protocol {PROTOCOL} with externalId {channel.Id.ToString()}");
c = new Channel()
{
Users = []
};
}
c.ExternalId = channel.Id.ToString();
c.ChannelType = (channel is IPrivateChannel) ? vassago.Models.Enumerations.ChannelType.DM : vassago.Models.Enumerations.ChannelType.Normal;
c.Messages ??= [];
c.Protocol = PROTOCOL;
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);
}
else if (channel is IPrivateChannel)
{
c.ParentChannel = protocolAsChannel;
Console.WriteLine("i'm a private channel so I'm setting my parent channel to the protocol as channel");
}
else
{
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.WriteLine($"upsertion of channel {c.DisplayName}, it's type {c.ChannelType}");
switch (c.ChannelType)
{
case vassago.Models.Enumerations.ChannelType.DM:
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;
}
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.SendMessage = (t) => { return channel.SendMessageAsync(t); };
c.SendFile = (f, t) => { return channel.SendFileAsync(f, t); };
c = Rememberer.RememberChannel(c);
var selfAccountInChannel = c.Users.FirstOrDefault(a => a.ExternalId == client.CurrentUser.Id.ToString());
if(selfAccountInChannel == null)
{
selfAccountInChannel = UpsertAccount(client.CurrentUser, c);
}
return c;
}
internal Channel UpsertChannel(IGuild channel)
{
Channel c = Rememberer.SearchChannel(ci => ci.ExternalId == channel.Id.ToString() && ci.Protocol == PROTOCOL);
if (c == null)
{
Console.WriteLine($"couldn't find channel under protocol {PROTOCOL} with externalId {channel.Id.ToString()}");
c = new Channel();
}
c.DisplayName = channel.Name;
c.ExternalId = channel.Id.ToString();
c.ChannelType = vassago.Models.Enumerations.ChannelType.OU;
c.Messages ??= [];
c.Protocol = protocolAsChannel.Protocol;
c.ParentChannel = protocolAsChannel;
c.SubChannels ??= [];
c.MaxAttachmentBytes = channel.MaxUploadLimit;
c.SendMessage = (t) => { throw new InvalidOperationException($"channel {channel.Name} is guild; cannot accept text"); };
c.SendFile = (f, t) => { throw new InvalidOperationException($"channel {channel.Name} is guild; send file"); };
return Rememberer.RememberChannel(c);
}
internal static Account UpsertAccount(IUser discordUser, Channel inChannel)
{
var acc = Rememberer.SearchAccount(ui => ui.ExternalId == discordUser.Id.ToString() && ui.SeenInChannel.Id == inChannel.Id);
Console.WriteLine($"upserting account, retrieved {acc?.Id}.");
if (acc != null)
{
Console.WriteLine($"acc's user: {acc.IsUser?.Id}");
}
acc ??= new Account() {
IsUser = Rememberer.SearchUser(u => u.Accounts.Any(a => a.ExternalId == discordUser.Id.ToString() && a.Protocol == PROTOCOL))
?? new User()
};
acc.Username = discordUser.Username;
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)
{
Console.WriteLine($"user has record of {acc.IsUser.Accounts?.Count ?? 0} accounts");
}
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;
}
private static Task AttemptReact(IUserMessage msg, string e)
{
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 = e;
if (Emoji.TryParse(preferredEmote, out Emoji emoji))
{
return msg.AddReactionAsync(emoji);
}
if (!Emote.TryParse(preferredEmote, out Emote emote))
{
if (preferredEmote == e)
Console.Error.WriteLine($"never heard of emote {e}");
return Task.CompletedTask;
}
return msg.AddReactionAsync(emote);
}
}

View File

@ -0,0 +1,121 @@
using System;
using System.Linq;
using System.Collections.Generic;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Discord.WebSocket;
using Discord;
using Discord.Net;
namespace vassago.ProtocolInterfaces.DiscordInterface
{
public static class SlashCommandsHelper
{
private static List<CommandSetup> slashCommands = new List<CommandSetup>()
{
new CommandSetup(){
Id = "freedomunits",
UpdatedAt = new DateTime(2023, 5, 21, 13, 3, 0),
guild = 825293851110801428, //TODO: demagic this magic number
register = register_FreedomUnits
}
};
public static async Task Register(DiscordSocketClient client)
{
return;
var commandsInContext = await client.GetGlobalApplicationCommandsAsync();
await Register(client, commandsInContext, null);
foreach (var guild in client.Guilds)
{
try
{
await Register(client, await guild.GetApplicationCommandsAsync(), guild);
}
catch (HttpException ex)
{
Console.Error.WriteLine($"error registering slash commands for guild {guild.Name} (id {guild.Id}) - {ex.Message}");
}
}
}
private static async Task Register(DiscordSocketClient client, IEnumerable<SocketApplicationCommand> commandsInContext, SocketGuild guild)
{
foreach (var existingCommand in commandsInContext)
{
var myVersion = slashCommands.FirstOrDefault(c => c.Id == existingCommand.Name && c.guild == guild?.Id);
if (myVersion == null)
{
Console.WriteLine($"deleting command {existingCommand.Name} - (created at {existingCommand.CreatedAt}, it's in guild {existingCommand.Guild?.Id} while I'm in {guild?.Id})");
await existingCommand.DeleteAsync();
}
else
{
Console.WriteLine(existingCommand.CreatedAt);
if (myVersion.UpdatedAt > existingCommand.CreatedAt)
{
Console.WriteLine($"overwriting command {existingCommand.Name}");
await myVersion.register(false, client, guild);
}
myVersion.alreadyRegistered = true;
}
}
foreach (var remaining in slashCommands.Where(sc => sc.alreadyRegistered == false && sc.guild == guild?.Id))
{
Console.WriteLine($"creating new command {remaining.Id} ({(remaining.guild == null ? "global" : $"for guild {remaining.guild}")})");
await remaining.register(true, client, guild);
}
}
private static async Task register_FreedomUnits(bool isNew, DiscordSocketClient client, SocketGuild guild)
{
var builtCommand = new SlashCommandBuilder()
.WithName("freedomunits")
.WithDescription("convert between misc units (currency: iso 4217 code)")
.AddOption("amount", ApplicationCommandOptionType.Number, "source amount", isRequired: true)
.AddOption(new SlashCommandOptionBuilder()
.WithName("src-unit")
.WithDescription("unit converting FROM")
.WithRequired(true)
.WithType(ApplicationCommandOptionType.String))
.AddOption(new SlashCommandOptionBuilder()
.WithName("dest-unit")
.WithDescription("unit converting TO")
.WithRequired(true)
.WithType(ApplicationCommandOptionType.String))
.Build();
try
{
if (guild != null)
{
if (isNew)
await guild.CreateApplicationCommandAsync(builtCommand);
else
await guild.BulkOverwriteApplicationCommandAsync(new ApplicationCommandProperties[] { builtCommand });
}
else
{
if (isNew)
await client.CreateGlobalApplicationCommandAsync(builtCommand);
else
await client.BulkOverwriteGlobalApplicationCommandsAsync(new ApplicationCommandProperties[] { builtCommand });
}
}
catch (HttpException exception)
{
var json = JsonConvert.SerializeObject(exception.Errors, Formatting.Indented);
Console.Error.WriteLine(json);
}
}
private class CommandSetup
{
public string Id { get; set; }
//the date/time you updated yours IN UTC.
public DateTimeOffset UpdatedAt { get; set; }
public Registration register { get; set; }
public ulong? guild { get; set; }
public bool alreadyRegistered {get;set; } = false;
public delegate Task Registration(bool isNew, DiscordSocketClient client, SocketGuild guild);
}
}
}

View File

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

View File

@ -0,0 +1,9 @@
namespace vassago.TwitchInterface;
public class TwitchConfig
{
public string username {get; set;}
public string clientId {get; set;}
public string secret {get; set;}
public string oauth {get; set;}
}

View File

@ -0,0 +1,290 @@
using System.Security.Cryptography.X509Certificates;
using System.Text.RegularExpressions;
using RestSharp;
using TwitchLib.Api;
using TwitchLib.Api.Helix.Models.Users.GetUsers;
using TwitchLib.Client;
using TwitchLib.Client.Events;
using TwitchLib.Client.Models;
using TwitchLib.Communication.Clients;
using TwitchLib.Communication.Models;
using vassago.Behavior;
using vassago.Models;
namespace vassago.TwitchInterface;
public class TwitchInterface
{
internal const string PROTOCOL = "twitch";
private bool eventsSignedUp = false;
private ChattingContext _db;
private static SemaphoreSlim twitchChannelSetup = new SemaphoreSlim(1, 1);
private Channel protocolAsChannel;
TwitchClient client;
TwitchAPI api;
public TwitchInterface()
{
_db = new ChattingContext();
}
private async Task SetupTwitchChannel()
{
await twitchChannelSetup.WaitAsync();
try
{
protocolAsChannel = _db.Channels.FirstOrDefault(c => c.ParentChannel == null && c.Protocol == PROTOCOL);
if (protocolAsChannel == null)
{
protocolAsChannel = new Channel()
{
DisplayName = "twitch (itself)",
MeannessFilterLevel = Enumerations.MeannessFilterLevel.Medium,
LewdnessFilterLevel = Enumerations.LewdnessFilterLevel.G,
MaxTextChars = 500,
MaxAttachmentBytes = 0,
LinksAllowed = false,
ReactionsPossible = false,
ExternalId = null,
Protocol = PROTOCOL,
SubChannels = new List<Channel>()
};
protocolAsChannel.SendMessage = (t) => { throw new InvalidOperationException($"twitch itself cannot accept text"); };
protocolAsChannel.SendFile = (f, t) => { throw new InvalidOperationException($"twitch itself cannot send file"); };
_db.Channels.Add(protocolAsChannel);
_db.SaveChanges();
}
}
finally
{
twitchChannelSetup.Release();
}
}
///<param name="oauth">https://www.twitchapps.com/tmi/</param>
public async Task Init(TwitchConfig tc)
{
await SetupTwitchChannel();
WebSocketClient customClient = new WebSocketClient(new ClientOptions
{
MessagesAllowedInPeriod = 750,
ThrottlingPeriod = TimeSpan.FromSeconds(30)
}
);
client = new TwitchClient(customClient);
client.Initialize(new ConnectionCredentials(tc.username, tc.oauth, capabilities: new Capabilities()));
client.OnLog += Client_OnLog;
client.OnJoinedChannel += Client_OnJoinedChannel;
client.OnMessageReceived += Client_OnMessageReceivedAsync;
client.OnWhisperReceived += Client_OnWhisperReceivedAsync;
client.OnConnected += Client_OnConnected;
Console.WriteLine("twitch client 1 connecting...");
client.Connect();
Console.WriteLine("twitch client 1 connected");
// Console.WriteLine("twitch API client connecting...");
// api = new TwitchAPI();
// Console.WriteLine("can I just use the same creds as the other client?");
// api.Settings.ClientId = tc.username;
// api.Settings.AccessToken = tc.oauth;
// try{
// var neckbreads = await api.Helix.Moderation.GetModeratorsAsync("silvermeddlists");
// Console.WriteLine($"{neckbreads?.Data?.Count()} shabby beards that need to be given up on");
// }
// catch(Exception e){
// Console.Error.WriteLine(e);
// }
// Console.WriteLine("k.");
}
private async void Client_OnWhisperReceivedAsync(object sender, OnWhisperReceivedArgs e)
{
Console.WriteLine($"whisper#{e.WhisperMessage.Username}[{DateTime.Now}][{e.WhisperMessage.DisplayName} [id={e.WhisperMessage.Username}]][msg id: {e.WhisperMessage.MessageId}] {e.WhisperMessage.Message}");
var old = _db.Messages.FirstOrDefault(m => m.ExternalId == e.WhisperMessage.MessageId && m.Protocol == PROTOCOL);
if (old != null)
{
Console.WriteLine($"[whisperreceived]: {e.WhisperMessage.MessageId}? already seent it. Internal id: {old.Id}");
return;
}
var m = UpsertMessage(e.WhisperMessage);
m.Channel.ChannelType = vassago.Models.Enumerations.ChannelType.DM;
m.MentionsMe = Regex.IsMatch(e.WhisperMessage.Message?.ToLower(), $"\\b@{e.WhisperMessage.BotUsername.ToLower()}\\b");
await _db.SaveChangesAsync();
await Behaver.Instance.ActOn(m);
await _db.SaveChangesAsync();
}
private async void Client_OnMessageReceivedAsync(object sender, OnMessageReceivedArgs e)
{
Console.WriteLine($"#{e.ChatMessage.Channel}[{DateTime.Now}][{e.ChatMessage.DisplayName} [id={e.ChatMessage.Username}]][msg id: {e.ChatMessage.Id}] {e.ChatMessage.Message}");
var old = _db.Messages.FirstOrDefault(m => m.ExternalId == e.ChatMessage.Id && m.Protocol == PROTOCOL);
if (old != null)
{
Console.WriteLine($"[messagereceived]: {e.ChatMessage.Id}? already seent it");
return;
}
Console.WriteLine($"[messagereceived]: {e.ChatMessage.Id}? new to me.");
var m = UpsertMessage(e.ChatMessage);
m.MentionsMe = Regex.IsMatch(e.ChatMessage.Message?.ToLower(), $"@{e.ChatMessage.BotUsername.ToLower()}\\b") ||
e.ChatMessage.ChatReply?.ParentUserLogin == e.ChatMessage.BotUsername;
await _db.SaveChangesAsync();
await Behaver.Instance.ActOn(m);
await _db.SaveChangesAsync();
}
private async void Client_OnConnected(object sender, OnConnectedArgs e)
{
Console.WriteLine($"twitch marking selfaccount as seeninchannel {protocolAsChannel.Id}");
var selfAccount = UpsertAccount(e.BotUsername, protocolAsChannel.Id);
Behaver.Instance.MarkSelf(selfAccount);
await _db.SaveChangesAsync();
Console.WriteLine($"Connected to {e.AutoJoinChannel}");
}
private void Client_OnJoinedChannel(object sender, OnJoinedChannelArgs e)
{
client.SendMessage(e.Channel, "beep boop");
}
private void Client_OnLog(object sender, OnLogArgs e)
{
Console.WriteLine($"{e.DateTime.ToString()}: {e.BotUsername} - {e.Data}");
}
private Account UpsertAccount(string username, Guid inChannel)
{
var seenInChannel = _db.Channels.FirstOrDefault(c => c.Id == inChannel);
var acc = _db.Accounts.FirstOrDefault(ui => ui.ExternalId == username && ui.SeenInChannel.Id == inChannel);
if (acc == null)
{
acc = new Account();
acc.SeenInChannel = seenInChannel;
_db.Accounts.Add(acc);
}
acc.Username = username;
acc.ExternalId = username;
//acc.IsBot =
acc.Protocol = PROTOCOL;
acc.IsUser = _db.Users.FirstOrDefault(u => u.Accounts.Any(a => a.ExternalId == acc.ExternalId && a.Protocol == acc.Protocol));
if (acc.IsUser == null)
{
acc.IsUser = new vassago.Models.User() { Accounts = new List<Account>() { acc } };
_db.Users.Add(acc.IsUser);
}
return acc;
}
private Channel UpsertChannel(string channelName)
{
Channel c = _db.Channels.FirstOrDefault(ci => ci.ExternalId == channelName && ci.Protocol == PROTOCOL);
if (c == null)
{
c = new Channel();
_db.Channels.Add(c);
}
c.DisplayName = channelName;
c.ExternalId = channelName;
c.ChannelType = vassago.Models.Enumerations.ChannelType.Normal;
c.Messages = c.Messages ?? new List<Message>();
c.Protocol = PROTOCOL;
c.ParentChannel = protocolAsChannel;
c.SubChannels = c.SubChannels ?? new List<Channel>();
c.SendMessage = (t) => { return Task.Run(() => { client.SendMessage(channelName, t); }); };
c.SendFile = (f, t) => { throw new InvalidOperationException($"twitch cannot send files"); };
return c;
}
private Channel UpsertDMChannel(string whisperWith)
{
Channel c = _db.Channels.FirstOrDefault(ci => ci.ExternalId == $"w_{whisperWith}" && ci.Protocol == PROTOCOL);
if (c == null)
{
c = new Channel();
_db.Channels.Add(c);
}
c.DisplayName = $"Whisper: {whisperWith}";
c.ExternalId = $"w_{whisperWith}";
c.ChannelType = vassago.Models.Enumerations.ChannelType.DM;
c.Messages = c.Messages ?? new List<Message>();
c.Protocol = PROTOCOL;
c.ParentChannel = protocolAsChannel;
c.SubChannels = c.SubChannels ?? new List<Channel>();
c.SendMessage = (t) => { return Task.Run(() => {
try
{
client.SendWhisper(whisperWith, t);
}
catch(Exception e)
{
Console.Error.WriteLine(e);
}
});
};
c.SendFile = (f, t) => { throw new InvalidOperationException($"twitch cannot send files"); };
return c;
}
private Message UpsertMessage(ChatMessage chatMessage)
{
var m = _db.Messages.FirstOrDefault(mi => mi.ExternalId == chatMessage.Id);
if (m == null)
{
m = new Message();
m.Protocol = PROTOCOL;
_db.Messages.Add(m);
m.Timestamp = (DateTimeOffset)DateTime.SpecifyKind(DateTime.UtcNow, DateTimeKind.Utc);
}
m.Content = chatMessage.Message;
m.ExternalId = chatMessage.Id;
m.Channel = UpsertChannel(chatMessage.Channel);
m.Author = UpsertAccount(chatMessage.Username, m.Channel.Id);
m.Author.SeenInChannel = m.Channel;
m.Reply = (t) => { return Task.Run(() => { client.SendReply(chatMessage.Channel, chatMessage.Id, t); }); };
m.React = (e) => { throw new InvalidOperationException($"twitch cannot react"); };
return m;
}
private Message UpsertMessage(WhisperMessage whisperMessage)
{
var m = _db.Messages.FirstOrDefault(mi => mi.ExternalId == whisperMessage.MessageId);
if (m == null)
{
m = new Message();
m.Protocol = PROTOCOL;
_db.Messages.Add(m);
m.Timestamp = (DateTimeOffset)DateTime.SpecifyKind(DateTime.UtcNow, DateTimeKind.Utc);
}
m.Content = whisperMessage.Message;
m.ExternalId = whisperMessage.MessageId;
m.Channel = UpsertDMChannel(whisperMessage.Username);
m.Channel.ChannelType = vassago.Models.Enumerations.ChannelType.DM;
m.Author = UpsertAccount(whisperMessage.Username, m.Channel.Id);
m.Author.SeenInChannel = m.Channel;
m.Reply = (t) => { return Task.Run(() => { client.SendWhisper(whisperMessage.Username, t); }); };
m.React = (e) => { throw new InvalidOperationException($"twitch cannot react"); };
return m;
}
public string AttemptJoin(string channelTarget)
{
client.JoinChannel(channelTarget);
return $"attempt join {channelTarget} - o7";
}
internal void AttemptLeave(string channelTarget)
{
client.SendMessage(channelTarget, "o7");
client.LeaveChannel(channelTarget);
}
}

View File

@ -1,7 +1,52 @@
# 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
# TODO
# auth link
listen to kafka, send the message back over it
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?
# 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.

110
Rememberer.cs Normal file
View File

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

13
Shared.cs Normal file
View File

@ -0,0 +1,13 @@
namespace vassago;
using System;
using System.Net.Http;
using vassago.Models;
public static class Shared
{
public static Random r = new Random();
public static string DBConnectionString { get; set; }
public static HttpClient HttpClient { get; internal set; } = new HttpClient();
}

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 async Task<IActionResult> Details(Guid id)
{
var allChannels = Rememberer.ChannelsOverview();
if(allChannels == null)
return Problem("Entity set '_db.Channels' is null.");
//"but adam", says the strawman, "why load *every* channel and walk your way up? surely there's a .Load command that works or something."
//eh. I checked. Not really. You could make an SQL view that recurses its way up, meh idk how. You could just eagerly load *every* related object...
//but that would take in all the messages.
//realistically I expect this will have less than 1MB of total "channels", and several GB of total messages per (text) channel.
var channel = allChannels.First(u => u.Id == id);
var walker = channel;
while(walker != null)
{
ViewData["breadcrumbs"] = $"<a href=\"{Url.ActionLink(action: "Details", controller: "Channels", values: new {id = walker.Id})}\">{walker.DisplayName}</a>/" +
ViewData["breadcrumbs"];
walker = walker.ParentChannel;
}
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("channelsTree", 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,192 @@
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('[');
sb.Append("{text: \"channels\", expanded:true, nodes: [");
var first = true;
var topLevelChannels = Rememberer.ChannelsOverview().Where(x => x.ParentChannel == null);
foreach (var topLevelChannel in topLevelChannels)
{
if (first)
{
first = false;
}
else
{
sb.Append(',');
}
serializeChannel(ref sb, ref allChannels, ref allAccounts, topLevelChannel);
}
sb.Append("]}");
if (allChannels.Any())
{
sb.Append(",{text: \"orphaned channels\", 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("]}");
}
var users = Rememberer.UsersOverview();// _db.Users.ToList();
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("]}");
}
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("]}");
}
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);
sb.Append("nodes: [");
sb.Append($"{{\"text\": \"owned accounts:\", \"expanded\":true, \"nodes\": [");
if (ownedAccounts != null)
{
foreach (var acc in ownedAccounts)
{
serializeAccount(ref sb, acc);
sb.Append(',');
}
}
sb.Append("]}]}");
}
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
return View(new ErrorPageViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
}

View File

@ -0,0 +1,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,90 @@
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 ChannelsController : ControllerBase
{
private readonly ILogger<ChannelsController> _logger;
public ChannelsController(ILogger<ChannelsController> logger)
{
_logger = logger;
}
[HttpGet("{id}")]
[Produces("application/json")]
public Channel Get(Guid id)
{
return Rememberer.ChannelDetail(id);
}
[HttpPatch]
[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);
}
[HttpDelete]
[Produces("application/json")]
public IActionResult Delete([FromBody] Channel channel)
{
var fromDb = Rememberer.ChannelDetail(channel.Id);
if (fromDb == null)
{
_logger.LogError($"attempt to delete channel {channel.Id}, not found");
return NotFound();
}
deleteChannel(fromDb);
return Ok();
}
private void deleteChannel(Channel channel)
{
if (channel.SubChannels?.Count > 0)
{
foreach (var childChannel in channel.SubChannels)
{
deleteChannel(childChannel);
}
}
if(channel.Users?.Count > 0)
{
foreach(var account in channel.Users)
{
deleteAccount(account);
}
}
Rememberer.ForgetChannel(channel);
}
private void deleteAccount(Account account)
{
var user = account.IsUser;
var usersOnlyAccount = user.Accounts?.Count == 1;
Rememberer.ForgetAccount(account);
if(usersOnlyAccount)
Rememberer.ForgetUser(user);
}
}

View File

@ -0,0 +1,40 @@
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 UsersController : ControllerBase
{
private readonly ILogger<ChannelsController> _logger;
public UsersController(ILogger<ChannelsController> logger)
{
_logger = logger;
}
[HttpPatch]
[Produces("application/json")]
public IActionResult Patch([FromBody] User user)
{
var fromDb = Rememberer.SearchUser(u => u.Id == user.Id);
if (fromDb == null)
{
_logger.LogError($"attempt to update user {user.Id}, not found");
return NotFound();
}
else
{
_logger.LogDebug($"patching {user.DisplayName} (id: {user.Id})");
}
//TODO: settable values: display name
//fromDb.DisplayName = user.DisplayName;
Rememberer.RememberUser(fromDb);
return Ok(fromDb);
}
}

View File

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

View File

@ -0,0 +1,68 @@
@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 scope="row">belongs to user</th>
<td>@Model.IsUser.DisplayName</td>
<td><button alt="to do" disabled>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">Permission Tags</th>
<td>
<div id="tagsTree"></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 getTagsTree() {
@{
var sb = new StringBuilder();
sb.Append("[{text: \"permission tags\", \"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;
}
$('#tagsTree').bstreeview({ data: getTagsTree() });
document.querySelectorAll("input[type=checkbox]").forEach(node => { node.onchange = () => { patchModel(jsonifyUser(), '/api/Users/') } });
</script>
}

View File

@ -0,0 +1,169 @@
@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>@(ThisChannel.ChannelType != null ? Enumerations.GetDescription(ThisChannel.ChannelType) : "?")</td>
</tr>
<tr>
<th scope="row">Lewdness Filter Level</th>
<td>
<select name="LewdnessFilterLevel" id="LewdnessFilterLevel" onchange="patchModel(jsonifyChannel(), '/api/Channels/')">
<!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(), '/api/Channels/')">
<!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>
<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(), '/api/Channels/');
}
}
function channelsTree() {
var tree = @Html.Raw(ViewData["channelsTree"]);
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() });
</script>
}

View File

@ -0,0 +1,42 @@
@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

@ -0,0 +1,16 @@
@{
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);
return tree;
}
$('#tree').bstreeview({ data: getTree() });
</script>
}

View File

@ -0,0 +1,25 @@
@model vassago.WebInterface.Models.ErrorPageViewModel
@{
ViewData["Title"] = "Error";
}
<h1 class="text-danger">Error.</h1>
<h2 class="text-danger">An error occurred while processing your request.</h2>
@if (Model.ShowRequestId)
{
<p>
<strong>Request ID:</strong> <code>@Model.RequestId</code>
</p>
}
<h3>Development Mode</h3>
<p>
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
</p>
<p>
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
It can result in displaying sensitive information from exceptions to end users.
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
and restarting the app.
</p>

View File

@ -0,0 +1,25 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - vassago</title>
<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="~/vassago.styles.css" asp-append-version="true" />
</head>
<body>
<div class="container">
<main role="main" class="pb-3">
@RenderBody()
</main>
</div>
<script src="~/lib/jquery/dist/jquery.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>
@await RenderSectionAsync("Scripts", required: false)
</body>
</html>

View File

@ -0,0 +1,48 @@
/* Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification
for details on configuring this project to bundle and minify static web assets. */
a.navbar-brand {
white-space: normal;
text-align: center;
word-break: break-all;
}
a {
color: #0077cc;
}
.btn-primary {
color: #fff;
background-color: #1b6ec2;
border-color: #1861ac;
}
.nav-pills .nav-link.active, .nav-pills .show > .nav-link {
color: #fff;
background-color: #1b6ec2;
border-color: #1861ac;
}
.border-top {
border-top: 1px solid #e5e5e5;
}
.border-bottom {
border-bottom: 1px solid #e5e5e5;
}
.box-shadow {
box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05);
}
button.accept-policy {
font-size: 1rem;
line-height: inherit;
}
.footer {
position: absolute;
bottom: 0;
width: 100%;
white-space: nowrap;
line-height: 60px;
}

View File

@ -0,0 +1,2 @@
<script src="~/lib/jquery-validation/dist/jquery.validate.min.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script>

View File

@ -0,0 +1,90 @@
@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(), @Html.Raw("'/api/Users/'"))" disabled alt"todo">update</button></td>
</tr>
<tr>
<th scope="row">Accounts</th>
<td>
<div id="accountsTree"></div>
</td>
</tr>
<tr>
<th scope="row">Permission Tags</th>
<td>
<div id="tagsTree"></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;
}
function getTagsTree() {
@{
sb = new StringBuilder();
sb.Append("[{text: \"permission tags\", \"expanded\":true, nodes: [");
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;
}
$('#accountsTree').bstreeview({ data: getAccountsTree() });
$('#tagsTree').bstreeview({ data: getTagsTree() });
document.querySelectorAll("input[type=checkbox]").forEach(node => { node.onchange = () => { patchModel(jsonifyUser(), '/api/Users/') } });
</script>
}

View File

@ -0,0 +1,38 @@
@model IEnumerable<User>
@{
ViewData["Title"] = "Users";
}
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.Id)
</th>
<th>
name*
</th>
<th>
number of associated accounts
</th>
</tr>
</thead>
<tbody>
@foreach (var item in Model) {
<tr>
<td>
@Html.DisplayFor(modelItem => item.Id)
</td>
<td>
@Html.DisplayFor(modelItem => item.DisplayName)
</td>
<td>
@Html.DisplayFor(modelItem => item.Accounts.Count)x
</td>
<td>
<a asp-action="Details" asp-route-id="@item.Id">Details</a>
</td>
</tr>
}
</tbody>
</table>

View File

@ -0,0 +1,3 @@
@using vassago
@using vassago.Models
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

View File

@ -0,0 +1,3 @@
@{
Layout = "_Layout";
}

View File

@ -1,5 +0,0 @@
{
"token": "59 chars",
"botChatterChannel": 0,
"announcementChannel": 0
}

17
appsettings.json Normal file
View File

@ -0,0 +1,17 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Microsoft.EntityFrameworkCore": "None"
}
},
"AllowedHosts": "*",
"DiscordTokens": [
],
"TwitchConfigs": [
],
"exchangePairsLocation": "assets/exchangepairs.json",
"DBConnectionString": "Host=azure.club;Database=db;Username=user;Password=password"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 293 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

307
assets/conversion.json Normal file
View File

@ -0,0 +1,307 @@
{
"units":[
{
"canonical": "℉",
"aliases": [
"degrees f",
"deg f",
"degf",
"fahrenheit",
"deg fahrenheit",
"degrees fahrenheit",
"°f",
"° f",
"℉",
"f"
]
},
{
"canonical":"°C",
"aliases": [
"degrees c",
"deg c",
"degc",
"celsius",
"deg celsiu",
"degrees celsiu",
"°c",
"° c",
"c"
]
},
{
"canonical":"m",
"aliases": [
"meter",
"metre"
]
},
{
"canonical":"mm",
"aliases": [
"millimeter",
"millimetre"
]
},
{
"canonical":"km",
"aliases": [
"kilometer",
"kilometre"
]
},
{
"canonical":"kg",
"aliases": [
"kilogram",
"kilo"
]
},
{
"canonical":"g",
"aliases": [
"gram"
]
},
{
"canonical":"lb",
"aliases": [
"pound"
]
},
{
"canonical":"oz",
"aliases": [
"ounce"
]
},
{
"canonical":"floz",
"aliases": [
"us fl oz",
"us fl ounce",
"us fluid ounce",
"us fluid oz"
]
},
{
"canonical":"mL",
"aliases": [
"ml",
"milliletre",
"millileter"
]
},
{
"canonical":"ft",
"aliases": [
"'",
"feet",
"foot"
]
},
{
"canonical":"in",
"aliases": [
"\"",
"inch"
]
},
{
"canonical":"yd",
"aliases": [
"yard"
]
},
{
"canonical":"hhd",
"aliases": [
"hogshead"
]
},
{
"canonical":"gal",
"aliases": [
"gallon"
]
},
{
"canonical":"tbsp",
"aliases": [
"tablespoon",
"table spoon"
]
},
{
"canonical":"tsp",
"aliases": [
"teaspoon",
"tea spoon"
]
},
{
"canonical":"qt",
"aliases": [
"quart"
]
},
{
"canonical":"ac",
"aliases": [
"acres"
]
},
{
"canonical":"AU",
"aliases": [
"au",
"astronomical unit"
]
},
{
"canonical":"Pa",
"aliases": [
"pascal",
"pa"
]
},
{
"canonical":"kPa",
"aliases": [
"kilopascal",
"kpa"
]
},
{
"canonical":"atm",
"aliases": [
"atmosphere"
]
},
{
"canonical":"m^2",
"aliases": [
"square meter",
"meter squared",
"meters squared",
"sq meter",
"sq m",
"sqm"
]
},
{
"canonical":"km^2",
"aliases": [
"square kilometer",
"kilometer squared",
"kilometers squared",
"sq kilometer",
"sq km",
"sqkm"
]
},
{
"canonical":"ly",
"aliases": [
"light year"
]
},
{
"canonical":"pc",
"aliases": [
"parsec"
]
},
{
"canonical":"mi",
"aliases": [
"mile"
]
},
{
"canonical":"blue whale length",
"aliases": [
"bwl",
"whales"
]
},
{
"canonical":"ångström",
"aliases": [
"angstrom",
"Å"
]
},
{
"canonical":"μm",
"aliases": [
"micrometers",
"micrometres",
"microns"
]
},
{
"canonical":"uncle jordan",
"aliases":[
"uj"
]
}
],
"linearPairs":[
{"item1":"kg", "item2":"g", "factor":1000},
{"item1":"lb", "item2":"oz", "factor":16},
{"item1":"kg", "item2":"lb", "factor":2.204623},
{"item1":"kg", "item2":"stone", "factor":0.157473},
{"item1":"km", "item2":"m", "factor":1000},
{"item1":"mi", "item2":"ft", "factor":5280},
{"item1":"m", "item2":"in", "factor":39.37008},
{"item1":"m", "item2":"cm", "factor":100},
{"item1":"cm", "item2":"mm", "factor":10},
{"item1":"m", "item2":"μm", "factor":1000000},
{"item1":"km", "item2":"mi", "factor":0.6213712},
{"item1":"ft", "item2":"in", "factor":12},
{"item1":"yd", "item2":"ft", "factor":3},
{"item1":"football field", "item2":"yd", "factor":100},
{"item1":"chain", "item2":"yd", "factor":22},
{"item1":"chain", "item2":"link", "factor":100},
{"item1":"furlong", "item2":"mi", "factor":8},
{"item1":"rod", "item2":"ft", "factor":16.5},
{"item1":"AU", "item2":"ly", "factor": 0.0000158125},
{"item1":"ly", "item2":"km", "factor": 946070000000},
{"item1":"pc", "item2":"AU", "factor":206266.3},
{"item1":"blue whale length", "item2": "m", "factor": 29.9},
{"item1":"m", "item2": "ångström", "factor": 10000000000},
{"item1":"smoot", "item2": "ft", "factor": 5.583333333333},
{"item1":"uncle jordan", "item2": "cm", "factor":192.405},
{"item1":"floz", "item2":"mL", "factor":29.57344},
{"item1":"L", "item2":"mL", "factor":1000},
{"item1":"L", "item2":"floz", "factor":33.81402},
{"item1":"hhd", "item2":"gal", "factor":54},
{"item1":"barrel", "item2":"kilderkin", "factor":2},
{"item1":"barrel", "item2":"firkin", "factor":4},
{"item1":"firkin", "item2":"gal", "factor":10.8},
{"item1":"pint", "item2":"floz", "factor":16},
{"item1":"cup", "item2":"floz", "factor":8},
{"item1":"gill", "item2":"floz", "factor":4},
{"item1":"tbsp", "item2":"tsp", "factor":3},
{"item1":"tbsp", "item2":"floz", "factor":0.5},
{"item1":"gal", "item2":"floz", "factor":128},
{"item1":"gal", "item2":"qt", "factor":4},
{"item1":"acre", "item2":"yd^2", "factor":4840},
{"item1":"yd^2", "item2":"m^2", "factor":0.836127},
{"item1":"mph", "item2":"knot", "factor":0.868976},
{"item1":"mph", "item2":"kph", "factor":1.609343550606653},
{"item1":"kPa", "item2":"Pa", "factor":1000},
{"item1":"Nm^2", "item2":"Pa", "factor":1},
{"item1":"Pa", "item2":"bar", "factor":100},
{"item1":"atm", "item2":"Pa", "factor":101325},
{"item1":"bar", "item2":"psi", "factor":14.5038}
]
}

BIN
assets/ekgblip.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

78
assets/jokes.txt Normal file
View File

@ -0,0 +1,78 @@
I got a joke for you: wealth trickles down.
Why did the celebrity egg start losing her friends? They called her a shell-out.
What's the difference between a hippo and a zippo? One is really heavy and the other is a little lighter.
The best time on a watch is 6:30, hands down. (ask your parents, young ones)
What killed the painter? He had too many strokes
Artists know how to draw the line, so you cant really peer pressure them.
Why did the hand cross the road? To get to the secondhand store.
Why do seagulls fly over the ocean? Because if they flew over the bay, we'd call them bagels.
Why don't oysters donate to charity? Because they're shellfish
I only know 25 letters of the alphabet. I don't know y.
What did the fish say when it ran into a wall? Dam.
What do you call a factory that makes okay products? A satisfactory.
What do you call a fly without wings? A walk.
Don't trust those trees. They seem kind of shady.
I don't trust stairs. They're up to something.
What did the teacher do with the students report on cheese? Grated it.
What do you call a man with no legs and arms in a pool? Bob.
I was going to tell a joke about hammers but ...I don't think I'll nail it
why did the can recycler quit his job? because it was so depressing.
They told me to stop impersonating a flamingo. I had to put my foot down.
I failed math so many times at school, I cant even count.
When I was a child, I threw a boomerang, but it didn't come back. I live in constant fear.
When life gives you melons, you might be dyslexic.
Dont you hate it when someone answers their own questions? I do.
It takes a lot of balls to golf the way I do.
The problem with kleptomaniacs is that they always take things literally.
Can you believe I got fired from the calendar factory? All I did was take a day off.
Most people are shocked when they find out how bad I am as an electrician.
I was addicted to the hokey pokey, but then I turned myself around.
A termite walks into the bar and asks; is the bartender here?
Just burned 2,000 calories. Thats the last time I leave brownies in the oven while I nap.
A recent study has found that women who carry a little extra weight live longer than the men who mention it.
I got a new pair of gloves today, but theyre both lefts, which on the one hand is great, but on the other, its just not right.
I can tell when people are being judgmental just by looking at them.
Are people born with photographic memories? or does it take time to develop (ask your parents, young ones)
I buy all my guns from a guy called T-Rex. Hes a small arms dealer.
A book fell on my head the other day. I only have my shelf to blame though.
If you dont pay your exorcist, do you get repossessed?
A ghost walked into a bar and ordered a shot of vodka. The bartender said, Sorry, we dont serve spirits here.
A blind man walked into a bar. and a table. and a chair.
How do you make holy water? You boil the hell out of it.
My teachers told me Id never amount to much because I procrastinate so much. I told them, “Just you wait!”
what's the best part about living in switzerland? well the flag is a big plus.
I asked my date to meet me at the gym today. She didn't show up. That's when I knew we weren't gonna work out.
The CEO of IKEA was elected Prime Minister in Sweden. He should have his cabinet together by the end of the weekend.
The problem with troubleshooting is that trouble shoots back.
Today a man knocked on my door and asked for a small donation towards the local swimming pool. I gave him a glass of water.
Apparently I snore so loudly that it scares everyone in the car I'm driving.
I think my neighbor is stalking me. she's been googling my name on her computer. I saw it through my telescope last night.
I wasn't originally going to get a brain transplant, but then I changed my mind.
I just found out I'm colorblind. The diagnosis came completely out of the purple.
My girlfriend told me she was leaving me because I keep pretending to be a Transformer. I said, "No, wait! I can change."
I started out with nothing, and I still have most of it.
eBay is so useless. I tried to look up lighters and all they had was 13,749 matches.
About a month before he died, my uncle had his back covered in lard. After that, he went down hill fast.
I was addicted to the hokey pokey... but thankfully, I turned myself around.
I'm glad I know sign language, it's pretty handy.
What did Cinderella say when she got to the ball? "ggggh!"
R.I.P boiled water. You will be mist.
To the mathematicians who thought of the idea of zero, thanks for nothing!
I hate people who use big words just to make themselves look perspicacious.
Two wrongs don't make a right, take your parents as an example.
People used to laugh at me when I would say "I want to be a comedian", but nobody's laughing now.
I threw a ball for my dog... It's a bit extravagant I know, but it was his birthday and he looks great in a dinner jacket.
I refused to believe my road worker father was stealing from his job, but when I got home, all the signs were there.
Did you hear about the kidnapping at school? It's okay. He woke up.
Pavlov walks into a bar. The phone rings, and he says, "Damn, I forgot to feed the dog." (ask your parents, young ones.)
Cleaning mirrors is a job I could really see myself doing.
What's the difference between a poorly dressed man on a bicycle and a nicely dressed man on a tricycle? Attire.
When does a pun become a dad joke? When the punchline becomes apparent.
There's 10 kinds of people in the world. Those who think in decimal, those who think in binary, and those who knew this joke would be in base 3.
There's 2 kinds of people in the world. Those who divide the entire human population into 2 arbitrary groups, and those who don't.
A horse walks into a bar. the bartender says "hey man, you're in here kind of a lot. do you ever think you might be an alcoholic?" the horse says "no" and promptly vanishes. (the joke is a reference the famous philosophical phrase "i think, therefore i am" but if i explained that before the rest of the joke that would be putting descartes before the horse)
Someone broke into my house and stole all my fruits. I'm peachless.
Did I tell you guys about that flat earther i got into an argument with? he got so mad he stormed off saying he'd walk to the edge of the earth to prove me wrong, but he'll come around eventually.
What do you call your friend who stands in a hole? Phil.
What happened when the bear swallowed a clock? He got ticks.
What do you call a wolf who gets lost? A where-wolf.

BIN
assets/loud sweating.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 KiB

36
devuitls.sh Executable file
View File

@ -0,0 +1,36 @@
#!/bin/bash
servicename="vassago"
pw_developmentdatabase="wnmhOttjA0wCiR9hVoG7jjrf90SxWvAV"
connnectionstr="Host=localhost;Database=${servicename}_dev;Username=${servicename};Password=${pw_developmentdatabase};IncludeErrorDetail=true;"
case "$1" in
"initial")
sudo -u postgres psql -c "create database ${servicename}_dev;"
sudo -u postgres psql -c "create user $servicename with encrypted password '$pw_developmentdatabase';"
sudo -u postgres psql -c "grant all privileges on database ${servicename}_dev to $servicename;"
sudo -u postgres psql -d "${servicename}_dev" -c "GRANT ALL ON SCHEMA public TO $servicename"
cp appsettings.sample.json appsettings.json
dotnet ef database update --connection "$connnectionstr"
;;
"add-migration")
dotnet ef migrations add "$2"
dotnet ef database update --connection "$connnectionstr"
;;
"dbupdate")
dotnet ef database update --connection "$connnectionstr"
;;
"db-fullreset")
sudo -u postgres psql -c "drop database ${servicename}_dev;"
sudo -u postgres psql -c "drop user $servicename"
$0 "initial"
;;
*)
echo "Unknown command '$1', try 'initial'"
;;
esac

80
externalProcess.cs Normal file
View File

@ -0,0 +1,80 @@
using System;
using System.Linq;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using Newtonsoft.Json;
namespace vassago
{
public class ExternalProcess
{
public static bool GoPlz(string commandPath, string commandArguments)
{
var process = readableProcess(commandPath, commandArguments);
var outputData = new StringBuilder();
process.OutputDataReceived += new DataReceivedEventHandler((s, e) =>
{
outputData.Append(e.Data);
});
var errorData = new StringBuilder();
process.ErrorDataReceived += new DataReceivedEventHandler((s, e) =>
{
errorData.Append(e.Data);
});
try
{
process.Start();
process.BeginErrorReadLine();
process.BeginOutputReadLine();
process.WaitForExit();
}
catch(Exception e)
{
var dumpDir = $"fail{DateTime.Now.ToFileTimeUtc()}";
var outputFilename = $"{dumpDir}/output0.log";
var errorFilename = $"{dumpDir}/error0.err";
if(!Directory.Exists(dumpDir))
{
Directory.CreateDirectory(dumpDir);
}
else
{
var i = 0;
foreach(var file in Directory.GetFiles(dumpDir))
{
var thisNummatch = Regex.Matches(Path.GetFileNameWithoutExtension(file), "[^\\d](\\d+)\\.err$").LastOrDefault().Value;
if(!string.IsNullOrWhiteSpace(thisNummatch))
{
var thisNumval = 0;
if(int.TryParse(thisNummatch, out thisNumval) && thisNumval > i)
{
i = thisNumval;
}
}
}
outputFilename = $"{dumpDir}/output{i}.log";
errorFilename = $"{dumpDir}/error{i}.err";
}
File.WriteAllText(outputFilename, outputData.ToString());
File.WriteAllText(errorFilename, JsonConvert.SerializeObject(e, Formatting.Indented));
return false;
}
return true;
}
private static Process readableProcess(string commandPath, string commandArguments)
{
var pi = new ProcessStartInfo(commandPath, commandArguments);
pi.UseShellExecute = false;
pi.CreateNoWindow = true;
pi.RedirectStandardError = true;
pi.RedirectStandardOutput = true;
var process = new Process();
process.StartInfo = pi;
return process;
}
}
}

View File

@ -1,21 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework>
<RootNamespace>silverworker_discord</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="discord.net" Version="2.4.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="5.0.0" />
</ItemGroup>
<ItemGroup>
<None Update="AppSettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

51
vassago.csproj Normal file
View File

@ -0,0 +1,51 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<NoWarn>$(NoWarn);CA2254</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="bootstrap" Version="5.3.3" />
<PackageReference Include="discord.net" Version="3.10.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="7.0.20" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Configuration" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="5.0.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="7.0.4" />
<PackageReference Include="QRCoder" Version="1.4.2" />
<PackageReference Include="RestSharp" Version="110.2.0" />
<PackageReference Include="Swashbuckle.AspNetCore.Swagger" Version="6.6.2" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="6.6.2" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="6.6.2" />
<PackageReference Include="TwitchLib" Version="3.5.3" />
<PackageReference Include="youtubedlsharp" Version="0.3.1" />
</ItemGroup>
<ItemGroup>
<None Update="assets/jokes.txt">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="assets/coding and algorithms.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="assets/ekgblip.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="assets/conversion.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="assets/loud sweating.gif">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="appsettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

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

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

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

File diff suppressed because one or more lines are too long

46
wwwroot/css/site.css Normal file
View File

@ -0,0 +1,46 @@
html {
font-size: 14px;
}
@media (min-width: 768px) {
html {
font-size: 16px;
}
}
.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
}
html {
position: relative;
min-height: 100%;
}
body {
margin-bottom: 60px;
}
.protocol-icon,.channel-type-icon{
display:inline-block;
width: 32px;
height: 32px;
background-size: 32px;
}
.discord .protocol-icon{
background-image: url("../imgs/discord_logo1600.png");
}
.twitch .protocol-icon{
background-image: url("../imgs/twitch.png");
}
.Normal .channel-type-icon{
background-color: black;
}
.DM .channel-type-icon{
background-color: black;
}
.Protocol .channel-type-icon{
background-color: black;
}
.OU .channel-type-icon{
background-color: black;
}

BIN
wwwroot/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
wwwroot/imgs/twitch.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

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

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

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