Compare commits

...

197 Commits
4.1.5 ... 4.3.4

Author SHA1 Message Date
Kwoth
7d5c4666b8 Fixed VoiceXP bug, closes #374. Upped version to 4.3.4, Updated changelog 2022-08-07 13:07:51 +02:00
Kwoth
8d3f2f186a Merge branch 'v4' of https://gitlab.com/kwoth/nadekobot into v4 2022-08-06 17:57:32 +02:00
Kwoth
9572b9dc43 Updated changelog.md 2022-08-06 17:56:53 +02:00
Kwoth
ca32086089 Updated changelog.md 2022-08-06 17:56:14 +02:00
Kwoth
57f839dbcd Added client id as a potential fix for VoiceXp 'bug'. The solution may be to use different redis instances for each bot, or to switch from botCache: from 'redis' to 'memory' in creds.yml 2022-08-06 17:47:24 +02:00
Kwoth
71d6eeb9dd Possibly fixed #375 trivia bug 2022-08-06 17:28:31 +02:00
Kwoth
8c51cf8537 Added .xpshopbuy and .xpshopuse convenience commands 2022-08-06 13:26:41 +02:00
Kwoth
b683026cf3 Owner should also be able to buy items from the xp shop 2022-08-01 21:14:28 +02:00
Kwoth
bca2bc5af1 Fixed awarded amount position on the xp card 2022-08-01 20:59:35 +02:00
Kwoth
b385a83bdd Updated position of the username and club in the .xp image 2022-08-01 17:46:41 +02:00
Kwoth
3bf0286c81 Feature limit should have IsPatronLimit enabled 2022-08-01 16:08:23 +02:00
Kwoth
98272f66e7 Made improvements to the XP card text visibility with other backgrounds 2022-08-01 12:01:01 +02:00
Kwoth
cf3788c6ea Added an optional preview url to the xp shop items 2022-08-01 10:53:59 +02:00
Kwoth
4b3fc53cb6 Updated changelog 2022-07-31 21:31:29 +02:00
Kwoth
4e17dca856 Merge branch 'v4' of https://gitlab.com/kwoth/nadekobot into v4 2022-07-31 21:26:46 +02:00
Kwoth
82d89148f3 Added betroll bettest, fixed a bug which caused betroll to have very low payout 2022-07-31 21:26:34 +02:00
Kwoth
cc4c09b4d7 Merge branch 'hokutochen-v4-patch-85503' into 'v4'
updated redis requirement for release

See merge request Kwoth/nadekobot!262
2022-07-30 10:37:29 +00:00
Kwoth
616f01f8b2 Make sure broken youtube-dl didn't cache invalid data, closes #373 2022-07-30 10:29:33 +02:00
Hokuto Chen
56f89a02bc updated redis requirement for release 2022-07-30 07:32:30 +00:00
Kwoth
48ce988d20 Fixed a mistake in CHANGELOG.md 2022-07-29 19:53:45 +02:00
Kwoth
119b1cdec2 Changelog fixed 2022-07-29 18:38:54 +02:00
Kwoth
43fa5a22f5 Updated changelog.md, Upped version to 4.3.2 2022-07-28 14:33:32 +02:00
Kwoth
3c715a29ca .bank withdraw <expression> will now correctly use bank amount for calculations. Fixed .br giving double win amounts 2022-07-28 12:41:38 +02:00
Kwoth
31e1cbb19f Fixed an unreported bug with postgresql reminder loop, ref #370 2022-07-28 09:21:19 +02:00
Kwoth
8e464e9f09 Fixed mysql and postgresql reactionrole migration, closes #370 2022-07-28 09:10:21 +02:00
Kwoth
a190a3d933 Fixed Reaction Roles not working properly with animated emojis, closes #369 2022-07-28 08:30:28 +02:00
Kwoth
bedba98130 Fixed medusa Reply*LocalizedAsync not working with placeholders 2022-07-28 07:39:46 +02:00
Kwoth
d69f8435f6 Fixed .slot alignment, closes #372 2022-07-28 05:47:40 +02:00
Kwoth
8440b34338 Forgot to update the version to 4.3.1 2022-07-27 11:21:08 +02:00
Kwoth
a2715740c1 Merge branch 'v4' of https://gitlab.com/kwoth/nadekobot into v4 2022-07-27 11:20:06 +02:00
Kwoth
36b7fd2352 Possible fix for .reroa working even for non-patrons 2022-07-27 11:18:35 +02:00
Kwoth
6563cb507a Merge branch 'feature/v4-creds.yml-location' into 'v4'
NEW: add NadekoBot__creds env to specify alternative creds.yml

See merge request Kwoth/nadekobot!257
2022-07-27 05:00:28 +00:00
Veovis
cc6128997e NEW: add NadekoBot__creds env to specify alternative creds.yml 2022-07-27 05:00:27 +00:00
Kwoth
e942da4470 Merge branch 'hokutochen-v4-patch-80656' into 'v4'
updated notepad ++ descrption

See merge request Kwoth/nadekobot!261
2022-07-27 04:59:33 +00:00
Hokuto Chen
ab2bcdf00d updated notepad ++ descrption 2022-07-27 04:59:33 +00:00
Kwoth
d7c1dad4f0 Check for updates should run once per hour, not every 15 seconds 2022-07-27 06:53:16 +02:00
Kwoth
8bd6b887b8 Fixed docker build 2022-07-27 06:37:04 +02:00
Kwoth
d326e19196 Merge branch '4.3' into 'v4'
Base for 4.3 work.

See merge request Kwoth/nadekobot!259
2022-07-27 03:47:47 +00:00
Kwoth
fa822853df Added mysql and postgresql linkonly migration 2022-07-27 05:40:14 +02:00
Kwoth
22bff7838a Cleaned up around xp.yml. Fixed a typo, added an error to .xpshop if the feature is disabled. 2022-07-27 03:56:10 +02:00
Kwoth
25fce1bd75 Small formatting fix 2022-07-27 03:24:07 +02:00
Kwoth
a73482e838 Imageonly channels should be properly disabled now 2022-07-27 03:22:38 +02:00
Kwoth
bb395f18a2 Implemented .linkonly (as an alternative to .imageonly). Updated pokaman with latest generations. Updated responses, fixed some warnings 2022-07-27 03:14:47 +02:00
Kwoth
e20212a6cb Added .repeatskip 2022-07-26 16:56:33 +02:00
Kwoth
0f8291c589 .xpadd will now work on roles 2022-07-26 15:47:55 +02:00
Kwoth
a7b3a238f5 Added release notifications to data/bot.yml Notifications are sent to all owners in dms 2022-07-26 12:25:22 +02:00
Kwoth
bb800f4e38 Fixed xpshop footer 2022-07-26 09:25:34 +02:00
Kwoth
b10eded334 Added a line break 2022-07-26 08:37:27 +02:00
Kwoth
3e15e50667 Final touches to the xp shop, added privacy-policy.md 2022-07-26 08:35:43 +02:00
Kwoth
516bc1e484 Bugfix to buttons not working when there's only one item, and buy db query 2022-07-26 04:08:37 +02:00
Kwoth
6a042c3faa Finished implementing xp shop. It allows users to buy frames and backgrounds if the user enables the feature in data/xp.yml. It can also be available only to patrons 2022-07-25 18:10:00 +02:00
Kwoth
967784c860 Added MultipleDeck which can be used in place of the old QuadDeck, converted cards from classes to records as it is useful to have default equality checks. 2022-07-23 07:43:18 +02:00
Kwoth
ccf92ca702 Added .GetName extension method which will show human readable hand values in text. Also completely moved .betdraw the the new deck implementation. A renamed to Ace 2022-07-23 04:52:40 +02:00
Kwoth
c20b851dc7 Betdraw reimplemented (mostly) using the new deck implementation 2022-07-22 17:36:43 +02:00
Kwoth
39fc21d41c Added error logging to pagination as there seems to be an issue 2022-07-22 16:54:58 +02:00
Kwoth
c8c0b27d6a Mostly finished implementation of the new deck? ALso added some tests 2022-07-21 06:55:15 +02:00
Kwoth
9a21ba3d53 Experimenting with new deck 2022-07-20 23:47:57 +02:00
Kwoth
0042c22ceb Fix for patron errors showing up even with permissions explicitly disabling that command 2022-07-19 02:34:44 +02:00
Kwoth
a0ba9be34e Show if there's a problem planting an image 2022-07-19 02:28:25 +02:00
Kwoth
91da78a2ee Removed completed todos 2022-07-18 04:46:01 +02:00
Kwoth
8eca8e1dfb Avoid awaiting possible null 2022-07-18 04:45:05 +02:00
Kwoth
b12e97a0a7 Added uncommited files. Fixed nullref in update loop when users gain voice xp 2022-07-18 04:44:27 +02:00
Kwoth
99c60459f8 Removed cmd source generator. Commands are no longer partial methods. Compilations should be slightly faster now. Updated packages and adapted drawing code to the new apis. There may be some bugs. 2022-07-18 04:33:50 +02:00
Kwoth
3db194c186 Fixed .clubleave, cleanup 2022-07-15 17:55:56 +02:00
Kwoth
0b720a0439 Many changes. Will update merge request description with details 2022-07-15 05:04:01 +02:00
Kwoth
d9011106ac Replaced .wheel with .lula (lucky ladder). It looks nicer but plays the same. Also it is more customizable as you can have more or less multipliers. Cleaned up some trivia code. Sorted lula multipliers in gambling.yml. Improved .slottest 2022-07-14 03:52:30 +02:00
Kwoth
17ca609fe9 Moved .rps to the new service, reimplemented logic, fixed an unknown bug with 0 amount (?!) 2022-07-13 23:48:03 +02:00
Kwoth
0f1ba400db Merge branch '4.3' of https://gitlab.com/kwoth/nadekobot into 4.3 2022-07-13 06:22:46 +02:00
Kwoth
2b8daa2177 Improved how .bf and .br look like. Improved .slot result calculation performance (because of .slottest). Some string changes 2022-07-13 06:22:39 +02:00
Kwoth
f3ed14de5b .slot should now show correct messages if multipliers are changed in the config 2022-07-13 04:01:56 +02:00
Kwoth
251d5a4df4 Merge branch 'v4' into 4.3 2022-07-13 03:07:03 +02:00
Kwoth
f761714f15 Removed transaction from the xp loop as it's causing database locking 2022-07-13 02:32:19 +02:00
Kwoth
93b0e38264 Slight tweaks to xp loop 2022-07-13 02:22:15 +02:00
Kwoth
5f7b030a66 More work on gambling 2022-07-13 02:21:53 +02:00
Kwoth
0b122e8d3f Slight tweaks to xp loop 2022-07-13 01:15:28 +02:00
Kwoth
34955f88f6 Merge branch 'v4' into 4.3 2022-07-12 03:59:36 +02:00
Kwoth
b34fd6da4e Upped version to 4.2.15 2022-07-12 03:58:41 +02:00
Kwoth
9a35716331 Work on porting over Ayu gambling command rewrites 2022-07-12 03:44:17 +02:00
Kwoth
156069db0e Merge branch '4.3-trivia' into '4.3'
Trivia game cleanup

See merge request Kwoth/nadekobot!260
2022-07-11 23:11:45 +00:00
Kwoth
ea8e444b10 Trivia game cleanup 2022-07-11 23:11:45 +00:00
Kwoth
ff779ad494 Merge branch 'v4' into 4.3 2022-07-11 03:33:29 +02:00
Kwoth
c9287dc166 Merge branch 'v4' of https://gitlab.com/kwoth/nadekobot into v4 2022-07-11 02:58:01 +02:00
Kwoth
7885106266 Bot should no longer always notify server level gains 2022-07-11 02:57:24 +02:00
Kwoth
946b095d4b tests fixed 2022-07-11 00:23:30 +02:00
Kwoth
5bb8f3f7c8 See if removing seq fixes the pipeline 2022-07-11 00:11:33 +02:00
Kwoth
f41b1fb93c Base for 4.3 work. Split Nadeko.Common into a separate project 2022-07-11 00:06:34 +02:00
Kwoth
8efdd3dffe Merge branch 'hokutochen-v4-patch-33031' into 'v4'
added redis as optional to install in windows guide docs

See merge request Kwoth/nadekobot!258
2022-07-09 22:51:37 +00:00
Hokuto Chen
fb9a7964df added redis as optional to install in windows guide docs 2022-07-09 22:51:36 +00:00
Kwoth
1396d9d55a Updated changelog 2022-07-07 22:42:51 +02:00
Kwoth
e7ddcebeab Merge branch 'v4' of https://gitlab.com/kwoth/nadekobot into v4 2022-07-07 22:08:57 +02:00
Kwoth
9d3a386f32 nsfw shouldn't be disabled on private bots by default anymore 2022-07-07 22:08:45 +02:00
Kwoth
83c9c372e4 Fixed a certain command, scraping as the api is closed 2022-07-07 22:08:22 +02:00
Kwoth
4bb4209c92 Merge branch 'inner-xp-loop-rewrite' into 'v4'
Rewrite xp gain loop to be faster

See merge request Kwoth/nadekobot!253
2022-07-07 17:09:11 +00:00
Kwoth
744018802f Rewrite xp gain loop. Hopefully faster and fixes xp freeze bugs. 2022-07-07 17:09:11 +00:00
Kwoth
470bb9657f Fixed .timely button for sure this time 2022-07-06 13:41:27 +02:00
Kwoth
2fb4bb2ea4 images.yml should once again support local file paths 2022-07-06 04:15:16 +02:00
Kwoth
43dd37c4f1 .die should now set to invisible, not dnd, but it doesn't seem to have an effect anyway 2022-07-06 03:03:03 +02:00
Kwoth
5fac500dcf remind me interaction on .timely should now work correctly 2022-07-06 03:00:57 +02:00
Kwoth
fd25f5bf45 Multiword aliases are once again supported 2022-07-05 18:08:11 +02:00
Kwoth
9d3e80eb32 Fixed commands.en-US.yml 2022-07-03 23:13:00 +02:00
Kwoth
42cbb7f626 Updated CHANGELOG.md, Upped version to 4.2.14 2022-07-03 23:00:12 +02:00
Kwoth
4d175477f5 Bot will now 'try' to set status to invisible before going offline when '.die' command is used, but it doesn't seem to have (much/any) effect. .qsearch is more powerful 2022-07-03 22:26:41 +02:00
Kwoth
643987c41f Added .log userwarned 2022-07-03 21:58:05 +02:00
Kwoth
03396642a4 Added ban reason to .log userbanned (and if you used nadeko to ban someone, it will also show the mod who did the ban because nadeko adds it to the reason) 2022-07-03 17:03:27 +02:00
Kwoth
3fd5f0c97a Added warn punishment action for protection commands 2022-07-03 14:55:31 +02:00
Kwoth
5d78f29329 Added %server.icon% placeholder 2022-07-03 13:47:41 +02:00
Kwoth
2a98aceae6 Fixed elipsis alias bug, closes #295 2022-07-02 15:03:24 +02:00
Kwoth
5c933b676d .timely will now have a button to set a reminder 2022-07-02 14:15:02 +02:00
Kwoth
2e4de7723e Fixed .cash bank interaction not being ephemeral 2022-06-30 22:09:26 +02:00
Kwoth
a8e00a19ba Upped version to 4.2.12 2022-06-30 11:33:45 +02:00
Kwoth
8acf6b1194 Fixed .trivia --pokemon showing pokemon with id + 1 2022-06-30 11:32:36 +02:00
Kwoth
11d9db99ff Draw fixed, version upped 2022-06-29 17:53:15 +02:00
Kwoth
c66e0fb6b7 Possible fix for constant source generator crashes 2022-06-29 15:28:21 +02:00
Kwoth
1517a35ef7 Upped version to 4.2.10 2022-06-29 11:16:32 +02:00
Kwoth
c5179979d7 Fixed an issue with currency generation working only once 2022-06-29 00:18:15 +02:00
Kwoth
6b14c04e37 Merge branch 'v4' of https://gitlab.com/kwoth/nadekobot into v4 2022-06-28 10:55:59 +02:00
Kwoth
4ec3eb7855 Possible fix for the filterservice duplicate key bug 2022-06-28 10:55:35 +02:00
Kwoth
4752c4b7cd Update CHANGELOG.md 2022-06-26 08:54:40 +00:00
Kwoth
dfec2f589e Nuked nsfw from the public bot, it shouldn't show up anymore. Uncommented patron owner check 2022-06-25 21:03:16 +02:00
Kwoth
f616364d8a Upped version to 4.2.9 2022-06-25 10:42:16 +02:00
Kwoth
4294f8efd5 copy creds_example to output directory 2022-06-25 10:39:34 +02:00
Kwoth
69eb5f2c56 Fixed .timley 2022-06-24 13:24:36 +02:00
Kwoth
8d26d16fff Merge branch 'v4' of https://gitlab.com/kwoth/nadekobot into v4 2022-06-24 10:48:17 +02:00
Kwoth
bdde065209 Upped version to 4.2.7, Updated CHANGELOG.md 2022-06-24 10:47:27 +02:00
Kwoth
e2477638b5 Merge branch 'nonblocking-dict' into 'v4'
Replaced all usages of ConcurrentDictionary with NonBlocking.ConcurrentDictionary

See merge request Kwoth/nadekobot!256
2022-06-24 08:37:25 +00:00
Kwoth
f398cddaf0 Replaced all usages of ConcurrentDictionary with NonBlocking.ConcurrentDictionary 2022-06-24 08:37:25 +00:00
Kwoth
dc846965ae Fixed redis track cacher 2022-06-23 19:54:09 +02:00
Kwoth
dbbdc66dca Merge branch 'eval' into 'v4'
Added .eval command. Very dangerous, don't use.

See merge request Kwoth/nadekobot!254
2022-06-23 13:19:41 +00:00
Kwoth
df85b3b250 Added .eval command. Very dangerous, don't use. 2022-06-23 13:19:41 +00:00
Kwoth
f1d9db699f Merge branch 'v4-noredis' into 'v4'
Abstract away cache. 2 implementations: redis and memory

See merge request Kwoth/nadekobot!255
2022-06-23 13:07:47 +00:00
Kwoth
210da263ad Abstract away cache. 2 implementations: redis and memory 2022-06-23 13:07:45 +00:00
Kwoth
1716c69132 Upped version to 4.2.6 2022-06-22 09:56:42 +02:00
Kwoth
14bfcb54dc Patron sytem should be *disabled* on selfhosted bots by default. Commited true by mistake. 2022-06-22 09:55:09 +02:00
Kwoth
9f445c0866 Fixed a patron bug which didn't let patrons execute patron commands in non-patron servers 2022-06-18 10:29:33 +02:00
Kwoth
3343fd2f6e Allow docker build to fail 2022-06-18 02:13:43 +02:00
Kwoth
9103dd9fdb Upped version to 4.2.5 as ci didn't run for 4.2.4 2022-06-18 02:07:25 +02:00
Kwoth
1a8c9a6cba [skip ci] Version upped to 4.2.4, updated CHANGELOG.md 2022-06-17 22:57:03 +02:00
Kwoth
9d2f251923 Fixed crypto deserialization issue 2022-06-17 14:04:22 +02:00
Kwoth
3744dd287c Revert "Fixed .crypto - some extra fields which were causing deserialization issues"
This reverts commit f65ba100af.
2022-06-17 14:03:30 +02:00
Kwoth
f65ba100af Fixed .crypto - some extra fields which were causing deserialization issues 2022-06-17 14:02:38 +02:00
Kwoth
cc52605c90 [skip ci] Upped version to 4.2.3 2022-06-17 04:42:30 +02:00
Kwoth
3d3dc532dc Made .timely use timestamp tags and fixed a bug 2022-06-17 04:37:08 +02:00
Kwoth
6c58a6a72d Merge branch 'v4' of https://gitlab.com/kwoth/nadekobot into v4 2022-06-16 21:28:16 +02:00
Kwoth
cefd81d810 [skip ci] Use shared coinmarket key instead of public bot's 2022-06-16 21:27:51 +02:00
Kwoth
34c96c697a Merge branch 'hokutochen-v4-patch-06658' into 'v4'
updating docs and some code

See merge request Kwoth/nadekobot!250
2022-06-16 15:56:30 +00:00
Hokuto Chen
1cc5e0e1d8 updating docs and some code 2022-06-16 15:56:30 +00:00
Kwoth
deaedce6c7 Renamed some of the classes which still had 'Cr' instead of 'Expr' in them 2022-06-16 04:03:59 +02:00
Kwoth
91e4d9dffc permission commands should now work for global expressions too 2022-06-16 03:59:45 +02:00
Kwoth
a826f4245f Fixed .streamrole not updating in real time, closes #345 2022-06-16 03:37:19 +02:00
Kwoth
780eec62b3 [ci skip] undoed .gencmdlist path, no effect 2022-06-16 00:56:31 +02:00
Kwoth
dbeb83561a Upped version, updated CHANGELOG.md 2022-06-15 21:39:42 +02:00
Kwoth
6c11d11645 Rss errors will now show error counter until deletion 2022-06-15 13:18:00 +02:00
Kwoth
e9923a7691 Fixed bugs when users update their patreon pledge. Updated some packages 2022-06-15 13:11:27 +02:00
Kwoth
5fbe93d898 Possible fix for pledge updates 2022-06-15 10:24:09 +02:00
Kwoth
65995bdca4 Added missing patron tiers. Servers whose owner is bot owner will get excluded from patron quota. Use discord slowmode or cmdcd + nadeko permissions to limit usage 2022-06-15 10:11:46 +02:00
Kwoth
f7c333b671 Ignoring error if creds_example.yml fails to generate, as this happens in docker containers 2022-06-15 07:48:57 +02:00
Kwoth
f9d18aa086 Added enabled property to .config patron just so it's not empty 2022-06-14 14:43:00 +02:00
Kwoth
571e1c801f Upped version to 4.2.1, updated CHANGELOG.md 2022-06-14 14:36:53 +02:00
Kwoth
f922543d33 Fixed .exexport, .savechat, and .quoteexport, closes #358 2022-06-14 14:31:43 +02:00
Kwoth
6bec67006c Fixed plaintext-only embeds. closes #360 2022-06-14 14:26:59 +02:00
Kwoth
050eaa48eb Fixed greet message footer not showing, closes #359 2022-06-14 13:57:10 +02:00
Kwoth
248ce8b3d2 Merge branch 'v4' of https://gitlab.com/kwoth/nadekobot into v4 2022-06-14 13:55:26 +02:00
Kwoth
04a488cdf2 Updated .gitlab-ci.yml, to fix medusa versioning and docker builds. hopefully for the last time 2022-06-14 13:55:19 +02:00
Kwoth
6bc2fc88f9 Update responses.uk-UA.json (POEditor.com) 2022-06-14 11:34:10 +00:00
Kwoth
69b6ed6a49 Update responses.es-ES.json (POEditor.com) 2022-06-14 11:34:09 +00:00
Kwoth
e30b126726 Update responses.ru-RU.json (POEditor.com) 2022-06-14 11:34:08 +00:00
Kwoth
a5e2321c5b Update responses.pt-BR.json (POEditor.com) 2022-06-14 11:34:07 +00:00
Kwoth
322e9a329d Update responses.pl-PL.json (POEditor.com) 2022-06-14 11:34:06 +00:00
Kwoth
7ca6ab8562 Update responses.it-IT.json (POEditor.com) 2022-06-14 11:34:05 +00:00
Kwoth
8a27dcc481 Update responses.id-ID.json (POEditor.com) 2022-06-14 11:34:03 +00:00
Kwoth
bed61c521f Update responses.de-DE.json (POEditor.com) 2022-06-14 11:34:02 +00:00
Kwoth
46ea1698eb Update responses.fr-FR.json (POEditor.com) 2022-06-14 11:34:01 +00:00
Kwoth
c47417024d Update responses.nl-NL.json (POEditor.com) 2022-06-14 11:34:00 +00:00
Kwoth
eedc2d05ff Update responses.zh-TW.json (POEditor.com) 2022-06-14 11:33:59 +00:00
Kwoth
d24dba7ed0 Update responses.zh-CN.json (POEditor.com) 2022-06-14 11:33:58 +00:00
Kwoth
9bdf58ec27 Update responses.ar.json (POEditor.com) 2022-06-14 11:33:56 +00:00
Kwoth
5de9c5d067 4.2.0 release 2022-06-14 13:21:12 +02:00
Kwoth
64b2a46c95 Re-added .google search result scraper and set it as the default again. Also added versioning to searches.yml as it was forgotten previously 2022-06-14 13:18:35 +02:00
Kwoth
f42deda3e2 Updated .gitlab-ci.yml. Fixed tags in Nadeko.Medusa and docker images 2022-06-14 11:21:18 +02:00
Kwoth
a464e7c643 Updated .gitlab-ci.yml 2022-06-14 11:07:28 +02:00
Kwoth
1f36fa75c4 Updated .gitlab-ci.yml 2022-06-14 10:52:08 +02:00
Kwoth
ad6d732687 Fixed .gitlab-ci for medusa build 2022-06-14 09:42:58 +02:00
Kwoth
1f51c54449 Merge branch 'v4-prem' into 'v4'
NadekoBot Patronage system, Search commands improvements + fixes

See merge request Kwoth/nadekobot!252
2022-06-14 07:24:34 +00:00
Kwoth
7b5145f116 NadekoBot Patronage system, Search commands improvements + fixes 2022-06-14 07:24:33 +00:00
Kwoth
18b10b8c6f Merge branch 'hokutochen-v4-patch-19985' into 'v4'
Updated Docs regarding source and expressions along with help text

See merge request Kwoth/nadekobot!247
2022-05-27 23:55:57 +00:00
Hokuto Chen
f05435f864 Updated Docs regarding source and expressions along with help text 2022-05-27 23:55:57 +00:00
Kwoth
7cbedc82bf Merge branch 'fix/docker' into 'v4'
Reduce Docker image size

See merge request Kwoth/nadekobot!249
2022-05-26 15:11:52 +00:00
Manuel
3be208f1b3 Reduce Docker image size 2022-05-26 17:01:06 +02:00
Kwoth
2606bda8df Upped Nadeko.Medusa.csproj to 1.0.3 (thx cata) 2022-05-20 10:35:16 +02:00
Kwoth
ab1272b491 Fixed .crypto sparklines 2022-05-17 17:05:30 +02:00
Kwoth
43047c0ab0 [ci] updated gitlab-ci - reverted dotnet sdk and runtime versions 2022-05-14 05:09:58 +02:00
Kwoth
34471abd64 Fixed a warning during windows publish, thx kotz 2022-05-14 04:34:53 +02:00
Kwoth
fa259384f1 Possible fix for the windows release, updated packages 2022-05-14 04:27:16 +02:00
Kwoth
cb865d5012 Updated packages 2022-05-12 23:56:12 +02:00
Kwoth
1db97decd1 Fixed rule34, small refactor of the downloader classes 2022-05-12 10:23:01 +02:00
Kwoth
b02768a08e User friendlier errors in the console when getting rss feeds fails 2022-05-11 12:43:59 +02:00
Kwoth
e55d60f1aa Small fix for the changelog 2022-05-11 12:39:02 +02:00
499 changed files with 65306 additions and 8454 deletions

View File

@@ -1,6 +1,11 @@
# Ignore all files
*
# Don't ignore nugetconfig
!./NuGet.Config
# Don't ignore src projects
!src/Nadeko.Econ/**
!src/Nadeko.Common/**
# Use Nadeko.Medusa project
!src/Nadeko.Medusa/**
# Use NadekoBot project

2
.gitignore vendored
View File

@@ -1,5 +1,7 @@
#Manually added files
src/NadekoBot/data/last_known_version.txt
# medusa stuff
!src/NadekoBot/data/medusae/medusa.yml
src/NadekoBot/data/medusae/**

View File

@@ -7,6 +7,7 @@ stages:
- release
- publish-windows
- upload-windows-updater-release
- publish-medusa-package
variables:
project: "NadekoBot"
@@ -18,41 +19,41 @@ variables:
PACKAGE_REGISTRY_URL: "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/NadekoBot-build/${CI_COMMIT_TAG}"
INSTALLER_OUTPUT_DIR: "nadeko-installers/${CI_COMMIT_TAG}"
INSTALLER_FILE_NAME: "nadeko-setup-${CI_COMMIT_TAG}.exe"
build:
stage: build
script:
- "dotnet publish -c Release -r linux-x64 -o $LINUX_X64_OUTPUT_DIR src/NadekoBot/NadekoBot.csproj"
- "dotnet publish -c Release -r win7-x64 -o $WIN_X64_OUTPUT_DIR src/NadekoBot/NadekoBot.csproj"
- "dotnet publish -c Release -r linux-x64 --self-contained -o $LINUX_X64_OUTPUT_DIR src/NadekoBot/NadekoBot.csproj"
- "dotnet publish -c Release -r win7-x64 --self-contained -o $WIN_X64_OUTPUT_DIR src/NadekoBot/NadekoBot.csproj"
artifacts:
paths:
- "$LINUX_X64_OUTPUT_DIR/"
- "$WIN_X64_OUTPUT_DIR/"
upload-builds:
stage: upload-builds
image: alpine:latest
rules:
- if: $CI_COMMIT_TAG
script:
- apk add --no-cache curl tar zip
- "tar cvf $LINUX_X64_RELEASE $LINUX_X64_OUTPUT_DIR/*"
- "zip -r $WIN_X64_RELEASE $WIN_X64_OUTPUT_DIR/*"
- |
curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file $LINUX_X64_RELEASE $PACKAGE_REGISTRY_URL/$LINUX_X64_RELEASE
- |
curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file $WIN_X64_RELEASE $PACKAGE_REGISTRY_URL/$WIN_X64_RELEASE
stage: upload-builds
image: alpine:latest
rules:
- if: $CI_COMMIT_TAG
script:
- apk add --no-cache curl tar zip
- "tar cvf $LINUX_X64_RELEASE $LINUX_X64_OUTPUT_DIR/*"
- "zip -r $WIN_X64_RELEASE $WIN_X64_OUTPUT_DIR/*"
- |
curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file $LINUX_X64_RELEASE $PACKAGE_REGISTRY_URL/$LINUX_X64_RELEASE
- |
curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file $WIN_X64_RELEASE $PACKAGE_REGISTRY_URL/$WIN_X64_RELEASE
release:
stage: release
image: registry.gitlab.com/gitlab-org/release-cli:latest
rules:
- if: $CI_COMMIT_TAG
script:
- |
release-cli create --name "NadekoBot v$CI_COMMIT_TAG" --description "## [Changelog](https://gitlab.com/Kwoth/nadekobot/-/blob/v4/CHANGELOG.md#$(echo "$CI_COMMIT_TAG" | sed "s/\.//g")-$(date +%d%m%Y))" --tag-name $CI_COMMIT_TAG \
--assets-link "{\"name\":\"${LINUX_X64_RELEASE}\",\"url\":\"${PACKAGE_REGISTRY_URL}/${LINUX_X64_RELEASE}\"}" \
--assets-link "{\"name\":\"${WIN_X64_RELEASE}\",\"url\":\"${PACKAGE_REGISTRY_URL}/${WIN_X64_RELEASE}\"}"
stage: release
image: registry.gitlab.com/gitlab-org/release-cli:latest
rules:
- if: $CI_COMMIT_TAG
script:
- |
release-cli create --name "NadekoBot v$CI_COMMIT_TAG" --description "## [Changelog](https://gitlab.com/Kwoth/nadekobot/-/blob/v4/CHANGELOG.md#$(echo "$CI_COMMIT_TAG" | sed "s/\.//g")-$(date +%d%m%Y))" --tag-name $CI_COMMIT_TAG \
--assets-link "{\"name\":\"${LINUX_X64_RELEASE}\",\"url\":\"${PACKAGE_REGISTRY_URL}/${LINUX_X64_RELEASE}\"}" \
--assets-link "{\"name\":\"${WIN_X64_RELEASE}\",\"url\":\"${PACKAGE_REGISTRY_URL}/${WIN_X64_RELEASE}\"}"
test:
stage: test
@@ -63,63 +64,75 @@ test:
- "dotnet test"
publish-windows:
stage: publish-windows
rules:
- if: '$CI_COMMIT_TAG'
image: scottyhardy/docker-wine
before_script:
- choco install dotnet-6.0-runtime -y
- choco install dotnet-6.0-sdk -y
- choco install innosetup -y
artifacts:
paths:
- "$INSTALLER_OUTPUT_DIR/$INSTALLER_FILE_NAME"
script:
- dotnet clean
- dotnet restore
- dotnet publish -c Release --runtime win7-x64 /p:Version=$CI_COMMIT_TAG src/NadekoBot
- $env:NADEKOBOT_INSTALL_VERSION = $CI_COMMIT_TAG
- iscc.exe "/O+" ".\exe_builder.iss"
tags:
- windows
stage: publish-windows
rules:
- if: "$CI_COMMIT_TAG"
image: scottyhardy/docker-wine
before_script:
- choco install dotnet-6.0-runtime --version=6.0.4 -y
- choco install dotnet-6.0-sdk --version=6.0.202 -y
- choco install innosetup -y
artifacts:
paths:
- "$INSTALLER_OUTPUT_DIR/$INSTALLER_FILE_NAME"
script:
- dotnet clean
- dotnet restore -f --no-cache -v n
- dotnet publish -c Release --runtime win7-x64 /p:Version=$CI_COMMIT_TAG src/NadekoBot
- $env:NADEKOBOT_INSTALL_VERSION = $CI_COMMIT_TAG
- iscc.exe "/O+" ".\exe_builder.iss"
tags:
- windows
upload-windows-updater-release:
stage: upload-windows-updater-release
rules:
- if: '$CI_COMMIT_TAG'
image:
name: amazon/aws-cli
entrypoint: [""]
script:
- sed -i "s/_INSTALLER_FILE_NAME_/$INSTALLER_FILE_NAME/g" releases-v3.json
- sed -i "s/_VERSION_/$CI_COMMIT_TAG/g" releases-v3.json
- aws --version
- aws --endpoint-url $AWS_SERVICE_URL s3api put-object --bucket "$AWS_BUCKET_NAME" --key "dl/bot/$INSTALLER_FILE_NAME" --acl public-read --body "$INSTALLER_OUTPUT_DIR/$INSTALLER_FILE_NAME"
- aws --endpoint-url $AWS_SERVICE_URL s3api put-object --bucket "$AWS_BUCKET_NAME" --key "dl/bot/releases-v3.json" --acl public-read --body "releases-v3.json"
stage: upload-windows-updater-release
rules:
- if: "$CI_COMMIT_TAG"
image:
name: amazon/aws-cli
entrypoint: [""]
script:
- sed -i "s/_INSTALLER_FILE_NAME_/$INSTALLER_FILE_NAME/g" releases-v3.json
- sed -i "s/_VERSION_/$CI_COMMIT_TAG/g" releases-v3.json
- aws --version
- aws --endpoint-url $AWS_SERVICE_URL s3api put-object --bucket "$AWS_BUCKET_NAME" --key "dl/bot/$INSTALLER_FILE_NAME" --acl public-read --body "$INSTALLER_OUTPUT_DIR/$INSTALLER_FILE_NAME"
- aws --endpoint-url $AWS_SERVICE_URL s3api put-object --bucket "$AWS_BUCKET_NAME" --key "dl/bot/releases-v3.json" --acl public-read --body "releases-v3.json"
publish-medusa-package:
stage: publish-medusa-package
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH || $CI_COMMIT_TAG
script:
- LAST_TAG=$(git describe --tags --abbrev=0)
- if [ $CI_COMMIT_TAG ];then MEDUSA_VERSION="$CI_COMMIT_TAG"; else MEDUSA_VERSION="$LAST_TAG-$CI_COMMIT_SHA"; fi
- cd src/Nadeko.Medusa/
- dotnet pack -c Release /p:Version=$MEDUSA_VERSION -o bin/Release/packed
- dotnet nuget push bin/Release/packed/ --source https://www.myget.org/F/nadeko/api/v2/package --api-key "$MYGET_API_KEY"
docker-build:
# Use the official docker image.
image: docker:latest
stage: build
services:
- docker:dind
before_script:
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
# Default branch leaves tag empty (= latest tag)
# All other branches are tagged with the escaped branch name (commit ref slug)
script:
- |
if [[ "$CI_COMMIT_BRANCH" == "$CI_DEFAULT_BRANCH" ]]; then
tag=""
echo "Running on default branch '$CI_DEFAULT_BRANCH': tag = 'latest'"
else
tag=":$CI_COMMIT_REF_SLUG"
echo "Running on branch '$CI_COMMIT_BRANCH': tag = $tag"
fi
- docker build --pull -t "$CI_REGISTRY_IMAGE${tag}" .
- docker push "$CI_REGISTRY_IMAGE${tag}"
# Run this job in a branch where a Dockerfile exists
rules:
- if: $CI_COMMIT_BRANCH
exists:
- Dockerfile
# Use the official docker image.
image: docker:latest
stage: build
allow_failure: true
services:
- docker:dind
before_script:
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
# Default branch leaves tag empty (= latest tag)
# All other branches are tagged with the escaped branch name (commit ref slug)
script:
- |
if [[ "$CI_COMMIT_BRANCH" == "$CI_DEFAULT_BRANCH" ]]; then
tag=""
echo "Running on default branch '$CI_DEFAULT_BRANCH': tag = 'latest'"
else
tag=":$CI_COMMIT_SHA"
echo "Running on branch '$CI_COMMIT_BRANCH': tag = $tag"
fi
- docker build --pull -t "$CI_REGISTRY_IMAGE${tag}" .
- docker push "$CI_REGISTRY_IMAGE${tag}"
# Run this job in a branch where a Dockerfile exists
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH || $CI_COMMIT_TAG
exists:
- Dockerfile

View File

@@ -1,10 +1,360 @@
# Changelog
Experimental changelog. Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except date format. a-c-f-r-o
## Unreleased
## [4.3.3] - 06.08.2022
### Added
- Added `betroll` option to `.bettest` command
- Added `.xpshopbuy` and `.xpshopuse` convenience commands
- Added an optional preview url to teh xp shop item config model which will be shown instead of the real Url
### Changed
- Updated position of Username and Club name on the .xp card
- Improved text visibility on the .xp card
### Fixed
- Possibly fixed .trivia not stopping bug
- Fixed very low payout rate on `.betroll`
- Fixed an issue with youtube song resolver which caused invalid data to be cached
- Added client id to the cache key as a potential fix for VoiceXp 'bug'. The solution may be to use different redis instances for each bot, or to switch from botCache: from 'redis' to 'memory' in creds.yml
- Bot owner should now be able to buy items from the xpshop when patron requirement is set
- Fixed youtube-dl caching invalid data. Please use yt-dlp instead
## [4.3.2] - 28.07.2022
### Fixed
- Fixed Reaction Roles not working properly with animated emojis
- Fixed `.slot` alignment
- Fixed `mysql` and `postgresql` reactionrole migration
- Fixed repeat loop with `postgresql` db provider
- Fixed `.bank withdraw <expression>` will now correctly use bank amount for calculation
- [dev] Fixed medusa Reply*LocalizedAsync not working with placeholders
## [4.3.1] - 27.07.2022
### Changed
- Check for updates will run once per hour as it was supposed to
## [4.3.0] - 27.07.2022
### Added
- Added `.bettest` command which lets you test many gambling commands
- Better than .slottest
- Counts win/loss streaks too
- Doesn't count 1x returns as neither wins nor losses
- multipliers < 1 are considered losses, > 1 considered wins
- Added `.betdraw` command which lets you guess red/black and/or high/low for a random card
- They payouts are very good, but seven always loses
- Added `.lula` command. Plays the same as `.wof` but looks much nicer, and is easily customizable from gambling.yml without any changes to the sourcecode needed.
- Added `.repeatskip` command which makes the next repeat trigger not post anything
- Added `.linkonly` which will make the bot only allow link posts in the channel. Exclusive with `.imageonly`
- Added release notifications. Bot owners will now receive new release notifications in dms if they have `checkForUpdates` set to `true` in data/bot.yml
- You can also configure it via `.conf bot checkfor
- updates <true/false>`
- Added `.xpshop` which lets bot owners add xp backgrounds and xp frames for sale by configuring `data/xp.yml`
- You can also toggle xpshop feature via `.conf xp shop.is_enabled`
### Changed
- `.t` Trivia code cleaned up, added ALL pokemon generations
- `.xpadd` will now work on roles too. It will add the specified xp to each user (visible to the bot) in the role
- Improved / cleaned up / modernized how most gambling commands look
- `.roll`
- `.rolluo`
- `.draw`
- `.flip`
- `.slot`
- `.betroll`
- `.betflip`
- Try them out!
- `.draw`, `.betdraw` and some other card commands (not all) will use the new, rewritten deck system
- Error will be printed to the console if there's a problem in `.plant`
- [dev] Split Nadeko.Common into a separate project
- [dev] It will contain classes/utilities which can be shared across different nadeko related projects
- [dev] Split Nadeko.Econ into a separate project
- [dev] It should be home for the backend any gambling/currency/economy feature
- [dev] It will contain most gambling games and any shared logic
- [dev] Compliation should take less time and RAM
- [dev] No longer using generator and partial methods for commands
### Fixed
- `.slot` will now show correct multipliers if they've been modified
- Fix patron errors showing up even with permissions disabling the command
- Fixed an issue with voice xp breaking xp gain.
### Removed
- Removed `.slottest`, replaced by `.bettest`
- Removed `.wof`, replaced by `.lula`
- [dev] Removed a lot of unused methods
- [dev] Removed several unused response strings
## [4.2.15] - 12.07.2022
### Fixed
- Fixed `.nh*ntai` nsfw command
- Xp Freezes may have been fixed
- `data/images.yml` should once again support local file paths
- Fixed multiword aliases
## [4.2.14] - 03.07.2022
### Added
- Added `.log userwarned` (Logging user warnings)
- Claiming `.timely` will now show a button which you can click to set a reminder
- Added `%server.icon%` placeholder
- Added `warn` punishment action for protection commands (it won't work with `.warnp`)
### Changed
- `.log userbanned` will now have a ban reason
- When `.die` is used, bot will try to update it's status to `Invisible`
### Fixed
- Fixed elipsis character issue with aliases/quotes. You should now be able to set an elipsis to be an alias of `.quoteprint`
## [4.2.13] - 30.06.2022
### Fixed
- Fixed `.cash` bank interaction not being ephemeral anymore
## [4.2.12] - 30.06.2022
### Fixed
- Fixed `.trivia --pokemon` showing incorrect pokemons
## [4.2.11] - 29.06.2022
### Fixed
- Fixed `.draw` command
## [4.2.10] - 29.06.2022
- Fixed currency generation working only once
## [4.2.9] - 25.06.2022
### Fixed
- Fixed `creds_example.yml` misssing from output directory
## [4.2.8] - 24.06.2022
### Fixed
- `.timely` should be fixed
## [4.2.7] - 24.06.2022
### Changed
- New cache abstraction added
- 2 implemenations: redis and memory
- All current bots will stay on redis cache, all new bots will use **in-process memory cache by default**
- This change removes bot's hard dependency on redis
- Configurable in `creds.yml` (please read the comments)
- You **MUST** use 'redis' if your bot runs on more than 1 shard (2000+ servers)
- [dev] Using new non-locking ConcurrentDictionary
### Fixed
- `.xp` will now show default user avatars too
### Removed
- Removed `.imagesreload` as images are now lazily loaded on request and then cached
## [4.2.6] - 22.06.2022
### Fixed
- Patron system should now properly by disabled on selfhosts by default.
## [4.2.5] - 18.06.2022
### Fixed
- Fixed `.crypto`, you will still need coinmarketcapApiKey in `creds.yml` in order to make it run consistently as the key is shared
## [4.2.3] - 17.06.2022
### Fixed
- Fixed `.timely` nullref bug and made it nicer
- Fixed `.streamrole` not updating in real time!
- Disabling specific Global Expressions should now work with `.sc` (and other permission commands)
## [4.2.2] - 15.06.2022
### Fixed
- Added missing Patron Tiers and fixed Patron pledge update bugs
- Prevented creds_example.yml error in docker containers from crashing it
### Changed
- Rss feeds will now show error counter before deletion
## [4.2.1] - 14.06.2022
### Added
- Localized strings updated
### Fixed
- Fixed `.exexport`, `.savechat`, and `.quoteexport`
- Fixed plaintext-only embeds
- Fixed greet message footer not showing origin server
## [4.2.0] - 14.06.2022
### Added
- Added `data/searches.yml` file which configures some of the new search functionality
The file comments explaining what each property does.
Explained briefly here:
```yml
# what will be used for .google command. Either google (official api) or searx
webSearchEngine: Google
# what will be used for .img command. Either google (official api) or searx
imgSearchEngine: Google
# how will yt results be retrieved: ytdataapi or ytdl or ytdlp
ytProvider: YtDataApiv3
# in case web or img search is set to searx, the following instances will be used:
searxInstances: []
# in case ytProvider is set to invidious, the following instances will be used
invidiousInstances: []
```
- Added new properties to `creds.yml`. google -> searchId and google -> searchImageId.
- These properties are used as `cx` (google api query parameter) in case you've setup your `data/searches.yml` to use the official google api.
`searchId` is used for web search
`searchimageId` is used for image search
```yml
google:
searchId: ""
searchImageId: ""
```
- Check `creds_example.yml` for comments explaining how to obtain them.
#### Patronage system added
- Added `data/patron.yml` for configuration
- Implemented only for patreon so far
- Patreon subscription code completely rewritten
- Users who pledge on patreon get benefits based on the amount they pledged
- Public nadeko only. But selfhosters can adapt it to their own patreon pages by configuring their patreon credentials in `creds.yml` and enabling the system in `data/patron.yml` file.
- Most of the patronage system strings are hardcoded atm, so if you wish to use this system on selfhosts, you will have to modify the source
- Pledge amounts are split into tiers. This is not configurable atm.
- Tier I - 1$ - 4.99$ a month
- Tier V - 5$ - 9.99$ a month
- Tier X - 10$ - 19.99$ a month
- Tier XX - 20$ - 49.99$ a month
- Tier L - 50$ - 99.99$ a month
- Tier C - 100$+ a month
- Rewards and command quotas for each of the tiers are configurable
- Limitations to certain features are also configurable. ex:
```yml
quotas:
features:
"rero:max_count":
x: 50
```
- ^ this setting would set the maximum number of reaction roles to be 50 for a user who is in Patron Tier X
- Read the comments in the .yml file for (much) more info
- Quota system allows the owner to set up hourly, daily and monthly quota usage for each tier
- Quota system applies to entire server owner by a patron
- Patron spends own quota by using the commands on any server
- Any user on *any* server owned by a patron spends that patron's quota
- When users subscribe to patreon they will receive a welcome message
- If you're enabling patron system for a selfhost, you will want to edit it
Added `.patron` and `.patronmessage` commands
- `.patron` checks your patronage status, and quotas. Requires patron system to be enabled.
- `.patronmessage` (owner only) sends message to all patrons with the specified tier or higher. Supports embeds
- Added a fake `.cmdcd` command `cleverbot:response` which can be used to limit how often users can talk to the cleverbot.
### Changed
- CurrencyReward now support adding additional flowers to patrons.
- `.donate` command completely reworked.
- Works only on public bot (OnlyPublicBotAttribute)
- Guides user on how to donate to support the project
- Added interaction explaining selfhosting
- `.google` reimplemented. It now has 2 modes configurable in `data/searches.yml` under the `webSearchengine` property
- If set to `google`, official custom search api will be used. You will need to set googleapikey and google.searchId in `creds.yml`
- if set to `searx` one of the instances specified in the `searxInstances:` property will be randomly chosen for each request
- instances must have `format=json` allowed (public ones usually don't allow it)
- instances are specified as a fully qualified url, example: `https://my.cool.searx.instance.io`
- `.image` reimplemented. Same as `.google` - it uses either `google` official api (in which case it uses `google.searchImageId` from `creds.yml`) or `searx`
- `.youtube` reimplemented. It will use a `ytProvider:` property from `data/searches.yml` to determine how to retrieve results
- `ytdataapi` will use the official google api (requires `GoogleApiKey` specified in `creds.yml`) and YoutubeDataApi enabled in the dev console
- `ytdl` will use `youtube-dl` program from the host machine. It must be downloaded and it's location must be added to path env variable.
- `ytdlp` will use `yt-dlp` program from the host machine. Same as `youtube-dl` - must be in path env variable.
- `invidious` will use one of invidious instances specified in the `invidiousInstances` property. Very good.
- `.google`, `.youtube` and `.image` moved to the new Search group
Note: Results of each `.youtube` query will be cached for 1 hour to improve perfomance
- Removed 30 second `.ping` ratelimit on public nadeko
- xp image generation changes
- In case you have default settings, your xp image will look slightly different
- If you've modified xp_template.json, your xp image might look broken. Your old template will be saved in xp_template.json.old
- Xp number outline is now slightly thicker
- Xp number will now have Center vertical and horizontal alignment
- LastLevelUp no longer supported
- Some commands will now use timestamp tags for better user experience
- `.prune` was slightly slowed down to avoid ratelimits
- `.wof` moved from it's own group to the default Gambling group
- `.feed` urls which error for more than 100 times will be automatically removed.
- `.ve` is now enabled by default
- [dev] nadeko interaction slightly improved to make it less nonsense (they still don't make sense)
- [dev] RewardedUsers table slightly changed to make it more general
- [dev] renamed `// todo`s which aren't planned soon to `// FUTURE`
- [dev] currency rewards have been reimplemented and moved to a separate service
### Fixed
- `.rh` no longer needs quotes for multi word roles
- `.deletexp` will now properly delete server xp too
- Fixed `.crypto` sparklines
- [dev] added support for configs to properly parse enums without case sensitivity (ConfigParsers.InsensitiveEnum)
- [dev] Fixed a bug in .gencmdlist
- [dev] small fixes to creds provider
### Removed
- `.ddg` removed.
- [dev] removed some dead code and comments
## [4.1.6] - 14.05.2022
### Fixed
- Fixed windows release and updated packages
## [4.1.5] - 11.05.2022
### Changed
@@ -17,7 +367,7 @@ Experimental changelog. Mostly based on [keepachangelog](https://keepachangelog.
- Fixed an issue with filters not working and with custom reactions no longer being able to override commands.
- Fixed `.stock` command
## [4.1.4] - 06-05-2022
## [4.1.4] - 06.05.2022
### Fixed

View File

@@ -2,10 +2,13 @@ FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
WORKDIR /source
COPY src/Nadeko.Medusa/*.csproj src/Nadeko.Medusa/
COPY src/Nadeko.Econ/*.csproj src/Nadeko.Econ/
COPY src/Nadeko.Common/*.csproj src/Nadeko.Common/
COPY src/NadekoBot/*.csproj src/NadekoBot/
COPY src/NadekoBot.Coordinator/*.csproj src/NadekoBot.Coordinator/
COPY src/NadekoBot.Generators/*.csproj src/NadekoBot.Generators/
COPY src/ayu/Ayu.Discord.Voice/*.csproj src/ayu/Ayu.Discord.Voice/
COPY NuGet.Config ./
RUN dotnet restore src/NadekoBot/
COPY . .
@@ -25,18 +28,21 @@ WORKDIR /app
RUN set -xe; \
useradd -m nadeko; \
apt-get update; \
apt-get install -y libopus0 libsodium23 libsqlite3-0 curl ffmpeg python3 python3-pip sudo; \
apt-get install -y --no-install-recommends libopus0 libsodium23 libsqlite3-0 curl ffmpeg python3 python3-pip sudo; \
update-alternatives --install /usr/bin/python python /usr/bin/python3.9 1; \
echo 'Defaults>nadeko env_keep+="ASPNETCORE_* DOTNET_* NadekoBot_* shard_id total_shards TZ"' > /etc/sudoers.d/nadeko; \
pip3 install --upgrade youtube-dl; \
apt-get remove -y python3-pip; \
chmod +x /usr/local/bin/youtube-dl
pip3 install --no-cache-dir --upgrade youtube-dl; \
apt-get purge -y python3-pip; \
chmod +x /usr/local/bin/youtube-dl; \
apt-get autoremove -y; \
apt-get autoclean -y
COPY --from=build /app ./
COPY docker-entrypoint.sh /usr/local/sbin
ENV shard_id=0
ENV total_shards=1
ENV NadekoBot__creds=/app/data/creds.yml
VOLUME [ "/app/data" ]
ENTRYPOINT [ "/usr/local/sbin/docker-entrypoint.sh" ]

View File

@@ -12,6 +12,7 @@ ProjectSection(SolutionItems) = preProject
README.md = README.md
.gitlab-ci.yml = .gitlab-ci.yml
Dockerfile = Dockerfile
NuGet.Config = NuGet.Config
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NadekoBot", "src\NadekoBot\NadekoBot.csproj", "{45EC1473-C678-4857-A544-07DFE0D0B478}"
@@ -30,6 +31,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NadekoBot.VotesApi", "src\N
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nadeko.Medusa", "src\Nadeko.Medusa\Nadeko.Medusa.csproj", "{E685977E-31A4-46F4-A5D7-4E3E39E82E43}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nadeko.Common", "src\Nadeko.Common\Nadeko.Common.csproj", "{A6022F5F-A764-4D3F-847B-36F0391FF659}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nadeko.Econ", "src\Nadeko.Econ\Nadeko.Econ.csproj", "{4F4FBF7C-74F0-4AE4-B451-9E60BDCA9C37}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -79,6 +84,18 @@ Global
{E685977E-31A4-46F4-A5D7-4E3E39E82E43}.GlobalNadeko|Any CPU.Build.0 = Debug|Any CPU
{E685977E-31A4-46F4-A5D7-4E3E39E82E43}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E685977E-31A4-46F4-A5D7-4E3E39E82E43}.Release|Any CPU.Build.0 = Release|Any CPU
{A6022F5F-A764-4D3F-847B-36F0391FF659}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A6022F5F-A764-4D3F-847B-36F0391FF659}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A6022F5F-A764-4D3F-847B-36F0391FF659}.GlobalNadeko|Any CPU.ActiveCfg = Debug|Any CPU
{A6022F5F-A764-4D3F-847B-36F0391FF659}.GlobalNadeko|Any CPU.Build.0 = Debug|Any CPU
{A6022F5F-A764-4D3F-847B-36F0391FF659}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A6022F5F-A764-4D3F-847B-36F0391FF659}.Release|Any CPU.Build.0 = Release|Any CPU
{4F4FBF7C-74F0-4AE4-B451-9E60BDCA9C37}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4F4FBF7C-74F0-4AE4-B451-9E60BDCA9C37}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4F4FBF7C-74F0-4AE4-B451-9E60BDCA9C37}.GlobalNadeko|Any CPU.ActiveCfg = Debug|Any CPU
{4F4FBF7C-74F0-4AE4-B451-9E60BDCA9C37}.GlobalNadeko|Any CPU.Build.0 = Debug|Any CPU
{4F4FBF7C-74F0-4AE4-B451-9E60BDCA9C37}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4F4FBF7C-74F0-4AE4-B451-9E60BDCA9C37}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -92,6 +109,8 @@ Global
{3BC3BDF8-1A0B-45EB-AB2B-C0891D4D37B8} = {04929013-5BAB-42B0-B9B2-8F2BB8F16AF2}
{3BC82CFE-BEE7-451F-986B-17EDD1570C4F} = {04929013-5BAB-42B0-B9B2-8F2BB8F16AF2}
{E685977E-31A4-46F4-A5D7-4E3E39E82E43} = {04929013-5BAB-42B0-B9B2-8F2BB8F16AF2}
{A6022F5F-A764-4D3F-847B-36F0391FF659} = {04929013-5BAB-42B0-B9B2-8F2BB8F16AF2}
{4F4FBF7C-74F0-4AE4-B451-9E60BDCA9C37} = {04929013-5BAB-42B0-B9B2-8F2BB8F16AF2}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {5F3F555C-855F-4BE8-B526-D062D3E8ACA4}

6
NuGet.Config Normal file
View File

@@ -0,0 +1,6 @@
<configuration>
<packageSources>
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
<add key="nadeko.bot" value="https://www.myget.org/F/nadeko/api/v3/index.json" protocolVersion="3" />
</packageSources>
</configuration>

View File

@@ -13,7 +13,15 @@ do
fi
done
# fix folder permissions
# creds.yml migration
if [ -f /app/creds.yml ]; then
echo "Default location for creds.yml is now /app/data/creds.yml."
echo "Please move your creds.yml and update your docker-compose.yml accordingly."
export Nadeko_creds=/app/creds.yml
fi
# ensure nadeko can write on /app/data
chown -R nadeko:nadeko "$data"
# drop to regular user and launch command

View File

@@ -1,23 +1,23 @@
## Custom Reactions / Expressions
## Expressions
### Important
- For modifying **global** custom reactions, the ones which will work across all the servers your bot is connected to, you **must** be a Bot Owner.
You must also use the commands for adding, deleting and listing these reactions in a direct message with the bot.
- For modifying **local** custom reactions, the ones which will only work on the server that they are added on, it is required to have the **Administrator** permission.
You must also use the commands for adding, deleting and listing these reactions in the server you want the custom reactions to work on.
- For modifying **global** expressions, the ones which will work across all the servers your bot is connected to, you **must** be a Bot Owner.
You must also use the commands for adding, deleting and listing these expressions in a direct message with the bot.
- For modifying **local** expressions, the ones which will only work on the server that they are added on, it is required to have the **Administrator** permission.
You must also use the commands for adding, deleting and listing these expressions in the server you want the expressions to work on.
### Commands and Their Use
| Command Name | Description | Example |
| :----------: | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------- |
| `.acr` | Add a custom reaction with a trigger and a response. Running this command in a server requries the Administrator permission. Running this command in DM is Bot Owner only, and adds a new global custom reaction. | `.acr "hello" Hi there, %user%!` |
| `.lcr` | Lists a page of global or server custom reactions (15 reactions per page). Running this command in a DM will list the global custom reactions, while running it in a server will list that server's custom reactions. | `.lcr 1` |
| `.dcr` | Deletes a custom reaction based on the provided index. Running this command in a server requires the Administrator permission. Running this command in DM is Bot Owner only, and will delete a global custom reaction. | `.dcr 5` |
| `.exa` | Add an expression with a trigger and a response. Running this command in a server requries the Administrator permission. Running this command in DM is Bot Owner only, and adds a new global expression. | `.exadd "hello" Hi there, %user%!` |
| `exl` | Lists a page of global or server expression(15 expressions per page). Running this command in a DM will list the global expression, while running it in a server will list that server's expression. | `.exl 1` |
| `.exd` | Deletes an expression based on the provided index. Running this command in a server requires the Administrator permission. Running this command in DM is Bot Owner only, and will delete a global expression. | `.exd 5` |
#### Now that we know the commands let's take a look at an example of adding a command with `.acr`,
#### Now that we know the commands let's take a look at an example of adding a command with `.exa`,
`.acr "Nice Weather" It sure is, %user%!`
`.exadd "Nice Weather" It sure is, %user%!`
This command can be split into two different arguments:
@@ -28,16 +28,16 @@ An important thing to note about the triger is that, to be more than one word, w
There's no special requirement for the formatting of the response, so we could just write it in exactly the same way we want it to respond, albeit with a placeholder - which will be explained in this next section.
Now, if that command was ran in a server, anyone on that server can make the bot mention them, saying `It sure is, @Username` anytime they say "Nice Weather". If the command is ran in a direct message with the bot, then the custom reaction can be used on every server the bot is connected to.
Now, if that command was ran in a server, anyone on that server can make the bot mention them, saying `It sure is, @Username` anytime they say "Nice Weather". If the command is ran in a direct message with the bot, then the expression can be used on every server the bot is connected to.
### Block global Custom Reactions
### Block global Expressions
If you want to disable a global custom reaction which you do not like, and you do not want to remove it, or you are not the bot owner, you can do so by adding a new Custom Reaction with the same trigger on your server, and set the response to `-`.
If you want to disable a global expression which you do not like, and you do not want to remove it, or you are not the bot owner, you can do so by adding a new expression with the same trigger on your server, and set the response to `-`.
For example:
`.acr /o/ -`
`.exa /o/ -`
Now if you try to trigger `/o/`, it won't print anything even if there is a global custom reaction with the same name.
Now if you try to trigger `/o/`, it won't print anything even if there is a global expression with the same name.
### Placeholders!

View File

@@ -10,7 +10,7 @@ Donating to us also gives you the following benefits:
- A hoisted **Donators role** in our [Discord server][discord-server]
- Access to exclusive **#noticed** text and voice channels
- **1000 flowers** on the public bot per dollar donated (after fees)
- **Custom Reactions** on the public bot for [Patreon pledges][patreon] of $5 or higher
- **Expressions** on the public bot for [Patreon pledges][patreon] of $5 or higher
## Patreon

View File

@@ -17,7 +17,7 @@ It is recommended that you use **Ubuntu 20.04**, as there have been nearly no pr
##### Compatible operating systems:
- Ubuntu: 16.04, 18.04, 20.04, 21.04, 21.10
- Ubuntu: 16.04, 18.04, 20.04, 21.04, 21.10 22.04
- Mint: 19, 20
- Debian: 9, 10
- CentOS: 7
@@ -63,9 +63,20 @@ Open Terminal (if you're on an installation with a window manager) and navigate
4. Run the bot (type `3` and press enter)
5. 🎉
## **⚠ IF YOU ARE FOLLOWING THE GUIDE ABOVE, IGNORE THIS SECTION ⚠**
## Linux Release
**⚠ IF YOU ARE FOLLOWING THE GUIDE ABOVE, IGNORE THIS SECTION ⚠**
###### Prerequisites
1. (Optional) Installing Redis
- ubuntu installation command: `sudo apt-get install redis-server`
2. Playing music requires `ffmpeg`, `libopus`, `libsodium` and `youtube-dl` (which in turn requires python3)
- ubuntu installation command: `sudo apt-get install ffmpeg libopus0 opus-tools libopus-dev libsodium-dev -y`
3. Make sure your python is version 3+ with `python --version`
- if it's not, you can install python 3 and make it the default with: `sudo apt-get install python3.8 python-is-python3`
*You can use nadeko bash script [prerequisites installer](https://gitlab.com/Kwoth/nadeko-bash-installer/-/blob/v4/n-prereq.sh) as a reference*
##### Installation Instructions
@@ -92,19 +103,6 @@ Open Terminal (if you're on an installation with a window manager) and navigate
##### Release Update Instructions
###### Prerequisites
1. Nadeko requires redis to function
- ubuntu installation command: `sudo apt-get install redis-server`
2. Playing music requires `ffmpeg`, `libopus`, `libsodium` and `youtube-dl` (which in turn requires python3)
- ubuntu installation command: `sudo apt-get install ffmpeg libopus0 opus-tools libopus-dev libsodium-dev -y`
3. Make sure your python is version 3+ with `python --version`
- if it's not, you can install python 3 and make it the default with: `sudo apt-get install python3.8 python-is-python3`
*You can use nadeko bash script [prerequisites installer](https://gitlab.com/Kwoth/nadeko-bash-installer/-/blob/v4/n-prereq.sh) as a reference*
###### Installation
1. Stop the bot
2. Download the latest release from <https://gitlab.com/Kwoth/nadekobot/-/releases>
- Look for the file called "x.x.x-linux-x64-build.tar" (where `X.X.X` is a version, for example 3.0.4) and download it

View File

@@ -19,7 +19,7 @@
**Optional**
- [Notepad++] (makes it easier to edit your credentials)
- [Visual Studio Code](https://code.visualstudio.com/Download) (Highly suggested if you plan on editing files)
- [Visual C++ 2010 (x86)] and [Visual C++ 2017 (x64)] (both are required if you want Nadeko to play music - restart Windows after installation)
#### Setup
@@ -31,7 +31,8 @@
![Create a new bot](https://i.imgur.com/JxtRk9e.png "Create a new bot")
- Click on **`DOWNLOAD`** at the lower right
![Bot Setup](https://i.imgur.com/HqAl36p.png "Bot Setup")
- Click on **`Install`** next to **`Redis`**.
- Click on **`Install`** next to **`Redis`**.
- **(Note: Redis is optional unless you are are using the bot on 2000+ servers)**
- Note: If Redis fails to install, install Redis manually here: [Redis Installer](https://github.com/MicrosoftArchive/redis/releases/tag/win-3.0.504) Download and run the **`.msi`** file.
- If you will use the music module, click on **`Install`** next to **`FFMPEG`** and **`Youtube-DL`**.
- If any dependencies fail to install, you can temporarily disable your Windows Defender/AV until you install them. If you don't want to, then read [the last section of this guide](#Manual-Prerequisite-Installation).
@@ -63,9 +64,9 @@ You can still install them manually:
- [ffmpeg-32bit] | [ffmpeg-64bit] - Download the **appropriate version** for your system (32 bit if you're running a 32 bit OS, or 64 if you're running a 64bit OS). Unzip it, and move `ffmpeg.exe` to a path that's in your PATH environment variable. If you don't know what that is, then just move the `ffmpeg.exe` file to NadekoBot/system
- [youtube-dl] - Click to download the file. Then put `youtube-dl.exe` in a path that's in your PATH environment variable. If you don't know what that is, then just move the `youtube-dl.exe` file to NadekoBot/system
### Windows From Source
## **⚠ IF YOU ARE FOLLOWING THE GUIDE ABOVE, IGNORE THIS SECTION ⚠**
⚠ IF YOU ARE FOLLOWING THE GUIDE ABOVE, IGNORE THIS SECTION ⚠
### Windows From Source
##### Prerequisites

View File

@@ -17,8 +17,6 @@ To self-host your own Nadeko, use the guides below:
- [:material-linux: Linux guide][linux-guide]
- [:material-apple: Mac OS guide][macos-guide]
Alternatively, you may also setup the bot [from source][from-source-guide] if you want to modify the code.
In case you need any help, join our [Discord server][discord-server] where we may provide support.
---

View File

@@ -73,14 +73,14 @@ Say you want to only enable NSFW commands for a specific role, just do the follo
2. `.rm NSFW enable Lewd`
- Enables usage of the NSFW module for the Lewd role
#### How do I disable custom reactions from triggering?
#### How do I disable Expressions from triggering?
If you don't want server or global custom reactions, just block the module that controls their usage:
If you don't want server or global Expressions, just block the module that controls their usage:
1. `.sm ActualCustomReactions disable`
- Disables the ActualCustomReactions module from being used
1. `.sm ActualExpressions disable`
- Disables the ActualExpression module from being used
**Note**: The `ActualCustomReactions` module controls the usage of custom reactions. The `CustomReactions` module controls commands related to custom reactions (such as `.acr`, `.lcr`, `.crca`, etc).
**Note**: The `Expressions` module controls the usage of Expressions. The `Expressions` module controls commands related to Expressions (such as `.acr`, `.lcr`, `.crca`, etc).
#### I've broken permissions and am stuck, can I reset permissions?

View File

@@ -1,6 +1,6 @@
# Placeholders
Placeholders are used in Quotes, Custom Reactions, Greet/Bye messages, playing statuses, and a few other places.
Placeholders are used in Quotes, Expressions, Greet/Bye messages, playing statuses, and a few other places.
They can be used to make the message more user friendly, generate random numbers or pictures, etc.
@@ -88,7 +88,7 @@ Some features have their own specific placeholders which are noted in that featu
### Miscellaneous placeholders
- `%rngX-Y%` - Returns a random number between X and Y
- `%target%` - Returns anything the user has written after the trigger (only works on custom reactions)
- `%img:stuff%` - Returns an `imgur.com` search for "stuff" (only works on custom reactions)
- `%target%` - Returns anything the user has written after the trigger (only works on Expressions)
- `%img:stuff%` - Returns an `imgur.com` search for "stuff" (only works on Expressions)
![img](https://puu.sh/B7mgI.png)

View File

@@ -92,4 +92,3 @@ nav:
- medusa/snek-lifecycle.md
- Contribution Guide: contribution-guide.md
- Donate: donate.md
- License: license.md

11
privacy-policy.md Normal file
View File

@@ -0,0 +1,11 @@
# Privacy Policy
## Profile Information
Nadeko stores userids, avatars, usernames, discriminators and nicknames of users who were targeted by or have used commands which require Xp, Clubs or Waifu features (not limited to these, as other features may be added over time).
## Other
Nadeko doesn't do analytics, doesn't store messages, doesn't track users, doesn't store their emails etc.
Nadeko only stores user settings and states as the result of executed commands or as the effect of administration tools (for example warnings or protection commands).
## Sensitive Information
Nadeko doesn't store sensitive information, and users are strongly discouraged from adding their passwords, keys, or other important information as quotes or expressions.

View File

@@ -1,7 +1,6 @@
#nullable disable
using System.Runtime.CompilerServices;
using System.Runtime.CompilerServices;
namespace NadekoBot.Common;
namespace Nadeko.Common;
public class AsyncLazy<T> : Lazy<Task<T>>
{

View File

@@ -0,0 +1,88 @@
using System.Diagnostics;
namespace System.Collections.Generic;
[DebuggerDisplay("{_backingStore.Count}")]
public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ICollection<T> where T : notnull
{
private readonly ConcurrentDictionary<T, bool> _backingStore;
public ConcurrentHashSet()
=> _backingStore = new();
public ConcurrentHashSet(IEnumerable<T> values, IEqualityComparer<T>? comparer = null)
=> _backingStore = new(values.Select(x => new KeyValuePair<T, bool>(x, true)), comparer);
public IEnumerator<T> GetEnumerator()
=> _backingStore.Keys.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
/// <summary>
/// Adds the specified item to the <see cref="ConcurrentHashSet{T}" />.
/// </summary>
/// <param name="item">The item to add.</param>
/// <returns>
/// true if the items was added to the <see cref="ConcurrentHashSet{T}" />
/// successfully; false if it already exists.
/// </returns>
/// <exception cref="T:System.OverflowException">
/// The <see cref="ConcurrentHashSet{T}" />
/// contains too many items.
/// </exception>
public bool Add(T item)
=> _backingStore.TryAdd(item, true);
void ICollection<T>.Add(T item)
=> Add(item);
public void Clear()
=> _backingStore.Clear();
public bool Contains(T item)
=> _backingStore.ContainsKey(item);
public void CopyTo(T[] array, int arrayIndex)
{
ArgumentNullException.ThrowIfNull(array);
if (arrayIndex < 0)
throw new ArgumentOutOfRangeException(nameof(arrayIndex));
if (arrayIndex >= array.Length)
throw new ArgumentOutOfRangeException(nameof(arrayIndex));
CopyToInternal(array, arrayIndex);
}
private void CopyToInternal(T[] array, int arrayIndex)
{
var len = array.Length;
foreach (var (k, _) in _backingStore)
{
if (arrayIndex >= len)
throw new IndexOutOfRangeException(nameof(arrayIndex));
array[arrayIndex++] = k;
}
}
bool ICollection<T>.Remove(T item)
=> TryRemove(item);
public bool TryRemove(T item)
=> _backingStore.TryRemove(item, out _);
public void RemoveWhere(Func<T, bool> predicate)
{
foreach (var elem in this.Where(predicate))
TryRemove(elem);
}
public int Count
=> _backingStore.Count;
public bool IsReadOnly
=> false;
}

View File

@@ -1,8 +1,11 @@
#nullable disable
using NadekoBot.Services.Database.Models;
using System.Collections;
using System.Collections;
namespace NadekoBot.Common.Collections;
namespace Nadeko.Common;
public interface IIndexed
{
int Index { get; set; }
}
public class IndexedCollection<T> : IList<T>
where T : class, IIndexed

View File

@@ -1,6 +1,4 @@
using System.Buffers;
namespace NadekoBot.Extensions;
namespace Nadeko.Common;
// made for expressions because they almost never get added
// and they get looped through constantly
@@ -32,7 +30,15 @@ public static class ArrayExtensions
public static TOut[] Map<TIn, TOut>(this TIn[] arr, Func<TIn, TOut> f)
=> Array.ConvertAll(arr, x => f(x));
public static IReadOnlyCollection<TOut> Map<TIn, TOut>(this IReadOnlyCollection<TIn> col, Func<TIn, TOut> f)
/// <summary>
/// Creates a new array by applying the specified function to every element in the input array
/// </summary>
/// <param name="col">Array to modify</param>
/// <param name="f">Function to apply</param>
/// <typeparam name="TIn">Orignal type of the elements in the array</typeparam>
/// <typeparam name="TOut">Output type of the elements of the array</typeparam>
/// <returns>New array with updated elements</returns>
public static TOut[] Map<TIn, TOut>(this IReadOnlyCollection<TIn> col, Func<TIn, TOut> f)
{
var toReturn = new TOut[col.Count];

View File

@@ -1,8 +1,6 @@
using NadekoBot.Common.Collections;
using NadekoBot.Services.Database.Models;
using System.Security.Cryptography;
namespace NadekoBot.Extensions;
namespace Nadeko.Common;
public static class EnumerableExtensions
{

View File

@@ -0,0 +1,35 @@
using System.Net.Http.Headers;
namespace Nadeko.Common;
public static class HttpClientExtensions
{
public static HttpClient AddFakeHeaders(this HttpClient http)
{
AddFakeHeaders(http.DefaultRequestHeaders);
return http;
}
public static void AddFakeHeaders(this HttpHeaders dict)
{
dict.Clear();
dict.Add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");
dict.Add("User-Agent",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.835.202 Safari/535.1");
}
public static bool IsImage(this HttpResponseMessage msg)
=> IsImage(msg, out _);
public static bool IsImage(this HttpResponseMessage msg, out string? mimeType)
{
mimeType = msg.Content.Headers.ContentType?.MediaType;
if (mimeType is "image/png" or "image/jpeg" or "image/gif")
return true;
return false;
}
public static long GetContentLength(this HttpResponseMessage msg)
=> msg.Content.Headers.ContentLength ?? long.MaxValue;
}

View File

@@ -1,5 +1,4 @@
namespace NadekoBot.Extensions;
namespace Nadeko.Common;
public delegate TOut PipeFunc<TIn, out TOut>(in TIn a);
public delegate TOut PipeFunc<TIn1, TIn2, out TOut>(in TIn1 a, in TIn2 b);

View File

@@ -0,0 +1 @@
global using NonBlocking;

View File

@@ -1,9 +1,9 @@
#nullable disable
using Serilog.Events;
using Serilog.Sinks.SystemConsole.Themes;
using System.Text;
using Serilog;
namespace NadekoBot.Services;
namespace Nadeko.Common;
public static class LogSetup
{

View File

@@ -1,5 +1,4 @@
#nullable disable
namespace NadekoBot.Services;
namespace Nadeko.Common;
public static class StandardConversions
{

View File

@@ -1,7 +1,6 @@
#nullable disable
using System.Runtime.CompilerServices;
using System.Runtime.CompilerServices;
namespace NadekoBot.Common;
namespace Nadeko.Common;
// needs proper invalid input check (character array input out of range)
// needs negative number support
@@ -90,7 +89,7 @@ public readonly struct kwum : IEquatable<kwum>
return new(chars);
}
public override bool Equals(object obj)
public override bool Equals(object? obj)
=> obj is kwum kw && kw == this;
public bool Equals(kwum other)

View File

@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="NonBlocking" Version="2.1.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="4.0.1" />
</ItemGroup>
</Project>

View File

@@ -1,7 +1,7 @@
#nullable disable
using System.Security.Cryptography;
namespace NadekoBot.Common;
namespace Nadeko.Common;
public class NadekoRandom : Random
{

View File

@@ -0,0 +1,63 @@
using System.Threading.Channels;
using Serilog;
namespace Nadeko.Common;
public sealed class QueueRunner
{
private readonly Channel<Func<Task>> _channel;
private readonly int _delayMs;
public QueueRunner(int delayMs = 0, int maxCapacity = -1)
{
if (delayMs < 0)
throw new ArgumentOutOfRangeException(nameof(delayMs));
_delayMs = delayMs;
_channel = maxCapacity switch
{
0 or < -1 => throw new ArgumentOutOfRangeException(nameof(maxCapacity)),
-1 => Channel.CreateUnbounded<Func<Task>>(new UnboundedChannelOptions()
{
SingleReader = true,
SingleWriter = false,
AllowSynchronousContinuations = true,
}),
_ => Channel.CreateBounded<Func<Task>>(new BoundedChannelOptions(maxCapacity)
{
Capacity = maxCapacity,
FullMode = BoundedChannelFullMode.DropOldest,
SingleReader = true,
SingleWriter = false,
AllowSynchronousContinuations = true
})
};
}
public async Task RunAsync(CancellationToken cancel = default)
{
while (true)
{
var func = await _channel.Reader.ReadAsync(cancel);
try
{
await func();
}
catch (Exception ex)
{
Log.Warning(ex, "Exception executing a staggered func: {ErrorMessage}", ex.Message);
}
finally
{
if (_delayMs != 0)
{
await Task.Delay(_delayMs, cancel);
}
}
}
}
public ValueTask EnqueueAsync(Func<Task> action)
=> _channel.Writer.WriteAsync(action);
}

View File

@@ -0,0 +1,19 @@
namespace Nadeko.Common;
public readonly struct ShmartBankAmount
{
public long Amount { get; }
public ShmartBankAmount(long amount)
{
Amount = amount;
}
public static implicit operator ShmartBankAmount(long num)
=> new(num);
public static implicit operator long(ShmartBankAmount num)
=> num.Amount;
public static implicit operator ShmartBankAmount(int num)
=> new(num);
}

View File

@@ -1,17 +1,12 @@
#nullable disable
using System;
namespace Nadeko.Common;
namespace NadekoBot.Common;
public struct ShmartNumber : IEquatable<ShmartNumber>
public readonly struct ShmartNumber : IEquatable<ShmartNumber>
{
public long Value { get; }
public string Input { get; }
public ShmartNumber(long val, string input = null)
public ShmartNumber(long val)
{
Value = val;
Input = input;
}
public static implicit operator ShmartNumber(long num)
@@ -26,14 +21,14 @@ public struct ShmartNumber : IEquatable<ShmartNumber>
public override string ToString()
=> Value.ToString();
public override bool Equals(object obj)
public override bool Equals(object? obj)
=> obj is ShmartNumber sn && Equals(sn);
public bool Equals(ShmartNumber other)
=> other.Value == Value;
public override int GetHashCode()
=> Value.GetHashCode() ^ Input.GetHashCode(StringComparison.InvariantCulture);
=> Value.GetHashCode();
public static bool operator ==(ShmartNumber left, ShmartNumber right)
=> left.Equals(right);

View File

@@ -1,5 +1,5 @@
#nullable disable
namespace NadekoBot.Modules.Gambling.Common;
namespace Nadeko.Econ;
public class Deck
{
@@ -272,7 +272,7 @@ public class Deck
public string GetValueText()
=> _cardNames[Number];
public override string ToString()
=> _cardNames[Number] + " Of " + Suit;

View File

@@ -0,0 +1,5 @@
namespace Nadeko.Econ;
public abstract record class NewCard<TSuit, TValue>(TSuit Suit, TValue Value)
where TSuit : struct, Enum
where TValue : struct, Enum;

View File

@@ -0,0 +1,54 @@
namespace Nadeko.Econ;
public abstract class NewDeck<TCard, TSuit, TValue>
where TCard: NewCard<TSuit, TValue>
where TSuit : struct, Enum
where TValue : struct, Enum
{
protected static readonly TSuit[] _suits = Enum.GetValues<TSuit>();
protected static readonly TValue[] _values = Enum.GetValues<TValue>();
public virtual int CurrentCount
=> _cards.Count;
public virtual int TotalCount { get; }
protected readonly LinkedList<TCard> _cards = new();
public NewDeck()
{
TotalCount = _suits.Length * _values.Length;
}
public virtual TCard? Draw()
{
var first = _cards.First;
if (first is not null)
{
_cards.RemoveFirst();
return first.Value;
}
return null;
}
public virtual TCard? Peek(int x = 0)
{
var card = _cards.First;
for (var i = 0; i < x; i++)
{
card = card?.Next;
}
return card?.Value;
}
public virtual void Shuffle()
{
var cards = _cards.ToList();
var newCards = cards.Shuffle();
_cards.Clear();
foreach (var card in newCards)
_cards.AddFirst(card);
}
}

View File

@@ -0,0 +1,28 @@
namespace Nadeko.Econ;
public class MultipleRegularDeck : NewDeck<RegularCard, RegularSuit, RegularValue>
{
private int Decks { get; }
public override int TotalCount { get; }
public MultipleRegularDeck(int decks = 1)
{
if (decks < 1)
throw new ArgumentOutOfRangeException(nameof(decks), "Has to be more than 0");
Decks = decks;
TotalCount = base.TotalCount * decks;
for (var i = 0; i < Decks; i++)
{
foreach (var suit in _suits)
{
foreach (var val in _values)
{
_cards.AddLast((RegularCard)Activator.CreateInstance(typeof(RegularCard), suit, val)!);
}
}
}
}
}

View File

@@ -0,0 +1,4 @@
namespace Nadeko.Econ;
public sealed record class RegularCard(RegularSuit Suit, RegularValue Value)
: NewCard<RegularSuit, RegularValue>(Suit, Value);

View File

@@ -0,0 +1,15 @@
namespace Nadeko.Econ;
public sealed class RegularDeck : NewDeck<RegularCard, RegularSuit, RegularValue>
{
public RegularDeck()
{
foreach (var suit in _suits)
{
foreach (var val in _values)
{
_cards.AddLast((RegularCard)Activator.CreateInstance(typeof(RegularCard), suit, val)!);
}
}
}
}

View File

@@ -0,0 +1,56 @@
namespace Nadeko.Econ;
public static class RegularDeckExtensions
{
public static string GetEmoji(this RegularSuit suit)
=> suit switch
{
RegularSuit.Hearts => "♥️",
RegularSuit.Spades => "♠️",
RegularSuit.Diamonds => "♦️",
_ => "♣️",
};
public static string GetEmoji(this RegularValue value)
=> value switch
{
RegularValue.Ace => "🇦",
RegularValue.Two => "2⃣",
RegularValue.Three => "3⃣",
RegularValue.Four => "4⃣",
RegularValue.Five => "5⃣",
RegularValue.Six => "6⃣",
RegularValue.Seven => "7⃣",
RegularValue.Eight => "8⃣",
RegularValue.Nine => "9⃣",
RegularValue.Ten => "🔟",
RegularValue.Jack => "🇯",
RegularValue.Queen => "🇶",
_ => "🇰",
};
public static string GetEmoji(this RegularCard card)
=> $"{card.Value.GetEmoji()} {card.Suit.GetEmoji()}";
public static string GetName(this RegularValue value)
=> value.ToString();
public static string GetName(this RegularSuit suit)
=> suit.ToString();
public static string GetName(this RegularCard card)
=> $"{card.Value.ToString()} of {card.Suit.GetName()}";
}

View File

@@ -0,0 +1,9 @@
namespace Nadeko.Econ;
public enum RegularSuit
{
Hearts,
Diamonds,
Clubs,
Spades
}

View File

@@ -0,0 +1,18 @@
namespace Nadeko.Econ;
public enum RegularValue
{
Ace = 1,
Two = 2,
Three = 3,
Four = 4,
Five = 5,
Six = 6,
Seven = 7,
Eight = 8,
Nine = 9,
Ten = 10,
Jack = 12,
Queen = 13,
King = 14,
}

View File

@@ -0,0 +1,7 @@
namespace Nadeko.Econ.Gambling.Betdraw;
public enum BetdrawColorGuess
{
Red,
Black
}

View File

@@ -0,0 +1,86 @@
using Serilog;
namespace Nadeko.Econ.Gambling.Betdraw;
public sealed class BetdrawGame
{
private static readonly NadekoRandom _rng = new();
private readonly RegularDeck _deck;
private const decimal SINGLE_GUESS_MULTI = 2.075M;
private const decimal DOUBLE_GUESS_MULTI = 4.15M;
public BetdrawGame()
{
_deck = new RegularDeck();
}
public BetdrawResult Draw(BetdrawValueGuess? val, BetdrawColorGuess? col, decimal amount)
{
if (val is null && col is null)
throw new ArgumentNullException(nameof(val));
var card = _deck.Peek(_rng.Next(0, 52))!;
var realVal = (int)card.Value < 7
? BetdrawValueGuess.Low
: BetdrawValueGuess.High;
var realCol = card.Suit is RegularSuit.Diamonds or RegularSuit.Hearts
? BetdrawColorGuess.Red
: BetdrawColorGuess.Black;
// if card is 7, autoloss
if (card.Value == RegularValue.Seven)
{
return new()
{
Won = 0M,
Multiplier = 0M,
ResultType = BetdrawResultType.Lose,
Card = card,
};
}
byte win = 0;
if (val is BetdrawValueGuess valGuess)
{
if (realVal != valGuess)
return new()
{
Won = 0M,
Multiplier = 0M,
ResultType = BetdrawResultType.Lose,
Card = card
};
++win;
}
if (col is BetdrawColorGuess colGuess)
{
if (realCol != colGuess)
return new()
{
Won = 0M,
Multiplier = 0M,
ResultType = BetdrawResultType.Lose,
Card = card
};
++win;
}
var multi = win == 1
? SINGLE_GUESS_MULTI
: DOUBLE_GUESS_MULTI;
return new()
{
Won = amount * multi,
Multiplier = multi,
ResultType = BetdrawResultType.Win,
Card = card
};
}
}

View File

@@ -0,0 +1,9 @@
namespace Nadeko.Econ.Gambling.Betdraw;
public readonly struct BetdrawResult
{
public decimal Won { get; init; }
public decimal Multiplier { get; init; }
public BetdrawResultType ResultType { get; init; }
public RegularCard Card { get; init; }
}

View File

@@ -0,0 +1,7 @@
namespace Nadeko.Econ.Gambling.Betdraw;
public enum BetdrawResultType
{
Win,
Lose
}

View File

@@ -0,0 +1,7 @@
namespace Nadeko.Econ.Gambling.Betdraw;
public enum BetdrawValueGuess
{
High,
Low,
}

View File

@@ -0,0 +1,33 @@
namespace Nadeko.Econ.Gambling;
public sealed class BetflipGame
{
private readonly decimal _winMulti;
private static readonly NadekoRandom _rng = new NadekoRandom();
public BetflipGame(decimal winMulti)
{
_winMulti = winMulti;
}
public BetflipResult Flip(byte guess, decimal amount)
{
var side = (byte)_rng.Next(0, 2);
if (side == guess)
{
return new BetflipResult()
{
Side = side,
Won = amount * _winMulti,
Multiplier = _winMulti
};
}
return new BetflipResult()
{
Side = side,
Won = 0,
Multiplier = 0,
};
}
}

View File

@@ -0,0 +1,8 @@
namespace Nadeko.Econ.Gambling;
public readonly struct BetflipResult
{
public decimal Won { get; init; }
public byte Side { get; init; }
public decimal Multiplier { get; init; }
}

View File

@@ -0,0 +1,42 @@
namespace Nadeko.Econ.Gambling;
public sealed class BetrollGame
{
private readonly (int WhenAbove, decimal MultiplyBy)[] _thresholdPairs;
private readonly NadekoRandom _rng;
public BetrollGame(IReadOnlyList<(int WhenAbove, decimal MultiplyBy)> pairs)
{
_thresholdPairs = pairs.OrderByDescending(x => x.WhenAbove).ToArray();
_rng = new();
}
public BetrollResult Roll(decimal amount = 0)
{
var roll = _rng.Next(1, 101);
for (var i = 0; i < _thresholdPairs.Length; i++)
{
ref var pair = ref _thresholdPairs[i];
if (pair.WhenAbove < roll)
{
return new()
{
Multiplier = pair.MultiplyBy,
Roll = roll,
Threshold = pair.WhenAbove,
Won = amount * pair.MultiplyBy
};
}
}
return new()
{
Multiplier = 0,
Roll = roll,
Threshold = -1,
Won = 0,
};
}
}

View File

@@ -0,0 +1,9 @@
namespace Nadeko.Econ.Gambling;
public readonly struct BetrollResult
{
public int Roll { get; init; }
public decimal Multiplier { get; init; }
public decimal Threshold { get; init; }
public decimal Won { get; init; }
}

View File

@@ -0,0 +1,75 @@
namespace Nadeko.Econ.Gambling.Rps;
public sealed class RpsGame
{
private static readonly NadekoRandom _rng = new NadekoRandom();
const decimal WIN_MULTI = 1.95m;
const decimal DRAW_MULTI = 1m;
const decimal LOSE_MULTI = 0m;
public RpsGame()
{
}
public RpsResult Play(RpsPick pick, decimal amount)
{
var compPick = (RpsPick)_rng.Next(0, 3);
if (compPick == pick)
{
return new()
{
Won = amount * DRAW_MULTI,
Multiplier = DRAW_MULTI,
ComputerPick = compPick,
Result = RpsResultType.Draw,
};
}
if ((compPick == RpsPick.Paper && pick == RpsPick.Rock)
|| (compPick == RpsPick.Rock && pick == RpsPick.Scissors)
|| (compPick == RpsPick.Scissors && pick == RpsPick.Paper))
{
return new()
{
Won = amount * LOSE_MULTI,
Multiplier = LOSE_MULTI,
Result = RpsResultType.Lose,
ComputerPick = compPick,
};
}
return new()
{
Won = amount * WIN_MULTI,
Multiplier = WIN_MULTI,
Result = RpsResultType.Win,
ComputerPick = compPick,
};
}
}
public enum RpsPick : byte
{
Rock = 0,
Paper = 1,
Scissors = 2,
}
public enum RpsResultType : byte
{
Win,
Draw,
Lose
}
public readonly struct RpsResult
{
public decimal Won { get; init; }
public decimal Multiplier { get; init; }
public RpsResultType Result { get; init; }
public RpsPick ComputerPick { get; init; }
}

View File

@@ -0,0 +1,113 @@
namespace Nadeko.Econ.Gambling;
public class SlotGame
{
private static readonly NadekoRandom _rng = new NadekoRandom();
public SlotResult Spin(decimal bet)
{
var rolls = new[]
{
(byte)_rng.Next(0, 6),
(byte)_rng.Next(0, 6),
(byte)_rng.Next(0, 6)
};
ref var a = ref rolls[0];
ref var b = ref rolls[1];
ref var c = ref rolls[2];
var multi = 0;
var winType = SlotWinType.None;
if (a == b && b == c)
{
if (a == 5)
{
winType = SlotWinType.TrippleJoker;
multi = 30;
}
else
{
winType = SlotWinType.TrippleNormal;
multi = 10;
}
}
else if (a == 5 && (b == 5 || c == 5)
|| (b == 5 && c == 5))
{
winType = SlotWinType.DoubleJoker;
multi = 4;
}
else if (a == 5 || b == 5 || c == 5)
{
winType = SlotWinType.SingleJoker;
multi = 1;
}
return new()
{
Won = bet * multi,
WinType = winType,
Multiplier = multi,
Rolls = rolls,
};
}
}
public enum SlotWinType : byte
{
None,
SingleJoker,
DoubleJoker,
TrippleNormal,
TrippleJoker,
}
/*
var rolls = new[]
{
_rng.Next(default(byte), 6),
_rng.Next(default(byte), 6),
_rng.Next(default(byte), 6)
};
var multi = 0;
var winType = SlotWinType.None;
ref var a = ref rolls[0];
ref var b = ref rolls[1];
ref var c = ref rolls[2];
if (a == b && b == c)
{
if (a == 5)
{
winType = SlotWinType.TrippleJoker;
multi = 30;
}
else
{
winType = SlotWinType.TrippleNormal;
multi = 10;
}
}
else if (a == 5 && (b == 5 || c == 5)
|| (b == 5 && c == 5))
{
winType = SlotWinType.DoubleJoker;
multi = 4;
}
else if (rolls.Any(x => x == 5))
{
winType = SlotWinType.SingleJoker;
multi = 1;
}
return new()
{
Won = bet * multi,
WinType = winType,
Multiplier = multi,
Rolls = rolls,
};
}
*/

View File

@@ -0,0 +1,9 @@
namespace Nadeko.Econ.Gambling;
public readonly struct SlotResult
{
public decimal Multiplier { get; init; }
public byte[] Rolls { get; init; }
public decimal Won { get; init; }
public SlotWinType WinType { get; init; }
}

View File

@@ -0,0 +1,9 @@
namespace Nadeko.Econ.Gambling;
public readonly struct LuLaResult
{
public int Index { get; init; }
public decimal Multiplier { get; init; }
public decimal Won { get; init; }
public IReadOnlyList<decimal> Multipliers { get; init; }
}

View File

@@ -0,0 +1,34 @@
namespace Nadeko.Econ.Gambling;
public sealed class LulaGame
{
private static readonly IReadOnlyList<decimal> DEFAULT_MULTIPLIERS = new[] { 1.7M, 1.5M, 0.2M, 0.1M, 0.3M, 0.5M, 1.2M, 2.4M };
private readonly IReadOnlyList<decimal> _multipliers;
private static readonly NadekoRandom _rng = new();
public LulaGame(IReadOnlyList<decimal> multipliers)
{
_multipliers = multipliers;
}
public LulaGame() : this(DEFAULT_MULTIPLIERS)
{
}
public LuLaResult Spin(long bet)
{
var result = _rng.Next(0, _multipliers.Count);
var multi = _multipliers[result];
var amount = bet * multi;
return new()
{
Index = result,
Multiplier = multi,
Won = amount,
Multipliers = _multipliers.ToArray(),
};
}
}

View File

@@ -0,0 +1 @@
global using Nadeko.Common;

View File

@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Nadeko.Common\Nadeko.Common.csproj" />
</ItemGroup>
</Project>

View File

@@ -47,7 +47,7 @@ public static class MedusaExtensions
=> ctx.Message.AddReactionAsync(new Emoji("🤔"));
public static Task<IUserMessage> ErrorLocalizedAsync(this AnyContext ctx, string key, params object[]? args)
=> ctx.SendErrorAsync(ctx.GetText(key));
=> ctx.SendErrorAsync(ctx.GetText(key, args));
public static Task<IUserMessage> PendingLocalizedAsync(this AnyContext ctx, string key, params object[]? args)
=> ctx.SendPendingAsync(ctx.GetText(key, args));
@@ -56,11 +56,11 @@ public static class MedusaExtensions
=> ctx.SendConfirmAsync(ctx.GetText(key, args));
public static Task<IUserMessage> ReplyErrorLocalizedAsync(this AnyContext ctx, string key, params object[]? args)
=> ctx.SendErrorAsync($"{Format.Bold(ctx.User.ToString())} {ctx.GetText(key)}");
=> ctx.SendErrorAsync($"{Format.Bold(ctx.User.ToString())} {ctx.GetText(key, args)}");
public static Task<IUserMessage> ReplyPendingLocalizedAsync(this AnyContext ctx, string key, params object[]? args)
=> ctx.SendPendingAsync($"{Format.Bold(ctx.User.ToString())} {ctx.GetText(key)}");
=> ctx.SendPendingAsync($"{Format.Bold(ctx.User.ToString())} {ctx.GetText(key, args)}");
public static Task<IUserMessage> ReplyConfirmLocalizedAsync(this AnyContext ctx, string key, params object[]? args)
=> ctx.SendConfirmAsync($"{Format.Bold(ctx.User.ToString())} {ctx.GetText(key)}");
=> ctx.SendConfirmAsync($"{Format.Bold(ctx.User.ToString())} {ctx.GetText(key, args)}");
}

View File

@@ -5,7 +5,7 @@ namespace NadekoBot;
public interface IEmbedBuilder
{
IEmbedBuilder WithDescription(string? desc);
IEmbedBuilder WithTitle(string title);
IEmbedBuilder WithTitle(string? title);
IEmbedBuilder AddField(string title, object value, bool isInline = false);
IEmbedBuilder WithFooter(string text, string? iconUrl = null);
IEmbedBuilder WithAuthor(string name, string? iconUrl = null, string? url = null);

View File

@@ -9,12 +9,11 @@
<RootNamespace>Nadeko.Snake</RootNamespace>
<Authors>The NadekoBot Team</Authors>
<Version>1.0.2</Version>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Discord.Net.Core" Version="3.5.0" />
<PackageReference Include="Serilog" Version="2.10.0" />
<PackageReference Include="Discord.Net.Core" Version="3.103.0" />
<PackageReference Include="Serilog" Version="2.11.0" />
<PackageReference Include="YamlDotNet" Version="11.2.1" />
</ItemGroup>

View File

@@ -9,10 +9,10 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Grpc.AspNetCore" Version="2.44.0" />
<PackageReference Include="Serilog" Version="2.10.0" />
<PackageReference Include="Grpc.AspNetCore" Version="2.47.0" />
<PackageReference Include="Serilog" Version="2.11.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="4.0.1" />
<PackageReference Include="Serilog.Sinks.File" Version="4.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageReference Include="YamlDotNet" Version="11.2.1" />
</ItemGroup>

View File

@@ -1,322 +1,336 @@
#nullable enable
using System;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
namespace NadekoBot.Generators.Command;
[Generator]
public class CommandAttributesGenerator : IIncrementalGenerator
{
public const string ATTRIBUTE = @"// <AutoGenerated />
namespace NadekoBot.Common;
[System.AttributeUsage(System.AttributeTargets.Method)]
public class CmdAttribute : System.Attribute
{
}";
public class MethodModel
{
public string? Namespace { get; }
public IReadOnlyCollection<string> Classes { get; }
public string ReturnType { get; }
public string MethodName { get; }
public IEnumerable<string> Params { get; }
public MethodModel(string? ns, IReadOnlyCollection<string> classes, string returnType, string methodName, IEnumerable<string> @params)
{
Namespace = ns;
Classes = classes;
ReturnType = returnType;
MethodName = methodName;
Params = @params;
}
}
public class FileModel
{
public string? Namespace { get; }
public IReadOnlyCollection<string> ClassHierarchy { get; }
public IReadOnlyCollection<MethodModel> Methods { get; }
public FileModel(string? ns, IReadOnlyCollection<string> classHierarchy, IReadOnlyCollection<MethodModel> methods)
{
Namespace = ns;
ClassHierarchy = classHierarchy;
Methods = methods;
}
}
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// #if DEBUG
// SpinWait.SpinUntil(() => Debugger.IsAttached);
// #endif
context.RegisterPostInitializationOutput(static ctx => ctx.AddSource(
"CmdAttribute.g.cs",
SourceText.From(ATTRIBUTE, Encoding.UTF8)));
var methods = context.SyntaxProvider
.CreateSyntaxProvider(
static (node, _) => node is MethodDeclarationSyntax { AttributeLists.Count: > 0 },
static (ctx, cancel) => Transform(ctx, cancel))
.Where(static m => m is not null)
.Where(static m => m?.ChildTokens().Any(static x => x.IsKind(SyntaxKind.PublicKeyword)) ?? false);
var compilationMethods = context.CompilationProvider.Combine(methods.Collect());
context.RegisterSourceOutput(compilationMethods,
static (ctx, tuple) => RegisterAction(in ctx, tuple.Left, in tuple.Right));
}
private static void RegisterAction(in SourceProductionContext ctx,
Compilation comp,
in ImmutableArray<MethodDeclarationSyntax?> methods)
{
if (methods is { IsDefaultOrEmpty: true })
return;
var models = GetModels(comp, methods, ctx.CancellationToken);
foreach (var model in models)
{
var name = $"{model.Namespace}.{string.Join(".", model.ClassHierarchy)}.g.cs";
try
{
var source = GetSourceText(model);
ctx.AddSource(name, SourceText.From(source, Encoding.UTF8));
}
catch (Exception ex)
{
Console.WriteLine($"Error writing source file {name}\n" + ex);
}
}
}
private static string GetSourceText(FileModel model)
{
using var sw = new StringWriter();
using var tw = new IndentedTextWriter(sw);
tw.WriteLine("// <AutoGenerated />");
tw.WriteLine("#pragma warning disable CS1066");
if (model.Namespace is not null)
{
tw.WriteLine($"namespace {model.Namespace};");
tw.WriteLine();
}
foreach (var className in model.ClassHierarchy)
{
tw.WriteLine($"public partial class {className}");
tw.WriteLine("{");
tw.Indent ++;
}
foreach (var method in model.Methods)
{
tw.WriteLine("[NadekoCommand]");
tw.WriteLine("[NadekoDescription]");
tw.WriteLine("[Aliases]");
tw.WriteLine($"public partial {method.ReturnType} {method.MethodName}({string.Join(", ", method.Params)});");
}
foreach (var _ in model.ClassHierarchy)
{
tw.Indent --;
tw.WriteLine("}");
}
tw.Flush();
return sw.ToString();
}
private static IReadOnlyCollection<FileModel> GetModels(Compilation compilation,
in ImmutableArray<MethodDeclarationSyntax?> inputMethods,
CancellationToken cancel)
{
var models = new List<FileModel>();
var methods = inputMethods
.Where(static x => x is not null)
.Distinct();
var methodModels = methods
.Select(x => MethodDeclarationToMethodModel(compilation, x!));
var groups = methodModels
.GroupBy(static x => $"{x.Namespace}.{string.Join(".", x.Classes)}");
foreach (var group in groups)
{
if (cancel.IsCancellationRequested)
return new Collection<FileModel>();
if (group is null)
continue;
var elems = group.ToList();
if (elems.Count is 0)
continue;
var model = new FileModel(
methods: elems,
ns: elems[0].Namespace,
classHierarchy: elems[0].Classes
);
models.Add(model);
}
return models;
}
private static MethodModel MethodDeclarationToMethodModel(Compilation comp, MethodDeclarationSyntax decl)
{
// SpinWait.SpinUntil(static () => Debugger.IsAttached);
var semanticModel = comp.GetSemanticModel(decl.SyntaxTree);
var methodModel = new MethodModel(
@params: decl.ParameterList.Parameters
.Where(p => p.Type is not null)
.Select(p =>
{
var prefix = p.Modifiers.Any(static x => x.IsKind(SyntaxKind.ParamsKeyword))
? "params "
: string.Empty;
var type = semanticModel
.GetTypeInfo(p.Type!)
.Type
?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
var name = p.Identifier.Text;
var suffix = string.Empty;
if (p.Default is not null)
{
if (p.Default.Value is LiteralExpressionSyntax)
{
suffix = " = " + p.Default.Value;
}
else if (p.Default.Value is MemberAccessExpressionSyntax maes)
{
var maesSemModel = comp.GetSemanticModel(maes.SyntaxTree);
var sym = maesSemModel.GetSymbolInfo(maes.Name);
if (sym.Symbol is null)
{
suffix = " = " + p.Default.Value;
}
else
{
suffix = " = " + sym.Symbol.ToDisplayString();
}
}
}
return $"{prefix}{type} {name}{suffix}";
})
.ToList(),
methodName: decl.Identifier.Text,
returnType: decl.ReturnType.ToString(),
ns: GetNamespace(decl),
classes: GetClasses(decl)
);
return methodModel;
}
//https://github.com/andrewlock/NetEscapades.EnumGenerators/blob/main/src/NetEscapades.EnumGenerators/EnumGenerator.cs
static string? GetNamespace(MethodDeclarationSyntax declarationSyntax)
{
// determine the namespace the class is declared in, if any
string? nameSpace = null;
var parentOfInterest = declarationSyntax.Parent;
while (parentOfInterest is not null)
{
parentOfInterest = parentOfInterest.Parent;
if (parentOfInterest is BaseNamespaceDeclarationSyntax ns)
{
nameSpace = ns.Name.ToString();
while (true)
{
if (ns.Parent is not NamespaceDeclarationSyntax parent)
{
break;
}
ns = parent;
nameSpace = $"{ns.Name}.{nameSpace}";
}
return nameSpace;
}
}
return nameSpace;
}
static IReadOnlyCollection<string> GetClasses(MethodDeclarationSyntax declarationSyntax)
{
// determine the namespace the class is declared in, if any
var classes = new LinkedList<string>();
var parentOfInterest = declarationSyntax.Parent;
while (parentOfInterest is not null)
{
if (parentOfInterest is ClassDeclarationSyntax cds)
{
classes.AddFirst(cds.Identifier.ToString());
}
parentOfInterest = parentOfInterest.Parent;
}
Debug.WriteLine($"Method {declarationSyntax.Identifier.Text} has {classes.Count} classes");
return classes;
}
private static MethodDeclarationSyntax? Transform(GeneratorSyntaxContext ctx, CancellationToken cancel)
{
var methodDecl = ctx.Node as MethodDeclarationSyntax;
if (methodDecl is null)
return default;
foreach (var attListSyntax in methodDecl.AttributeLists)
{
foreach (var attSyntax in attListSyntax.Attributes)
{
if (cancel.IsCancellationRequested)
return default;
var symbol = ctx.SemanticModel.GetSymbolInfo(attSyntax).Symbol;
if (symbol is not IMethodSymbol attSymbol)
continue;
if (attSymbol.ContainingType.ToDisplayString() == "NadekoBot.Common.CmdAttribute")
return methodDecl;
}
}
return default;
}
}
// #nullable enable
// using System;
// using System.CodeDom.Compiler;
// using System.Collections.Generic;
// using System.Collections.Immutable;
// using System.Collections.ObjectModel;
// using System.Diagnostics;
// using System.IO;
// using System.Linq;
// using System.Text;
// using System.Threading;
// using Microsoft.CodeAnalysis;
// using Microsoft.CodeAnalysis.CSharp;
// using Microsoft.CodeAnalysis.CSharp.Syntax;
// using Microsoft.CodeAnalysis.Text;
//
// namespace NadekoBot.Generators.Command;
//
// [Generator]
// public class CommandAttributesGenerator : IIncrementalGenerator
// {
// public const string ATTRIBUTE = @"// <AutoGenerated />
//
// namespace NadekoBot.Common;
//
// [System.AttributeUsage(System.AttributeTargets.Method)]
// public class CmdAttribute : System.Attribute
// {
//
// }";
//
// public class MethodModel
// {
// public string? Namespace { get; }
// public IReadOnlyCollection<string> Classes { get; }
// public string ReturnType { get; }
// public string MethodName { get; }
// public IEnumerable<string> Params { get; }
//
// public MethodModel(string? ns, IReadOnlyCollection<string> classes, string returnType, string methodName, IEnumerable<string> @params)
// {
// Namespace = ns;
// Classes = classes;
// ReturnType = returnType;
// MethodName = methodName;
// Params = @params;
// }
// }
//
// public class FileModel
// {
// public string? Namespace { get; }
// public IReadOnlyCollection<string> ClassHierarchy { get; }
// public IReadOnlyCollection<MethodModel> Methods { get; }
//
// public FileModel(string? ns, IReadOnlyCollection<string> classHierarchy, IReadOnlyCollection<MethodModel> methods)
// {
// Namespace = ns;
// ClassHierarchy = classHierarchy;
// Methods = methods;
// }
// }
//
// public void Initialize(IncrementalGeneratorInitializationContext context)
// {
// // #if DEBUG
// // if (!Debugger.IsAttached)
// // Debugger.Launch();
// // // SpinWait.SpinUntil(() => Debugger.IsAttached);
// // #endif
// context.RegisterPostInitializationOutput(static ctx => ctx.AddSource(
// "CmdAttribute.g.cs",
// SourceText.From(ATTRIBUTE, Encoding.UTF8)));
//
// var methods = context.SyntaxProvider
// .CreateSyntaxProvider(
// static (node, _) => node is MethodDeclarationSyntax { AttributeLists.Count: > 0 },
// static (ctx, cancel) => Transform(ctx, cancel))
// .Where(static m => m is not null)
// .Where(static m => m?.ChildTokens().Any(static x => x.IsKind(SyntaxKind.PublicKeyword)) ?? false);
//
// var compilationMethods = context.CompilationProvider.Combine(methods.Collect());
//
// context.RegisterSourceOutput(compilationMethods,
// static (ctx, tuple) => RegisterAction(in ctx, tuple.Left, in tuple.Right));
// }
//
// private static void RegisterAction(in SourceProductionContext ctx,
// Compilation comp,
// in ImmutableArray<MethodDeclarationSyntax?> methods)
// {
// if (methods is { IsDefaultOrEmpty: true })
// return;
//
// var models = GetModels(comp, methods, ctx.CancellationToken);
//
// foreach (var model in models)
// {
// var name = $"{model.Namespace}.{string.Join(".", model.ClassHierarchy)}.g.cs";
// try
// {
// var source = GetSourceText(model);
// ctx.AddSource(name, SourceText.From(source, Encoding.UTF8));
// }
// catch (Exception ex)
// {
// Console.WriteLine($"Error writing source file {name}\n" + ex);
// }
// }
// }
//
// private static string GetSourceText(FileModel model)
// {
// using var sw = new StringWriter();
// using var tw = new IndentedTextWriter(sw);
//
// tw.WriteLine("// <AutoGenerated />");
// tw.WriteLine("#pragma warning disable CS1066");
//
// if (model.Namespace is not null)
// {
// tw.WriteLine($"namespace {model.Namespace};");
// tw.WriteLine();
// }
//
// foreach (var className in model.ClassHierarchy)
// {
// tw.WriteLine($"public partial class {className}");
// tw.WriteLine("{");
// tw.Indent ++;
// }
//
// foreach (var method in model.Methods)
// {
// tw.WriteLine("[NadekoCommand]");
// tw.WriteLine("[NadekoDescription]");
// tw.WriteLine("[Aliases]");
// tw.WriteLine($"public partial {method.ReturnType} {method.MethodName}({string.Join(", ", method.Params)});");
// }
//
// foreach (var _ in model.ClassHierarchy)
// {
// tw.Indent --;
// tw.WriteLine("}");
// }
//
// tw.Flush();
// return sw.ToString();
// }
//
// private static IReadOnlyCollection<FileModel> GetModels(Compilation compilation,
// in ImmutableArray<MethodDeclarationSyntax?> inputMethods,
// CancellationToken cancel)
// {
// var models = new List<FileModel>();
//
// var methods = inputMethods
// .Where(static x => x is not null)
// .Distinct();
//
// var methodModels = methods
// .Select(x => MethodDeclarationToMethodModel(compilation, x!))
// .Where(static x => x is not null)
// .Cast<MethodModel>();
//
// var groups = methodModels
// .GroupBy(static x => $"{x.Namespace}.{string.Join(".", x.Classes)}");
//
// foreach (var group in groups)
// {
// if (cancel.IsCancellationRequested)
// return new Collection<FileModel>();
//
// if (group is null)
// continue;
//
// var elems = group.ToList();
// if (elems.Count is 0)
// continue;
//
// var model = new FileModel(
// methods: elems,
// ns: elems[0].Namespace,
// classHierarchy: elems![0].Classes
// );
//
// models.Add(model);
// }
//
//
// return models;
// }
//
// private static MethodModel? MethodDeclarationToMethodModel(Compilation comp, MethodDeclarationSyntax decl)
// {
// // SpinWait.SpinUntil(static () => Debugger.IsAttached);
//
// SemanticModel semanticModel;
// try
// {
// semanticModel = comp.GetSemanticModel(decl.SyntaxTree);
// }
// catch
// {
// // for some reason this method can throw "Not part of this compilation" argument exception
// return null;
// }
//
// var methodModel = new MethodModel(
// @params: decl.ParameterList.Parameters
// .Where(p => p.Type is not null)
// .Select(p =>
// {
// var prefix = p.Modifiers.Any(static x => x.IsKind(SyntaxKind.ParamsKeyword))
// ? "params "
// : string.Empty;
//
// var type = semanticModel
// .GetTypeInfo(p.Type!)
// .Type
// ?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
//
//
// var name = p.Identifier.Text;
//
// var suffix = string.Empty;
// if (p.Default is not null)
// {
// if (p.Default.Value is LiteralExpressionSyntax)
// {
// suffix = " = " + p.Default.Value;
// }
// else if (p.Default.Value is MemberAccessExpressionSyntax maes)
// {
// var maesSemModel = comp.GetSemanticModel(maes.SyntaxTree);
// var sym = maesSemModel.GetSymbolInfo(maes.Name);
// if (sym.Symbol is null)
// {
// suffix = " = " + p.Default.Value;
// }
// else
// {
// suffix = " = " + sym.Symbol.ToDisplayString();
// }
// }
// }
//
// return $"{prefix}{type} {name}{suffix}";
// })
// .ToList(),
// methodName: decl.Identifier.Text,
// returnType: decl.ReturnType.ToString(),
// ns: GetNamespace(decl),
// classes: GetClasses(decl)
// );
//
// return methodModel;
// }
//
// //https://github.com/andrewlock/NetEscapades.EnumGenerators/blob/main/src/NetEscapades.EnumGenerators/EnumGenerator.cs
// static string? GetNamespace(MethodDeclarationSyntax declarationSyntax)
// {
// // determine the namespace the class is declared in, if any
// string? nameSpace = null;
// var parentOfInterest = declarationSyntax.Parent;
// while (parentOfInterest is not null)
// {
// parentOfInterest = parentOfInterest.Parent;
//
// if (parentOfInterest is BaseNamespaceDeclarationSyntax ns)
// {
// nameSpace = ns.Name.ToString();
// while (true)
// {
// if (ns.Parent is not NamespaceDeclarationSyntax parent)
// {
// break;
// }
//
// ns = parent;
// nameSpace = $"{ns.Name}.{nameSpace}";
// }
//
// return nameSpace;
// }
//
// }
//
// return nameSpace;
// }
//
// static IReadOnlyCollection<string> GetClasses(MethodDeclarationSyntax declarationSyntax)
// {
// // determine the namespace the class is declared in, if any
// var classes = new LinkedList<string>();
// var parentOfInterest = declarationSyntax.Parent;
// while (parentOfInterest is not null)
// {
// if (parentOfInterest is ClassDeclarationSyntax cds)
// {
// classes.AddFirst(cds.Identifier.ToString());
// }
//
// parentOfInterest = parentOfInterest.Parent;
// }
//
// Debug.WriteLine($"Method {declarationSyntax.Identifier.Text} has {classes.Count} classes");
//
// return classes;
// }
//
// private static MethodDeclarationSyntax? Transform(GeneratorSyntaxContext ctx, CancellationToken cancel)
// {
// var methodDecl = ctx.Node as MethodDeclarationSyntax;
// if (methodDecl is null)
// return default;
//
// foreach (var attListSyntax in methodDecl.AttributeLists)
// {
// foreach (var attSyntax in attListSyntax.Attributes)
// {
// if (cancel.IsCancellationRequested)
// return default;
//
// var symbol = ctx.SemanticModel.GetSymbolInfo(attSyntax).Symbol;
// if (symbol is not IMethodSymbol attSymbol)
// continue;
//
// if (attSymbol.ContainingType.ToDisplayString() == "NadekoBot.Common.CmdAttribute")
// return methodDecl;
// }
// }
//
// return default;
// }
// }

View File

@@ -21,7 +21,7 @@ namespace NadekoBot.Tests
var stringsSource = new LocalFileStringsSource(
responsesPath,
commandsPath);
var strings = new LocalBotStringsProvider(stringsSource);
var strings = new MemoryBotStringsProvider(stringsSource);
var culture = new CultureInfo("en-US");
@@ -47,7 +47,7 @@ namespace NadekoBot.Tests
|| !(type.GetCustomAttribute<GroupAttribute>(true) is null)) // or a submodule
.SelectMany(x => x.GetMethods()
.Where(mi => mi.CustomAttributes
.Any(ca => ca.AttributeType == typeof(NadekoCommandAttribute))))
.Any(ca => ca.AttributeType == typeof(CmdAttribute))))
.Select(x => x.Name.ToLowerInvariant())
.ToArray();

View File

@@ -0,0 +1,93 @@
using System.Collections.Generic;
using NUnit.Framework;
namespace NadekoBot.Tests;
public class ConcurrentHashSetTests
{
private ConcurrentHashSet<(int?, int?)> _set;
[SetUp]
public void SetUp()
{
_set = new();
}
[Test]
public void AddTest()
{
var result = _set.Add((1, 2));
Assert.AreEqual(true, result);
result = _set.Add((1, 2));
Assert.AreEqual(false, result);
}
[Test]
public void TryRemoveTest()
{
_set.Add((1, 2));
var result = _set.TryRemove((1, 2));
Assert.AreEqual(true, result);
result = _set.TryRemove((1, 2));
Assert.AreEqual(false, result);
}
[Test]
public void CountTest()
{
_set.Add((1, 2)); // 1
_set.Add((1, 2)); // 1
_set.Add((2, 2)); // 2
_set.Add((3, 2)); // 3
_set.Add((3, 2)); // 3
Assert.AreEqual(3, _set.Count);
}
[Test]
public void ClearTest()
{
_set.Add((1, 2));
_set.Add((1, 3));
_set.Add((1, 4));
_set.Clear();
Assert.AreEqual(0, _set.Count);
}
[Test]
public void ContainsTest()
{
_set.Add((1, 2));
_set.Add((3, 2));
Assert.AreEqual(true, _set.Contains((1, 2)));
Assert.AreEqual(true, _set.Contains((3, 2)));
Assert.AreEqual(false, _set.Contains((2, 1)));
Assert.AreEqual(false, _set.Contains((2, 3)));
}
[Test]
public void RemoveWhereTest()
{
_set.Add((1, 2));
_set.Add((1, 3));
_set.Add((1, 4));
_set.Add((2, 5));
// remove tuples which have even second item
_set.RemoveWhere(static x => x.Item2 % 2 == 0);
Assert.AreEqual(2, _set.Count);
Assert.AreEqual(true, _set.Contains((1, 3)));
Assert.AreEqual(true, _set.Contains((2, 5)));
}
}

View File

@@ -1,5 +1,6 @@
using System.Linq;
using System.Threading.Tasks;
using Nadeko.Common;
using NadekoBot.Extensions;
using NadekoBot.Services;
using NUnit.Framework;

View File

@@ -1,4 +1,4 @@
using NadekoBot.Common.Collections;
using Nadeko.Common;
using NadekoBot.Services.Database.Models;
using NUnit.Framework;
using System;

View File

@@ -1,4 +1,5 @@
using NadekoBot.Common;
using Nadeko.Common;
using NadekoBot.Common;
using NUnit.Framework;
namespace NadekoBot.Tests

View File

@@ -8,12 +8,14 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Nadeko.Common\Nadeko.Common.csproj" />
<ProjectReference Include="..\Nadeko.Econ\Nadeko.Econ.csproj" />
<ProjectReference Include="..\NadekoBot\NadekoBot.csproj" />
</ItemGroup>

View File

@@ -0,0 +1,83 @@
using Nadeko.Econ;
using NUnit.Framework;
namespace NadekoBot.Tests;
public class NewDeckTests
{
private RegularDeck _deck;
[SetUp]
public void Setup()
{
_deck = new RegularDeck();
}
[Test]
public void TestCount()
{
Assert.AreEqual(52, _deck.TotalCount);
Assert.AreEqual(52, _deck.CurrentCount);
}
[Test]
public void TestDeckDraw()
{
var card = _deck.Draw();
Assert.IsNotNull(card);
Assert.AreEqual(card.Suit, RegularSuit.Hearts);
Assert.AreEqual(card.Value, RegularValue.Ace);
Assert.AreEqual(_deck.CurrentCount, _deck.TotalCount - 1);
}
[Test]
public void TestDeckSpent()
{
for (var i = 0; i < _deck.TotalCount - 1; ++i)
{
_deck.Draw();
}
var lastCard = _deck.Draw();
Assert.IsNotNull(lastCard);
Assert.AreEqual(new RegularCard(RegularSuit.Spades, RegularValue.King), lastCard);
var noCard = _deck.Draw();
Assert.IsNull(noCard);
}
[Test]
public void TestCardGetName()
{
var ace = _deck.Draw()!;
var two = _deck.Draw()!;
Assert.AreEqual("Ace of Hearts", ace.GetName());
Assert.AreEqual("Two of Hearts", two.GetName());
}
[Test]
public void TestPeek()
{
var ace = _deck.Peek()!;
var tenOfSpades = _deck.Peek(48);
Assert.AreEqual(new RegularCard(RegularSuit.Hearts, RegularValue.Ace), ace);
Assert.AreEqual(new RegularCard(RegularSuit.Spades, RegularValue.Ten), tenOfSpades);
}
[Test]
public void TestMultipleDeck()
{
var quadDeck = new MultipleRegularDeck(4);
var count = quadDeck.TotalCount;
Assert.AreEqual(52 * 4, count);
var card = quadDeck.Peek(54);
Assert.AreEqual(new RegularCard(RegularSuit.Hearts, RegularValue.Three), card);
}
}

View File

@@ -7,7 +7,7 @@
<ItemGroup>
<PackageReference Include="MorseCode.ITask" Version="2.0.3" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.3.2" />
</ItemGroup>
</Project>

View File

@@ -4,10 +4,13 @@ using NadekoBot.Common.Configs;
using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Db;
using NadekoBot.Modules.Administration;
using NadekoBot.Modules.Utility;
using NadekoBot.Services.Database.Models;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Net;
using System.Reflection;
using Nadeko.Common;
using RunMode = Discord.Commands.RunMode;
namespace NadekoBot;
@@ -32,13 +35,13 @@ public sealed class Bot
private readonly IBotCredsProvider _credsProvider;
// private readonly InteractionService _interactionService;
public Bot(int shardId, int? totalShards)
public Bot(int shardId, int? totalShards, string credPath = null)
{
if (shardId < 0)
throw new ArgumentOutOfRangeException(nameof(shardId));
ShardId = shardId;
_credsProvider = new BotCredsProvider(totalShards);
_credsProvider = new BotCredsProvider(totalShards, credPath);
_creds = _credsProvider.GetCreds();
_db = new(_credsProvider);
@@ -68,6 +71,7 @@ public sealed class Bot
: GatewayIntents.AllUnprivileged,
LogGatewayIntentWarnings = false,
FormatUsersInBidirectionalUnicode = false,
DefaultRetryMode = RetryMode.AlwaysRetry ^ RetryMode.RetryRatelimit
});
_commandService = new(new()
@@ -100,20 +104,20 @@ public sealed class Bot
var svcs = new ServiceCollection().AddTransient(_ => _credsProvider.GetCreds()) // bot creds
.AddSingleton(_credsProvider)
.AddSingleton(_db) // database
.AddRedis(_creds.RedisOptions) // redis
.AddSingleton(Client) // discord socket client
.AddSingleton(_commandService)
// .AddSingleton(_interactionService)
.AddSingleton(this)
.AddSingleton<ISeria, JsonSeria>()
.AddSingleton<IPubSub, RedisPubSub>()
.AddSingleton<IConfigSeria, YamlSeria>()
.AddBotStringsServices(_creds.TotalShards)
.AddConfigServices()
.AddConfigMigrators()
.AddMemoryCache()
// music
.AddMusic();
.AddMusic()
// cache
.AddCache(_creds);
// admin
#if GLOBAL_NADEKO
svcs.AddSingleton<ILogCommandService, DummyLogCommandService>();
@@ -125,6 +129,12 @@ public sealed class Bot
{
AllowAutoRedirect = false
});
svcs.AddHttpClient("google:search")
.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler()
{
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate
});
if (Environment.GetEnvironmentVariable("NADEKOBOT_IS_COORDINATED") != "1")
svcs.AddSingleton<ICoordinator, SingleProcessCoordinator>();
@@ -135,13 +145,6 @@ public sealed class Bot
.AddSingleton<IReadyExecutor>(x => x.GetRequiredService<RemoteGrpcCoordinator>());
}
svcs.AddSingleton<RedisLocalDataCache>()
.AddSingleton<ILocalDataCache>(x => x.GetRequiredService<RedisLocalDataCache>())
.AddSingleton<RedisImagesCache>()
.AddSingleton<IImageCache>(x => x.GetRequiredService<RedisImagesCache>())
.AddSingleton<IReadyExecutor>(x => x.GetRequiredService<RedisImagesCache>())
.AddSingleton<IDataCache, RedisCache>();
svcs.Scan(scan => scan.FromAssemblyOf<IReadyExecutor>()
.AddClasses(classes => classes.AssignableToAny(
// services
@@ -164,6 +167,7 @@ public sealed class Bot
//initialize Services
Services = svcs.BuildServiceProvider();
Services.GetRequiredService<IBehaviorHandler>().Initialize();
Services.GetRequiredService<CurrencyRewardService>();
if (Client.ShardId == 0)
ApplyConfigMigrations();
@@ -258,6 +262,7 @@ public sealed class Bot
Client.JoinedGuild += Client_JoinedGuild;
Client.LeftGuild += Client_LeftGuild;
// _ = Client.SetStatusAsync(UserStatus.Online);
Log.Information("Shard {ShardId} logged in", Client.ShardId);
}

View File

@@ -0,0 +1,11 @@
#nullable disable
namespace NadekoBot.Common;
/// <summary>
/// Classed marked with this attribute will not be added to the service provider
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public class DontAddToIocContainerAttribute : Attribute
{
}

View File

@@ -3,11 +3,16 @@ using System.Runtime.CompilerServices;
namespace NadekoBot.Common.Attributes;
[AttributeUsage(AttributeTargets.Method)]
public sealed class NadekoCommandAttribute : CommandAttribute
public sealed class CmdAttribute : CommandAttribute
{
public string MethodName { get; }
public NadekoCommandAttribute([CallerMemberName] string memberName = "")
public CmdAttribute([CallerMemberName] string memberName = "")
: base(CommandNameLoadHelper.GetCommandNameFor(memberName))
=> MethodName = memberName.ToLowerInvariant();
{
MethodName = memberName.ToLowerInvariant();
Aliases = CommandNameLoadHelper.GetAliasesFor(memberName);
Remarks = memberName.ToLowerInvariant();
Summary = memberName.ToLowerInvariant();
}
}

View File

@@ -1,30 +0,0 @@
using System.Runtime.CompilerServices;
namespace NadekoBot.Common.Attributes;
[AttributeUsage(AttributeTargets.Class)]
internal sealed class NadekoModuleAttribute : GroupAttribute
{
public NadekoModuleAttribute(string moduleName)
: base(moduleName)
{
}
}
[AttributeUsage(AttributeTargets.Method)]
internal sealed class NadekoDescriptionAttribute : SummaryAttribute
{
public NadekoDescriptionAttribute([CallerMemberName] string name = "")
: base(name.ToLowerInvariant())
{
}
}
[AttributeUsage(AttributeTargets.Method)]
internal sealed class NadekoUsageAttribute : RemarksAttribute
{
public NadekoUsageAttribute([CallerMemberName] string name = "")
: base(name.ToLowerInvariant())
{
}
}

View File

@@ -20,11 +20,19 @@ public sealed class NoPublicBotAttribute : PreconditionAttribute
}
}
/// <summary>
/// Classed marked with this attribute will not be added to the service provider
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public class DontAddToIocContainerAttribute : Attribute
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
[SuppressMessage("Style", "IDE0022:Use expression body for methods")]
public sealed class OnlyPublicBotAttribute : PreconditionAttribute
{
public override Task<PreconditionResult> CheckPermissionsAsync(
ICommandContext context,
CommandInfo command,
IServiceProvider services)
{
#if GLOBAL_NADEKO || DEBUG
return Task.FromResult(PreconditionResult.FromSuccess());
#else
return Task.FromResult(PreconditionResult.FromError("Only available on the public bot."));
#endif
}
}

View File

@@ -15,22 +15,24 @@ public sealed class RatelimitAttribute : PreconditionAttribute
Seconds = seconds;
}
public override Task<PreconditionResult> CheckPermissionsAsync(
public override async Task<PreconditionResult> CheckPermissionsAsync(
ICommandContext context,
CommandInfo command,
IServiceProvider services)
{
if (Seconds == 0)
return Task.FromResult(PreconditionResult.FromSuccess());
return PreconditionResult.FromSuccess();
var cache = services.GetRequiredService<IDataCache>();
var rem = cache.TryAddRatelimit(context.User.Id, command.Name, Seconds);
var cache = services.GetRequiredService<IBotCache>();
var rem = await cache.GetRatelimitAsync(
new($"precondition:{context.User.Id}:{command.Name}"),
Seconds.Seconds());
if (rem is null)
return Task.FromResult(PreconditionResult.FromSuccess());
return PreconditionResult.FromSuccess();
var msgContent = $"You can use this command again in {rem.Value.TotalSeconds:F1}s.";
return Task.FromResult(PreconditionResult.FromError(msgContent));
return PreconditionResult.FromError(msgContent);
}
}

View File

@@ -0,0 +1,46 @@
using OneOf;
using OneOf.Types;
namespace NadekoBot.Common;
public static class BotCacheExtensions
{
public static async ValueTask<T?> GetOrDefaultAsync<T>(this IBotCache cache, TypedKey<T> key)
{
var result = await cache.GetAsync(key);
if (result.TryGetValue(out var val))
return val;
return default;
}
private static TypedKey<byte[]> GetImgKey(Uri uri)
=> new($"image:{uri}");
public static ValueTask SetImageDataAsync(this IBotCache c, string key, byte[] data)
=> c.SetImageDataAsync(new Uri(key), data);
public static async ValueTask SetImageDataAsync(this IBotCache c, Uri key, byte[] data)
=> await c.AddAsync(GetImgKey(key), data, expiry: TimeSpan.FromHours(48));
public static async ValueTask<OneOf<byte[], None>> GetImageDataAsync(this IBotCache c, Uri key)
=> await c.GetAsync(GetImgKey(key));
public static async Task<TimeSpan?> GetRatelimitAsync(
this IBotCache c,
TypedKey<long> key,
TimeSpan length)
{
var now = DateTime.UtcNow;
var nowB = now.ToBinary();
var cachedValue = await c.GetOrAddAsync(key,
() => Task.FromResult(now.ToBinary()),
expiry: length);
if (cachedValue == nowB)
return null;
var diff = now - DateTime.FromBinary(cachedValue);
return length - diff;
}
}

View File

@@ -0,0 +1,47 @@
using OneOf;
using OneOf.Types;
namespace NadekoBot.Common;
public interface IBotCache
{
/// <summary>
/// Adds an item to the cache
/// </summary>
/// <param name="key">Key to add</param>
/// <param name="value">Value to add to the cache</param>
/// <param name="expiry">Optional expiry</param>
/// <param name="overwrite">Whether old value should be overwritten</param>
/// <typeparam name="T">Type of the value</typeparam>
/// <returns>Returns whether add was sucessful. Always true unless ovewrite = false</returns>
ValueTask<bool> AddAsync<T>(TypedKey<T> key, T value, TimeSpan? expiry = null, bool overwrite = true);
/// <summary>
/// Get an element from the cache
/// </summary>
/// <param name="key">Key</param>
/// <typeparam name="T">Type of the value</typeparam>
/// <returns>Either a value or <see cref="None"/></returns>
ValueTask<OneOf<T, None>> GetAsync<T>(TypedKey<T> key);
/// <summary>
/// Remove a key from the cache
/// </summary>
/// <param name="key">Key to remove</param>
/// <typeparam name="T">Type of the value</typeparam>
/// <returns>Whether there was item</returns>
ValueTask<bool> RemoveAsync<T>(TypedKey<T> key);
/// <summary>
/// Get the key if it exists or add a new one
/// </summary>
/// <param name="key">Key to get and potentially add</param>
/// <param name="createFactory">Value creation factory</param>
/// <param name="expiry">Optional expiry</param>
/// <typeparam name="T">Type of the value</typeparam>
/// <returns>The retrieved or newly added value</returns>
ValueTask<T?> GetOrAddAsync<T>(
TypedKey<T> key,
Func<Task<T?>> createFactory,
TimeSpan? expiry = null);
}

View File

@@ -0,0 +1,71 @@
using Microsoft.Extensions.Caching.Memory;
using OneOf;
using OneOf.Types;
// ReSharper disable InconsistentlySynchronizedField
namespace NadekoBot.Common;
public sealed class MemoryBotCache : IBotCache
{
// needed for overwrites and Delete return value
private readonly object _cacheLock = new object();
private readonly MemoryCache _cache;
public MemoryBotCache()
{
_cache = new MemoryCache(new MemoryCacheOptions());
}
public ValueTask<bool> AddAsync<T>(TypedKey<T> key, T value, TimeSpan? expiry = null, bool overwrite = true)
{
if (overwrite)
{
using var item = _cache.CreateEntry(key.Key);
item.Value = value;
item.AbsoluteExpirationRelativeToNow = expiry;
return new(true);
}
lock (_cacheLock)
{
if (_cache.TryGetValue(key.Key, out var old) && old is not null)
return new(false);
using var item = _cache.CreateEntry(key.Key);
item.Value = value;
item.AbsoluteExpirationRelativeToNow = expiry;
return new(true);
}
}
public async ValueTask<T?> GetOrAddAsync<T>(
TypedKey<T> key,
Func<Task<T?>> createFactory,
TimeSpan? expiry = null)
=> await _cache.GetOrCreateAsync(key.Key,
async ce =>
{
ce.AbsoluteExpirationRelativeToNow = expiry;
var val = await createFactory();
return val;
});
public ValueTask<OneOf<T, None>> GetAsync<T>(TypedKey<T> key)
{
if (!_cache.TryGetValue(key.Key, out var val) || val is null)
return new(new None());
return new((T)val);
}
public ValueTask<bool> RemoveAsync<T>(TypedKey<T> key)
{
lock (_cacheLock)
{
var toReturn = _cache.TryGetValue(key.Key, out var old ) && old is not null;
_cache.Remove(key.Key);
return new(toReturn);
}
}
}

View File

@@ -0,0 +1,119 @@
using OneOf;
using OneOf.Types;
using StackExchange.Redis;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace NadekoBot.Common;
public sealed class RedisBotCache : IBotCache
{
private static readonly Type[] _supportedTypes = new []
{
typeof(bool), typeof(int), typeof(uint), typeof(long),
typeof(ulong), typeof(float), typeof(double),
typeof(string), typeof(byte[]), typeof(ReadOnlyMemory<byte>), typeof(Memory<byte>),
typeof(RedisValue),
};
private static readonly JsonSerializerOptions _opts = new()
{
PropertyNameCaseInsensitive = true,
NumberHandling = JsonNumberHandling.AllowReadingFromString,
AllowTrailingCommas = true,
IgnoreReadOnlyProperties = false,
};
private readonly ConnectionMultiplexer _conn;
public RedisBotCache(ConnectionMultiplexer conn)
{
_conn = conn;
}
public async ValueTask<bool> AddAsync<T>(TypedKey<T> key, T value, TimeSpan? expiry = null, bool overwrite = true)
{
// if a null value is passed, remove the key
if (value is null)
{
await RemoveAsync(key);
return false;
}
var db = _conn.GetDatabase();
RedisValue val = IsSupportedType(typeof(T))
? RedisValue.Unbox(value)
: JsonSerializer.Serialize(value, _opts);
var success = await db.StringSetAsync(key.Key,
val,
expiry: expiry,
when: overwrite ? When.Always : When.NotExists);
return success;
}
public bool IsSupportedType(Type type)
{
if (type.IsGenericType)
{
var typeDef = type.GetGenericTypeDefinition();
if (typeDef == typeof(Nullable<>))
return IsSupportedType(type.GenericTypeArguments[0]);
}
foreach (var t in _supportedTypes)
{
if (type == t)
return true;
}
return false;
}
public async ValueTask<OneOf<T, None>> GetAsync<T>(TypedKey<T> key)
{
var db = _conn.GetDatabase();
var val = await db.StringGetAsync(key.Key);
if (val == default)
return new None();
if (IsSupportedType(typeof(T)))
return (T)((IConvertible)val).ToType(typeof(T), null);
return JsonSerializer.Deserialize<T>(val.ToString(), _opts)!;
}
public async ValueTask<bool> RemoveAsync<T>(TypedKey<T> key)
{
var db = _conn.GetDatabase();
return await db.KeyDeleteAsync(key.Key);
}
public async ValueTask<T?> GetOrAddAsync<T>(TypedKey<T> key, Func<Task<T?>> createFactory, TimeSpan? expiry = null)
{
var result = await GetAsync(key);
return await result.Match<Task<T?>>(
v => Task.FromResult<T?>(v),
async _ =>
{
var factoryValue = await createFactory();
if (factoryValue is null)
return default;
await AddAsync(key, factoryValue, expiry);
// get again to make sure it's the cached value
// and not the late factory value, in case there's a race condition
var newResult = await GetAsync(key);
// it's fine to do this, it should blow up if something went wrong.
return newResult.Match<T?>(
v => v,
_ => default);
});
}
}

View File

@@ -1,821 +0,0 @@
#nullable disable
#pragma warning disable
// License MIT
// Source: https://github.com/i3arnon/ConcurrentHashSet
using System.Diagnostics;
namespace System.Collections.Generic;
/// <summary>
/// Represents a thread-safe hash-based unique collection.
/// </summary>
/// <typeparam name="T">The type of the items in the collection.</typeparam>
/// <remarks>
/// All public members of <see cref="ConcurrentHashSet{T}" /> are thread-safe and may be used
/// concurrently from multiple threads.
/// </remarks>
[DebuggerDisplay("Count = {Count}")]
public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ICollection<T>
{
private const int DEFAULT_CAPACITY = 31;
private const int MAX_LOCK_NUMBER = 1024;
private static int DefaultConcurrencyLevel
=> PlatformHelper.ProcessorCount;
/// <summary>
/// Gets a value that indicates whether the <see cref="ConcurrentHashSet{T}" /> is empty.
/// </summary>
/// <value>
/// true if the <see cref="ConcurrentHashSet{T}" /> is empty; otherwise,
/// false.
/// </value>
public bool IsEmpty
{
get
{
var acquiredLocks = 0;
try
{
AcquireAllLocks(ref acquiredLocks);
for (var i = 0; i < tables.CountPerLock.Length; i++)
{
if (tables.CountPerLock[i] != 0)
return false;
}
}
finally
{
ReleaseLocks(0, acquiredLocks);
}
return true;
}
}
bool ICollection<T>.IsReadOnly
=> false;
/// <summary>
/// Gets the number of items contained in the
/// <see
/// cref="ConcurrentHashSet{T}" />
/// .
/// </summary>
/// <value>
/// The number of items contained in the
/// <see
/// cref="ConcurrentHashSet{T}" />
/// .
/// </value>
/// <remarks>
/// Count has snapshot semantics and represents the number of items in the
/// <see
/// cref="ConcurrentHashSet{T}" />
/// at the moment when Count was accessed.
/// </remarks>
public int Count
{
get
{
var count = 0;
var acquiredLocks = 0;
try
{
AcquireAllLocks(ref acquiredLocks);
for (var i = 0; i < tables.CountPerLock.Length; i++)
count += tables.CountPerLock[i];
}
finally
{
ReleaseLocks(0, acquiredLocks);
}
return count;
}
}
private readonly IEqualityComparer<T> _comparer;
private readonly bool _growLockArray;
private int budget;
private volatile Tables tables;
/// <summary>
/// Initializes a new instance of the
/// <see
/// cref="ConcurrentHashSet{T}" />
/// class that is empty, has the default concurrency level, has the default initial capacity, and
/// uses the default comparer for the item type.
/// </summary>
public ConcurrentHashSet()
: this(DefaultConcurrencyLevel, DEFAULT_CAPACITY, true, EqualityComparer<T>.Default)
{
}
/// <summary>
/// Initializes a new instance of the
/// <see
/// cref="ConcurrentHashSet{T}" />
/// class that is empty, has the specified concurrency level and capacity, and uses the default
/// comparer for the item type.
/// </summary>
/// <param name="concurrencyLevel">
/// The estimated number of threads that will update the
/// <see cref="ConcurrentHashSet{T}" /> concurrently.
/// </param>
/// <param name="capacity">
/// The initial number of elements that the
/// <see
/// cref="ConcurrentHashSet{T}" />
/// can contain.
/// </param>
/// <exception cref="T:System.ArgumentOutOfRangeException">
/// <paramref name="concurrencyLevel" /> is
/// less than 1.
/// </exception>
/// <exception cref="T:System.ArgumentOutOfRangeException">
/// <paramref name="capacity" /> is less than
/// 0.
/// </exception>
public ConcurrentHashSet(int concurrencyLevel, int capacity)
: this(concurrencyLevel, capacity, false, EqualityComparer<T>.Default)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ConcurrentHashSet{T}" />
/// class that contains elements copied from the specified
/// <see
/// cref="T:System.Collections.IEnumerable{T}" />
/// , has the default concurrency
/// level, has the default initial capacity, and uses the default comparer for the item type.
/// </summary>
/// <param name="collection">
/// The
/// <see
/// cref="T:System.Collections.IEnumerable{T}" />
/// whose elements are copied to
/// the new
/// <see cref="ConcurrentHashSet{T}" />.
/// </param>
/// <exception cref="T:System.ArgumentNullException"><paramref name="collection" /> is a null reference.</exception>
public ConcurrentHashSet(IEnumerable<T> collection)
: this(collection, EqualityComparer<T>.Default)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ConcurrentHashSet{T}" />
/// class that is empty, has the specified concurrency level and capacity, and uses the specified
/// <see cref="T:System.Collections.Generic.IEqualityComparer{T}" />.
/// </summary>
/// <param name="comparer">
/// The <see cref="T:System.Collections.Generic.IEqualityComparer{T}" />
/// implementation to use when comparing items.
/// </param>
/// <exception cref="T:System.ArgumentNullException"><paramref name="comparer" /> is a null reference.</exception>
public ConcurrentHashSet(IEqualityComparer<T> comparer)
: this(DefaultConcurrencyLevel, DEFAULT_CAPACITY, true, comparer)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ConcurrentHashSet{T}" />
/// class that contains elements copied from the specified
/// <see
/// cref="T:System.Collections.IEnumerable" />
/// , has the default concurrency level, has the default
/// initial capacity, and uses the specified
/// <see cref="T:System.Collections.Generic.IEqualityComparer{T}" />.
/// </summary>
/// <param name="collection">
/// The
/// <see
/// cref="T:System.Collections.IEnumerable{T}" />
/// whose elements are copied to
/// the new
/// <see cref="ConcurrentHashSet{T}" />.
/// </param>
/// <param name="comparer">
/// The <see cref="T:System.Collections.Generic.IEqualityComparer{T}" />
/// implementation to use when comparing items.
/// </param>
/// <exception cref="T:System.ArgumentNullException">
/// <paramref name="collection" /> is a null reference
/// (Nothing in Visual Basic). -or-
/// <paramref name="comparer" /> is a null reference (Nothing in Visual Basic).
/// </exception>
public ConcurrentHashSet(IEnumerable<T> collection, IEqualityComparer<T> comparer)
: this(comparer)
{
if (collection is null)
throw new ArgumentNullException(nameof(collection));
InitializeFromCollection(collection);
}
/// <summary>
/// Initializes a new instance of the <see cref="ConcurrentHashSet{T}" />
/// class that contains elements copied from the specified <see cref="T:System.Collections.IEnumerable" />,
/// has the specified concurrency level, has the specified initial capacity, and uses the specified
/// <see cref="T:System.Collections.Generic.IEqualityComparer{T}" />.
/// </summary>
/// <param name="concurrencyLevel">
/// The estimated number of threads that will update the
/// <see cref="ConcurrentHashSet{T}" /> concurrently.
/// </param>
/// <param name="collection">
/// The <see cref="T:System.Collections.IEnumerable{T}" /> whose elements are copied to the new
/// <see cref="ConcurrentHashSet{T}" />.
/// </param>
/// <param name="comparer">
/// The <see cref="T:System.Collections.Generic.IEqualityComparer{T}" /> implementation to use
/// when comparing items.
/// </param>
/// <exception cref="T:System.ArgumentNullException">
/// <paramref name="collection" /> is a null reference.
/// -or-
/// <paramref name="comparer" /> is a null reference.
/// </exception>
/// <exception cref="T:System.ArgumentOutOfRangeException">
/// <paramref name="concurrencyLevel" /> is less than 1.
/// </exception>
public ConcurrentHashSet(int concurrencyLevel, IEnumerable<T> collection, IEqualityComparer<T> comparer)
: this(concurrencyLevel, DEFAULT_CAPACITY, false, comparer)
{
if (collection is null)
throw new ArgumentNullException(nameof(collection));
if (comparer is null)
throw new ArgumentNullException(nameof(comparer));
InitializeFromCollection(collection);
}
/// <summary>
/// Initializes a new instance of the <see cref="ConcurrentHashSet{T}" />
/// class that is empty, has the specified concurrency level, has the specified initial capacity, and
/// uses the specified <see cref="T:System.Collections.Generic.IEqualityComparer{T}" />.
/// </summary>
/// <param name="concurrencyLevel">
/// The estimated number of threads that will update the
/// <see cref="ConcurrentHashSet{T}" /> concurrently.
/// </param>
/// <param name="capacity">
/// The initial number of elements that the
/// <see
/// cref="ConcurrentHashSet{T}" />
/// can contain.
/// </param>
/// <param name="comparer">
/// The <see cref="T:System.Collections.Generic.IEqualityComparer{T}" />
/// implementation to use when comparing items.
/// </param>
/// <exception cref="T:System.ArgumentOutOfRangeException">
/// <paramref name="concurrencyLevel" /> is less than 1. -or-
/// <paramref name="capacity" /> is less than 0.
/// </exception>
/// <exception cref="T:System.ArgumentNullException"><paramref name="comparer" /> is a null reference.</exception>
public ConcurrentHashSet(int concurrencyLevel, int capacity, IEqualityComparer<T> comparer)
: this(concurrencyLevel, capacity, false, comparer)
{
}
private ConcurrentHashSet(
int concurrencyLevel,
int capacity,
bool growLockArray,
IEqualityComparer<T> comparer)
{
if (concurrencyLevel < 1)
throw new ArgumentOutOfRangeException(nameof(concurrencyLevel));
if (capacity < 0)
throw new ArgumentOutOfRangeException(nameof(capacity));
// The capacity should be at least as large as the concurrency level. Otherwise, we would have locks that don't guard
// any buckets.
if (capacity < concurrencyLevel)
capacity = concurrencyLevel;
var locks = new object[concurrencyLevel];
for (var i = 0; i < locks.Length; i++)
locks[i] = new();
var countPerLock = new int[locks.Length];
var buckets = new Node[capacity];
tables = new(buckets, locks, countPerLock);
_growLockArray = growLockArray;
budget = buckets.Length / locks.Length;
_comparer = comparer ?? throw new ArgumentNullException(nameof(comparer));
}
/// <summary>
/// Removes all items from the <see cref="ConcurrentHashSet{T}" />.
/// </summary>
public void Clear()
{
var locksAcquired = 0;
try
{
AcquireAllLocks(ref locksAcquired);
var newTables = new Tables(new Node[DEFAULT_CAPACITY], tables.Locks, new int[tables.CountPerLock.Length]);
tables = newTables;
budget = Math.Max(1, newTables.Buckets.Length / newTables.Locks.Length);
}
finally
{
ReleaseLocks(0, locksAcquired);
}
}
/// <summary>
/// Determines whether the <see cref="ConcurrentHashSet{T}" /> contains the specified
/// item.
/// </summary>
/// <param name="item">The item to locate in the <see cref="ConcurrentHashSet{T}" />.</param>
/// <returns>true if the <see cref="ConcurrentHashSet{T}" /> contains the item; otherwise, false.</returns>
public bool Contains(T item)
{
var hashcode = _comparer.GetHashCode(item!);
// We must capture the _buckets field in a local variable. It is set to a new table on each table resize.
var localTables = tables;
var bucketNo = GetBucket(hashcode, localTables.Buckets.Length);
// We can get away w/out a lock here.
// The Volatile.Read ensures that the load of the fields of 'n' doesn't move before the load from buckets[i].
var current = Volatile.Read(ref localTables.Buckets[bucketNo]);
while (current is not null)
{
if (hashcode == current.Hashcode && _comparer.Equals(current.Item, item))
return true;
current = current.Next;
}
return false;
}
void ICollection<T>.Add(T item)
=> Add(item);
void ICollection<T>.CopyTo(T[] array, int arrayIndex)
{
if (array is null)
throw new ArgumentNullException(nameof(array));
if (arrayIndex < 0)
throw new ArgumentOutOfRangeException(nameof(arrayIndex));
var locksAcquired = 0;
try
{
AcquireAllLocks(ref locksAcquired);
var count = 0;
for (var i = 0; i < tables.Locks.Length && count >= 0; i++)
count += tables.CountPerLock[i];
if (array.Length - count < arrayIndex || count < 0) //"count" itself or "count + arrayIndex" can overflow
{
throw new ArgumentException(
"The index is equal to or greater than the length of the array, or the number of elements in the set is greater than the available space from index to the end of the destination array.");
}
CopyToItems(array, arrayIndex);
}
finally
{
ReleaseLocks(0, locksAcquired);
}
}
bool ICollection<T>.Remove(T item)
=> TryRemove(item);
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
/// <summary>
/// Returns an enumerator that iterates through the
/// <see
/// cref="ConcurrentHashSet{T}" />
/// .
/// </summary>
/// <returns>An enumerator for the <see cref="ConcurrentHashSet{T}" />.</returns>
/// <remarks>
/// The enumerator returned from the collection is safe to use concurrently with
/// reads and writes to the collection, however it does not represent a moment-in-time snapshot
/// of the collection. The contents exposed through the enumerator may contain modifications
/// made to the collection after <see cref="GetEnumerator" /> was called.
/// </remarks>
public IEnumerator<T> GetEnumerator()
{
var buckets = tables.Buckets;
for (var i = 0; i < buckets.Length; i++)
{
// The Volatile.Read ensures that the load of the fields of 'current' doesn't move before the load from buckets[i].
var current = Volatile.Read(ref buckets[i]);
while (current is not null)
{
yield return current.Item;
current = current.Next;
}
}
}
/// <summary>
/// Adds the specified item to the <see cref="ConcurrentHashSet{T}" />.
/// </summary>
/// <param name="item">The item to add.</param>
/// <returns>
/// true if the items was added to the <see cref="ConcurrentHashSet{T}" />
/// successfully; false if it already exists.
/// </returns>
/// <exception cref="T:System.OverflowException">
/// The <see cref="ConcurrentHashSet{T}" />
/// contains too many items.
/// </exception>
public bool Add(T item)
=> AddInternal(item, _comparer.GetHashCode(item), true);
/// <summary>
/// Attempts to remove the item from the <see cref="ConcurrentHashSet{T}" />.
/// </summary>
/// <param name="item">The item to remove.</param>
/// <returns>true if an item was removed successfully; otherwise, false.</returns>
public bool TryRemove(T item)
{
var hashcode = _comparer.GetHashCode(item);
while (true)
{
var localTables = tables;
GetBucketAndLockNo(hashcode,
out var bucketNo,
out var lockNo,
localTables.Buckets.Length,
localTables.Locks.Length);
lock (localTables.Locks[lockNo])
{
// If the table just got resized, we may not be holding the right lock, and must retry.
// This should be a rare occurrence.
if (localTables != tables)
continue;
Node previous = null;
for (var current = localTables.Buckets[bucketNo]; current is not null; current = current.Next)
{
Debug.Assert((previous is null && current == localTables.Buckets[bucketNo])
|| previous!.Next == current);
if (hashcode == current.Hashcode && _comparer.Equals(current.Item, item))
{
if (previous is null)
Volatile.Write(ref localTables.Buckets[bucketNo], current.Next);
else
previous.Next = current.Next;
localTables.CountPerLock[lockNo]--;
return true;
}
previous = current;
}
}
return false;
}
}
private void InitializeFromCollection(IEnumerable<T> collection)
{
foreach (var item in collection)
AddInternal(item, _comparer.GetHashCode(item), false);
if (budget == 0)
budget = tables.Buckets.Length / tables.Locks.Length;
}
private bool AddInternal(T item, int hashcode, bool acquireLock)
{
while (true)
{
var localTables = tables;
GetBucketAndLockNo(hashcode,
out var bucketNo,
out var lockNo,
localTables.Buckets.Length,
localTables.Locks.Length);
var resizeDesired = false;
var lockTaken = false;
try
{
if (acquireLock)
Monitor.Enter(localTables.Locks[lockNo], ref lockTaken);
// If the table just got resized, we may not be holding the right lock, and must retry.
// This should be a rare occurrence.
if (localTables != tables)
continue;
// Try to find this item in the bucket
Node previous = null;
for (var current = localTables.Buckets[bucketNo]; current is not null; current = current.Next)
{
Debug.Assert((previous is null && current == localTables.Buckets[bucketNo])
|| previous!.Next == current);
if (hashcode == current.Hashcode && _comparer.Equals(current.Item, item))
return false;
previous = current;
}
// The item was not found in the bucket. Insert the new item.
Volatile.Write(ref localTables.Buckets[bucketNo], new(item, hashcode, localTables.Buckets[bucketNo]));
checked
{
localTables.CountPerLock[lockNo]++;
}
//
// If the number of elements guarded by this lock has exceeded the budget, resize the bucket table.
// It is also possible that GrowTable will increase the budget but won't resize the bucket table.
// That happens if the bucket table is found to be poorly utilized due to a bad hash function.
//
if (localTables.CountPerLock[lockNo] > budget)
resizeDesired = true;
}
finally
{
if (lockTaken)
Monitor.Exit(localTables.Locks[lockNo]);
}
//
// The fact that we got here means that we just performed an insertion. If necessary, we will grow the table.
//
// Concurrency notes:
// - Notice that we are not holding any locks at when calling GrowTable. This is necessary to prevent deadlocks.
// - As a result, it is possible that GrowTable will be called unnecessarily. But, GrowTable will obtain lock 0
// and then verify that the table we passed to it as the argument is still the current table.
//
if (resizeDesired)
GrowTable(localTables);
return true;
}
}
private static int GetBucket(int hashcode, int bucketCount)
{
var bucketNo = (hashcode & 0x7fffffff) % bucketCount;
Debug.Assert(bucketNo >= 0 && bucketNo < bucketCount);
return bucketNo;
}
private static void GetBucketAndLockNo(
int hashcode,
out int bucketNo,
out int lockNo,
int bucketCount,
int lockCount)
{
bucketNo = (hashcode & 0x7fffffff) % bucketCount;
lockNo = bucketNo % lockCount;
Debug.Assert(bucketNo >= 0 && bucketNo < bucketCount);
Debug.Assert(lockNo >= 0 && lockNo < lockCount);
}
private void GrowTable(Tables localTables)
{
const int maxArrayLength = 0X7FEFFFFF;
var locksAcquired = 0;
try
{
// The thread that first obtains _locks[0] will be the one doing the resize operation
AcquireLocks(0, 1, ref locksAcquired);
// Make sure nobody resized the table while we were waiting for lock 0:
if (localTables != tables)
// We assume that since the table reference is different, it was already resized (or the budget
// was adjusted). If we ever decide to do table shrinking, or replace the table for other reasons,
// we will have to revisit this logic.
return;
// Compute the (approx.) total size. Use an Int64 accumulation variable to avoid an overflow.
long approxCount = 0;
for (var i = 0; i < localTables.CountPerLock.Length; i++)
approxCount += localTables.CountPerLock[i];
//
// If the bucket array is too empty, double the budget instead of resizing the table
//
if (approxCount < localTables.Buckets.Length / 4)
{
budget = 2 * budget;
if (budget < 0)
budget = int.MaxValue;
return;
}
// Compute the new table size. We find the smallest integer larger than twice the previous table size, and not divisible by
// 2,3,5 or 7. We can consider a different table-sizing policy in the future.
var newLength = 0;
var maximizeTableSize = false;
try
{
checked
{
// Double the size of the buckets table and add one, so that we have an odd integer.
newLength = (localTables.Buckets.Length * 2) + 1;
// Now, we only need to check odd integers, and find the first that is not divisible
// by 3, 5 or 7.
while (newLength % 3 == 0 || newLength % 5 == 0 || newLength % 7 == 0)
newLength += 2;
Debug.Assert(newLength % 2 != 0);
if (newLength > maxArrayLength)
maximizeTableSize = true;
}
}
catch (OverflowException)
{
maximizeTableSize = true;
}
if (maximizeTableSize)
{
newLength = maxArrayLength;
// We want to make sure that GrowTable will not be called again, since table is at the maximum size.
// To achieve that, we set the budget to int.MaxValue.
//
// (There is one special case that would allow GrowTable() to be called in the future:
// calling Clear() on the ConcurrentHashSet will shrink the table and lower the budget.)
budget = int.MaxValue;
}
// Now acquire all other locks for the table
AcquireLocks(1, localTables.Locks.Length, ref locksAcquired);
var newLocks = localTables.Locks;
// Add more locks
if (_growLockArray && localTables.Locks.Length < MAX_LOCK_NUMBER)
{
newLocks = new object[localTables.Locks.Length * 2];
Array.Copy(localTables.Locks, 0, newLocks, 0, localTables.Locks.Length);
for (var i = localTables.Locks.Length; i < newLocks.Length; i++)
newLocks[i] = new();
}
var newBuckets = new Node[newLength];
var newCountPerLock = new int[newLocks.Length];
// Copy all data into a new table, creating new nodes for all elements
for (var i = 0; i < localTables.Buckets.Length; i++)
{
var current = localTables.Buckets[i];
while (current is not null)
{
var next = current.Next;
GetBucketAndLockNo(current.Hashcode,
out var newBucketNo,
out var newLockNo,
newBuckets.Length,
newLocks.Length);
newBuckets[newBucketNo] = new(current.Item, current.Hashcode, newBuckets[newBucketNo]);
checked
{
newCountPerLock[newLockNo]++;
}
current = next;
}
}
// Adjust the budget
budget = Math.Max(1, newBuckets.Length / newLocks.Length);
// Replace tables with the new versions
tables = new(newBuckets, newLocks, newCountPerLock);
}
finally
{
// Release all locks that we took earlier
ReleaseLocks(0, locksAcquired);
}
}
public int RemoveWhere(Func<T, bool> predicate)
{
var elems = this.Where(predicate);
var removed = 0;
foreach (var elem in elems)
{
if (TryRemove(elem))
removed++;
}
return removed;
}
private void AcquireAllLocks(ref int locksAcquired)
{
// First, acquire lock 0
AcquireLocks(0, 1, ref locksAcquired);
// Now that we have lock 0, the _locks array will not change (i.e., grow),
// and so we can safely read _locks.Length.
AcquireLocks(1, tables.Locks.Length, ref locksAcquired);
Debug.Assert(locksAcquired == tables.Locks.Length);
}
private void AcquireLocks(int fromInclusive, int toExclusive, ref int locksAcquired)
{
Debug.Assert(fromInclusive <= toExclusive);
var locks = tables.Locks;
for (var i = fromInclusive; i < toExclusive; i++)
{
var lockTaken = false;
try
{
Monitor.Enter(locks[i], ref lockTaken);
}
finally
{
if (lockTaken)
locksAcquired++;
}
}
}
private void ReleaseLocks(int fromInclusive, int toExclusive)
{
Debug.Assert(fromInclusive <= toExclusive);
for (var i = fromInclusive; i < toExclusive; i++)
Monitor.Exit(tables.Locks[i]);
}
private void CopyToItems(T[] array, int index)
{
var buckets = tables.Buckets;
for (var i = 0; i < buckets.Length; i++)
for (var current = buckets[i]; current is not null; current = current.Next)
{
array[index] = current.Item;
index++; //this should never flow, CopyToItems is only called when there's no overflow risk
}
}
private sealed class Tables
{
public readonly Node[] Buckets;
public readonly object[] Locks;
public volatile int[] CountPerLock;
public Tables(Node[] buckets, object[] locks, int[] countPerLock)
{
Buckets = buckets;
Locks = locks;
CountPerLock = countPerLock;
}
}
private sealed class Node
{
public readonly int Hashcode;
public readonly T Item;
public volatile Node Next;
public Node(T item, int hashcode, Node next)
{
Item = item;
Hashcode = hashcode;
Next = next;
}
}
}

View File

@@ -12,7 +12,7 @@ namespace NadekoBot.Common.Configs;
public sealed partial class BotConfig : ICloneable<BotConfig>
{
[Comment(@"DO NOT CHANGE")]
public int Version { get; set; } = 2;
public int Version { get; set; } = 4;
[Comment(@"Most commands, when executed, have a small colored line
next to the response. The color depends whether the command
@@ -29,12 +29,8 @@ and copy the hex code fo your selected color (marked as #)")]
Allowed values: Simple, Normal, None")]
public ConsoleOutputType ConsoleOutputType { get; set; }
// [Comment(@"For what kind of updates will the bot check.
// Allowed values: Release, Commit, None")]
// public UpdateCheckType CheckForUpdates { get; set; }
// [Comment(@"How often will the bot check for updates, in hours")]
// public int CheckUpdateInterval { get; set; }
[Comment(@"Whether the bot will check for new releases every hour")]
public bool CheckForUpdates { get; set; } = true;
[Comment(@"Do you want any messages sent by users in Bot's DM to be forwarded to the owner(s)?")]
public bool ForwardMessages { get; set; }
@@ -182,4 +178,4 @@ public enum ConsoleOutputType
Normal = 0,
Simple = 1,
None = 2
}
}

View File

@@ -18,8 +18,11 @@ public sealed class Creds : IBotCredentials
[Comment("Keep this on 'true' unless you're sure your bot shouldn't use privileged intents or you're waiting to be accepted")]
public bool UsePrivilegedIntents { get; set; }
[Comment(@"The number of shards that the bot will running on.
Leave at 1 if you don't know what you're doing.")]
[Comment(@"The number of shards that the bot will be running on.
Leave at 1 if you don't know what you're doing.
note: If you are planning to have more than one shard, then you must change botCache to 'redis'.
Also, in that case you should be using NadekoBot.Coordinator to start the bot, and it will correctly override this value.")]
public int TotalShards { get; set; }
[Comment(
@@ -27,6 +30,16 @@ Leave at 1 if you don't know what you're doing.")]
Then, go to APIs and Services -> Credentials and click Create credentials -> API key.
Used only for Youtube Data Api (at the moment).")]
public string GoogleApiKey { get; set; }
[Comment(
@"Create a new custom search here https://programmablesearchengine.google.com/cse/create/new
Enable SafeSearch
Remove all Sites to Search
Enable Search the entire web
Copy the 'Search Engine ID' to the SearchId field
Do all steps again but enable image search for the ImageSearchId")]
public GoogleApiConfig Google { get; set; }
[Comment(@"Settings for voting system for discordbots. Meant for use on global Nadeko.")]
public VotesSettings Votes { get; set; }
@@ -40,8 +53,14 @@ go to https://www.patreon.com/portal -> my clients -> create client")]
[Comment(@"Official cleverbot api key.")]
public string CleverbotApiKey { get; set; }
[Comment(@"Redis connection string. Don't change if you don't know what you're doing.")]
[Comment(@"Which cache implementation should bot use.
'memory' - Cache will be in memory of the bot's process itself. Only use this on bots with a single shard. When the bot is restarted the cache is reset.
'redis' - Uses redis (which needs to be separately downloaded and installed). The cache will persist through bot restarts. You can configure connection string in creds.yml")]
public BotCacheImplemenation BotCache { get; set; }
[Comment(@"Redis connection string. Don't change if you don't know what you're doing.
Only used if botCache is set to 'redis'")]
public string RedisOptions { get; set; }
[Comment(@"Database options. Don't change if you don't know what you're doing. Leave null for default values")]
@@ -94,12 +113,12 @@ Linux default
args: ""NadekoBot.dll -- {0}""
Windows default
cmd: NadekoBot.exe
args: {0}")]
args: ""{0}""")]
public RestartConfig RestartCommand { get; set; }
public Creds()
{
Version = 5;
Version = 6;
Token = string.Empty;
UsePrivilegedIntents = true;
OwnerIds = new List<ulong>();
@@ -109,6 +128,7 @@ Windows default
Patreon = new(string.Empty, string.Empty, string.Empty, string.Empty);
BotListToken = string.Empty;
CleverbotApiKey = string.Empty;
BotCache = BotCacheImplemenation.Memory;
RedisOptions = "localhost:6379,syncTimeout=30000,responseTimeout=30000,allowAdmin=true,password=";
Db = new()
{
@@ -119,6 +139,7 @@ Windows default
CoordinatorUrl = "http://localhost:3442";
RestartCommand = new();
Google = new();
}
@@ -200,4 +221,16 @@ This should be equivalent to the DiscordsKey in your NadekoBot.Votes api appsett
DiscordsKey = discordsKey;
}
}
}
public class GoogleApiConfig
{
public string SearchId { get; init; }
public string ImageSearchId { get; init; }
}
public enum BotCacheImplemenation
{
Memory,
Redis
}

View File

@@ -25,6 +25,8 @@ public interface IBotCredentials
string CoordinatorUrl { get; set; }
string TwitchClientId { get; set; }
string TwitchClientSecret { get; set; }
GoogleApiConfig Google { get; set; }
BotCacheImplemenation BotCache { get; set; }
}
public class RestartConfig

View File

@@ -27,5 +27,6 @@ public enum LogType
UserPresence,
VoicePresence,
VoicePresenceTts,
UserMuted
UserMuted,
UserWarned,
}

View File

@@ -1,9 +1,11 @@
#nullable disable
using NadekoBot.Common.Yml;
using Cloneable;
namespace NadekoBot.Common;
public class ImageUrls
[Cloneable]
public partial class ImageUrls : ICloneable<ImageUrls>
{
[Comment("DO NOT CHANGE")]
public int Version { get; set; } = 3;

View File

@@ -1,11 +1,11 @@
namespace NadekoBot;
public sealed class NadekoActionInteraction : NadekoOwnInteraction
public sealed class NadekoButtonActionInteraction : NadekoButtonOwnInteraction
{
private readonly NadekoInteractionData _data;
private readonly Func<SocketMessageComponent, Task> _action;
public NadekoActionInteraction(
public NadekoButtonActionInteraction(
DiscordSocketClient client,
ulong authorId,
NadekoInteractionData data,
@@ -17,10 +17,12 @@ public sealed class NadekoActionInteraction : NadekoOwnInteraction
_action = action;
}
public override string Name
protected override string Name
=> _data.CustomId;
public override IEmote Emote
protected override IEmote Emote
=> _data.Emote;
protected override string? Text
=> _data.Text;
public override Task ExecuteOnActionAsync(SocketMessageComponent smc)
=> _action(smc);

View File

@@ -1,23 +1,24 @@
namespace NadekoBot;
public abstract class NadekoInteraction
public abstract class NadekoButtonInteraction
{
// improvements:
// - state in OnAction
// - configurable delay
// -
public abstract string Name { get; }
public abstract IEmote Emote { get; }
protected abstract string Name { get; }
protected abstract IEmote Emote { get; }
protected virtual string? Text { get; } = null;
protected readonly DiscordSocketClient _client;
public DiscordSocketClient Client { get; }
protected readonly TaskCompletionSource<bool> _interactionCompletedSource;
protected IUserMessage message = null!;
protected NadekoInteraction(DiscordSocketClient client)
protected NadekoButtonInteraction(DiscordSocketClient client)
{
_client = client;
Client = client;
_interactionCompletedSource = new(TaskCreationOptions.RunContinuationsAsynchronously);
}
@@ -25,9 +26,9 @@ public abstract class NadekoInteraction
{
message = msg;
_client.InteractionCreated += OnInteraction;
Client.InteractionCreated += OnInteraction;
await Task.WhenAny(Task.Delay(10_000), _interactionCompletedSource.Task);
_client.InteractionCreated -= OnInteraction;
Client.InteractionCreated -= OnInteraction;
await msg.ModifyAsync(m => m.Components = new ComponentBuilder().Build());
}
@@ -65,13 +66,18 @@ public abstract class NadekoInteraction
}
public MessageComponent CreateComponent()
public virtual MessageComponent CreateComponent()
{
var comp = new ComponentBuilder()
.WithButton(new ButtonBuilder(style: ButtonStyle.Secondary, emote: Emote, customId: Name));
.WithButton(GetButtonBuilder());
return comp.Build();
}
public ButtonBuilder GetButtonBuilder()
=> new ButtonBuilder(style: ButtonStyle.Secondary, emote: Emote, customId: Name, label: Text);
public abstract Task ExecuteOnActionAsync(SocketMessageComponent smc);
}
}
// this is all so wrong ...

View File

@@ -0,0 +1,43 @@
// namespace NadekoBot;
//
// public class NadekoButtonInteractionArray : NadekoButtonInteraction
// {
// private readonly ButtonBuilder[] _bbs;
// private readonly NadekoButtonInteraction[] _inters;
//
// public NadekoButtonInteractionArray(params NadekoButtonInteraction[] inters)
// : base(inters[0].Client)
// {
// _inters = inters;
// _bbs = inters.Map(x => x.GetButtonBuilder());
// }
//
// protected override string Name
// => throw new NotSupportedException();
// protected override IEmote Emote
// => throw new NotSupportedException();
//
// protected override ValueTask<bool> Validate(SocketMessageComponent smc)
// => new(true);
//
// public override Task ExecuteOnActionAsync(SocketMessageComponent smc)
// {
// for (var i = 0; i < _bbs.Length; i++)
// {
// if (_bbs[i].CustomId == smc.Data.CustomId)
// return _inters[i].ExecuteOnActionAsync(smc);
// }
//
// return Task.CompletedTask;
// }
//
// public override MessageComponent CreateComponent()
// {
// var comp = new ComponentBuilder();
//
// foreach (var bb in _bbs)
// comp.WithButton(bb);
//
// return comp.Build();
// }
// }

View File

@@ -20,6 +20,7 @@ public class NadekoInteractionBuilder
// {
// this.isOwn = isOwn;
// return this;
// }
public NadekoInteractionBuilder WithAction(in Func<SocketMessageComponent, Task> fn)
@@ -28,7 +29,7 @@ public class NadekoInteractionBuilder
return this;
}
public NadekoActionInteraction Build(DiscordSocketClient client, ulong userId)
public NadekoButtonActionInteraction Build(DiscordSocketClient client, ulong userId)
{
if (iData is null)
throw new InvalidOperationException("You have to specify the data before building the interaction");

View File

@@ -5,4 +5,4 @@
/// </summary>
/// <param name="Emote">Emote which will show on a button</param>
/// <param name="CustomId">Custom interaction id</param>
public record NadekoInteractionData(IEmote Emote, string CustomId);
public record NadekoInteractionData(IEmote Emote, string CustomId, string? Text = null);

View File

@@ -3,11 +3,11 @@
/// <summary>
/// Interaction which only the author can use
/// </summary>
public abstract class NadekoOwnInteraction : NadekoInteraction
public abstract class NadekoButtonOwnInteraction : NadekoButtonInteraction
{
protected readonly ulong _authorId;
protected NadekoOwnInteraction(DiscordSocketClient client, ulong authorId) : base(client)
protected NadekoButtonOwnInteraction(DiscordSocketClient client, ulong authorId) : base(client)
=> _authorId = authorId;
protected override ValueTask<bool> Validate(SocketMessageComponent smc)

View File

@@ -0,0 +1,20 @@
namespace NadekoBot;
public class SimpleInteraction<T>
{
public ButtonBuilder Button { get; }
private readonly Func<SocketMessageComponent, T, Task> _onClick;
private readonly T? _state;
public SimpleInteraction(ButtonBuilder button, Func<SocketMessageComponent, T?, Task> onClick, T? state = default)
{
Button = button;
_onClick = onClick;
_state = state;
}
public async Task TriggerAsync(SocketMessageComponent smc)
{
await _onClick(smc, _state!);
}
}

View File

@@ -0,0 +1,26 @@
namespace NadekoBot.Common;
public abstract class NInteraction
{
private readonly DiscordSocketClient _client;
private readonly ulong _userId;
private readonly Func<SocketMessageComponent, Task> _action;
protected abstract NadekoInteractionData Data { get; }
public NInteraction(
DiscordSocketClient client,
ulong userId,
Func<SocketMessageComponent, Task> action)
{
_client = client;
_userId = userId;
_action = action;
}
public NadekoButtonInteraction GetInteraction()
=> new NadekoInteractionBuilder()
.WithData(Data)
.WithAction(_action)
.Build(_client, _userId);
}

View File

@@ -36,7 +36,7 @@ public abstract class NadekoModule : ModuleBase
string error,
string url = null,
string footer = null,
NadekoInteraction inter = null)
NadekoButtonInteraction inter = null)
=> ctx.Channel.SendErrorAsync(_eb, title, error, url, footer);
public Task<IUserMessage> SendConfirmAsync(
@@ -47,33 +47,33 @@ public abstract class NadekoModule : ModuleBase
=> ctx.Channel.SendConfirmAsync(_eb, title, text, url, footer);
//
public Task<IUserMessage> SendErrorAsync(string text, NadekoInteraction inter = null)
public Task<IUserMessage> SendErrorAsync(string text, NadekoButtonInteraction inter = null)
=> ctx.Channel.SendAsync(_eb, text, MessageType.Error, inter);
public Task<IUserMessage> SendConfirmAsync(string text, NadekoInteraction inter = null)
public Task<IUserMessage> SendConfirmAsync(string text, NadekoButtonInteraction inter = null)
=> ctx.Channel.SendAsync(_eb, text, MessageType.Ok, inter);
public Task<IUserMessage> SendPendingAsync(string text, NadekoInteraction inter = null)
public Task<IUserMessage> SendPendingAsync(string text, NadekoButtonInteraction inter = null)
=> ctx.Channel.SendAsync(_eb, text, MessageType.Pending, inter);
// localized normal
public Task<IUserMessage> ErrorLocalizedAsync(LocStr str, NadekoInteraction inter = null)
public Task<IUserMessage> ErrorLocalizedAsync(LocStr str, NadekoButtonInteraction inter = null)
=> SendErrorAsync(GetText(str), inter);
public Task<IUserMessage> PendingLocalizedAsync(LocStr str, NadekoInteraction inter = null)
public Task<IUserMessage> PendingLocalizedAsync(LocStr str, NadekoButtonInteraction inter = null)
=> SendPendingAsync(GetText(str), inter);
public Task<IUserMessage> ConfirmLocalizedAsync(LocStr str, NadekoInteraction inter = null)
public Task<IUserMessage> ConfirmLocalizedAsync(LocStr str, NadekoButtonInteraction inter = null)
=> SendConfirmAsync(GetText(str), inter);
// localized replies
public Task<IUserMessage> ReplyErrorLocalizedAsync(LocStr str, NadekoInteraction inter = null)
=> SendErrorAsync($"{Format.Bold(ctx.User.ToString())} {GetText(str)}");
public Task<IUserMessage> ReplyErrorLocalizedAsync(LocStr str, NadekoButtonInteraction inter = null)
=> SendErrorAsync($"{Format.Bold(ctx.User.ToString())} {GetText(str)}", inter);
public Task<IUserMessage> ReplyPendingLocalizedAsync(LocStr str, NadekoInteraction inter = null)
=> SendPendingAsync($"{Format.Bold(ctx.User.ToString())} {GetText(str)}");
public Task<IUserMessage> ReplyPendingLocalizedAsync(LocStr str, NadekoButtonInteraction inter = null)
=> SendPendingAsync($"{Format.Bold(ctx.User.ToString())} {GetText(str)}", inter);
public Task<IUserMessage> ReplyConfirmLocalizedAsync(LocStr str, NadekoInteraction inter = null)
=> SendConfirmAsync($"{Format.Bold(ctx.User.ToString())} {GetText(str)}");
public Task<IUserMessage> ReplyConfirmLocalizedAsync(LocStr str, NadekoButtonInteraction inter = null)
=> SendConfirmAsync($"{Format.Bold(ctx.User.ToString())} {GetText(str)}", inter);
public async Task<bool> PromptUserConfirmAsync(IEmbedBuilder embed)
{

View File

@@ -1,47 +0,0 @@
#nullable disable
namespace NadekoBot.Common;
public class OldImageUrls
{
public int Version { get; set; } = 2;
public CoinData Coins { get; set; }
public Uri[] Currency { get; set; }
public Uri[] Dice { get; set; }
public RategirlData Rategirl { get; set; }
public XpData Xp { get; set; }
//new
public RipData Rip { get; set; }
public SlotData Slots { get; set; }
public class RipData
{
public Uri Bg { get; set; }
public Uri Overlay { get; set; }
}
public class SlotData
{
public Uri[] Emojis { get; set; }
public Uri[] Numbers { get; set; }
public Uri Bg { get; set; }
}
public class CoinData
{
public Uri[] Heads { get; set; }
public Uri[] Tails { get; set; }
}
public class RategirlData
{
public Uri Matrix { get; set; }
public Uri Dot { get; set; }
}
public class XpData
{
public Uri Bg { get; set; }
}
}

View File

@@ -0,0 +1,10 @@
using OneOf.Types;
using OneOf;
namespace NadekoBot.Common;
public static class OneOfExtensions
{
public static bool TryGetValue<T>(this OneOf<T, None> oneOf, out T value)
=> oneOf.TryPickT0(out value, out _);
}

View File

@@ -1,25 +0,0 @@
#nullable disable
namespace NadekoBot.Common;
public static class PlatformHelper
{
private const int PROCESSOR_COUNT_REFRESH_INTERVAL_MS = 30000;
private static volatile int processorCount;
private static volatile int lastProcessorCountRefreshTicks;
public static int ProcessorCount
{
get
{
var now = Environment.TickCount;
if (processorCount == 0 || now - lastProcessorCountRefreshTicks >= PROCESSOR_COUNT_REFRESH_INTERVAL_MS)
{
processorCount = Environment.ProcessorCount;
lastProcessorCountRefreshTicks = now;
}
return processorCount;
}
}
}

View File

@@ -1,11 +1,12 @@
#nullable disable
using Newtonsoft.Json;
using System.Text.Json.Serialization;
namespace NadekoBot.Common.Pokemon;
public class SearchPokemon
{
[JsonProperty("num")]
[JsonPropertyName("num")]
public int Id { get; set; }
public string Species { get; set; }

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