Compare commits

..

177 Commits

Author SHA1 Message Date
Kwoth
ab0fd44b46 Updated changelog 2022-01-14 20:50:19 +01:00
Kwoth
b61f499f91 Merge branch 'hokutochen-v3-patch-35474' into 'v3'
Updated Linux guide to list the following supported versions.

See merge request Kwoth/nadekobot!221
2022-01-11 10:31:43 +00:00
Hokuto Chen
53d365db3a Updated Linux guide to list the following supported versions. 2022-01-11 10:31:43 +00:00
Kwoth
140c4f7fd6 Merge branch 'hokutochen-v3-patch-28697' into 'v3'
added warning to not follow release if following source

See merge request Kwoth/nadekobot!218
2022-01-07 18:12:58 +00:00
Kwoth
5627a3b172 Merge branch 'hokutochen-v3-patch-45634' into 'v3'
added warning to not follow manual release if following source

See merge request Kwoth/nadekobot!219
2022-01-07 18:12:43 +00:00
Kwoth
4795fa98a0 Merge branch 'hokutochen-v3-patch-32876' into 'v3'
added warning to not follow source guide if using windows updater

See merge request Kwoth/nadekobot!217
2022-01-07 18:11:49 +00:00
Hokuto Chen
93453ba522 added warning to not follow manual release if following source 2022-01-07 08:05:15 +00:00
Hokuto Chen
c6a9108474 added warning to not follow release if following source 2022-01-07 06:50:00 +00:00
Kwoth
c3ba805acf Possible fix for patreon auto-creds update 2022-01-07 06:28:58 +01:00
Kwoth
c0ce22a6b7 .greetdm staggering to avoid ratelimits during raids 2022-01-06 22:53:59 +01:00
Kwoth
22183501fe Fixed .gelbooru 2022-01-06 21:07:32 +01:00
Hokuto Chen
2fbdab3235 added warning to not follow source guide if using windows updater 2022-01-06 20:00:48 +00:00
Kwoth
804d3f79fd Updated changelog 2022-01-06 05:26:45 +01:00
Kwoth
fb119cca4c Merge branch 'v3' of https://gitlab.com/kwoth/nadekobot into v3 2022-01-06 05:16:20 +01:00
Kwoth
31af5ea8c2 Upped version and changelog 2022-01-06 05:15:11 +01:00
Kwoth
e1776d6093 GreetDmMessage will now show a footer with the source server -.- 2022-01-06 05:09:01 +01:00
Kwoth
33dd4bbf0e Merge branch 'make-image-use-safesearch-for-google-images' into 'v3'
Add safesearch to .img when using google

See merge request Kwoth/nadekobot!216
2022-01-05 02:28:58 +00:00
ZeroNyan
af343ac1f0 Add safesearch to .img when using google 2022-01-05 02:28:58 +00:00
Kwoth
065807c180 Merge branch 'hokutochen-v3-patch-71695' into 'v3'
updated creds.yml example owner ID section

See merge request Kwoth/nadekobot!215
2022-01-05 02:28:18 +00:00
Kwoth
9cd24feccc Merge branch 'hokutochen-v3-patch-15423' into 'v3'
Transferred over VPS guide from 1.9

See merge request Kwoth/nadekobot!214
2022-01-05 02:27:47 +00:00
Hokuto Chen
a2d1506915 Transferred over VPS guide from 1.9 2022-01-05 02:27:46 +00:00
Kwoth
54a32a5770 Merge branch 'hokutochen-v3-patch-85263' into 'v3'
Omitted comma explanation for multi owner ID section to avoid confusion

See merge request Kwoth/nadekobot!213
2022-01-05 02:26:53 +00:00
Hokuto Chen
5b9abeb0b2 Omitted comma explanation for multi owner ID section to avoid confusion 2022-01-05 02:26:53 +00:00
Hokuto Chen
accfb2d1ac updated creds.yml example owner ID section 2022-01-04 17:58:47 +00:00
Kwoth
71d383c4db Merge branch 'hokutochen-v3-patch-31256' into 'v3'
fixed GoogleApiKey, formatting error, thanks to alaruba for catching the mistake

See merge request Kwoth/nadekobot!212
2022-01-04 06:03:05 +00:00
Hokuto Chen
197ee9f5ff fixed GoogleApiKey, formatting error, thanks to alaruba for catching the mistake 2022-01-04 00:19:28 +00:00
Kwoth
d51d159962 Merge branch 'hokutochen-v3-patch-51803' into 'v3'
added: Enable "custom search api" for GoogleAPIKey section.

See merge request Kwoth/nadekobot!211
2022-01-03 12:09:06 +00:00
Hokuto Chen
89b0eabd41 added: Enable "custom search api" for GoogleAPIKey section. 2022-01-03 12:09:06 +00:00
Kwoth
8d932d546a Merge branch 'hangman-patch' into 'v3'
small bugfix for hangman

See merge request Kwoth/nadekobot!210
2022-01-03 12:08:41 +00:00
Alan Beatty
9ea3460e3d small bugfix for hangman 2022-01-03 12:08:41 +00:00
Kwoth
7bd4db60a8 Wrong condition in downloadtracker 2022-01-01 16:31:23 +01:00
Kwoth
42e1f35df2 Removed useless #if 2022-01-01 16:28:16 +01:00
Kwoth
179784da3e Possible fix for slowdown with inrole and xplb clean commands 2022-01-01 16:27:30 +01:00
Kwoth
9ed0c870d1 Merge branch 'v3' of https://gitlab.com/kwoth/nadekobot into v3 2021-12-28 10:59:11 +01:00
Kwoth
77e288ee54 Possible fix for .smch 2021-12-28 10:59:02 +01:00
Kwoth
58adaa9110 Merge branch 'hokutochen-v3-patch-89665' into 'v3'
fixed "from source guide" links and "manual prereq" link

See merge request Kwoth/nadekobot!209
2021-12-27 19:10:05 +00:00
Hokuto Chen
d3a73945e7 fixed "from source guide" links and "manual prereq" link 2021-12-27 19:10:05 +00:00
Kwoth
caca407abd Merge branch 'hokutochen-v3-patch-31383' into 'v3'
fixed error in Source guide (accidentally used quotes)

See merge request Kwoth/nadekobot!208
2021-12-24 22:02:51 +00:00
Hokuto Chen
4fd7b2d8cd fixed error in Source guide (accidentally used quotes) 2021-12-24 21:46:24 +00:00
Kwoth
eaea6e3c54 Merge branch 'hokutochen-v3-patch-44970' into 'v3'
Update step 4 of "linux from source" to be more specific.

See merge request Kwoth/nadekobot!206
2021-12-21 20:18:15 +00:00
Hokuto Chen
0bb68c7723 Update step 4 of "linux from source" to be more specific. 2021-12-21 08:48:22 +00:00
Kwoth
52b2c0910c Merge branch 'trans-patch' into 'v3'
ToLower for `.trans` language parameters

See merge request Kwoth/nadekobot!205
2021-12-21 01:20:52 +00:00
Alan Beatty
9a4bb7bff9 ToLower for .trans language parameters 2021-12-21 01:20:52 +00:00
Kwoth
ab5450a125 Merge branch 'hokutochen-v3-patch-59240' into 'v3'
updated "GoogleAPIKey" section

See merge request Kwoth/nadekobot!203
2021-12-21 00:46:13 +00:00
Kwoth
bcce32423c Merge branch 'banmsgpatch' into 'v3'
Fix color for ban DMs with plain text ban message.

Closes #324

See merge request Kwoth/nadekobot!204
2021-12-21 00:00:45 +00:00
Alan Beatty
c42d529016 Fix color for ban DMs with plain text ban message. 2021-12-21 00:00:45 +00:00
Hokuto Chen
cbea5077be updated "GoogleAPIKey" section 2021-12-20 04:40:53 +00:00
Kwoth
cdc2cc1439 Merge branch 'hokutochen-v3-patch-62793' into 'v3'
fixed broken "Enter your bot's token" link in "linux release" portion

See merge request Kwoth/nadekobot!202
2021-12-19 01:12:47 +00:00
Kwoth
87819f21bf Merge branch 'v3' of https://gitlab.com/kwoth/nadekobot into v3 2021-12-19 01:56:42 +01:00
Kwoth
d1be56fbc1 Another attempt at fixing a weird coordinator bug 2021-12-19 01:56:30 +01:00
Hokuto Chen
14016a761d fixed broken "Enter your bot's token" link in "linux release" portion 2021-12-19 00:24:55 +00:00
Kwoth
12a64c4c4d Merge branch 'xpstrspatch' into 'v3'
Update xpex channel to use the right string

See merge request Kwoth/nadekobot!201
2021-12-18 21:11:06 +00:00
Kwoth
d922120f58 Merge branch 'transpatch' into 'v3'
Small `.trans` fixes

See merge request Kwoth/nadekobot!200
2021-12-18 21:10:06 +00:00
Alan Beatty
8e8e349e65 Small .trans fixes 2021-12-18 21:10:06 +00:00
Kwoth
ccdf0fc077 Merge branch 'hokutochen-v3-patch-43568' into 'v3'
separated "cd output && cp creds_example.yml creds.yml"

See merge request Kwoth/nadekobot!199
2021-12-18 21:09:16 +00:00
Hokuto Chen
8c66bcb1e1 separated "cd output && cp creds_example.yml creds.yml" 2021-12-18 21:09:16 +00:00
Kwoth
77fb47183f Possible fix for #322 2021-12-18 22:07:03 +01:00
Alan Beatty
d275dc36b2 Update xpex channel to use the right string 2021-12-18 11:13:02 +00:00
Kwoth
7bff20cc70 Upped version to 3.0.11 2021-12-17 23:41:31 +01:00
Kwoth
29f5dcc359 Merge branch 'v3' of https://gitlab.com/kwoth/nadekobot into v3 2021-12-17 23:39:08 +01:00
Kwoth
14f2851072 - you should be able to update your .atl now without disabling it
- capitalization of language input in .atl should no longer matter
2021-12-17 23:38:06 +01:00
Kwoth
a2b25f8246 atl no longer pings if in nodelete mode 2021-12-17 16:32:35 +01:00
Kwoth
a38951b5ad atl will no longer post the translation if it's equivalent to the input message
updated packages
2021-12-17 16:26:14 +01:00
Kwoth
4c1b911cb7 Inrole string fix 2021-12-17 15:59:18 +01:00
Kwoth
6c9f231453 Update responses.nl-NL.json (POEditor.com) 2021-12-15 19:09:59 +00:00
Kwoth
83daf3c30f Made .showembed always output lowercase field names and no null values 2021-12-14 19:41:07 +01:00
Kwoth
9be8140d4d Added .showembed <msgid> and .showembed #channel <msgid> which will show you embed json from the specified message 2021-12-13 20:29:45 +01:00
Kwoth
96c9b699aa Merge branch 'v3' of https://gitlab.com/kwoth/nadekobot into v3 2021-12-13 19:28:34 +01:00
Kwoth
3c0768a372 .atl / .at reworked 2021-12-13 19:28:22 +01:00
Kwoth
58b22e3d9e Merge branch 'award-patch' into 'v3'
Award no longer misinterprets IDs

See merge request Kwoth/nadekobot!195
2021-12-13 13:11:56 +00:00
Alan Beatty
0474551e2f Award no longer misinterprets IDs 2021-12-13 13:11:56 +00:00
Kwoth
c85bdec396 Merge branch 'qdel-patch' into 'v3'
Clarity on qdel

See merge request Kwoth/nadekobot!197
2021-12-13 13:07:00 +00:00
Alan Beatty
5fa39eaa9f Clarity on qdel 2021-12-13 13:07:00 +00:00
Kwoth
8eaaa35c7a Merge branch 'perm-patch' into 'v3'
Replace `RequireUserPemission` with `UserPerm`

See merge request Kwoth/nadekobot!196
2021-12-13 13:06:28 +00:00
Alan Beatty
1c24f95efa Replace RequireUserPemission with UserPerm 2021-12-13 13:06:28 +00:00
Kwoth
fcc49dbbdb Made .sinfo slightly better 2021-12-11 16:13:52 +01:00
Kwoth
3a317590b4 Merge branch 'v3' of https://gitlab.com/kwoth/nadekobot into v3 2021-12-11 15:27:37 +01:00
Kwoth
36c013fbb5 Added .qimport and .qexport commands, fixed some response strings 2021-12-11 15:27:32 +01:00
Kwoth
a9ec8049f6 Update responses.id-ID.json (POEditor.com) 2021-12-11 01:04:37 +00:00
Kwoth
ced0d97e3a Update responses.pt-BR.json (POEditor.com) 2021-12-11 00:37:34 +00:00
Kwoth
24d0f57dc3 Update responses.pl-PL.json (POEditor.com) 2021-12-11 00:37:33 +00:00
Kwoth
514ecd6be8 Update responses.fr-FR.json (POEditor.com) 2021-12-11 00:37:32 +00:00
Kwoth
02eb6e172b added slots.currencyFontColor to gambling.yml 2021-12-09 20:54:26 +01:00
Kwoth
d22c579875 Merge branch 'v3' of https://gitlab.com/kwoth/nadekobot into v3 2021-12-09 02:07:12 +01:00
Kwoth
f70e49fc6a Removed unwanted command 2021-12-09 02:07:03 +01:00
Kwoth
8b410561f9 Update responses.uk-UA.json (POEditor.com) 2021-12-09 01:04:48 +00:00
Kwoth
3c79fd1d6f Update responses.es-ES.json (POEditor.com) 2021-12-09 01:04:46 +00:00
Kwoth
e9f1a9b1dd Update responses.ru-RU.json (POEditor.com) 2021-12-09 01:04:45 +00:00
Kwoth
3c293ae6db Update responses.pt-BR.json (POEditor.com) 2021-12-09 01:04:43 +00:00
Kwoth
a5d9e7de66 Update responses.de-DE.json (POEditor.com) 2021-12-09 01:04:42 +00:00
Kwoth
4d9e48cd41 Update responses.fr-FR.json (POEditor.com) 2021-12-09 01:04:42 +00:00
Kwoth
b7ead22e09 .remindl and .remindrm commands now supports optional 'server' parameter for Administrators which allows them to delete any reminder created on the server 2021-12-09 01:48:39 +01:00
Kwoth
9f219cddbb Updated osx guide with dotnet 5 installation instructions 2021-12-09 01:11:12 +01:00
Kwoth
cf9792f24a Fixed .rule34, closes #320 2021-12-07 23:44:57 +01:00
Kwoth
0187dd57ac Fixed an exception in coordinator preventing some shards from restarting 2021-12-05 08:47:33 +01:00
Kwoth
2d2e54e31e Bot will now check for permissions when trying to apply punishments 2021-12-05 08:47:10 +01:00
Kwoth
cb7c5e48fd Fixed gambling vote reward getting overwritten and reset to 100 every time 2021-12-05 08:20:30 +01:00
Kwoth
771c2745dc .crypto now supports top 5k coins. closes #138 2021-12-04 15:45:04 +01:00
Kwoth
59c0f2f4b3 Added graceful option to die (kill coordinator without killing shards) 2021-12-01 09:47:41 +01:00
Kwoth
219ca39cd1 Added .coordreload which will reload coord.yml when using NadekoBot.Coordinator
- bots with more than 1 shard will now use redis strings provider
2021-12-01 09:41:23 +01:00
Kwoth
1e6d0806d7 Fixed some console log spam which was incorrectly done by streamrole feature, upped version to 3.0.10 in stats 2021-12-01 07:02:01 +01:00
Kwoth
71f1e43272 .xprewsreset now has correct permissions 2021-12-01 05:41:03 +01:00
Kwoth
8499e1da70 Updated changelog, upped version in the stats to 3.0.9 2021-11-24 01:51:34 +01:00
Kwoth
a2ea806bed Removed slot.numbers from images.yml as they're no longer used anywhere, thx ala 2021-11-24 01:49:03 +01:00
Kwoth
732b5dfeed Merge branch 'v3' of https://gitlab.com/kwoth/nadekobot into v3 2021-11-21 03:01:18 +01:00
Kwoth
d4dcdc761a .economy should not overflow so easily anymore, and big numbers look nicer 2021-11-21 03:01:04 +01:00
Kwoth
57996ba290 Merge branch 'take-award-patch' into 'v3'
Change award and take to not use ShmartNumber

See merge request Kwoth/nadekobot!192
2021-11-21 01:37:30 +00:00
Alan Beatty
4b29b3a239 Change award and take to not use ShmartNumber 2021-11-21 01:37:30 +00:00
Kwoth
54ac955395 Merge branch 'xpservicepatch' into 'v3'
Remove deprecated method from XpService.

See merge request Kwoth/nadekobot!191
2021-11-21 01:09:45 +00:00
Kwoth
f4fa298866 Merge branch 'plantpatch' into 'v3'
ShmartNumber for .plant

See merge request Kwoth/nadekobot!193
2021-11-21 01:09:14 +00:00
Kwoth
b2fafc964f Merge branch 'v3' into 'v3'
Move plant/pick where they belong

See merge request Kwoth/nadekobot!178
2021-11-21 01:08:46 +00:00
Kwoth
22b452e449 Added .warn weights, improved .warnlog 2021-11-21 02:01:21 +01:00
Alan Beatty
fda385a5e4 ShmartNumber for .plant 2021-11-20 18:36:05 -06:00
Alan Beatty
c28f7cfa07 Remove deprecated method from XpService. 2021-11-20 17:55:33 -06:00
Kwoth
0a029a7847 Added image attachment support for .ea if you omit imageUrl 2021-11-21 00:22:10 +01:00
Kwoth
c050ce2123 Added .emojiadd command 2021-11-21 00:07:19 +01:00
Kwoth
27613410dd Another migration fix for users who manually edited their databasea and are unable to update to v3 due to invalid db state. 2021-11-18 18:01:02 +01:00
Kwoth
1513008b4b Merge branch 'v3' of https://gitlab.com/kwoth/nadekobot into v3 2021-11-18 16:41:40 +01:00
Kwoth
bf97cffd84 Fixed cleanup migration if there are waifus which don't have a corresponding entry in DiscordUser 2021-11-18 16:41:27 +01:00
Kwoth
e37d1c46db Merge branch 'patreon-token-refresh' into 'v3'
Patreon Access and Refresh Tokens should now be automatically updated

See merge request Kwoth/nadekobot!189
2021-11-17 18:45:50 +00:00
Kwoth
06c20c6fa4 Patreon Access and Refresh Tokens should now be automatically updated 2021-11-17 18:45:49 +00:00
Kwoth
aa518d60a5 Merge branch 'v3' of https://gitlab.com/kwoth/nadekobot into v3 2021-11-17 16:27:54 +01:00
Kwoth
d55ce7accc Merge branch 'massban2' into 'v3'
Add an audit log reason to massban

See merge request Kwoth/nadekobot!188
2021-11-14 23:41:04 +00:00
Alan Beatty
502c5cec07 Add an audit log reason to massban 2021-11-13 14:43:30 -06:00
Kwoth
ee5c13607b Add support for hackbans on massban
Closes #307

See merge request Kwoth/nadekobot!187
2021-11-13 20:38:01 +00:00
Alan Beatty
5a681a5194 Add support for hackbans on massban
Closes #307
2021-11-13 20:38:01 +00:00
Kwoth
68395372f0 Merge branch 'v3' of https://gitlab.com/kwoth/nadekobot into v3 2021-11-09 16:08:58 +01:00
Kwoth
c8e01bd158 Merge branch 'rerorace' into 'v3'
Fix possible race condition for reaction roles

### Description  
This MR aims to fix a possible race condition on the addition of exclusive reaction roles when the reactions are spammed by the user.

### Changes Proposed  
- Add a `ConcurrentHashSet<(ulong, ulong)>` to keep track of exclusive reaction roles that are being processed.
- Added logic that takes the collection above in consideration.
    - If entry is present, quit silently.
    - Else, perform the reaction role stuff then remove the entry.

### Details  
Exclusive reaction roles are meant to be exactly that - exclusive.

Normally, when a user selects an exclusive role they receive that role. If they select another role, their previous role is removed and the new one is added. There is a bug where if the user spams the reactions for a short period of time, Nadeko will eventually assign them multiple roles that are meant to be exclusive with each other. This happens because the events that handle the addition and removal work in a weird way - first they offload the removal of the roles to a `Task.Run()`, which also happens to have a `Task.Delay()` in it (possibly to avoid Discord ratelimits).

Concurrently, it proceeds to add the role that the user picked. The problem with this approach is that the Task that handles the role removal takes long enough for another reaction event to trigger and start the same work for a different reaction role. Then mayhem ensues, with different events concomitantly adding and removing the roles that previous events have removed or added. In the end, the user ends up with multiple exclusive roles they are not supposed to.

This MR fixes this by having a local field that keeps track of the reaction roles that are being currently processed (a `ConcurrentHashSet<T>` where T is a tuple `(ulong, ulong)` - (message ID, user ID)). When a reaction event runs, it adds itself to the concurrent hashset. If another event triggers and tries to add itself while the previous event still hasn't finished, it silently quits so it doesn't interfere with the current event. I was not entirely satisfied with the way this works, so I tried another system that cancels the old events (with a CancellationToken) instead of just making the new events quit, but that resulted in multiple exclusive roles being temporarily assigned to the user (just for a few seconds, but still).

If another approach is preferred, then please do let me know.

### Notes  
- Methods in that entire file need to be broken down into smaller methods.
- The loop that removes old reactions is **very slow**. Using `Task.WhenAll()` instead of awaiting the removals could help improve performance (but could also trigger ratelimits).

See merge request Kwoth/nadekobot!183
2021-11-09 10:41:49 +00:00
Kaoticz
1d57191700 Fix possible race condition for reaction roles 2021-11-09 10:41:49 +00:00
Kwoth
02c7ded457 Merge branch 'hokutochen-v3-patch-18181' into 'v3'
Fixed broken link

See merge request Kwoth/nadekobot!185
2021-11-09 08:32:37 +00:00
Hokuto Chen
12c483d222 fixed broken "Create a Discord Bot application and invite the bot to your server" link 2021-11-09 08:17:15 +00:00
Kwoth
c80898a7bf Fixed an error that would show up in the console when a club image couldn't be drawn in certain circumstances 2021-11-04 17:01:18 +01:00
Kwoth
aae2805785 Updated changelog.md - Fixed 3.0.7 notes title 2021-11-04 09:05:22 +01:00
Kwoth
fc3695d090 Updated changelog.md for 3.0.8 2021-11-04 09:04:49 +01:00
Kwoth
428429ff44 Fixed creating new users in the database when awarding currency 2021-11-03 16:27:48 +01:00
Kwoth
dc344caec6 Version upped to 3.0.8 2021-11-03 15:23:45 +01:00
Kwoth
2a4d55f81d Merge branch 'v3-dev' into 'v3'
Slots redesign nad images moved to images.yml

See merge request Kwoth/nadekobot!181
2021-11-03 14:22:51 +00:00
Kwoth
d090aa23ee Slots redesign nad images moved to images.yml 2021-11-03 14:22:51 +00:00
Kwoth
65062306c6 Merge branch 'tbodt-v3-patch-66982' into 'v3'
Reduce required permissions for deleting quotes

See merge request Kwoth/nadekobot!182
2021-10-30 11:54:03 +00:00
tbodt
9ae3b66fc2 Reduce required permissions for deleting quotes 2021-10-27 01:25:40 +00:00
Kwoth
c4ba43ec6d Merge branch 'v3-dev' into 'v3'
backport of public nsfw module

See merge request Kwoth/nadekobot!176
2021-10-21 23:35:58 +00:00
Kwoth
1141791ce5 backport of public nsfw module 2021-10-21 23:35:58 +00:00
Kwoth
49f1ef7db0 Merge branch 'hokutochen-v3-patch-79292' into 'v3'
Updated images for V3 updater

See merge request Kwoth/nadekobot!179
2021-10-21 23:35:11 +00:00
Hokuto Chen
a70c35e101 Updated images for V3 updater 2021-10-21 23:35:11 +00:00
Yuno Gasai
717543f6c2 Move plant/pick where they belong 2021-10-19 09:28:55 -04:00
Kwoth
b61b1dbfaa Merge branch 'memfix' into 'v3'
Fixed memory counter not refreshing over time

See merge request Kwoth/nadekobot!177
2021-10-17 22:56:10 +00:00
Kaoticz
92365fd22d Fixed memory counter not refreshing over time 2021-10-17 22:56:10 +00:00
Kwoth
24a4745193 Merge branch 'v3-dev' into 'v3'
Created VotesApi project nad re-worked vote rewards handling

See merge request Kwoth/nadekobot!172
2021-10-15 22:06:30 +00:00
Kwoth
1af75fd813 Created VotesApi project nad re-worked vote rewards handling 2021-10-15 22:06:30 +00:00
Kwoth
18160164eb Merge branch 'memsize' into 'v3'
Corrected memory usage on StatusService

See merge request Kwoth/nadekobot!175
2021-10-15 22:04:30 +00:00
Kaoticz
2fd7d97025 Corrected memory usage on StatusService 2021-10-15 22:04:30 +00:00
Kwoth
6ada15049d Merge branch 'ban-kick-patch' into 'v3'
TrimTo to avoid length limit for ban/kick reasons

See merge request Kwoth/nadekobot!174
2021-10-13 17:51:08 +00:00
Alan Beatty
0ebc40b95c TrimTo to avoid length limit for ban/kick reasons 2021-10-12 17:07:46 -05:00
Kwoth
02de25a931 Typo 2021-10-09 19:55:19 +02:00
Kwoth
0b395e9176 Merge branch 'v3' of https://gitlab.com/kwoth/nadekobot into v3 2021-10-09 19:49:26 +02:00
Kwoth
4532f992cd Fixed field not preserving inline after string replacements. closes #308 2021-10-09 19:48:54 +02:00
Kwoth
34201f0558 Merge branch 'v3' into 'v3'
Docs fix

See merge request Kwoth/nadekobot!168
2021-10-09 17:43:18 +00:00
Hunter T
d2f4d63183 Docs fix 2021-10-09 17:43:18 +00:00
Kwoth
b41c014869 Merge branch 'Kieteyuku-v3-patch-34025' into 'v3'
Use … in TrimTo to decrease necessary deletions

See merge request Kwoth/nadekobot!171
2021-10-09 17:42:24 +00:00
Ene
d348347762 Update StringExtensions.cs 2021-10-08 20:20:50 +00:00
Kwoth
db7cf3d757 Upped version to 3.0.7, Updated changelog 2021-10-05 13:12:28 +02:00
Kwoth
83ea046d5f Fixed %users% and %shard.usercount% placeholders not showing correct member count 2021-10-05 13:09:52 +02:00
Kwoth
d5c94424e9 Merge branch 'v3-docker' into 'v3'
install youtube-dl through pip for a better availability of cdn

See merge request Kwoth/nadekobot!165
2021-10-01 08:26:15 +00:00
Kwoth
ff95b3d00f - .streamsclear re-added. It will remove all followed streams on the server.
- .gifts now have 3 new ✂️ Haircut 🧻 ToiletPaper and 🥀 WiltedRose which **reduce** waifu's value
- Added a new multiplier (waifu.multi.negative_gift_effect default 0.5, changeable via .config gambling or data/gambling.yml)
2021-09-30 08:53:15 +02:00
Veovis
4b5fa3bb04 install youtube-dl through pip for a better availability of cdn 2021-09-27 21:21:44 +02:00
Kwoth
52c9ec670d Upped version to 3.0.6. Slightly updated hangman help string 2021-09-27 10:57:10 +02:00
Kwoth
1ac472c676 Urls will now properly work in embeds. Fixes #306 2021-09-27 10:52:14 +02:00
Kwoth
39f01a3164 Merge branch 'v3-dev' into 'v3'
Hangman Rewrite

See merge request Kwoth/nadekobot!164
2021-09-27 02:00:19 +00:00
Kwoth
486916944b Hangman Rewrite 2021-09-27 02:00:19 +00:00
Kwoth
68e96cd1bb Merge branch 'v3-docker' into 'v3'
V3 docker

See merge request Kwoth/nadekobot!163
2021-09-27 00:10:18 +00:00
Veovis
5a647d100a V3 docker 2021-09-27 00:10:18 +00:00
Kwoth
a562a571e2 - .shopadd will now ignore negative price input
- Make sure bot has manage roles permission for .xprr
- Small cleanup
2021-09-22 23:07:23 +02:00
Kwoth
12146ad2da Make sure the bot has manageroles permission for .aar 2021-09-22 22:41:00 +02:00
Kwoth
453ac3efd2 Re-did migration as the old one was wrong 2021-09-21 22:48:18 +02:00
Kwoth
c9e89e1911 - .logignore now supports targeting channels and users
- .logignore now lists all ignored channels and users when no parameter is provided
- Renamed and cleaned up some log-related fields
2021-09-21 22:39:24 +02:00
Kwoth
611817f78a Yes, it's just a warning 2021-09-21 20:59:05 +02:00
Kwoth
2e66137f3e Fixed an exception which was breaking repeater loop 2021-09-21 20:28:37 +02:00
215 changed files with 22286 additions and 10125 deletions

View File

@@ -9,6 +9,7 @@
!src/NadekoBot.Generators/**
# Use Ayu stuff
!src/ayu/**
!docker-entrypoint.sh
# ignore bin and obj folders in projects
src/**/bin/*

View File

@@ -95,4 +95,31 @@ upload-windows-updater-release:
- 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"
- 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"
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

View File

@@ -2,17 +2,138 @@
Experimental changelog. Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except date format. a-c-f-r-o
## Unreleased
## [3.0.13] - 14.04.2021
### Fixed
- Fixed `.greetdm` causing ratelimits during raids
- Fixed `.gelbooru`
## [3.0.12] - 06.01.2021
### Fixed
- `.smch` Fixed
- `.trans` command will now work properly with capitilized language names
- Ban message color with plain text fixed
- Fixed some grpc coordinator bugs
- Fixed a string in `.xpex`
- Google version of .img will now have safe search enabled
- Fixed a small bug in `.hangman`
## [3.0.11] - 17.12.2021
### Added
- `.remindl` and `.remindrm` commands now supports optional 'server' parameter for Administrators which allows them to delete any reminder created on the server
- Added slots.currencyFontColor to gambling.yml
- Added `.qexport` and `.qimport` commands which allow you to export and import quotes just like `.crsexport`
- Added `.showembed <msgid>` and `.showembed #channel <msgid>` which will show you embed json from the specified message
### Changed
- `.at` and `.atl` commands reworked
- Persist restarts
- Will now only translate non-commands
- You can switch between `.at del` and `.at` without clearing the user language registrations
- Disabling `.at` will clear all user language registrations on that channel
- Users can't register languages if the `.at` is not enabled
- Looks much nicer
- Bot will now reply to user messages with a translation if `del` is disabled
- Bot will make an embed with original and translated text with user avatar and name if `del` is enabled
- If the bot is unable to delete messages while having `del` enabled, it will reset back to the no-del behavior for the current session
### Fixed
- `.crypto` now supports top 5000 coins
## [3.0.10] - 01.12.2021
### Changed
- `.warn` now supports weighted warnings
- `.warnlog` will now show current amount and total amount of warnings
### Fixed
- `.xprewsreset` now has correct permissions
### Removed
- Removed slot.numbers from `images.yml` as they're no longer used
## [3.0.9] - 21.11.2021
### Changed
- `.ea` will now use an image attachments if you omit imageUrl
### Added
- Added `.emojiadd` with 3 overloads
- `.ea :customEmoji:` which copies another server's emoji
- `.ea newName :customEmoji:` which copies emoji under a different name
- `.ea emojiName <imagelink.png>` which creates a new emoji from the specified image
- Patreon Access and Refresh Tokens should now be automatically updated once a month as long as the user has provided the necessary credentials in creds.yml file:
- `Patreon.ClientId`
- `Patreon.RefreshToken` (will also get updated once a month but needs an initial value)
- `Patreon.ClientSecret`
- `Patreon.CampaignId`
### Fixed
- Fixed an error that would show up in the console when a club image couldn't be drawn in certain circumstances
## [3.0.8] - 03.11.2021
### Added
- Created VotesApi project nad re-worked vote rewards handling
- Updated votes entries in creds.yml with explanations on how to set up vote links
### Fixed
- Fixed adding currency to users who don't exist in the database
- Memory used by the bot is now correct (thanks to kotz)
- Ban/kick will no longer fail due to too long reasons
- Fixed some fields not preserving inline after string replacements
### Changed
- `images.json` moved to `images.yml`
- Links will use the new cdn url
- Heads and Tails images will be updated if you haven't changed them already
- `.slot` redesigned (and updated entries in `images.yml`)
- Reduced required permissions for .qdel (thanks to tbodt)
## [3.0.7] - 05.10.2021
### Added
- `.streamsclear` re-added. It will remove all followed streams on the server.
- `.gifts` now have 3 new ✂️ Haircut 🧻 ToiletPaper and 🥀 WiltedRose which **reduce** waifu's value
- They are called negative gifts
- They show up at the end of the `.gifts` page and are marked with a broken heart
- They have a separate multiplier (`waifu.multi.negative_gift_effect` default 0.5, changeable via `.config gambling` or `data/gambling.yml`)
- When gifted, the waifu's price will be reduced by the `price * multiplier`
- Negative gifts don't show up in `.waifuinfo` nor is the record of them kept in the database
### Fixed
- Fixed `%users%` and `%shard.usercount%` placeholders not showing correct values
## [3.0.6] - 27.09.2021
### Added
- .logignore now supports ignoring users and channels. Use without parameters to see the ignore list
### Changed
- Hangman rewrite
- Hangman categories are now held in separate .yml files in data/hangman/XYZ.yml where XYZ is the category name
### Fixed
- Fixed an exception which caused repeater queue to break
- Fixed url field not working in embeds
## [3.0.5] - 20.09.2021
### Fixed
- Fixed images not automatically reloading on startup if the keys don't exist
- Fixed `.logserver` - it should no longer throw an exception if you had no logsettings previously
## [3.0.4] - 16.09.2021
### Added
- Fully translated to Brazilian Portuguese 🎉
- Added `%server.boosters%` and `%server.boost_level%` placeholders
- Added `DmHelpTextKeywords` to `data/bot.yml`
@@ -20,6 +141,7 @@ Experimental changelog. Mostly based on [keepachangelog](https://keepachangelog.
- If no keywords are specified, bot will reply to every DM (like before)
### Fixed
- Possible fix for `.repeat` bug
- Slight adjustment for repeater logic
- Timer should no longer increase on some repeaters

View File

@@ -1,4 +1,4 @@
FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build
FROM mcr.microsoft.com/dotnet/sdk:5.0-buster-slim AS build
WORKDIR /source
COPY src/NadekoBot/*.csproj src/NadekoBot/
@@ -9,14 +9,34 @@ RUN dotnet restore src/NadekoBot/
COPY . .
WORKDIR /source/src/NadekoBot
RUN dotnet --version
RUN dotnet publish -c Release -o /app --no-restore
RUN set -xe; \
dotnet --version; \
dotnet publish -c Release -o /app --no-restore; \
mv /app/data /app/data_init; \
rm -Rf libopus* libsodium* opus.* runtimes/win* runtimes/osx* runtimes/linux-arm* runtimes/linux-mips*; \
find /app -type f -exec chmod -x {} \; ;\
chmod +x /app/NadekoBot
# final stage/image
FROM mcr.microsoft.com/dotnet/runtime:5.0
FROM mcr.microsoft.com/dotnet/runtime:5.0-buster-slim
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; \
update-alternatives --install /usr/bin/python python /usr/bin/python3.7 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
COPY --from=build /app ./
COPY docker-entrypoint.sh /usr/local/sbin
ENV shard_id=0
ENV total_shards=1
WORKDIR /app
COPY --from=build /app ./
VOLUME [ "app/data", "app/creds.yml", "app/creds_example.yml" ]
ENTRYPOINT dotnet NadekoBot.dll "$shard_id" "$total_shards"
VOLUME [ "app/data" ]
ENTRYPOINT [ "/usr/local/sbin/docker-entrypoint.sh" ]
CMD dotnet NadekoBot.dll "$shard_id" "$total_shards"

View File

@@ -25,6 +25,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NadekoBot.Coordinator", "sr
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NadekoBot.Generators", "src\NadekoBot.Generators\NadekoBot.Generators.csproj", "{3BC3BDF8-1A0B-45EB-AB2B-C0891D4D37B8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NadekoBot.VotesApi", "src\NadekoBot.VotesApi\NadekoBot.VotesApi.csproj", "{3BC82CFE-BEE7-451F-986B-17EDD1570C4F}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -62,6 +64,12 @@ Global
{3BC3BDF8-1A0B-45EB-AB2B-C0891D4D37B8}.GlobalNadeko|Any CPU.Build.0 = Debug|Any CPU
{3BC3BDF8-1A0B-45EB-AB2B-C0891D4D37B8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3BC3BDF8-1A0B-45EB-AB2B-C0891D4D37B8}.Release|Any CPU.Build.0 = Release|Any CPU
{3BC82CFE-BEE7-451F-986B-17EDD1570C4F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3BC82CFE-BEE7-451F-986B-17EDD1570C4F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3BC82CFE-BEE7-451F-986B-17EDD1570C4F}.GlobalNadeko|Any CPU.ActiveCfg = Debug|Any CPU
{3BC82CFE-BEE7-451F-986B-17EDD1570C4F}.GlobalNadeko|Any CPU.Build.0 = Debug|Any CPU
{3BC82CFE-BEE7-451F-986B-17EDD1570C4F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3BC82CFE-BEE7-451F-986B-17EDD1570C4F}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -73,6 +81,7 @@ Global
{DB448DD4-C97F-40E9-8BD3-F605FF1FF833} = {04929013-5BAB-42B0-B9B2-8F2BB8F16AF2}
{AE9B7F8C-81D7-4401-83A3-643B38258374} = {04929013-5BAB-42B0-B9B2-8F2BB8F16AF2}
{3BC3BDF8-1A0B-45EB-AB2B-C0891D4D37B8} = {04929013-5BAB-42B0-B9B2-8F2BB8F16AF2}
{3BC82CFE-BEE7-451F-986B-17EDD1570C4F} = {04929013-5BAB-42B0-B9B2-8F2BB8F16AF2}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {5F3F555C-855F-4BE8-B526-D062D3E8ACA4}

20
docker-entrypoint.sh Executable file
View File

@@ -0,0 +1,20 @@
#!/bin/sh
set -e;
data_init=/app/data_init
data=/app/data
# populate /app/data if empty
for i in $(ls $data_init)
do
if [ ! -e "$data/$i" ]; then
[ -f "$data_init/$i" ] && cp "$data_init/$i" "$data/$i"
[ -d "$data_init/$i" ] && cp -r "$data_init/$i" "$data/$i"
fi
done
# fix folder permissions
chown -R nadeko:nadeko "$data"
# drop to regular user and launch command
exec sudo -u nadeko "$@"

View File

@@ -13,8 +13,13 @@ This document aims to guide you through the process of creating a Discord accoun
- Click on the `Add a Bot` button and confirm that you do want to add a bot to this app.
- **Optional:** Add bot's avatar and description.
- Copy your Token to `creds.yml` as shown above.
- Scroll down to the `Privileged Gateway Intents` section and enable both intents.
These are required for a number of features to function properly, and should both be on.
- Scroll down to the `Privileged Gateway Intents` section
- Enabled the following:
- PRESENCE INTENT
- SERVER MEMBERS INTENT
- MESSAGE CONTENT INTENT
These are required for a number of features to function properly, and all should be on.
##### Getting Owner ID*(s)*:
@@ -32,7 +37,7 @@ For a single owner, it should look like this:
- 105635576866156544
```
For multiple owners, it should look like this (pay attention to the commas, the last ID should **never** have a comma next to it):
For multiple owners, it should look like this:
```yml
OwnerIds:
@@ -56,4 +61,4 @@ For multiple owners, it should look like this (pay attention to the commas, the
That's it! You may now go back to the installation guide you were following before 🎉
[DiscordApp]: https://discordapp.com/developers/applications/me
[DiscordApp]: https://discordapp.com/developers/applications/me

View File

@@ -4,6 +4,19 @@
#### [Linux migration instructions](../migration-guide/#linux)
#### Operating System Compatibility
It is recommended that you use **Ubuntu 20.04**, as there have been nearly no problems with it. Also, **32-bit systems are incompatible**.
##### Compatible operating systems:
- Ubuntu: 16.04, 18.04, 20.04, 21.04, 21.10
- Mint: 19, 20
- Debian: 9, 10
- CentOS: 7
- openSUSE
- Fedora: 33, 34, 35
## Linux From Source
Open Terminal (if you're on an installation with a window manager) and navigate to the location where you want to install the bot (for example `cd ~`)
@@ -13,11 +26,11 @@ Open Terminal (if you're on an installation with a window manager) and navigate
1. Download and run the **new** installer script `cd ~ && wget -N https://gitlab.com/Kwoth/nadeko-bash-installer/-/raw/master/linuxAIO.sh && bash linuxAIO.sh`
2. Install prerequisites (type `1` and press enter)
3. Download the bot (type `2` and press enter)
4. Exit the installer in order to set up your `creds.yml`
4. Exit the installer (type `5` and press enter)
5. Copy the creds.yml template `cp nadekobot/output/creds_example.yml nadekobot/output/creds.yml`
6. Open `nadekobot/output/creds.yml` with your favorite text editor. We will use nano here
- `nano nadekobot/output/creds.yml`
7. [Enter your bot's token](../../creds-guide)
7. [Click here to follow creds guide](../../creds-guide)
- After you're done, you can close nano (and save the file) by inputting, in order
- `CTRL` + `X`
- `Y`
@@ -34,6 +47,8 @@ Open Terminal (if you're on an installation with a window manager) and navigate
## Linux Release
⚠ IF YOU ARE FOLLOWING THE GUIDE ABOVE, IGNORE THIS SECTION ⚠
##### Installation Instructions
1. Download the latest release from <https://gitlab.com/Kwoth/nadekobot/-/releases>
@@ -49,7 +64,7 @@ Open Terminal (if you're on an installation with a window manager) and navigate
- `cp creds_example.yml creds.yml`
6. Open `creds.yml` with your favorite text editor. We will use nano here
- `nano nadekobot/output/creds.yml`
8. [Enter your bot's token](#creds-guide)
8. [Click here to follow creds guide](../../creds-guide)
- After you're done, you can close nano (and save the file) by inputting, in order
- `CTRL` + `X`
- `Y`
@@ -134,6 +149,8 @@ Compared to using tmux, this method requires a little bit more work to set up, b
echo "[Unit]
Description=NadekoBot service
After=network.target
StartLimitIntervalSec=60
StartLimitBurst=2
[Service]
Type=simple
@@ -144,10 +161,11 @@ Compared to using tmux, this method requires a little bit more work to set up, b
# source code.
#ExecStartPre=/usr/bin/dotnet build ../src/NadekoBot/NadekoBot.csproj -c Release -o output/
ExecStart=/usr/bin/dotnet NadekoBot.dll
Restart=on-failure
RestartSec=5
StandardOutput=syslog
StandardError=syslog
SyslogIdentifier=NadekoBot
Restart=always
[Install]
WantedBy=multi-user.target" | sudo tee /etc/systemd/system/nadeko.service
@@ -172,12 +190,16 @@ This method is similar to the one above, but requires one extra step, with the a
echo "[Unit]
Description=NadekoBot service
After=network.target
StartLimitIntervalSec=60
StartLimitBurst=2
[Service]
Type=simple
User=$USER
WorkingDirectory=$PWD
WorkingDirectory=$_WORKING_DIR
ExecStart=/bin/bash NadekoRun.sh
Restart=on-failure
RestartSec=5
StandardOutput=syslog
StandardError=syslog
SyslogIdentifier=NadekoBot
@@ -191,14 +213,14 @@ This method is similar to the one above, but requires one extra step, with the a
4. Use the following command to create a script that will be used to start Nadeko:
```bash
echo "#\!/bin/bash
echo \"\"
echo \"Running NadekoBot in the background with auto restart\"
{
echo '#!/bin/bash'
echo ""
echo "echo \"Running NadekoBot in the background with auto restart\"
youtube-dl -U
# If you want Nadeko to be compiled prior to every startup, uncomment the lines
# below. Note that it's not neccessary unless you are personally modifying the
# below. Note that it's not necessary unless you are personally modifying the
# source code.
#echo \"Compiling NadekoBot...\"
#cd \"$PWD\"/nadekobot
@@ -207,23 +229,64 @@ This method is similar to the one above, but requires one extra step, with the a
echo \"Starting NadekoBot...\"
while true; do
{
cd \"$PWD\"/nadekobot/output
dotnet NadekoBot.dll
## If a non-zero exit code is produced, exit this script.
} || {
error_code=\"\$?\"
if [[ -d $PWD/nadekobot/output ]]; then
cd $PWD/nadekobot/output || {
echo \"Failed to change working directory to $PWD/nadekobot/output\" >&2
echo \"Ensure that the working directory inside of '/etc/systemd/system/nadeko.service' is correct\"
echo \"Exiting...\"
exit 1
}
else
echo \"$PWD/nadekobot/output doesn't exist\"
exit 1
fi
dotnet NadekoBot.dll || {
echo \"An error occurred when trying to start NadekBot\"
echo \"EXIT CODE: \$?\"
exit \"\$error_code\"
echo \"Exiting...\"
exit 1
}
echo \"Waiting for 5 seconds...\"
sleep 5
youtube-dl -U
echo \"Restarting NadekoBot...\"
done
echo \"Stopping NadekoBot...\"" > NadekoRun.sh
echo \"Stopping NadekoBot...\""
} > NadekoRun.sh
```
5. Start Nadeko:
- `sudo systemctl start nadeko.service && sudo systemctl enable nadeko.service`
- `sudo systemctl start nadeko.service && sudo systemctl enable nadeko.service`
### Setting up Nadeko on a Linux VPS (Digital Ocean Droplet)
If you want Nadeko to play music for you 24/7 without having to hosting it on your PC and want to keep it cheap, reliable and convenient as possible, you can try Nadeko on Linux Digital Ocean Droplet using the link [DigitalOcean](http://m.do.co/c/46b4d3d44795/) (by using this link, you will get **$10 credit** and also support Nadeko)
**Setting up NadekoBot**
Assuming you have followed the link above to setup an account and a Droplet with a 64-bit operational system on Digital Ocean and got the `IP address and root password (in your e-mail)` to login, it's time to get started.
**This section is only relevant to those who want to host Nadeko on DigitalOcean. Go through this whole section before setting the bot up.**
#### Prerequisites
- Download [PuTTY](http://www.chiark.greenend.org.uk/~sgtatham/putty/download.html)
- Download [WinSCP](https://winscp.net/eng/download.php) *(optional)*
- [Create and invite the bot](../../creds-guide).
#### Starting up
- **Open PuTTY** and paste or enter your `IP address` and then click **Open**.
If you entered your Droplets IP address correctly, it should show **login as:** in a newly opened window.
- Now for **login as:**, type `root` and press enter.
- It should then ask for a password. Type the `root password` you have received in your e-mail address, then press Enter.
If you are running your droplet for the first time, it will most likely ask you to change your root password. To do that, copy the **password you've received by e-mail** and paste it on PuTTY.
- To paste, just right-click the window (it won't show any changes on the screen), then press Enter.
- Type a **new password** somewhere, copy and paste it on PuTTY. Press Enter then paste it again.
**Save the new password somewhere safe.**
After that, your droplet should be ready for use. [Follow the guide from the beginning](#linux-from-source) to set Nadeko up on your newly created VPS.

View File

@@ -2,13 +2,32 @@
Open Terminal (if you don't know how to, click on the magnifying glass on the top right corner of your screen and type **Terminal** on the window that pops up) and navigate to the location where you want to install the bot (for example `cd ~`)
##### Installing Homebrew and wget
##### Installing Homebrew, wget and dotnet
###### Homebrew/wget
*Skip this step if you already have homebrew installed*
- Copy and paste this command, then press Enter:
- `/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"`
- Install wget
- `brew install wget`
- `brew install wget`
###### Dotnet
- Download [.net5 SDK](https://dotnet.microsoft.com/download/dotnet/5.0)
- Open the `.pkg` file you've downloaded and install it.
- Run this command in Terminal. There might be output. If there is, disregard it. (copy-paste the entire block)
```bash
sudo mkdir /usr/local/bin
sudo mkdir /usr/local/lib
```
- Run this command in Terminal. There won't be any output. (copy-paste the entire block):
```bash
sudo ln -s /usr/local/share/dotnet/dotnet /usr/local/bin
sudo ln -s /usr/local/opt/openssl/lib/libcrypto.1.0.0.dylib /usr/local/lib/
sudo ln -s /usr/local/opt/openssl/lib/libssl.1.0.0.dylib /usr/local/lib/
```
##### Installation Instructions
@@ -37,6 +56,8 @@ Open Terminal (if you don't know how to, click on the magnifying glass on the to
## MacOS Manual Release installation instructions
⚠ IF YOU ARE FOLLOWING THE GUIDE ABOVE, IGNORE THIS SECTION ⚠
##### Installation Instructions
1. Download the latest release from <https://gitlab.com/Kwoth/nadekobot/-/releases>
@@ -101,4 +122,4 @@ rm -r nadekobot-old/data/strings && \
cp -RT nadekobot-old/data/ nadekobot/data/ && \
cp nadekobot-old/creds.yml nadekobot/ && \
cd nadekobot && chmod +x NadekoBot
```
```

View File

@@ -12,16 +12,16 @@
| [Setup](#setup) |
| [Starting the Bot](#starting-the-bot) |
| [Updating Nadeko](#updating-nadeko) |
| [Manually Installing the Prerequisites from the Updater](#if-the-updater-fails-to-install-the-prerequisites-for-any-reason) |
| [Manually Installing the Prerequisites from the Updater](#music-prerequisites) |
*Note: If you want to make changes to Nadeko's source code, please follow the [From Source][SourceGuide] guide instead.*
*Note: If you want to make changes to Nadeko's source code, please follow the [From Source](#windows-from-source) guide instead.*
*If you have Windows 7 or a 32-bit system, please refer to the [From Source][SourceGuide] guide.*
*If you have Windows 7 or a 32-bit system, please refer to the [From Source](#windows-from-source)) guide.*
#### Prerequisites
- Windows 8 or later (64-bit)
- [Create a Discord Bot application and invite the bot to your server](../../creds-guide.md)
- [Create a Discord Bot application and invite the bot to your server](../creds-guide.md)
**Optional**
@@ -32,12 +32,13 @@
- Download and run the [NadekoBot v3 Updater][Updater].
- Click on the + at the top left to create a new bot.
![NadekoBot Updater](https://i.imgur.com/KZV49uf.png "NadekoBot Updater")
![NadekoBot Updater](https://i.imgur.com/FmR7F7o.png "NadekoBot Updater")
- Give your bot a name and then click **`Go to setup`** at the lower right.
![Create a new bot](https://i.imgur.com/Xnp7iQL.png "Create a new bot")
![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/6RMXNqw.png "Bot Setup")
![Bot Setup](https://i.imgur.com/HqAl36p.png "Bot Setup")
- Click on **`Install`** next to **`Redis`**.
- 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).
- When installation is finished, click on **`CREDS`** to the left of **`RUN`** at the lower right.
@@ -68,6 +69,8 @@ You can still install them manually:
### Windows From Source
⚠ IF YOU ARE FOLLOWING THE GUIDE ABOVE, IGNORE THIS SECTION ⚠
##### Prerequisites
**Install these before proceeding or your bot will not work!**
@@ -82,11 +85,12 @@ Open PowerShell (press windows button on your keyboard and type powershell, it s
1. `git clone https://gitlab.com/kwoth/nadekobot -b v3 --depth 1`
2. `cd nadekobot`
3. `dotnet publish -c Release -o output/ src/NadekoBot/`
4. `cd output && cp creds_example.yml creds.yml`
5. Open `creds.yml` with your favorite text editor (Please don't use Notepad or WordPad. You can use Notepad++, VSCode, Atom, Sublime, or something similar)
6. [Enter your bot's token](#creds-guide)
7. Run the bot `dotnet NadekoBot.dll`
8. 🎉
4. `cd output`
5. `cp creds_example.yml creds.yml`
6. Open `creds.yml` with your favorite text editor (Please don't use Notepad or WordPad. You can use Notepad++, VSCode, Atom, Sublime, or something similar)
7. [Enter your bot's token](#creds-guide)
8. Run the bot `dotnet NadekoBot.dll`
9. 🎉
##### Update Instructions
@@ -128,8 +132,6 @@ In order to use music commands, you need ffmpeg and youtube-dl installed.
[Redis]: https://github.com/MicrosoftArchive/redis/releases/download/win-3.0.504/Redis-x64-3.0.504.msi
[Visual C++ 2010 (x86)]: https://download.microsoft.com/download/1/6/5/165255E7-1014-4D0A-B094-B6A430A6BFFC/vcredist_x86.exe
[Visual C++ 2017 (x64)]: https://aka.ms/vs/15/release/vc_redist.x64.exe
[SourceGuide]: ../from-source
[ffmpeg-32bit]: https://cdn.nadeko.bot/dl/ffmpeg-32.zip
[ffmpeg-64bit]: https://cdn.nadeko.bot/dl/ffmpeg-64.zip
[youtube-dl]: https://yt-dl.org/downloads/latest/youtube-dl.exe

View File

@@ -8,8 +8,14 @@ This part is completely optional, **however it's necessary for music and a few o
- Go to [Google Console][Google Console] and log in.
- Create a new project (name does not matter).
- Once the project is created, go into `Library`
- Under the `YouTube APIs` section, enable `YouTube Data API`
- On the left tab, access `Credentials`,
- Under the `YouTube APIs` section
- Select `YouTube Data API v3`,
- Click enable.
- Search for `Custom Search API`
- Select `Custom Search API`,
- Click enable.
- Open up the `Navigation menu` on the top right with the three lines.
- select `APIs & Services`, then select `Credentials`,
- Click `Create Credentials` button,
- Click on `API Key`
- A new window will appear with your `Google API key`
@@ -18,7 +24,7 @@ This part is completely optional, **however it's necessary for music and a few o
- Open up `creds.yml` and look for `GoogleAPIKey`, paste your API key after the `:`.
- It should look like this:
```yml
GoogleApiKey: "AIzaSyDSci1sdlWQOWNVj1vlXxxxxxbk0oWMEzM"
GoogleApiKey: AIzaSyDSci1sdlWQOWNVj1vlXxxxxxbk0oWMEzM
```
- **MashapeKey**
- Required for Hearthstone cards.
@@ -92,11 +98,10 @@ version: 1
token: 'MTE5Nzc3MDIxMzE5NTc3NjEw.VlhNCw.BuqJFyzdIUAK1PRf1eK1Cu89Jew'
# List of Ids of the users who have bot owner permissions
# **DO NOT ADD PEOPLE YOU DON'T TRUST**
ownerIds: [
105635123466156544,
145521851676884992,
341420590009417729
]
ownerIds:
- 105635123466156544
- 145521851676884992
- 341420590009417729
# The number of shards that the bot will running on.
# Leave at 1 if you don't know what you're doing.
totalShards: 1

View File

@@ -9,9 +9,9 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Grpc.AspNetCore" Version="2.38.0" />
<PackageReference Include="Grpc.AspNetCore" Version="2.41.0" />
<PackageReference Include="Serilog" Version="2.10.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="4.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="4.0.1" />
<PackageReference Include="YamlDotNet" Version="11.2.1" />
</ItemGroup>

View File

@@ -160,18 +160,24 @@ namespace NadekoBot.Coordinator
private void StartShard(int shardId)
{
var status = _shardStatuses[shardId];
if (status.Process is {HasExited: false} p)
try
{
try
if (status.Process is { HasExited: false } p)
{
p.Kill(true);
try
{
p.Kill(true);
}
catch
{
}
}
catch
{
}
}
status.Process?.Dispose();
status.Process?.Dispose();
}
catch
{
}
var proc = StartShardProcess(shardId);
_shardStatuses[shardId] = status with

View File

@@ -8,7 +8,7 @@
<ItemGroup>
<PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
</ItemGroup>

View File

@@ -0,0 +1,25 @@
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/.idea
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/azds.yaml
**/bin
**/charts
**/docker-compose*
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md

1
src/NadekoBot.VotesApi/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
store/

View File

@@ -0,0 +1,44 @@
using System.Collections.Generic;
using System.Security.Claims;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using NadekoBot.VotesApi.Controllers;
namespace NadekoBot.VotesApi
{
public class AuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
public const string SchemeName = "AUTHORIZATION_SCHEME";
public const string DiscordsClaim = "DISCORDS_CLAIM";
public const string TopggClaim = "TOPGG_CLAIM";
private readonly IConfiguration _conf;
public AuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock,
IConfiguration conf)
: base(options, logger, encoder, clock)
{
_conf = conf;
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
var claims = new List<Claim>();
if (_conf[ConfKeys.DISCORDS_KEY].Trim() == Request.Headers["Authorization"].ToString().Trim())
claims.Add(new(DiscordsClaim, "true"));
if (_conf[ConfKeys.TOPGG_KEY] == Request.Headers["Authorization"].ToString().Trim())
claims.Add(new Claim(TopggClaim, "true"));
return Task.FromResult(AuthenticateResult.Success(new(new(new ClaimsIdentity(claims)), SchemeName)));
}
}
}

View File

@@ -0,0 +1,8 @@
namespace NadekoBot.VotesApi
{
public static class ConfKeys
{
public const string DISCORDS_KEY = "DiscordsKey";
public const string TOPGG_KEY = "TopGGKey";
}
}

View File

@@ -0,0 +1,26 @@
namespace NadekoBot.VotesApi
{
public class DiscordsVoteWebhookModel
{
/// <summary>
/// The ID of the user who voted
/// </summary>
public string User { get; set; }
/// <summary>
/// The ID of the bot which recieved the vote
/// </summary>
public string Bot { get; set; }
/// <summary>
/// Contains totalVotes, votesMonth, votes24, hasVoted - a list of IDs of users who have voted this month, and
/// Voted24 - a list of IDs of users who have voted today
/// </summary>
public string Votes { get; set; }
/// <summary>
/// The type of event, whether it is a vote event or test event
/// </summary>
public string Type { get; set; }
}
}

View File

@@ -0,0 +1,8 @@
namespace NadekoBot.VotesApi
{
public static class Policies
{
public const string DiscordsAuth = "DiscordsAuth";
public const string TopggAuth = "TopggAuth";
}
}

View File

@@ -0,0 +1,30 @@
namespace NadekoBot.VotesApi
{
public class TopggVoteWebhookModel
{
/// <summary>
/// Discord ID of the bot that received a vote.
/// </summary>
public string Bot { get; set; }
/// <summary>
/// Discord ID of the user who voted.
/// </summary>
public string User { get; set; }
/// <summary>
/// The type of the vote (should always be "upvote" except when using the test button it's "test").
/// </summary>
public string Type { get; set; }
/// <summary>
/// Whether the weekend multiplier is in effect, meaning users votes count as two.
/// </summary>
public bool Weekend { get; set; }
/// <summary>
/// Query string params found on the /bot/:ID/vote page. Example: ?a=1&b=2.
/// </summary>
public string Query { get; set; }
}
}

View File

@@ -0,0 +1,33 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using NadekoBot.VotesApi.Services;
namespace NadekoBot.VotesApi.Controllers
{
[ApiController]
[Route("[controller]")]
public class DiscordsController : ControllerBase
{
private readonly ILogger<TopGgController> _logger;
private readonly IVotesCache _cache;
public DiscordsController(ILogger<TopGgController> logger, IVotesCache cache)
{
_logger = logger;
_cache = cache;
}
[HttpGet("new")]
[Authorize(Policy = Policies.DiscordsAuth)]
public async Task<IEnumerable<Vote>> New()
{
var votes = await _cache.GetNewDiscordsVotesAsync();
if(votes.Count > 0)
_logger.LogInformation("Sending {NewDiscordsVotes} new discords votes.", votes.Count);
return votes;
}
}
}

View File

@@ -0,0 +1,34 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using NadekoBot.VotesApi.Services;
namespace NadekoBot.VotesApi.Controllers
{
[ApiController]
[Route("[controller]")]
public class TopGgController : ControllerBase
{
private readonly ILogger<TopGgController> _logger;
private readonly IVotesCache _cache;
public TopGgController(ILogger<TopGgController> logger, IVotesCache cache)
{
_logger = logger;
_cache = cache;
}
[HttpGet("new")]
[Authorize(Policy = Policies.TopggAuth)]
public async Task<IEnumerable<Vote>> New()
{
var votes = await _cache.GetNewTopGgVotesAsync();
if(votes.Count > 0)
_logger.LogInformation("Sending {NewTopggVotes} new topgg votes.", votes.Count);
return votes;
}
}
}

View File

@@ -0,0 +1,52 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using NadekoBot.VotesApi.Services;
namespace NadekoBot.VotesApi.Controllers
{
[ApiController]
public class WebhookController : ControllerBase
{
private readonly ILogger<WebhookController> _logger;
private readonly IVotesCache _votesCache;
private readonly IConfiguration _conf;
public WebhookController(ILogger<WebhookController> logger, IVotesCache votesCache, IConfiguration conf)
{
_logger = logger;
_votesCache = votesCache;
_conf = conf;
}
[HttpPost("/discordswebhook")]
[Authorize(Policy = Policies.DiscordsAuth)]
public async Task<IActionResult> DiscordsWebhook([FromBody]DiscordsVoteWebhookModel data)
{
_logger.LogInformation("User {UserId} has voted for Bot {BotId} on {Platform}",
data.User,
data.Bot,
"discords.com");
await _votesCache.AddNewDiscordsVote(data.User);
return Ok();
}
[HttpPost("/topggwebhook")]
[Authorize(Policy = Policies.TopggAuth)]
public async Task<IActionResult> TopggWebhook([FromBody] TopggVoteWebhookModel data)
{
_logger.LogInformation("User {UserId} has voted for Bot {BotId} on {Platform}",
data.User,
data.Bot,
"top.gg");
await _votesCache.AddNewTopggVote(data.User);
return Ok();
}
}
}

View File

@@ -0,0 +1,20 @@
FROM mcr.microsoft.com/dotnet/aspnet:5.0 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443
FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build
WORKDIR /src
COPY ["src/NadekoBot.VotesApi/NadekoBot.VotesApi.csproj", "NadekoBot.VotesApi/"]
RUN dotnet restore "src/NadekoBot.VotesApi/NadekoBot.VotesApi.csproj"
COPY . .
WORKDIR "/src/NadekoBot.VotesApi"
RUN dotnet build "NadekoBot.VotesApi.csproj" -c Release -o /app/build
FROM build AS publish
RUN dotnet publish "NadekoBot.VotesApi.csproj" -c Release -o /app/publish
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "NadekoBot.VotesApi.dll"]

View File

@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MorseCode.ITask" Version="2.0.3" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.6.3" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace NadekoBot.VotesApi
{
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); });
}
}

View File

@@ -0,0 +1,31 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:16451",
"sslPort": 44323
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"NadekoBot.VotesApi": {
"commandName": "Project",
"dotnetRunMessages": "true",
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "https://localhost:5001;http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@@ -0,0 +1,46 @@
## Votes Api
This api is used if you want your bot to be able to reward users who vote for it on discords.com or top.gg
#### [GET] `/discords/new`
Get the discords votes received after previous call to this endpoint.
Input full url of this endpoint in your creds.yml file under Discords url field.
For example "https://api.my.cool.bot/discords/new"
#### [GET] `/topgg/new`
Get the topgg votes received after previous call to this endpoint.
Input full url of this endpoint in your creds.yml file under Topgg url field.
For example "https://api.my.cool.bot/topgg/new"
#### [POST] `/discordswebhook`
Input this endpoint as the webhook on discords.com bot edit page
model: https://docs.botsfordiscord.com/methods/receiving-votes
For example "https://api.my.cool.bot/topggwebhook"
#### [POST] `/topggwebhook`
Input this endpoint as the webhook https://top.gg/bot/:your-bot-id/webhooks (replace :your-bot-id with your bot's id)
model: https://docs.top.gg/resources/webhooks/#schema
For example "https://api.my.cool.bot/discordswebhook"
Input your super-secret header value in appsettings.json's DiscordsKey and TopGGKey fields
They must match your DiscordsKey and TopGG key respectively, as well as your secrets in the discords.com and top.gg webhook setup pages
Full Example:
⚠ Change TopggKey and DiscordsKey to a secure long string
⚠ You can use https://www.random.org/strings/?num=1&len=20&digits=on&upperalpha=on&loweralpha=on&unique=on&format=html&rnd=new to generate it
`creds.yml`
```yml
votes:
TopggServiceUrl: "https://api.my.cool.bot/topgg"
TopggKey: "my_topgg_key"
DiscordsServiceUrl: "https://api.my.cool.bot/discords"
DiscordsKey: "my_discords_key"
```
`appsettings.json`
```json
...
"DiscordsKey": "my_discords_key",
"TopGGKey": "my_topgg_key",
...
```

View File

@@ -0,0 +1,105 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading;
using MorseCode.ITask;
namespace NadekoBot.VotesApi.Services
{
public class FileVotesCache : IVotesCache
{
private const string statsFile = "store/stats.json";
private const string topggFile = "store/topgg.json";
private const string discordsFile = "store/discords.json";
private readonly SemaphoreSlim locker = new SemaphoreSlim(1, 1);
public FileVotesCache()
{
if (!Directory.Exists("store"))
Directory.CreateDirectory("store");
if(!File.Exists(topggFile))
File.WriteAllText(topggFile, "[]");
if(!File.Exists(discordsFile))
File.WriteAllText(discordsFile, "[]");
}
public ITask AddNewTopggVote(string userId)
{
return AddNewVote(topggFile, userId);
}
public ITask AddNewDiscordsVote(string userId)
{
return AddNewVote(discordsFile, userId);
}
private async ITask AddNewVote(string file, string userId)
{
await locker.WaitAsync();
try
{
var votes = await GetVotesAsync(file);
votes.Add(userId);
await File.WriteAllTextAsync(file , JsonSerializer.Serialize(votes));
}
finally
{
locker.Release();
}
}
public async ITask<IList<Vote>> GetNewTopGgVotesAsync()
{
var votes = await EvictTopggVotes();
return votes;
}
public async ITask<IList<Vote>> GetNewDiscordsVotesAsync()
{
var votes = await EvictDiscordsVotes();
return votes;
}
private ITask<List<Vote>> EvictTopggVotes()
=> EvictVotes(topggFile);
private ITask<List<Vote>> EvictDiscordsVotes()
=> EvictVotes(discordsFile);
private async ITask<List<Vote>> EvictVotes(string file)
{
await locker.WaitAsync();
try
{
var ids = await GetVotesAsync(file);
await File.WriteAllTextAsync(file, "[]");
return ids?
.Select(x => (Ok: ulong.TryParse(x, out var r), Id: r))
.Where(x => x.Ok)
.Select(x => new Vote
{
UserId = x.Id
})
.ToList();
}
finally
{
locker.Release();
}
}
private async ITask<IList<string>> GetVotesAsync(string file)
{
await using var fs = File.Open(file, FileMode.Open);
var votes = await JsonSerializer.DeserializeAsync<List<string>>(fs);
return votes;
}
}
}

View File

@@ -0,0 +1,13 @@
using System.Collections.Generic;
using MorseCode.ITask;
namespace NadekoBot.VotesApi.Services
{
public interface IVotesCache
{
ITask<IList<Vote>> GetNewTopGgVotesAsync();
ITask<IList<Vote>> GetNewDiscordsVotesAsync();
ITask AddNewTopggVote(string userId);
ITask AddNewDiscordsVote(string userId);
}
}

View File

@@ -0,0 +1,69 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.OpenApi.Models;
using NadekoBot.VotesApi.Services;
namespace NadekoBot.VotesApi
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddSingleton<IVotesCache, FileVotesCache>();
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "NadekoBot.VotesApi", Version = "v1" });
});
services
.AddAuthentication(opts =>
{
opts.DefaultScheme = AuthHandler.SchemeName;
opts.AddScheme<AuthHandler>(AuthHandler.SchemeName, AuthHandler.SchemeName);
});
services
.AddAuthorization(opts =>
{
opts.DefaultPolicy = new AuthorizationPolicyBuilder(AuthHandler.SchemeName)
.RequireAssertion(x => false)
.Build();
opts.AddPolicy(Policies.DiscordsAuth, policy => policy.RequireClaim(AuthHandler.DiscordsClaim));
opts.AddPolicy(Policies.TopggAuth, policy => policy.RequireClaim(AuthHandler.TopggClaim));
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseSwagger();
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "NadekoBot.VotesApi v1"));
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
}
}
}

View File

@@ -0,0 +1,9 @@
using System;
namespace NadekoBot.VotesApi
{
public class Vote
{
public ulong UserId { get; set; }
}
}

View File

@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}

View File

@@ -0,0 +1,12 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"DiscordsKey": "my_discords_key",
"TopGGKey": "my_topgg_key",
"AllowedHosts": "*"
}

View File

@@ -19,6 +19,7 @@ using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Common.Configs;
using NadekoBot.Db;
using NadekoBot.Modules.Administration.Services;
using NadekoBot.Modules.Searches;
using Serilog;
namespace NadekoBot
@@ -28,7 +29,7 @@ namespace NadekoBot
private readonly IBotCredentials _creds;
private readonly CommandService _commandService;
private readonly DbService _db;
private readonly BotCredsProvider _credsProvider;
private readonly IBotCredsProvider _credsProvider;
public event Func<GuildConfig, Task> JoinedGuild = delegate { return Task.CompletedTask; };
@@ -95,8 +96,8 @@ namespace NadekoBot
}
var svcs = new ServiceCollection()
.AddTransient<IBotCredentials>(_ => _creds) // bot creds
.AddSingleton(_credsProvider)
.AddTransient<IBotCredentials>(_ => _credsProvider.GetCreds()) // bot creds
.AddSingleton<IBotCredsProvider>(_credsProvider)
.AddSingleton(_db) // database
.AddRedis(_creds.RedisOptions) // redis
.AddSingleton(Client) // discord socket client
@@ -105,7 +106,7 @@ namespace NadekoBot
.AddSingleton<ISeria, JsonSeria>()
.AddSingleton<IPubSub, RedisPubSub>()
.AddSingleton<IConfigSeria, YamlSeria>()
.AddBotStringsServices()
.AddBotStringsServices(_creds.TotalShards)
.AddConfigServices()
.AddConfigMigrators()
.AddMemoryCache()
@@ -145,7 +146,8 @@ namespace NadekoBot
svcs.Scan(scan => scan
.FromAssemblyOf<IReadyExecutor>()
.AddClasses(classes => classes.AssignableToAny(
.AddClasses(classes => classes
.AssignableToAny(
// services
typeof(INService),

View File

@@ -12,7 +12,7 @@ namespace NadekoBot.Common.Attributes
{
public override Task<PreconditionResult> CheckPermissionsAsync(ICommandContext context, CommandInfo executingCommand, IServiceProvider services)
{
var creds = services.GetRequiredService<BotCredsProvider>().GetCreds();
var creds = services.GetRequiredService<IBotCredsProvider>().GetCreds();
return Task.FromResult((creds.IsOwner(context.User) || context.Client.CurrentUser.Id == context.User.Id ? PreconditionResult.FromSuccess() : PreconditionResult.FromError("Not owner")));
}

View File

@@ -13,7 +13,7 @@ namespace NadekoBot.Common
OwnerIds = new List<ulong>();
TotalShards = 1;
GoogleApiKey = string.Empty;
Votes = new(string.Empty, string.Empty);
Votes = new(string.Empty, string.Empty, string.Empty, string.Empty);
Patreon = new(string.Empty, string.Empty, string.Empty, string.Empty);
BotListToken = string.Empty;
CleverbotApiKey = string.Empty;
@@ -73,16 +73,6 @@ go to https://www.patreon.com/portal -> my clients -> create client")]
Change only if you've changed the coordinator address or port.")]
public string CoordinatorUrl { get; set; }
[YamlIgnore]
public string PatreonCampaignId => Patreon?.CampaignId;
[YamlIgnore]
public string PatreonAccessToken => Patreon?.AccessToken;
[YamlIgnore]
public string VotesUrl => Votes?.Url;
[YamlIgnore]
public string VotesToken => Votes.Key;
[Comment(@"Api key obtained on https://rapidapi.com (go to MyApps -> Add New App -> Enter Name -> Application key)")]
public string RapidApiKey { get; set; }
@@ -126,11 +116,9 @@ Windows default
// todo fixup patreon
public sealed record PatreonSettings
{
[Comment(@"Access token. You have to manually update this 1st of each month by refreshing the token on https://patreon.com/portal")]
public string ClientId { get; set; }
public string AccessToken { get; set; }
[Comment(@"Unused atm")]
public string RefreshToken { get; set; }
[Comment(@"Unused atm")]
public string ClientSecret { get; set; }
[Comment(@"Campaign ID of your patreon page. Go to your patreon page (make sure you're logged in) and type ""prompt('Campaign ID', window.patreon.bootstrap.creator.data.id);"" in the console. (ctrl + shift + i)")]
@@ -143,19 +131,44 @@ Windows default
ClientSecret = clientSecret;
CampaignId = campaignId;
}
public PatreonSettings()
{
}
}
public sealed record VotesSettings
{
[Comment(@"")]
public string Url { get; set; }
[Comment(@"")]
public string Key { get; set; }
[Comment(@"top.gg votes service url
This is the url of your instance of the NadekoBot.Votes api
Example: https://votes.my.cool.bot.com")]
public string TopggServiceUrl { get; set; }
[Comment(@"Authorization header value sent to the TopGG service url with each request
This should be equivalent to the TopggKey in your NadekoBot.Votes api appsettings.json file")]
public string TopggKey { get; set; }
[Comment(@"discords.com votes service url
This is the url of your instance of the NadekoBot.Votes api
Example: https://votes.my.cool.bot.com")]
public string DiscordsServiceUrl { get; set; }
[Comment(@"Authorization header value sent to the Discords service url with each request
This should be equivalent to the DiscordsKey in your NadekoBot.Votes api appsettings.json file")]
public string DiscordsKey { get; set; }
public VotesSettings(string url, string key)
public VotesSettings()
{
Url = url;
Key = key;
}
public VotesSettings(string topggServiceUrl, string topggKey, string discordsServiceUrl, string discordsKey)
{
TopggServiceUrl = topggServiceUrl;
TopggKey = topggKey;
DiscordsServiceUrl = discordsServiceUrl;
DiscordsKey = discordsKey;
}
}

View File

@@ -19,6 +19,9 @@ namespace NadekoBot.Common
/// <returns>Task representing download state</returns>
public async Task EnsureUsersDownloadedAsync(IGuild guild)
{
#if GLOBAL_NADEKO
return;
#endif
await downloadUsersSemaphore.WaitAsync();
try
{

View File

@@ -14,11 +14,15 @@ namespace NadekoBot.Extensions
{
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddBotStringsServices(this IServiceCollection services)
=> services
.AddSingleton<IStringsSource, LocalFileStringsSource>()
.AddSingleton<IBotStringsProvider, LocalBotStringsProvider>()
.AddSingleton<IBotStrings, BotStrings>();
public static IServiceCollection AddBotStringsServices(this IServiceCollection services, int totalShards)
=> totalShards <= 1
? services
.AddSingleton<IStringsSource, LocalFileStringsSource>()
.AddSingleton<IBotStringsProvider, LocalBotStringsProvider>()
.AddSingleton<IBotStrings, BotStrings>()
: services.AddSingleton<IStringsSource, LocalFileStringsSource>()
.AddSingleton<IBotStringsProvider, RedisBotStringsProvider>()
.AddSingleton<IBotStrings, BotStrings>();
public static IServiceCollection AddConfigServices(this IServiceCollection services)
{

View File

@@ -12,16 +12,14 @@ namespace NadekoBot
string GoogleApiKey { get; }
ICollection<ulong> OwnerIds { get; }
string RapidApiKey { get; }
string PatreonAccessToken { get; }
Creds.DbOptions Db { get; }
string OsuApiKey { get; }
int TotalShards { get; }
string PatreonCampaignId { get; }
Creds.PatreonSettings Patreon { get; }
string CleverbotApiKey { get; }
RestartConfig RestartCommand { get; }
string VotesUrl { get; }
string VotesToken { get; }
Creds.VotesSettings Votes { get; }
string BotListToken { get; }
string RedisOptions { get; }
string LocationIqApiKey { get; }

View File

@@ -1,10 +1,12 @@
using System;
using NadekoBot.Common.Yml;
namespace NadekoBot.Common
{
public class ImageUrls
{
public int Version { get; set; } = 2;
[Comment("DO NOT CHANGE")]
public int Version { get; set; } = 3;
public CoinData Coins { get; set; }
public Uri[] Currency { get; set; }
@@ -25,7 +27,6 @@ namespace NadekoBot.Common
public class SlotData
{
public Uri[] Emojis { get; set; }
public Uri[] Numbers { get; set; }
public Uri Bg { get; set; }
}

View File

@@ -0,0 +1,49 @@
using System;
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

@@ -177,13 +177,13 @@ namespace NadekoBot.Common.Replacements
/*OBSOLETE*/
_reps.TryAdd("%servers%", () => c.Guilds.Count.ToString());
#if !GLOBAL_NADEKO
_reps.TryAdd("%users%", () => c.Guilds.Sum(s => s.Users.Count).ToString());
_reps.TryAdd("%users%", () => c.Guilds.Sum(g => g.MemberCount).ToString());
#endif
/*NEW*/
_reps.TryAdd("%shard.servercount%", () => c.Guilds.Count.ToString());
#if !GLOBAL_NADEKO
_reps.TryAdd("%shard.usercount%", () => c.Guilds.Sum(s => s.Users.Count).ToString());
_reps.TryAdd("%shard.usercount%", () => c.Guilds.Sum(g => g.MemberCount).ToString());
#endif
_reps.TryAdd("%shard.id%", () => c.ShardId.ToString());
return this;

View File

@@ -53,6 +53,7 @@ namespace NadekoBot.Common.Replacements
newEmbedData.Title = Replace(embedData.Title);
newEmbedData.Thumbnail = Replace(embedData.Thumbnail);
newEmbedData.Image = Replace(embedData.Image);
newEmbedData.Url = Replace(embedData.Url);
if (embedData.Author != null)
{
newEmbedData.Author = new SmartTextEmbedAuthor();
@@ -68,6 +69,7 @@ namespace NadekoBot.Common.Replacements
var newF = new SmartTextEmbedField();
newF.Name = Replace(f.Name);
newF.Value = Replace(f.Value);
newF.Inline = f.Inline;
fields.Add(newF);
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Linq;
using Discord;
using NadekoBot.Extensions;
using NadekoBot.Services;
@@ -29,6 +30,47 @@ namespace NadekoBot
(Footer != null && (!string.IsNullOrWhiteSpace(Footer.Text) || !string.IsNullOrWhiteSpace(Footer.IconUrl))) ||
(Fields != null && Fields.Length > 0);
public static SmartEmbedText FromEmbed(IEmbed eb, string plainText = null)
{
var set = new SmartEmbedText();
set.PlainText = plainText;
set.Title = eb.Title;
set.Description = eb.Description;
set.Url = eb.Url;
set.Thumbnail = eb.Thumbnail?.Url;
set.Image = eb.Image?.Url;
set.Author = eb.Author is EmbedAuthor ea
? new()
{
Name = ea.Name,
Url = ea.Url,
IconUrl = ea.IconUrl
}
: null;
set.Footer = eb.Footer is EmbedFooter ef
? new()
{
Text = ef.Text,
IconUrl = ef.IconUrl
}
: null;
if (eb.Fields.Length > 0)
set.Fields = eb
.Fields
.Select(field => new SmartTextEmbedField()
{
Inline = field.Inline,
Name = field.Name,
Value = field.Value,
})
.ToArray();
set.Color = eb.Color?.RawValue ?? 0;
return set;
}
public EmbedBuilder GetEmbed()
{
var embed = new EmbedBuilder()

View File

@@ -0,0 +1,17 @@
using System.Threading.Tasks;
using Discord;
using Discord.Commands;
namespace NadekoBot.Common.TypeReaders
{
public sealed class EmoteTypeReader : NadekoTypeReader<Emote>
{
public override Task<TypeReaderResult> ReadAsync(ICommandContext ctx, string input)
{
if (!Emote.TryParse(input, out var emote))
return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Input is not a valid emote"));
return Task.FromResult(TypeReaderResult.FromSuccess(emote));
}
}
}

View File

@@ -20,6 +20,7 @@ namespace NadekoBot.Common.Yml
.WithTypeConverter(new Rgba32Converter())
.WithTypeConverter(new CultureInfoConverter())
.WithTypeConverter(new UriConverter())
.IgnoreUnmatchedProperties()
.Build();
}
}

View File

@@ -1,4 +1,5 @@
using NadekoBot.Db.Models;
using System;
using NadekoBot.Db.Models;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Discord;
@@ -138,14 +139,15 @@ WHERE UserId={userId};");
// just update the amount, there is no new user data
if (!updatedUserData)
{
ctx.Database.ExecuteSqlInterpolated($@"
var rows = ctx.Database.ExecuteSqlInterpolated($@"
UPDATE OR IGNORE DiscordUser
SET CurrencyAmount=CurrencyAmount+{amount}
WHERE UserId={userId};
INSERT OR IGNORE INTO DiscordUser (UserId, Username, Discriminator, AvatarId, CurrencyAmount)
VALUES ({userId}, {name}, {discrim}, {avatarId}, {amount});
INSERT OR IGNORE INTO DiscordUser (UserId, Username, Discriminator, AvatarId, CurrencyAmount, TotalXp)
VALUES ({userId}, {name}, {discrim}, {avatarId}, {amount}, 0);
");
}
else
{
@@ -157,8 +159,8 @@ SET CurrencyAmount=CurrencyAmount+{amount},
AvatarId={avatarId}
WHERE UserId={userId};
INSERT OR IGNORE INTO DiscordUser (UserId, Username, Discriminator, AvatarId, CurrencyAmount)
VALUES ({userId}, {name}, {discrim}, {avatarId}, {amount});
INSERT OR IGNORE INTO DiscordUser (UserId, Username, Discriminator, AvatarId, CurrencyAmount, TotalXp)
VALUES ({userId}, {name}, {discrim}, {avatarId}, {amount}, 0);
");
}
return true;
@@ -167,7 +169,7 @@ VALUES ({userId}, {name}, {discrim}, {avatarId}, {amount});
public static decimal GetTotalCurrency(this DbSet<DiscordUser> users)
{
return users
.Sum(x => x.CurrencyAmount);
.Sum((Func<DiscordUser, decimal>)(x => x.CurrencyAmount));
}
public static decimal GetTopOnePercentCurrency(this DbSet<DiscordUser> users, ulong botId)

View File

@@ -48,12 +48,12 @@ namespace NadekoBot.Db
private static IQueryable<GuildConfig> IncludeEverything(this DbSet<GuildConfig> configs)
{
// todo split query
return configs
.AsQueryable()
.Include(gc => gc.CommandCooldowns)
.Include(gc => gc.FollowedStreams)
.Include(gc => gc.StreamRole)
.Include(gc => gc.NsfwBlacklistedTags)
.Include(gc => gc.XpSettings)
.ThenInclude(x => x.ExclusionList)
.Include(gc => gc.DelMsgOnCmdChannels)
@@ -117,7 +117,7 @@ namespace NadekoBot.Db
{
var logSetting = ctx.LogSettings
.AsQueryable()
.Include(x => x.IgnoredChannels)
.Include(x => x.LogIgnores)
.Where(x => x.GuildId == guildId)
.FirstOrDefault();

View File

@@ -9,6 +9,11 @@ namespace NadekoBot.Db
{
public static class QuoteExtensions
{
public static IEnumerable<Quote> GetForGuild(this DbSet<Quote> quotes, ulong guildId)
{
return quotes.AsQueryable().Where(x => x.GuildId == guildId);
}
public static IEnumerable<Quote> GetGroup(this DbSet<Quote> quotes, ulong guildId, int page, OrderType order)
{
var q = quotes.AsQueryable().Where(x => x.GuildId == guildId);

View File

@@ -18,5 +18,12 @@ namespace NadekoBot.Db
.OrderBy(x => x.DateAdded)
.Skip(page * 10)
.Take(10);
public static IEnumerable<Reminder> RemindersForServer(this DbSet<Reminder> reminders, ulong serverId, int page)
=> reminders.AsQueryable()
.Where(x => x.ServerId == serverId)
.OrderBy(x => x.DateAdded)
.Skip(page * 10)
.Take(10);
}
}

View File

@@ -0,0 +1,12 @@
using System.Collections.Generic;
namespace NadekoBot.Services.Database.Models
{
public class AutoTranslateChannel : DbEntity
{
public ulong GuildId { get; set; }
public ulong ChannelId { get; set; }
public bool AutoDelete { get; set; }
public IList<AutoTranslateUser> Users { get; set; } = new List<AutoTranslateUser>();
}
}

View File

@@ -0,0 +1,11 @@
namespace NadekoBot.Services.Database.Models
{
public class AutoTranslateUser : DbEntity
{
public int ChannelId { get; set; }
public AutoTranslateChannel Channel { get; set; }
public ulong UserId { get; set; }
public string Source { get; set; }
public string Target { get; set; }
}
}

View File

@@ -91,7 +91,6 @@ namespace NadekoBot.Services.Database.Models
public bool WarningsInitialized { get; set; }
public HashSet<SlowmodeIgnoredUser> SlowmodeIgnoredUsers { get; set; }
public HashSet<SlowmodeIgnoredRole> SlowmodeIgnoredRoles { get; set; }
public HashSet<NsfwBlacklitedTag> NsfwBlacklistedTags { get; set; } = new HashSet<NsfwBlacklitedTag>();
public List<ShopEntry> ShopEntries { get; set; }
public ulong? GameVoiceChannel { get; set; } = null;

View File

@@ -1,8 +0,0 @@
namespace NadekoBot.Services.Database.Models
{
public class IgnoredLogChannel : DbEntity
{
public LogSetting LogSetting { get; set; }
public ulong ChannelId { get; set; }
}
}

View File

@@ -0,0 +1,16 @@
namespace NadekoBot.Services.Database.Models
{
public class IgnoredLogItem : DbEntity
{
public int LogSettingId { get; set; }
public LogSetting LogSetting { get; set; }
public ulong LogItemId { get; set; }
public IgnoredItemType ItemType { get; set; }
}
public enum IgnoredItemType
{
Channel,
User,
}
}

View File

@@ -4,8 +4,7 @@ namespace NadekoBot.Services.Database.Models
{
public class LogSetting : DbEntity
{
public HashSet<IgnoredLogChannel> IgnoredChannels { get; set; } = new HashSet<IgnoredLogChannel>();
public HashSet<IgnoredVoicePresenceChannel> IgnoredVoicePresenceChannelIds { get; set; } = new HashSet<IgnoredVoicePresenceChannel>();
public List<IgnoredLogItem> LogIgnores { get; set; } = new List<IgnoredLogItem>();
public ulong GuildId { get; set; }
public ulong? LogOtherId { get; set; }

View File

@@ -0,0 +1,16 @@
using System;
namespace NadekoBot.Services.Database.Models
{
public class NsfwBlacklistedTag : DbEntity
{
public ulong GuildId { get; set; }
public string Tag { get; set; }
public override int GetHashCode()
=> Tag.GetHashCode(StringComparison.InvariantCulture);
public override bool Equals(object obj)
=> obj is NsfwBlacklistedTag x && x.Tag == Tag;
}
}

View File

@@ -1,21 +0,0 @@
using System;
namespace NadekoBot.Services.Database.Models
{
public class NsfwBlacklitedTag : DbEntity
{
public string Tag { get; set; }
public override int GetHashCode()
{
return Tag.GetHashCode(StringComparison.InvariantCulture);
}
public override bool Equals(object obj)
{
return obj is NsfwBlacklitedTag x
? x.Tag == Tag
: false;
}
}
}

View File

@@ -8,5 +8,6 @@
public bool Forgiven { get; set; }
public string ForgivenBy { get; set; }
public string Moderator { get; set; }
public int Weight { get; set; }
}
}

View File

@@ -42,8 +42,8 @@ namespace NadekoBot.Services.Database
//logging
public DbSet<LogSetting> LogSettings { get; set; }
public DbSet<IgnoredLogChannel> IgnoredLogChannels { get; set; }
public DbSet<IgnoredVoicePresenceChannel> IgnoredVoicePresenceCHannels { get; set; }
public DbSet<IgnoredLogItem> IgnoredLogChannels { get; set; }
public DbSet<RotatingPlayingStatus> RotatingStatus { get; set; }
public DbSet<BlacklistEntry> Blacklist { get; set; }
@@ -59,6 +59,9 @@ namespace NadekoBot.Services.Database
public DbSet<Poll> Poll { get; set; }
public DbSet<WaifuInfo> WaifuInfo { get; set; }
public DbSet<ImageOnlyChannel> ImageOnlyChannels { get; set; }
public DbSet<NsfwBlacklistedTag> NsfwBlacklistedTags { get; set; }
public DbSet<AutoTranslateChannel> AutoTranslateChannels { get; set; }
public DbSet<AutoTranslateUser> AutoTranslateUsers { get; set; }
public NadekoContext(DbContextOptions<NadekoContext> options) : base(options)
{
@@ -195,10 +198,16 @@ namespace NadekoBot.Services.Database
#endregion
#region Warnings
var warn = modelBuilder.Entity<Warning>();
warn.HasIndex(x => x.GuildId);
warn.HasIndex(x => x.UserId);
warn.HasIndex(x => x.DateAdded);
modelBuilder.Entity<Warning>(warn =>
{
warn.HasIndex(x => x.GuildId);
warn.HasIndex(x => x.UserId);
warn.HasIndex(x => x.DateAdded);
warn.Property(x => x.Weight)
.HasDefaultValue(1);
});
#endregion
#region PatreonRewards
@@ -342,12 +351,40 @@ namespace NadekoBot.Services.Database
modelBuilder.Entity<LogSetting>(ls => ls
.HasIndex(x => x.GuildId)
.IsUnique());
modelBuilder.Entity<LogSetting>(ls => ls
.HasMany(x => x.LogIgnores)
.WithOne(x => x.LogSetting)
.OnDelete(DeleteBehavior.Cascade));
modelBuilder.Entity<IgnoredLogItem>(ili => ili
.HasIndex(x => new { x.LogSettingId, x.LogItemId, x.ItemType })
.IsUnique());
#endregion
modelBuilder.Entity<ImageOnlyChannel>(ioc => ioc
.HasIndex(x => x.ChannelId)
.IsUnique());
modelBuilder.Entity<NsfwBlacklistedTag>(nbt => nbt
.HasIndex(x => x.GuildId)
.IsUnique(false));
var atch = modelBuilder.Entity<AutoTranslateChannel>();
atch.HasIndex(x => x.GuildId)
.IsUnique(false);
atch.HasIndex(x => x.ChannelId)
.IsUnique();
atch
.HasMany(x => x.Users)
.WithOne(x => x.Channel)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<AutoTranslateUser>(atu => atu
.HasAlternateKey(x => new { x.ChannelId, x.UserId }));
}
}
}

View File

@@ -17,6 +17,12 @@ namespace NadekoBot.Migrations
migrationBuilder.Sql("DELETE FROM FilterChannelId WHERE GuildConfigId NOT IN (SELECT Id from GuildConfigs)");
migrationBuilder.Sql("DELETE FROM CommandCooldown WHERE GuildConfigId NOT IN (SELECT Id from GuildConfigs)");
// fix for users who edited their waifuinfo table manually and are unable to update
migrationBuilder.Sql("DELETE FROM WaifuInfo where WaifuId not in (SELECT Id from DiscordUser);");
// fix for users who deleted clubs manually and are unable to update now
migrationBuilder.Sql("UPDATE DiscordUser SET ClubId = null WHERE ClubId is not null and ClubId not in (SELECT Id from Clubs);");
migrationBuilder.DropColumn(
name: "ChannelCreated",
table: "LogSettings");

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,97 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace NadekoBot.Migrations
{
public partial class logignoreuserchannel : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql("DELETE FROM IgnoredLogChannels WHERE LogSettingId is NULL");
migrationBuilder.DropForeignKey(
name: "FK_IgnoredLogChannels_LogSettings_LogSettingId",
table: "IgnoredLogChannels");
migrationBuilder.DropIndex(
name: "IX_IgnoredLogChannels_LogSettingId",
table: "IgnoredLogChannels");
migrationBuilder.RenameColumn(
name: "ChannelId",
table: "IgnoredLogChannels",
newName: "LogItemId");
migrationBuilder.AlterColumn<int>(
name: "LogSettingId",
table: "IgnoredLogChannels",
type: "INTEGER",
nullable: false,
defaultValue: 0,
oldClrType: typeof(int),
oldType: "INTEGER",
oldNullable: true);
migrationBuilder.AddColumn<int>(
name: "ItemType",
table: "IgnoredLogChannels",
type: "INTEGER",
nullable: false,
defaultValue: 0);
migrationBuilder.CreateIndex(
name: "IX_IgnoredLogChannels_LogSettingId_LogItemId_ItemType",
table: "IgnoredLogChannels",
columns: new[] { "LogSettingId", "LogItemId", "ItemType" },
unique: true);
migrationBuilder.AddForeignKey(
name: "FK_IgnoredLogChannels_LogSettings_LogSettingId",
table: "IgnoredLogChannels",
column: "LogSettingId",
principalTable: "LogSettings",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_IgnoredLogChannels_LogSettings_LogSettingId",
table: "IgnoredLogChannels");
migrationBuilder.DropIndex(
name: "IX_IgnoredLogChannels_LogSettingId_LogItemId_ItemType",
table: "IgnoredLogChannels");
migrationBuilder.DropColumn(
name: "ItemType",
table: "IgnoredLogChannels");
migrationBuilder.RenameColumn(
name: "LogItemId",
table: "IgnoredLogChannels",
newName: "ChannelId");
migrationBuilder.AlterColumn<int>(
name: "LogSettingId",
table: "IgnoredLogChannels",
type: "INTEGER",
nullable: true,
oldClrType: typeof(int),
oldType: "INTEGER");
migrationBuilder.CreateIndex(
name: "IX_IgnoredLogChannels_LogSettingId",
table: "IgnoredLogChannels",
column: "LogSettingId");
migrationBuilder.AddForeignKey(
name: "FK_IgnoredLogChannels_LogSettings_LogSettingId",
table: "IgnoredLogChannels",
column: "LogSettingId",
principalTable: "LogSettings",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,76 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
namespace NadekoBot.Migrations
{
public partial class nsfwblacklisttags : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "NsfwBlacklistedTags",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
GuildId = table.Column<ulong>(type: "INTEGER", nullable: false),
Tag = table.Column<string>(type: "TEXT", nullable: true),
DateAdded = table.Column<DateTime>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_NsfwBlacklistedTags", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_NsfwBlacklistedTags_GuildId",
table: "NsfwBlacklistedTags",
column: "GuildId");
migrationBuilder.Sql(@"INSERT INTO NsfwBlacklistedTags(Id, GuildId, Tag, DateAdded)
SELECT
Id,
(SELECT GuildId From GuildConfigs WHERE Id=GuildConfigId),
Tag,
DateAdded
FROM NsfwBlacklitedTag
WHERE GuildConfigId in (SELECT Id from GuildConfigs);");
migrationBuilder.DropTable(
name: "NsfwBlacklitedTag");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "NsfwBlacklistedTags");
migrationBuilder.CreateTable(
name: "NsfwBlacklitedTag",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
DateAdded = table.Column<DateTime>(type: "TEXT", nullable: true),
GuildConfigId = table.Column<int>(type: "INTEGER", nullable: true),
Tag = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_NsfwBlacklitedTag", x => x.Id);
table.ForeignKey(
name: "FK_NsfwBlacklitedTag_GuildConfigs_GuildConfigId",
column: x => x.GuildConfigId,
principalTable: "GuildConfigs",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateIndex(
name: "IX_NsfwBlacklitedTag_GuildConfigId",
table: "NsfwBlacklitedTag",
column: "GuildConfigId");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,24 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace NadekoBot.Migrations
{
public partial class weightedwarnings : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "Weight",
table: "Warnings",
type: "INTEGER",
nullable: false,
defaultValue: 1);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Weight",
table: "Warnings");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,71 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
namespace NadekoBot.Migrations
{
public partial class atlrework : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "AutoTranslateChannels",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
GuildId = table.Column<ulong>(type: "INTEGER", nullable: false),
ChannelId = table.Column<ulong>(type: "INTEGER", nullable: false),
AutoDelete = table.Column<bool>(type: "INTEGER", nullable: false),
DateAdded = table.Column<DateTime>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_AutoTranslateChannels", x => x.Id);
});
migrationBuilder.CreateTable(
name: "AutoTranslateUsers",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
ChannelId = table.Column<int>(type: "INTEGER", nullable: false),
UserId = table.Column<ulong>(type: "INTEGER", nullable: false),
Source = table.Column<string>(type: "TEXT", nullable: true),
Target = table.Column<string>(type: "TEXT", nullable: true),
DateAdded = table.Column<DateTime>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_AutoTranslateUsers", x => x.Id);
table.UniqueConstraint("AK_AutoTranslateUsers_ChannelId_UserId", x => new { x.ChannelId, x.UserId });
table.ForeignKey(
name: "FK_AutoTranslateUsers_AutoTranslateChannels_ChannelId",
column: x => x.ChannelId,
principalTable: "AutoTranslateChannels",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_AutoTranslateChannels_ChannelId",
table: "AutoTranslateChannels",
column: "ChannelId",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_AutoTranslateChannels_GuildId",
table: "AutoTranslateChannels",
column: "GuildId");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "AutoTranslateUsers");
migrationBuilder.DropTable(
name: "AutoTranslateChannels");
}
}
}

View File

@@ -187,29 +187,6 @@ namespace NadekoBot.Migrations
b.ToTable("FollowedStream");
});
modelBuilder.Entity("NadekoBot.Services.Database.ImageOnlyChannel", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<ulong>("ChannelId")
.HasColumnType("INTEGER");
b.Property<DateTime?>("DateAdded")
.HasColumnType("TEXT");
b.Property<ulong>("GuildId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("ChannelId")
.IsUnique();
b.ToTable("ImageOnlyChannels");
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiAltSetting", b =>
{
b.Property<int>("Id")
@@ -363,6 +340,62 @@ namespace NadekoBot.Migrations
b.ToTable("AutoCommands");
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.AutoTranslateChannel", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<bool>("AutoDelete")
.HasColumnType("INTEGER");
b.Property<ulong>("ChannelId")
.HasColumnType("INTEGER");
b.Property<DateTime?>("DateAdded")
.HasColumnType("TEXT");
b.Property<ulong>("GuildId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("ChannelId")
.IsUnique();
b.HasIndex("GuildId");
b.ToTable("AutoTranslateChannels");
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.AutoTranslateUser", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("ChannelId")
.HasColumnType("INTEGER");
b.Property<DateTime?>("DateAdded")
.HasColumnType("TEXT");
b.Property<string>("Source")
.HasColumnType("TEXT");
b.Property<string>("Target")
.HasColumnType("TEXT");
b.Property<ulong>("UserId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasAlternateKey("ChannelId", "UserId");
b.ToTable("AutoTranslateUsers");
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.BanTemplate", b =>
{
b.Property<int>("Id")
@@ -870,24 +903,28 @@ namespace NadekoBot.Migrations
b.ToTable("GuildConfigs");
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredLogChannel", b =>
modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredLogItem", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<ulong>("ChannelId")
.HasColumnType("INTEGER");
b.Property<DateTime?>("DateAdded")
.HasColumnType("TEXT");
b.Property<int?>("LogSettingId")
b.Property<int>("ItemType")
.HasColumnType("INTEGER");
b.Property<ulong>("LogItemId")
.HasColumnType("INTEGER");
b.Property<int>("LogSettingId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("LogSettingId");
b.HasIndex("LogSettingId", "LogItemId", "ItemType")
.IsUnique();
b.ToTable("IgnoredLogChannels");
});
@@ -914,6 +951,29 @@ namespace NadekoBot.Migrations
b.ToTable("IgnoredVoicePresenceCHannels");
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.ImageOnlyChannel", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<ulong>("ChannelId")
.HasColumnType("INTEGER");
b.Property<DateTime?>("DateAdded")
.HasColumnType("TEXT");
b.Property<ulong>("GuildId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("ChannelId")
.IsUnique();
b.ToTable("ImageOnlyChannels");
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.LogSetting", b =>
{
b.Property<int>("Id")
@@ -1058,7 +1118,7 @@ namespace NadekoBot.Migrations
b.ToTable("MutedUserId");
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.NsfwBlacklitedTag", b =>
modelBuilder.Entity("NadekoBot.Services.Database.Models.NsfwBlacklistedTag", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
@@ -1067,7 +1127,7 @@ namespace NadekoBot.Migrations
b.Property<DateTime?>("DateAdded")
.HasColumnType("TEXT");
b.Property<int?>("GuildConfigId")
b.Property<ulong>("GuildId")
.HasColumnType("INTEGER");
b.Property<string>("Tag")
@@ -1075,9 +1135,9 @@ namespace NadekoBot.Migrations
b.HasKey("Id");
b.HasIndex("GuildConfigId");
b.HasIndex("GuildId");
b.ToTable("NsfwBlacklitedTag");
b.ToTable("NsfwBlacklistedTags");
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.Permissionv2", b =>
@@ -1963,6 +2023,11 @@ namespace NadekoBot.Migrations
b.Property<ulong>("UserId")
.HasColumnType("INTEGER");
b.Property<int>("Weight")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(1);
b.HasKey("Id");
b.HasIndex("DateAdded");
@@ -2185,6 +2250,17 @@ namespace NadekoBot.Migrations
b.Navigation("GuildConfig");
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.AutoTranslateUser", b =>
{
b.HasOne("NadekoBot.Services.Database.Models.AutoTranslateChannel", "Channel")
.WithMany("Users")
.HasForeignKey("ChannelId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Channel");
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandAlias", b =>
{
b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null)
@@ -2269,11 +2345,13 @@ namespace NadekoBot.Migrations
b.Navigation("GuildConfig");
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredLogChannel", b =>
modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredLogItem", b =>
{
b.HasOne("NadekoBot.Services.Database.Models.LogSetting", "LogSetting")
.WithMany("IgnoredChannels")
.HasForeignKey("LogSettingId");
.WithMany("LogIgnores")
.HasForeignKey("LogSettingId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("LogSetting");
});
@@ -2281,7 +2359,7 @@ namespace NadekoBot.Migrations
modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredVoicePresenceChannel", b =>
{
b.HasOne("NadekoBot.Services.Database.Models.LogSetting", "LogSetting")
.WithMany("IgnoredVoicePresenceChannelIds")
.WithMany()
.HasForeignKey("LogSettingId");
b.Navigation("LogSetting");
@@ -2294,13 +2372,6 @@ namespace NadekoBot.Migrations
.HasForeignKey("GuildConfigId");
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.NsfwBlacklitedTag", b =>
{
b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null)
.WithMany("NsfwBlacklistedTags")
.HasForeignKey("GuildConfigId");
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.Permissionv2", b =>
{
b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null)
@@ -2537,6 +2608,11 @@ namespace NadekoBot.Migrations
b.Navigation("IgnoredChannels");
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.AutoTranslateChannel", b =>
{
b.Navigation("Users");
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.GuildConfig", b =>
{
b.Navigation("AntiAltSetting");
@@ -2567,8 +2643,6 @@ namespace NadekoBot.Migrations
b.Navigation("MutedUsers");
b.Navigation("NsfwBlacklistedTags");
b.Navigation("Permissions");
b.Navigation("ReactionRoleMessages");
@@ -2598,9 +2672,7 @@ namespace NadekoBot.Migrations
modelBuilder.Entity("NadekoBot.Services.Database.Models.LogSetting", b =>
{
b.Navigation("IgnoredChannels");
b.Navigation("IgnoredVoicePresenceChannelIds");
b.Navigation("LogIgnores");
});
modelBuilder.Entity("NadekoBot.Services.Database.Models.MusicPlaylist", b =>

View File

@@ -16,6 +16,7 @@ namespace NadekoBot.Modules.Administration
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)]
public async Task AutoAssignRole([Leftover] IRole role)
{
var guser = (IGuildUser) ctx.User;
@@ -47,6 +48,7 @@ namespace NadekoBot.Modules.Administration
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)]
public async Task AutoAssignRole()
{
if (!_service.TryGetRoles(ctx.Guild.Id, out var roles))

View File

@@ -7,6 +7,7 @@ using NadekoBot.Services.Database.Models;
using NadekoBot.Extensions;
using NadekoBot.Modules.Administration.Services;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
@@ -18,12 +19,6 @@ namespace NadekoBot.Modules.Administration
[NoPublicBot]
public class LogCommands : NadekoSubmodule<ILogCommandService>
{
public enum EnableDisable
{
Enable,
Disable
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
@@ -43,14 +38,51 @@ namespace NadekoBot.Modules.Administration
[OwnerOnly]
public async Task LogIgnore()
{
var channel = (ITextChannel)ctx.Channel;
var settings = _service.GetGuildLogSettings(ctx.Guild.Id);
var removed = _service.LogIgnore(ctx.Guild.Id, ctx.Channel.Id);
var chs = settings?.LogIgnores.Where(x => x.ItemType == IgnoredItemType.Channel).ToList()
?? new List<IgnoredLogItem>();
var usrs = settings?.LogIgnores.Where(x => x.ItemType == IgnoredItemType.User).ToList()
?? new List<IgnoredLogItem>();
var eb = _eb.Create(ctx)
.WithOkColor()
.AddField(GetText(strs.log_ignored_channels),
chs.Count == 0 ? "-" : string.Join('\n', chs.Select(x => $"{x.LogItemId} | <#{x.LogItemId}>")))
.AddField(GetText(strs.log_ignored_users),
usrs.Count == 0 ? "-" : string.Join('\n', usrs.Select(x => $"{x.LogItemId} | <@{x.LogItemId}>")));
await ctx.Channel.EmbedAsync(eb);
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
[OwnerOnly]
public async Task LogIgnore([Leftover]ITextChannel target)
{
target ??= (ITextChannel)ctx.Channel;
var removed = _service.LogIgnore(ctx.Guild.Id, target.Id, IgnoredItemType.Channel);
if (!removed)
await ReplyConfirmLocalizedAsync(strs.log_ignore(Format.Bold(channel.Mention + "(" + channel.Id + ")"))).ConfigureAwait(false);
await ReplyConfirmLocalizedAsync(strs.log_ignore_chan(Format.Bold(target.Mention + "(" + target.Id + ")"))).ConfigureAwait(false);
else
await ReplyConfirmLocalizedAsync(strs.log_not_ignore(Format.Bold(channel.Mention + "(" + channel.Id + ")"))).ConfigureAwait(false);
await ReplyConfirmLocalizedAsync(strs.log_not_ignore_chan(Format.Bold(target.Mention + "(" + target.Id + ")"))).ConfigureAwait(false);
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
[OwnerOnly]
public async Task LogIgnore([Leftover]IUser target)
{
var removed = _service.LogIgnore(ctx.Guild.Id, target.Id, IgnoredItemType.User);
if (!removed)
await ReplyConfirmLocalizedAsync(strs.log_ignore_user(Format.Bold(target.Mention + "(" + target.Id + ")"))).ConfigureAwait(false);
else
await ReplyConfirmLocalizedAsync(strs.log_not_ignore_user(Format.Bold(target.Mention + "(" + target.Id + ")"))).ConfigureAwait(false);
}
[NadekoCommand, Aliases]

View File

@@ -318,7 +318,7 @@ namespace NadekoBot.Modules.Administration
[NadekoCommand, Aliases]
[OwnerOnly]
public async Task Die()
public async Task Die(bool graceful = false)
{
try
{
@@ -329,7 +329,7 @@ namespace NadekoBot.Modules.Administration
// ignored
}
await Task.Delay(2000).ConfigureAwait(false);
_coord.Die();
_coord.Die(graceful);
}
[NadekoCommand, Aliases]
@@ -506,6 +506,14 @@ namespace NadekoBot.Modules.Administration
_strings.Reload();
await ReplyConfirmLocalizedAsync(strs.bot_strings_reloaded).ConfigureAwait(false);
}
[NadekoCommand, Aliases]
[OwnerOnly]
public async Task CoordReload()
{
await _coord.Reload();
await ctx.OkAsync();
}
private static UserStatus SettableUserStatusToUserStatus(SettableUserStatus sus)
{

View File

@@ -258,8 +258,7 @@ namespace NadekoBot.Modules.Administration
{
user = user ?? (IGuildUser) ctx.User;
var channel = await user.GetOrCreateDMChannelAsync();
var success = await _service.GreetDmTest(channel, user);
var success = await _service.GreetDmTest(user);
if (success)
await ctx.OkAsync();
else

View File

@@ -6,6 +6,8 @@ using System.Threading;
using System.Threading.Tasks;
using Discord;
using Discord.WebSocket;
using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using NadekoBot.Common.Collections;
@@ -21,7 +23,7 @@ namespace NadekoBot.Modules.Administration.Services
{
void AddDeleteIgnore(ulong xId);
Task LogServer(ulong guildId, ulong channelId, bool actionValue);
bool LogIgnore(ulong guildId, ulong channelId);
bool LogIgnore(ulong guildId, ulong itemId, IgnoredItemType itemType);
LogSetting GetGuildLogSettings(ulong guildId);
bool Log(ulong guildId, ulong? channelId, LogType type);
}
@@ -37,7 +39,7 @@ namespace NadekoBot.Modules.Administration.Services
return Task.CompletedTask;
}
public bool LogIgnore(ulong guildId, ulong channelId)
public bool LogIgnore(ulong guildId, ulong itemId, IgnoredItemType itemType)
{
return false;
}
@@ -97,7 +99,7 @@ namespace NadekoBot.Modules.Administration.Services
.AsQueryable()
.AsNoTracking()
.Where(x => guildIds.Contains(x.GuildId))
.Include(ls => ls.IgnoredChannels)
.Include(ls => ls.LogIgnores)
.ToList();
GuildLogSettings = configs
@@ -165,21 +167,23 @@ namespace NadekoBot.Modules.Administration.Services
_ignoreMessageIds.Add(messageId);
}
public bool LogIgnore(ulong gid, ulong cid)
public bool LogIgnore(ulong gid, ulong itemId, IgnoredItemType itemType)
{
int removed = 0;
using (var uow = _db.GetDbContext())
{
var logSetting = uow.LogSettingsFor(gid);
removed = logSetting.IgnoredChannels.RemoveWhere(ilc => ilc.ChannelId == cid);
removed = logSetting.LogIgnores
.RemoveAll(x => x.ItemType == itemType && itemId == x.LogItemId);
if (removed == 0)
{
var toAdd = new IgnoredLogChannel {ChannelId = cid};
logSetting.IgnoredChannels.Add(toAdd);
var toAdd = new IgnoredLogItem { LogItemId = itemId, ItemType = itemType};
logSetting.LogIgnores.Add(toAdd);
}
GuildLogSettings.AddOrUpdate(gid, logSetting, (_, _) => logSetting);
uow.SaveChanges();
GuildLogSettings.AddOrUpdate(gid, logSetting, (_, _) => logSetting);
}
return removed > 0;
@@ -580,7 +584,8 @@ namespace NadekoBot.Modules.Administration.Services
{
try
{
if (!GuildLogSettings.TryGetValue(before.Guild.Id, out LogSetting logSetting))
if (!GuildLogSettings.TryGetValue(before.Guild.Id, out LogSetting logSetting)
|| logSetting.LogIgnores.Any(ilc => ilc.LogItemId == after.Id && ilc.ItemType == IgnoredItemType.User))
return;
ITextChannel logChannel;
@@ -682,7 +687,7 @@ namespace NadekoBot.Modules.Administration.Services
if (!GuildLogSettings.TryGetValue(before.Guild.Id, out LogSetting logSetting)
|| (logSetting.ChannelUpdatedId is null)
|| logSetting.IgnoredChannels.Any(ilc => ilc.ChannelId == after.Id))
|| logSetting.LogIgnores.Any(ilc => ilc.LogItemId == after.Id && ilc.ItemType == IgnoredItemType.Channel))
return;
ITextChannel logChannel;
@@ -733,7 +738,7 @@ namespace NadekoBot.Modules.Administration.Services
if (!GuildLogSettings.TryGetValue(ch.Guild.Id, out LogSetting logSetting)
|| (logSetting.ChannelDestroyedId is null)
|| logSetting.IgnoredChannels.Any(ilc => ilc.ChannelId == ch.Id))
|| logSetting.LogIgnores.Any(ilc => ilc.LogItemId == ch.Id && ilc.ItemType == IgnoredItemType.Channel))
return;
ITextChannel logChannel;
@@ -772,7 +777,7 @@ namespace NadekoBot.Modules.Administration.Services
return;
if (!GuildLogSettings.TryGetValue(ch.Guild.Id, out LogSetting logSetting)
|| (logSetting.ChannelCreatedId is null))
|| logSetting.ChannelCreatedId is null)
return;
ITextChannel logChannel;
@@ -817,7 +822,8 @@ namespace NadekoBot.Modules.Administration.Services
return;
if (!GuildLogSettings.TryGetValue(usr.Guild.Id, out LogSetting logSetting)
|| (logSetting.LogVoicePresenceId is null))
|| (logSetting.LogVoicePresenceId is null)
|| logSetting.LogIgnores.Any(ilc => ilc.LogItemId == iusr.Id && ilc.ItemType == IgnoredItemType.User))
return;
ITextChannel logChannel;
@@ -862,49 +868,6 @@ namespace NadekoBot.Modules.Administration.Services
return Task.CompletedTask;
}
//private Task _client_UserPresenceUpdated(Optional<SocketGuild> optGuild, SocketUser usr, SocketPresence before, SocketPresence after)
//{
// var _ = Task.Run(async () =>
// {
// try
// {
// var guild = optGuild.GetValueOrDefault() ?? (usr as SocketGuildUser)?.Guild;
// if (guild is null)
// return;
// if (!GuildLogSettings.TryGetValue(guild.Id, out LogSetting logSetting)
// || (logSetting.LogUserPresenceId is null)
// || before.Status == after.Status)
// return;
// ITextChannel logChannel;
// if ((logChannel = await TryGetLogChannel(guild, logSetting, LogType.UserPresence)) is null)
// return;
// string str = "";
// if (before.Status != after.Status)
// str = "🎭" + Format.Code(PrettyCurrentTime(g)) +
// GetText(logChannel.Guild, strs.user_status_change(,
// "👤" + Format.Bold(usr.Username),
// Format.Bold(after.Status.ToString()));
// //if (before.Game?.Name != after.Game?.Name)
// //{
// // if (str != "")
// // str += "\n";
// // str += $"👾`{prettyCurrentTime}`👤__**{usr.Username}**__ is now playing **{after.Game?.Name}**.";
// //}
// PresenceUpdates.AddOrUpdate(logChannel, new List<string>() { str }, (id, list) => { list.Add(str); return list; });
// }
// catch
// {
// // ignored
// }
// });
// return Task.CompletedTask;
//}
private Task _client_UserLeft(IGuildUser usr)
{
var _ = Task.Run(async () =>
@@ -912,7 +875,8 @@ namespace NadekoBot.Modules.Administration.Services
try
{
if (!GuildLogSettings.TryGetValue(usr.Guild.Id, out LogSetting logSetting)
|| (logSetting.UserLeftId is null))
|| (logSetting.UserLeftId is null)
|| logSetting.LogIgnores.Any(ilc => ilc.LogItemId == usr.Id && ilc.ItemType == IgnoredItemType.User))
return;
ITextChannel logChannel;
@@ -987,7 +951,8 @@ namespace NadekoBot.Modules.Administration.Services
try
{
if (!GuildLogSettings.TryGetValue(guild.Id, out LogSetting logSetting)
|| (logSetting.UserUnbannedId is null))
|| (logSetting.UserUnbannedId is null)
|| logSetting.LogIgnores.Any(ilc => ilc.LogItemId == usr.Id && ilc.ItemType == IgnoredItemType.User))
return;
ITextChannel logChannel;
@@ -1021,7 +986,8 @@ namespace NadekoBot.Modules.Administration.Services
try
{
if (!GuildLogSettings.TryGetValue(guild.Id, out LogSetting logSetting)
|| (logSetting.UserBannedId is null))
|| (logSetting.UserBannedId is null)
|| logSetting.LogIgnores.Any(ilc => ilc.LogItemId == usr.Id && ilc.ItemType == IgnoredItemType.User))
return;
ITextChannel logChannel;
@@ -1069,7 +1035,7 @@ namespace NadekoBot.Modules.Administration.Services
if (!GuildLogSettings.TryGetValue(channel.Guild.Id, out LogSetting logSetting)
|| (logSetting.MessageDeletedId is null)
|| logSetting.IgnoredChannels.Any(ilc => ilc.ChannelId == channel.Id))
|| logSetting.LogIgnores.Any(ilc => ilc.LogItemId == channel.Id && ilc.ItemType == IgnoredItemType.Channel))
return;
ITextChannel logChannel;
@@ -1127,7 +1093,7 @@ namespace NadekoBot.Modules.Administration.Services
if (!GuildLogSettings.TryGetValue(channel.Guild.Id, out LogSetting logSetting)
|| (logSetting.MessageUpdatedId is null)
|| logSetting.IgnoredChannels.Any(ilc => ilc.ChannelId == channel.Id))
|| logSetting.LogIgnores.Any(ilc => ilc.LogItemId == channel.Id && ilc.ItemType == IgnoredItemType.Channel))
return;
ITextChannel logChannel;

View File

@@ -12,6 +12,8 @@ using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using NadekoBot.Db;
using Serilog;
using System.Threading;
using System;
namespace NadekoBot.Modules.Administration.Services
{
@@ -21,6 +23,11 @@ namespace NadekoBot.Modules.Administration.Services
private readonly DiscordSocketClient _client;
private readonly ConcurrentDictionary<ulong, IndexedCollection<ReactionRoleMessage>> _models;
/// <summary>
/// Contains the (Message ID, User ID) of reaction roles that are currently being processed.
/// </summary>
private readonly ConcurrentHashSet<(ulong, ulong)> _reacting = new();
public RoleCommandsService(DiscordSocketClient client, DbService db,
Bot bot)
{
@@ -38,75 +45,58 @@ namespace NadekoBot.Modules.Administration.Services
private Task _client_ReactionAdded(Cacheable<IUserMessage, ulong> msg, ISocketMessageChannel chan, SocketReaction reaction)
{
var _ = Task.Run(async () =>
_ = Task.Run(async () =>
{
try
if (!reaction.User.IsSpecified ||
reaction.User.Value.IsBot ||
reaction.User.Value is not SocketGuildUser gusr ||
chan is not SocketGuildChannel gch ||
!_models.TryGetValue(gch.Guild.Id, out var confs))
return;
var conf = confs.FirstOrDefault(x => x.MessageId == msg.Id);
if (conf is null)
return;
// compare emote names for backwards compatibility :facepalm:
var reactionRole = conf.ReactionRoles.FirstOrDefault(x => x.EmoteName == reaction.Emote.Name || x.EmoteName == reaction.Emote.ToString());
if (reactionRole != null)
{
if (!reaction.User.IsSpecified ||
reaction.User.Value.IsBot ||
!(reaction.User.Value is SocketGuildUser gusr))
return;
if (!(chan is SocketGuildChannel gch))
return;
if (!_models.TryGetValue(gch.Guild.Id, out var confs))
return;
var conf = confs.FirstOrDefault(x => x.MessageId == msg.Id);
if (conf is null)
return;
// compare emote names for backwards compatibility :facepalm:
var reactionRole = conf.ReactionRoles.FirstOrDefault(x => x.EmoteName == reaction.Emote.Name || x.EmoteName == reaction.Emote.ToString());
if (reactionRole != null)
if (!conf.Exclusive)
{
if (conf.Exclusive)
{
var roleIds = conf.ReactionRoles.Select(x => x.RoleId)
.Where(x => x != reactionRole.RoleId)
.Select(x => gusr.Guild.GetRole(x))
.Where(x => x != null);
var __ = Task.Run(async () =>
{
try
{
//if the role is exclusive,
// remove all other reactions user added to the message
var dl = await msg.GetOrDownloadAsync().ConfigureAwait(false);
foreach (var r in dl.Reactions)
{
if (r.Key.Name == reaction.Emote.Name)
continue;
try { await dl.RemoveReactionAsync(r.Key, gusr).ConfigureAwait(false); } catch { }
await Task.Delay(100).ConfigureAwait(false);
}
}
catch { }
});
await gusr.RemoveRolesAsync(roleIds).ConfigureAwait(false);
}
var toAdd = gusr.Guild.GetRole(reactionRole.RoleId);
if (toAdd != null && !gusr.Roles.Contains(toAdd))
{
await gusr.AddRolesAsync(new[] { toAdd }).ConfigureAwait(false);
}
await AddReactionRoleAsync(gusr, reactionRole);
return;
}
else
// If same (message, user) are being processed in an exclusive rero, quit
if (!_reacting.Add((msg.Id, reaction.UserId)))
return;
try
{
var dl = await msg.GetOrDownloadAsync().ConfigureAwait(false);
await dl.RemoveReactionAsync(reaction.Emote, dl.Author,
new RequestOptions()
{
RetryMode = RetryMode.RetryRatelimit | RetryMode.Retry502
}).ConfigureAwait(false);
Log.Warning("User {0} is adding unrelated reactions to the reaction roles message.", dl.Author);
var removeExclusiveTask = RemoveExclusiveReactionRoleAsync(msg, gusr, reaction, conf, reactionRole, CancellationToken.None);
var addRoleTask = AddReactionRoleAsync(gusr, reactionRole);
await Task.WhenAll(removeExclusiveTask, addRoleTask).ConfigureAwait(false);
}
finally
{
// Free (message/user) for another exclusive rero
_reacting.TryRemove((msg.Id, reaction.UserId));
}
}
catch { }
else
{
var dl = await msg.GetOrDownloadAsync().ConfigureAwait(false);
await dl.RemoveReactionAsync(reaction.Emote, dl.Author,
new RequestOptions()
{
RetryMode = RetryMode.RetryRatelimit | RetryMode.Retry502
}).ConfigureAwait(false);
Log.Warning("User {0} is adding unrelated reactions to the reaction roles message.", dl.Author);
}
});
return Task.CompletedTask;
@@ -114,16 +104,16 @@ namespace NadekoBot.Modules.Administration.Services
private Task _client_ReactionRemoved(Cacheable<IUserMessage, ulong> msg, ISocketMessageChannel chan, SocketReaction reaction)
{
var _ = Task.Run(async () =>
_ = Task.Run(async () =>
{
try
{
if (!reaction.User.IsSpecified ||
reaction.User.Value.IsBot ||
!(reaction.User.Value is SocketGuildUser gusr))
reaction.User.Value is not SocketGuildUser gusr)
return;
if (!(chan is SocketGuildChannel gch))
if (chan is not SocketGuildChannel gch)
return;
if (!_models.TryGetValue(gch.Guild.Id, out var confs))
@@ -193,5 +183,71 @@ namespace NadekoBot.Modules.Administration.Services
uow.SaveChanges();
}
}
/// <summary>
/// Adds a reaction role to the specified user.
/// </summary>
/// <param name="user">A Discord guild user.</param>
/// <param name="dbRero">The database settings of this reaction role.</param>
private Task AddReactionRoleAsync(SocketGuildUser user, ReactionRole dbRero)
{
var toAdd = user.Guild.GetRole(dbRero.RoleId);
return (toAdd != null && !user.Roles.Contains(toAdd))
? user.AddRoleAsync(toAdd)
: Task.CompletedTask;
}
/// <summary>
/// Removes the exclusive reaction roles and reactions from the specified user.
/// </summary>
/// <param name="reactionMessage">The Discord message that contains the reaction roles.</param>
/// <param name="user">A Discord guild user.</param>
/// <param name="reaction">The Discord reaction of the user.</param>
/// <param name="dbReroMsg">The database entry of the reaction role message.</param>
/// <param name="dbRero">The database settings of this reaction role.</param>
/// <param name="cToken">A cancellation token to cancel the operation.</param>
/// <exception cref="OperationCanceledException">Occurs when the operation is cancelled before it began.</exception>
/// <exception cref="TaskCanceledException">Occurs when the operation is cancelled while it's still executing.</exception>
private Task RemoveExclusiveReactionRoleAsync(Cacheable<IUserMessage, ulong> reactionMessage, SocketGuildUser user, SocketReaction reaction, ReactionRoleMessage dbReroMsg, ReactionRole dbRero, CancellationToken cToken = default)
{
cToken.ThrowIfCancellationRequested();
var roleIds = dbReroMsg.ReactionRoles.Select(x => x.RoleId)
.Where(x => x != dbRero.RoleId)
.Select(x => user.Guild.GetRole(x))
.Where(x => x != null);
var removeReactionsTask = RemoveOldReactionsAsync(reactionMessage, user, reaction, cToken);
var removeRolesTask = user.RemoveRolesAsync(roleIds);
return Task.WhenAll(removeReactionsTask, removeRolesTask);
}
/// <summary>
/// Removes old reactions from an exclusive reaction role.
/// </summary>
/// <param name="reactionMessage">The Discord message that contains the reaction roles.</param>
/// <param name="user">A Discord guild user.</param>
/// <param name="reaction">The Discord reaction of the user.</param>
/// <param name="cToken">A cancellation token to cancel the operation.</param>
/// <exception cref="OperationCanceledException">Occurs when the operation is cancelled before it began.</exception>
/// <exception cref="TaskCanceledException">Occurs when the operation is cancelled while it's still executing.</exception>
private async Task RemoveOldReactionsAsync(Cacheable<IUserMessage, ulong> reactionMessage, SocketGuildUser user, SocketReaction reaction, CancellationToken cToken = default)
{
cToken.ThrowIfCancellationRequested();
//if the role is exclusive,
// remove all other reactions user added to the message
var dl = await reactionMessage.GetOrDownloadAsync().ConfigureAwait(false);
foreach (var r in dl.Reactions)
{
if (r.Key.Name == reaction.Emote.Name)
continue;
try { await dl.RemoveReactionAsync(r.Key, user).ConfigureAwait(false); } catch { }
await Task.Delay(100, cToken).ConfigureAwait(false);
}
}
}
}

View File

@@ -41,8 +41,11 @@ namespace NadekoBot.Modules.Administration.Services
}, null, TimeSpan.FromSeconds(0), TimeSpan.FromHours(12));
}
public async Task<WarningPunishment> Warn(IGuild guild, ulong userId, IUser mod, string reason)
public async Task<WarningPunishment> Warn(IGuild guild, ulong userId, IUser mod, int weight, string reason)
{
if (weight <= 0)
throw new ArgumentOutOfRangeException(nameof(weight));
var modName = mod.ToString();
if (string.IsNullOrWhiteSpace(reason))
@@ -57,6 +60,7 @@ namespace NadekoBot.Modules.Administration.Services
Forgiven = false,
Reason = reason,
Moderator = modName,
Weight = weight,
};
int warnings = 1;
@@ -70,7 +74,7 @@ namespace NadekoBot.Modules.Administration.Services
.Warnings
.ForId(guildId, userId)
.Where(w => !w.Forgiven && w.UserId == userId)
.Count();
.Sum(x => x.Weight);
uow.Warnings.Add(warn);
@@ -95,6 +99,10 @@ namespace NadekoBot.Modules.Administration.Services
public async Task ApplyPunishment(IGuild guild, IGuildUser user, IUser mod, PunishmentAction p, int minutes,
ulong? roleId, string reason)
{
if (!await CheckPermission(guild, p))
return;
switch (p)
{
case PunishmentAction.Mute:
@@ -167,6 +175,40 @@ namespace NadekoBot.Modules.Administration.Services
}
}
/// <summary>
/// Used to prevent the bot from hitting 403's when it needs to
/// apply punishments with insufficient permissions
/// </summary>
/// <param name="guild">Guild the punishment is applied in</param>
/// <param name="punish">Punishment to apply</param>
/// <returns>Whether the bot has sufficient permissions</returns>
private async Task<bool> CheckPermission(IGuild guild, PunishmentAction punish)
{
var botUser = await guild.GetCurrentUserAsync();
switch (punish)
{
case PunishmentAction.Mute:
return botUser.GuildPermissions.MuteMembers && botUser.GuildPermissions.ManageRoles;
case PunishmentAction.Kick:
return botUser.GuildPermissions.KickMembers;
case PunishmentAction.Ban:
return botUser.GuildPermissions.BanMembers;
case PunishmentAction.Softban:
return botUser.GuildPermissions.BanMembers; // ban + unban
case PunishmentAction.RemoveRoles:
return botUser.GuildPermissions.ManageRoles;
case PunishmentAction.ChatMute:
return botUser.GuildPermissions.ManageRoles; // adds nadeko-mute role
case PunishmentAction.VoiceMute:
return botUser.GuildPermissions.MuteMembers;
case PunishmentAction.AddRole:
return botUser.GuildPermissions.ManageRoles;
default:
return true;
}
}
public async Task CheckAllWarnExpiresAsync()
{
using (var uow = _db.GetDbContext())
@@ -457,7 +499,9 @@ WHERE GuildId={guildId}
{
template = JsonConvert.SerializeObject(new
{
color = _bcs.Data.Color.Error,
//To get the decimal version of the color that's expected, take the packed value of the Rgba32
//and bitshift it to the right by 8 bits, thereby dropping the "a" and getting a reprensentation of the RGB value
color = _bcs.Data.Color.Error.PackedValue >> 8,
description = defaultMessage
});
}
@@ -472,7 +516,9 @@ WHERE GuildId={guildId}
{
template = JsonConvert.SerializeObject(new
{
color = _bcs.Data.Color.Error,
//To get the decimal version of the color that's expected, take the packed value of the Rgba32
//and bitshift it to the right by 8 bits, thereby dropping the "a" and getting a reprensentation of the RGB value
color = _bcs.Data.Color.Error.PackedValue >> 8,
description = template
});
}

View File

@@ -54,8 +54,17 @@ namespace NadekoBot.Modules.Administration
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.BanMembers)]
public async Task Warn(IGuildUser user, [Leftover] string reason = null)
public Task Warn(IGuildUser user, [Leftover] string reason = null)
=> Warn(1, user, reason);
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.BanMembers)]
public async Task Warn(int weight, IGuildUser user, [Leftover] string reason = null)
{
if (weight <= 0)
return;
if (!await CheckRoleHierarchy(user))
return;
@@ -76,7 +85,7 @@ namespace NadekoBot.Modules.Administration
WarningPunishment punishment;
try
{
punishment = await _service.Warn(ctx.Guild, user.Id, ctx.User, reason).ConfigureAwait(false);
punishment = await _service.Warn(ctx.Guild, user.Id, ctx.User, weight, reason).ConfigureAwait(false);
}
catch (Exception ex)
{
@@ -230,19 +239,29 @@ namespace NadekoBot.Modules.Administration
}
else
{
var descText = GetText(strs.warn_count(
Format.Bold(warnings.Where(x => !x.Forgiven).Sum(x => x.Weight).ToString()),
Format.Bold(warnings.Sum(x => x.Weight).ToString())));
embed.WithDescription(descText);
var i = page * 9;
foreach (var w in warnings)
{
i++;
var name = GetText(strs.warned_on_by(
w.DateAdded.Value.ToString("dd.MM.yyy"),
w.DateAdded.Value.ToString("HH:mm"),
w.DateAdded?.ToString("dd.MM.yyy"),
w.DateAdded?.ToString("HH:mm"),
w.Moderator));
if (w.Forgiven)
name = $"{Format.Strikethrough(name)} {GetText(strs.warn_cleared_by(w.ForgivenBy))}";
embed.AddField($"#`{i}` " + name, w.Reason.TrimTo(1020));
embed.AddField($"#`{i}` " + name,
Format.Code(GetText(strs.warn_weight(w.Weight))) +
'\n' +
w.Reason.TrimTo(1000));
}
}
@@ -450,7 +469,7 @@ namespace NadekoBot.Modules.Administration
}
}
await _mute.TimedBan(ctx.Guild, user, time.Time, ctx.User.ToString() + " | " + msg).ConfigureAwait(false);
await _mute.TimedBan(ctx.Guild, user, time.Time, (ctx.User.ToString() + " | " + msg).TrimTo(512)).ConfigureAwait(false);
var toSend = _eb.Create().WithOkColor()
.WithTitle("⛔️ " + GetText(strs.banned_user))
.AddField(GetText(strs.username), user.ToString(), true)
@@ -476,7 +495,7 @@ namespace NadekoBot.Modules.Administration
var user = await ((DiscordSocketClient)Context.Client).Rest.GetGuildUserAsync(ctx.Guild.Id, userId);
if (user is null)
{
await ctx.Guild.AddBanAsync(userId, 7, ctx.User.ToString() + " | " + msg);
await ctx.Guild.AddBanAsync(userId, 7, (ctx.User.ToString() + " | " + msg).TrimTo(512));
await ctx.Channel.EmbedAsync(_eb.Create().WithOkColor()
.WithTitle("⛔️ " + GetText(strs.banned_user))
@@ -516,7 +535,7 @@ namespace NadekoBot.Modules.Administration
dmFailed = true;
}
await ctx.Guild.AddBanAsync(user, 7, ctx.User.ToString() + " | " + msg).ConfigureAwait(false);
await ctx.Guild.AddBanAsync(user, 7, (ctx.User.ToString() + " | " + msg).TrimTo(512)).ConfigureAwait(false);
var toSend = _eb.Create().WithOkColor()
.WithTitle("⛔️ " + GetText(strs.banned_user))
@@ -692,7 +711,7 @@ namespace NadekoBot.Modules.Administration
dmFailed = true;
}
await ctx.Guild.AddBanAsync(user, 7, "Softban | " + ctx.User.ToString() + " | " + msg).ConfigureAwait(false);
await ctx.Guild.AddBanAsync(user, 7, ("Softban | " + ctx.User.ToString() + " | " + msg).TrimTo(512)).ConfigureAwait(false);
try { await ctx.Guild.RemoveBanAsync(user).ConfigureAwait(false); }
catch { await ctx.Guild.RemoveBanAsync(user).ConfigureAwait(false); }
@@ -749,7 +768,7 @@ namespace NadekoBot.Modules.Administration
dmFailed = true;
}
await user.KickAsync(ctx.User.ToString() + " | " + msg).ConfigureAwait(false);
await user.KickAsync((ctx.User.ToString() + " | " + msg).TrimTo(512)).ConfigureAwait(false);
var toSend = _eb.Create().WithOkColor()
.WithTitle(GetText(strs.kicked_user))
@@ -776,23 +795,32 @@ namespace NadekoBot.Modules.Administration
return;
var missing = new List<string>();
var banning = new HashSet<IGuildUser>();
var banning = new HashSet<IUser>();
await ctx.Channel.TriggerTypingAsync();
foreach (var userStr in userStrings)
{
if (ulong.TryParse(userStr, out var userId))
{
var user = await ctx.Guild.GetUserAsync(userId) ??
IUser user = await ctx.Guild.GetUserAsync(userId) ??
await ((DiscordSocketClient)Context.Client).Rest.GetGuildUserAsync(ctx.Guild.Id, userId);
if (user is null)
{
missing.Add(userStr);
continue;
// if IGuildUser is null, try to get IUser
user = await ((DiscordSocketClient)Context.Client).Rest.GetUserAsync(userId);
// only add to missing if *still* null
if (user is null)
{
missing.Add(userStr);
continue;
}
}
if (!await CheckRoleHierarchy(user))
//Hierachy checks only if the user is in the guild
if (user is IGuildUser gu && !await CheckRoleHierarchy(gu))
{
return;
}
@@ -820,7 +848,7 @@ namespace NadekoBot.Modules.Administration
{
try
{
await toBan.BanAsync(7);
await ctx.Guild.AddBanAsync(toBan.Id, 7, $"{ctx.User} | Massban");
}
catch (Exception ex)
{

View File

@@ -149,7 +149,7 @@ namespace NadekoBot.Modules.CustomReactions
await ctx.Channel.EmbedAsync(_eb.Create().WithOkColor()
.WithDescription($"#{id}")
.AddField(GetText(strs.trigger), found.Trigger.TrimTo(1024))
.AddField(GetText(strs.response), (found.Response + "\n```css\n" + found.Response).TrimTo(1020) + "```")
.AddField(GetText(strs.response), found.Response.TrimTo(1000).Replace("](", "]\\("))
).ConfigureAwait(false);
}
}
@@ -310,7 +310,7 @@ namespace NadekoBot.Modules.CustomReactions
_ = ctx.Channel.TriggerTypingAsync();
var serialized = _service.ExportCrs(ctx.Guild?.Id);
using var stream = await serialized.ToStream();
await using var stream = await serialized.ToStream();
await ctx.Channel.SendFileAsync(stream, "crs-export.yml", text: null);
}

View File

@@ -3,6 +3,8 @@ using System.Collections.Generic;
using Cloneable;
using NadekoBot.Common;
using NadekoBot.Common.Yml;
using SixLabors.ImageSharp.PixelFormats;
using YamlDotNet.Serialization;
namespace NadekoBot.Modules.Gambling.Common
{
@@ -19,10 +21,11 @@ namespace NadekoBot.Modules.Gambling.Common
Generation = new GenerationConfig();
Timely = new TimelyConfig();
Decay = new DecayConfig();
Slots = new SlotsConfig();
}
[Comment(@"DO NOT CHANGE")]
public int Version { get; set; } = 1;
public int Version { get; set; } = 2;
[Comment(@"Currency settings")]
public CurrencyConfig Currency { get; set; }
@@ -59,6 +62,13 @@ Set 0 for unlimited")]
[Comment(@"Amount of currency selfhosters will get PER pledged dollar CENT.
1 = 100 currency per $. Used almost exclusively on public nadeko.")]
public decimal PatreonCurrencyPerCent { get; set; } = 1;
[Comment(@"Currency reward per vote.
This will work only if you've set up VotesApi and correct credentials for topgg and/or discords voting")]
public long VoteReward { get; set; } = 100;
[Comment(@"Slot config")]
public SlotsConfig Slots { get; set; }
}
public class CurrencyConfig
@@ -179,7 +189,8 @@ default is 0.02, which is 2%")]
public MultipliersData Multipliers { get; set; } = new MultipliersData();
[Comment(@"List of items available for gifting.")]
[Comment(@"List of items available for gifting.
If negative is true, gift will instead reduce waifu value.")]
public List<WaifuItemModel> Items { get; set; } = new List<WaifuItemModel>();
public WaifuConfig()
@@ -260,6 +271,17 @@ Default 1 (meaning no effect)")]
Default 0.95 (meaning 95%)
Example: If a waifu is worth 1000, and she receives a gift worth 100, her new value will be 1095)")]
public decimal GiftEffect { get; set; } = 0.95M;
[Comment(@"What percentage of the value of the gift will a waifu lose when she's gifted a gift marked as 'negative'.
Default 0.5 (meaning 50%)
Example: If a waifu is worth 1000, and she receives a negative gift worth 100, her new value will be 950)")]
public decimal NegativeGiftEffect { get; set; } = 0.50M;
}
public sealed partial class SlotsConfig
{
[Comment(@"Hex value of the color which the numbers on the slot image will have.")]
public Rgba32 CurrencyFontColor { get; set; } = SixLabors.ImageSharp.Color.Red;
}
[Cloneable]
@@ -268,19 +290,24 @@ Example: If a waifu is worth 1000, and she receives a gift worth 100, her new va
public string ItemEmoji { get; set; }
public int Price { get; set; }
public string Name { get; set; }
[YamlMember(DefaultValuesHandling = DefaultValuesHandling.OmitDefaults)]
public bool Negative { get; set; }
public WaifuItemModel()
{
}
public WaifuItemModel(string itemEmoji, int price, string name)
public WaifuItemModel(string itemEmoji, int price, string name, bool negative = false)
{
ItemEmoji = itemEmoji;
Price = price;
Name = name;
Negative = negative;
}
public override string ToString() => Name;
}

View File

@@ -0,0 +1,8 @@
namespace NadekoBot.Modules.Gambling
{
public enum GamblingError
{
None,
NotEnough
}
}

View File

@@ -0,0 +1,44 @@
using System;
using System.Linq;
using NadekoBot.Common;
namespace NadekoBot.Modules.Gambling.Common.Slot
{
public class SlotGame
{
public class Result
{
public float Multiplier { get; }
public int[] Rolls { get; }
public Result(float multiplier, int[] rolls)
{
Multiplier = multiplier;
Rolls = rolls;
}
}
private static readonly Random _rng = new NadekoRandom();
public SlotGame()
{
}
public Result Spin()
{
var rolls = new int[] { _rng.Next(0, 6), _rng.Next(0, 6), _rng.Next(0, 6) };
var multi = 0;
if (rolls.All(x => x == 5))
multi = 30;
else if (rolls.All(x => x == rolls[0]))
multi = 10;
else if (rolls.Count(x => x == 5) == 2)
multi = 4;
else if (rolls.Any(x => x == 5))
multi = 1;
return new Result(multi, rolls);
}
}
}

View File

@@ -0,0 +1,12 @@
using System.Collections.Generic;
namespace NadekoBot.Modules.Gambling
{
public class SlotResponse
{
public float Multiplier { get; set; }
public long Won { get; set; }
public List<int> Rolls { get; set; } = new List<int>();
public GamblingError Error { get; set; }
}
}

View File

@@ -1,14 +0,0 @@
namespace NadekoBot.Modules.Gambling.Common.Waifu
{
public struct WaifuProfileTitle
{
public int Count { get; }
public string Title { get; }
public WaifuProfileTitle(int count, string title)
{
Count = count;
Title = title;
}
}
}

View File

@@ -17,11 +17,6 @@ namespace NadekoBot.Modules.Gambling
[Group]
public class CurrencyEventsCommands : GamblingSubmodule<CurrencyEventsService>
{
public enum OtherEvent
{
BotListUpvoters
}
public CurrencyEventsCommands(GamblingConfigService gamblingConf) : base(gamblingConf)
{
}
@@ -37,41 +32,36 @@ namespace NadekoBot.Modules.Gambling
ctx.Channel.Id,
ev,
opts,
GetEmbed
).ConfigureAwait(false))
GetEmbed))
{
await ReplyErrorLocalizedAsync(strs.start_event_fail).ConfigureAwait(false);
return;
}
}
private IEmbedBuilder GetEmbed(CurrencyEvent.Type type, EventOptions opts, long currentPot)
{
switch (type)
return type switch
{
case CurrencyEvent.Type.Reaction:
return _eb.Create()
.WithOkColor()
.WithTitle(GetText(strs.event_title(type.ToString())))
.WithDescription(GetReactionDescription(opts.Amount, currentPot))
.WithFooter(GetText(strs.event_duration_footer(opts.Hours)));
case CurrencyEvent.Type.GameStatus:
return _eb.Create()
.WithOkColor()
.WithTitle(GetText(strs.event_title(type.ToString())))
.WithDescription(GetGameStatusDescription(opts.Amount, currentPot))
.WithFooter(GetText(strs.event_duration_footer(opts.Hours)));
default:
break;
}
throw new ArgumentOutOfRangeException(nameof(type));
CurrencyEvent.Type.Reaction => _eb.Create()
.WithOkColor()
.WithTitle(GetText(strs.event_title(type.ToString())))
.WithDescription(GetReactionDescription(opts.Amount, currentPot))
.WithFooter(GetText(strs.event_duration_footer(opts.Hours))),
CurrencyEvent.Type.GameStatus => _eb.Create()
.WithOkColor()
.WithTitle(GetText(strs.event_title(type.ToString())))
.WithDescription(GetGameStatusDescription(opts.Amount, currentPot))
.WithFooter(GetText(strs.event_duration_footer(opts.Hours))),
_ => throw new ArgumentOutOfRangeException(nameof(type))
};
}
private string GetReactionDescription(long amount, long potSize)
{
string potSizeStr = Format.Bold(potSize == 0
var potSizeStr = Format.Bold(potSize == 0
? "∞" + CurrencySign
: potSize.ToString() + CurrencySign);
: potSize + CurrencySign);
return GetText(strs.new_reaction_event(
CurrencySign,
Format.Bold(amount + CurrencySign),
@@ -80,9 +70,10 @@ namespace NadekoBot.Modules.Gambling
private string GetGameStatusDescription(long amount, long potSize)
{
string potSizeStr = Format.Bold(potSize == 0
var potSizeStr = Format.Bold(potSize == 0
? "∞" + CurrencySign
: potSize.ToString() + CurrencySign);
: potSize + CurrencySign);
return GetText(strs.new_gamestatus_event(
CurrencySign,
Format.Bold(amount + CurrencySign),

View File

@@ -22,15 +22,12 @@ namespace NadekoBot.Modules.Gambling
{
private readonly IImageCache _images;
private readonly ICurrencyService _cs;
private readonly DbService _db;
private static readonly NadekoRandom rng = new NadekoRandom();
public FlipCoinCommands(IDataCache data, ICurrencyService cs, DbService db,
GamblingConfigService gss) : base(gss)
public FlipCoinCommands(IDataCache data, ICurrencyService cs, GamblingConfigService gss) : base(gss)
{
_images = data.LocalImages;
_cs = cs;
_db = db;
}
[NadekoCommand, Aliases]

View File

@@ -66,11 +66,11 @@ namespace NadekoBot.Modules.Gambling
}
var embed = _eb.Create()
.WithTitle(GetText(strs.economy_state))
.AddField(GetText(strs.currency_owned), ((BigInteger)(ec.Cash - ec.Bot)) + CurrencySign)
.AddField(GetText(strs.currency_owned), ((BigInteger)(ec.Cash - ec.Bot)).ToString("N", _enUsCulture) + CurrencySign)
.AddField(GetText(strs.currency_one_percent), (onePercent * 100).ToString("F2") + "%")
.AddField(GetText(strs.currency_planted), ((BigInteger)ec.Planted) + CurrencySign)
.AddField(GetText(strs.owned_waifus_total), ((BigInteger)ec.Waifus) + CurrencySign)
.AddField(GetText(strs.bot_currency), ec.Bot + CurrencySign)
.AddField(GetText(strs.bot_currency), ec.Bot.ToString("N", _enUsCulture) + CurrencySign)
.AddField(GetText(strs.total), ((BigInteger)(ec.Cash + ec.Planted + ec.Waifus)).ToString("N", _enUsCulture) + CurrencySign)
.WithOkColor();
// ec.Cash already contains ec.Bot as it's the total of all values in the CurrencyAmount column of the DiscordUser table
@@ -247,25 +247,33 @@ namespace NadekoBot.Modules.Gambling
[RequireContext(ContextType.Guild)]
[OwnerOnly]
[Priority(0)]
public Task Award(ShmartNumber amount, IGuildUser usr, [Leftover] string msg) =>
public Task Award(long amount, IGuildUser usr, [Leftover] string msg) =>
Award(amount, usr.Id, msg);
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[OwnerOnly]
[Priority(1)]
public Task Award(ShmartNumber amount, [Leftover] IGuildUser usr) =>
public Task Award(long amount, [Leftover] IGuildUser usr) =>
Award(amount, usr.Id);
[NadekoCommand, Aliases]
[OwnerOnly]
[Priority(2)]
public async Task Award(ShmartNumber amount, ulong usrId, [Leftover] string msg = null)
public async Task Award(long amount, ulong usrId, [Leftover] string msg = null)
{
if (amount <= 0)
return;
await _cs.AddAsync(usrId,
var usr = await ((DiscordSocketClient)Context.Client).Rest.GetUserAsync(usrId);
if(usr is null)
{
await ReplyErrorLocalizedAsync(strs.user_not_found).ConfigureAwait(false);
return;
}
await _cs.AddAsync(usr,
$"Awarded by bot owner. ({ctx.User.Username}/{ctx.User.Id}) {(msg ?? "")}",
amount,
gamble: (ctx.Client.CurrentUser.Id != usrId)).ConfigureAwait(false);
@@ -275,8 +283,8 @@ namespace NadekoBot.Modules.Gambling
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[OwnerOnly]
[Priority(2)]
public async Task Award(ShmartNumber amount, [Leftover] IRole role)
[Priority(3)]
public async Task Award(long amount, [Leftover] IRole role)
{
var users = (await ctx.Guild.GetUsersAsync().ConfigureAwait(false))
.Where(u => u.GetRoles().Contains(role))
@@ -284,7 +292,7 @@ namespace NadekoBot.Modules.Gambling
await _cs.AddBulkAsync(users.Select(x => x.Id),
users.Select(x => $"Awarded by bot owner to **{role.Name}** role. ({ctx.User.Username}/{ctx.User.Id})"),
users.Select(x => amount.Value),
users.Select(x => amount),
gamble: true)
.ConfigureAwait(false);
@@ -298,13 +306,13 @@ namespace NadekoBot.Modules.Gambling
[RequireContext(ContextType.Guild)]
[OwnerOnly]
[Priority(0)]
public async Task Take(ShmartNumber amount, [Leftover] IRole role)
public async Task Take(long amount, [Leftover] IRole role)
{
var users = (await role.GetMembersAsync()).ToList();
await _cs.RemoveBulkAsync(users.Select(x => x.Id),
users.Select(x => $"Taken by bot owner from **{role.Name}** role. ({ctx.User.Username}/{ctx.User.Id})"),
users.Select(x => amount.Value),
users.Select(x => amount),
gamble: true)
.ConfigureAwait(false);
@@ -318,7 +326,7 @@ namespace NadekoBot.Modules.Gambling
[RequireContext(ContextType.Guild)]
[OwnerOnly]
[Priority(1)]
public async Task Take(ShmartNumber amount, [Leftover] IGuildUser user)
public async Task Take(long amount, [Leftover] IGuildUser user)
{
if (amount <= 0)
return;
@@ -333,7 +341,7 @@ namespace NadekoBot.Modules.Gambling
[NadekoCommand, Aliases]
[OwnerOnly]
public async Task Take(ShmartNumber amount, [Leftover] ulong usrId)
public async Task Take(long amount, [Leftover] ulong usrId)
{
if (amount <= 0)
return;

View File

@@ -8,10 +8,11 @@ using NadekoBot.Modules.Gambling.Services;
using System.Linq;
using System.Threading.Tasks;
using NadekoBot.Modules.Gambling.Common;
using NadekoBot.Common;
namespace NadekoBot.Modules.Games
namespace NadekoBot.Modules.Gambling
{
public partial class Games
public partial class Gambling
{
[Group]
public class PlantPickCommands : GamblingSubmodule<PlantPickService>
@@ -53,7 +54,7 @@ namespace NadekoBot.Modules.Games
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
public async Task Plant(int amount = 1, string pass = null)
public async Task Plant(ShmartNumber amount, string pass = null)
{
if (amount < 1)
return;
@@ -63,18 +64,17 @@ namespace NadekoBot.Modules.Games
return;
}
var success = await _service.PlantAsync(ctx.Guild.Id, ctx.Channel, ctx.User.Id, ctx.User.ToString(), amount, pass);
if (!success)
{
await ReplyErrorLocalizedAsync(strs.not_enough( CurrencySign));
return;
}
if (((SocketGuild)ctx.Guild).CurrentUser.GuildPermissions.ManageMessages)
{
logService.AddDeleteIgnore(ctx.Message.Id);
await ctx.Message.DeleteAsync().ConfigureAwait(false);
}
var success = await _service.PlantAsync(ctx.Guild.Id, ctx.Channel, ctx.User.Id, ctx.User.ToString(), amount, pass);
if (!success)
{
await ReplyErrorLocalizedAsync(strs.not_enough( CurrencySign));
}
}
[NadekoCommand, Aliases]

View File

@@ -8,8 +8,6 @@ using System.Threading.Tasks;
using System;
using NadekoBot.Services.Database.Models;
using System.Net.Http;
using Newtonsoft.Json;
using System.Linq;
using NadekoBot.Modules.Gambling.Services;
using Serilog;
@@ -17,76 +15,22 @@ namespace NadekoBot.Modules.Gambling.Services
{
public class CurrencyEventsService : INService
{
public class VoteModel
{
public ulong User { get; set; }
public long Date { get; set; }
}
private readonly DiscordSocketClient _client;
private readonly ICurrencyService _cs;
private readonly IBotCredentials _creds;
private readonly IHttpClientFactory _http;
private readonly GamblingConfigService _configService;
private readonly ConcurrentDictionary<ulong, ICurrencyEvent> _events =
new ConcurrentDictionary<ulong, ICurrencyEvent>();
public CurrencyEventsService(DiscordSocketClient client,
IBotCredentials creds, ICurrencyService cs,
IHttpClientFactory http, GamblingConfigService configService)
public CurrencyEventsService(
DiscordSocketClient client,
ICurrencyService cs,
GamblingConfigService configService)
{
_client = client;
_cs = cs;
_creds = creds;
_http = http;
_configService = configService;
if (_client.ShardId == 0)
{
Task t = BotlistUpvoteLoop();
}
}
// todo future use votes api directly?
private async Task BotlistUpvoteLoop()
{
if (string.IsNullOrWhiteSpace(_creds.VotesUrl))
return;
while (true)
{
await Task.Delay(TimeSpan.FromHours(1)).ConfigureAwait(false);
await TriggerVoteCheck().ConfigureAwait(false);
}
}
private async Task TriggerVoteCheck()
{
try
{
using (var req = new HttpRequestMessage(HttpMethod.Get, _creds.VotesUrl))
{
if (!string.IsNullOrWhiteSpace(_creds.VotesToken))
req.Headers.Add("Authorization", _creds.VotesToken);
using (var http = _http.CreateClient())
using (var res = await http.SendAsync(req).ConfigureAwait(false))
{
if (!res.IsSuccessStatusCode)
{
Log.Warning("Botlist API not reached.");
return;
}
var resStr = await res.Content.ReadAsStringAsync().ConfigureAwait(false);
var ids = JsonConvert.DeserializeObject<VoteModel[]>(resStr)
.Select(x => x.User)
.Distinct();
await _cs.AddBulkAsync(ids, ids.Select(x => "Voted - <https://discordbots.org/bot/nadeko/vote>"), ids.Select(x => 10L), true).ConfigureAwait(false);
}
}
}
catch (Exception ex)
{
Log.Warning(ex, "Error in TriggerVoteCheck");
}
}
public async Task<bool> TryCreateEventAsync(ulong guildId, ulong channelId, CurrencyEvent.Type type,
@@ -127,6 +71,7 @@ namespace NadekoBot.Modules.Gambling.Services
return false;
}
}
return added;
}
@@ -136,4 +81,4 @@ namespace NadekoBot.Modules.Gambling.Services
return Task.CompletedTask;
}
}
}
}

View File

@@ -1,7 +1,11 @@
using NadekoBot.Common;
using System;
using System.Collections.Generic;
using System.Linq;
using NadekoBot.Common;
using NadekoBot.Common.Configs;
using NadekoBot.Modules.Gambling.Common;
using NadekoBot.Services;
using NadekoBot.Services.Database.Models;
namespace NadekoBot.Modules.Gambling.Services
{
@@ -34,9 +38,48 @@ namespace NadekoBot.Modules.Gambling.Services
AddParsedProp("waifu.multi.divorce_value", gs => gs.Waifu.Multipliers.DivorceNewValue, decimal.TryParse, ConfigPrinters.ToString, val => val > 0);
AddParsedProp("waifu.multi.all_gifts", gs => gs.Waifu.Multipliers.AllGiftPrices, decimal.TryParse, ConfigPrinters.ToString, val => val > 0);
AddParsedProp("waifu.multi.gift_effect", gs => gs.Waifu.Multipliers.GiftEffect, decimal.TryParse, ConfigPrinters.ToString, val => val >= 0);
AddParsedProp("waifu.multi.negative_gift_effect", gs => gs.Waifu.Multipliers.NegativeGiftEffect, decimal.TryParse, ConfigPrinters.ToString, val => val >= 0);
AddParsedProp("decay.percent", gs => gs.Decay.Percent, decimal.TryParse, ConfigPrinters.ToString, val => val >= 0 && val <= 1);
AddParsedProp("decay.maxdecay", gs => gs.Decay.MaxDecay, int.TryParse, ConfigPrinters.ToString, val => val >= 0);
AddParsedProp("decay.threshold", gs => gs.Decay.MinThreshold, int.TryParse, ConfigPrinters.ToString, val => val >= 0);
Migrate();
}
private readonly IEnumerable<WaifuItemModel> antiGiftSeed = new[]
{
new WaifuItemModel("🥀", 100, "WiltedRose", true),
new WaifuItemModel("✂️", 1000, "Haircut", true),
new WaifuItemModel("🧻", 10000, "ToiletPaper", true),
};
public void Migrate()
{
if (_data.Version < 2)
{
ModifyConfig(c =>
{
c.Waifu.Items = c.Waifu.Items.Concat(antiGiftSeed).ToList();
c.Version = 2;
});
}
if (_data.Version < 3)
{
ModifyConfig(c =>
{
c.Version = 3;
c.VoteReward = 100;
});
}
if (_data.Version < 4)
{
ModifyConfig(c =>
{
c.Version = 4;
});
}
}
}
}

View File

@@ -6,12 +6,14 @@ using NadekoBot.Modules.Gambling.Common.WheelOfFortune;
using Newtonsoft.Json;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using NadekoBot.Common;
using NadekoBot.Db;
using NadekoBot.Modules.Gambling.Common.Slot;
using NadekoBot.Modules.Gambling.Services;
using Serilog;
@@ -82,6 +84,41 @@ WHERE CurrencyAmount > {config.Decay.MinThreshold} AND UserId!={_client.CurrentU
}, null, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5));
}
}
public async Task<SlotResponse> SlotAsync(ulong userId, long amount)
{
var takeRes = await _cs.RemoveAsync(userId, "Slot Machine", amount, true);
if (!takeRes)
{
return new SlotResponse
{
Error = GamblingError.NotEnough
};
}
var game = new SlotGame();
var result = game.Spin();
long won = 0;
if (result.Multiplier > 0)
{
won = (long)(result.Multiplier * amount);
await _cs.AddAsync(userId, $"Slot Machine x{result.Multiplier}", won, true);
}
var toReturn = new SlotResponse
{
Multiplier = result.Multiplier,
Won = won,
};
toReturn.Rolls.AddRange(result.Rolls);
return toReturn;
}
public struct EconomyResult
{

View File

@@ -0,0 +1,122 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Services;
using Discord.WebSocket;
using Serilog;
namespace NadekoBot.Modules.Gambling.Services
{
public class VoteModel
{
[JsonPropertyName("userId")]
public ulong UserId { get; set; }
}
public class VoteRewardService : INService, IReadyExecutor
{
private readonly DiscordSocketClient _client;
private readonly IBotCredentials _creds;
private readonly IHttpClientFactory _httpClientFactory;
private readonly ICurrencyService _currencyService;
private readonly GamblingConfigService _gamb;
private HttpClient _http;
public VoteRewardService(
DiscordSocketClient client,
IBotCredentials creds,
IHttpClientFactory httpClientFactory,
ICurrencyService currencyService,
GamblingConfigService gamb)
{
_client = client;
_creds = creds;
_httpClientFactory = httpClientFactory;
_currencyService = currencyService;
_gamb = gamb;
}
public async Task OnReadyAsync()
{
if (_client.ShardId != 0)
return;
_http = new HttpClient(new HttpClientHandler()
{
AllowAutoRedirect = false,
ServerCertificateCustomValidationCallback = delegate { return true; }
});
while (true)
{
await Task.Delay(30000);
var topggKey = _creds.Votes?.TopggKey;
var topggServiceUrl = _creds.Votes?.TopggServiceUrl;
try
{
if (!string.IsNullOrWhiteSpace(topggKey)
&& !string.IsNullOrWhiteSpace(topggServiceUrl))
{
_http.DefaultRequestHeaders.Authorization = new(topggKey);
var uri = new Uri(new(topggServiceUrl), "topgg/new");
var res = await _http.GetStringAsync(uri);
var data = JsonSerializer.Deserialize<List<VoteModel>>(res);
if (data is { Count: > 0 })
{
var ids = data.Select(x => x.UserId).ToList();
await _currencyService.AddBulkAsync(ids,
data.Select(_ => "top.gg vote reward"),
data.Select(x => _gamb.Data.VoteReward),
true);
Log.Information("Rewarding {Count} top.gg voters", ids.Count());
}
}
}
catch (Exception ex)
{
Log.Error(ex, "Critical error loading top.gg vote rewards.");
}
var discordsKey = _creds.Votes?.DiscordsKey;
var discordsServiceUrl = _creds.Votes?.DiscordsServiceUrl;
try
{
if (!string.IsNullOrWhiteSpace(discordsKey)
&& !string.IsNullOrWhiteSpace(discordsServiceUrl))
{
_http.DefaultRequestHeaders.Authorization = new(discordsKey);
var res = await _http.GetStringAsync(new Uri(new(discordsServiceUrl), "discords/new"));
var data = JsonSerializer.Deserialize<List<VoteModel>>(res);
if (data is { Count: > 0 })
{
var ids = data.Select(x => x.UserId).ToList();
await _currencyService.AddBulkAsync(ids,
data.Select(_ => "discords.com vote reward"),
data.Select(x => _gamb.Data.VoteReward),
true);
Log.Information("Rewarding {Count} discords.com voters", ids.Count());
}
}
}
catch (Exception ex)
{
Log.Error(ex, "Critical error loading discords.com vote rewards.");
}
}
}
}
}

View File

@@ -394,19 +394,28 @@ namespace NadekoBot.Modules.Gambling.Services
});
}
w.Items.Add(new WaifuItem()
if (!itemObj.Negative)
{
Name = itemObj.Name.ToLowerInvariant(),
ItemEmoji = itemObj.ItemEmoji,
});
if (w.Claimer?.UserId == from.Id)
{
w.Price += (int) (itemObj.Price * _gss.Data.Waifu.Multipliers.GiftEffect);
w.Items.Add(new WaifuItem()
{
Name = itemObj.Name.ToLowerInvariant(),
ItemEmoji = itemObj.ItemEmoji,
});
if (w.Claimer?.UserId == from.Id)
{
w.Price += (int)(itemObj.Price * _gss.Data.Waifu.Multipliers.GiftEffect);
}
else
{
w.Price += itemObj.Price / 2;
}
}
else
{
w.Price += itemObj.Price / 2;
w.Price -= (int)(itemObj.Price * _gss.Data.Waifu.Multipliers.NegativeGiftEffect);
if (w.Price < 1)
w.Price = 1;
}
await uow.SaveChangesAsync();
@@ -512,7 +521,7 @@ namespace NadekoBot.Modules.Gambling.Services
{
var conf = _gss.Data;
return conf.Waifu.Items
.Select(x => new WaifuItemModel(x.ItemEmoji, (int)(x.Price * conf.Waifu.Multipliers.AllGiftPrices), x.Name))
.Select(x => new WaifuItemModel(x.ItemEmoji, (int)(x.Price * conf.Waifu.Multipliers.AllGiftPrices), x.Name, x.Negative))
.ToList();
}
}

View File

@@ -225,6 +225,9 @@ namespace NadekoBot.Modules.Gambling
[BotPerm(GuildPerm.ManageRoles)]
public async Task ShopAdd(Role _, int price, [Leftover] IRole role)
{
if (price < 1)
return;
var entry = new ShopEntry()
{
Name = "-",
@@ -252,8 +255,11 @@ namespace NadekoBot.Modules.Gambling
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
public async Task ShopAdd(List _, int price, [Leftover]string name)
public async Task ShopAdd(List _, int price, [Leftover] string name)
{
if (price < 1)
return;
var entry = new ShopEntry()
{
Name = name.TrimTo(100),
@@ -266,13 +272,14 @@ namespace NadekoBot.Modules.Gambling
{
var entries = new IndexedCollection<ShopEntry>(uow.GuildConfigsForId(ctx.Guild.Id,
set => set.Include(x => x.ShopEntries)
.ThenInclude(x => x.Items)).ShopEntries)
.ThenInclude(x => x.Items)).ShopEntries)
{
entry
};
uow.GuildConfigsForId(ctx.Guild.Id, set => set).ShopEntries = entries;
uow.SaveChanges();
}
await ctx.Channel.EmbedAsync(EntryToEmbed(entry)
.WithTitle(GetText(strs.shop_item_add))).ConfigureAwait(false);
}

View File

@@ -9,12 +9,17 @@ using System.Threading;
using System.Threading.Tasks;
using NadekoBot.Common;
using NadekoBot.Common.Attributes;
using NadekoBot.Db.Models;
using NadekoBot.Modules.Gambling.Services;
using NadekoBot.Modules.Gambling.Common;
using NadekoBot.Services;
using SixLabors.Fonts;
using Image = SixLabors.ImageSharp.Image;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Drawing.Processing;
using SixLabors.ImageSharp.PixelFormats;
using Color = SixLabors.ImageSharp.Color;
namespace NadekoBot.Modules.Gambling
{
@@ -33,12 +38,16 @@ namespace NadekoBot.Modules.Gambling
//thanks to judge for helping me with this
private readonly IImageCache _images;
private readonly ICurrencyService _cs;
private FontProvider _fonts;
private readonly DbService _db;
public SlotCommands(IDataCache data, ICurrencyService cs, GamblingConfigService gamb) : base(gamb)
public SlotCommands(IDataCache data,
FontProvider fonts, DbService db,
GamblingConfigService gamb) : base(gamb)
{
_images = data.LocalImages;
_cs = cs;
_fonts = fonts;
_db = db;
}
public sealed class SlotMachine
@@ -140,92 +149,119 @@ namespace NadekoBot.Modules.Gambling
[NadekoCommand, Aliases]
public async Task Slot(ShmartNumber amount)
{
if (!_runningUsers.Add(ctx.User.Id))
if (!_runningUsers.Add(ctx.User.Id))
return;
try
{
if (!await CheckBetMandatory(amount).ConfigureAwait(false))
return;
const int maxAmount = 9999;
if (amount > maxAmount)
{
await ReplyErrorLocalizedAsync(strs.max_bet_limit(maxAmount + CurrencySign));
return;
}
try
{
if (!await CheckBetMandatory(amount).ConfigureAwait(false))
return;
await ctx.Channel.TriggerTypingAsync().ConfigureAwait(false);
if (!await _cs.RemoveAsync(ctx.User, "Slot Machine", amount, false, gamble: true).ConfigureAwait(false))
{
await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign));
return;
}
Interlocked.Add(ref _totalBet, amount.Value);
using (var bgImage = Image.Load(_images.SlotBackground))
{
var result = SlotMachine.Pull();
int[] numbers = result.Numbers;
var result = await _service.SlotAsync(ctx.User.Id, amount);
for (int i = 0; i < 3; i++)
{
using (var randomImage = Image.Load(_images.SlotEmojis[numbers[i]]))
{
bgImage.Mutate(x => x.DrawImage(randomImage, new Point(95 + 142 * i, 330), new GraphicsOptions()));
}
}
if (result.Error != GamblingError.None)
{
if (result.Error == GamblingError.NotEnough)
{
await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign));
}
var won = amount * result.Multiplier;
var printWon = won;
var n = 0;
do
{
var digit = (int)(printWon % 10);
using (var img = Image.Load(_images.SlotNumbers[digit]))
{
bgImage.Mutate(x => x.DrawImage(img, new Point(230 - n * 16, 462), new GraphicsOptions()));
}
n++;
} while ((printWon /= 10) != 0);
return;
}
var printAmount = amount;
n = 0;
do
{
var digit = (int)(printAmount % 10);
using (var img = Image.Load(_images.SlotNumbers[digit]))
{
bgImage.Mutate(x => x.DrawImage(img, new Point(395 - n * 16, 462), new GraphicsOptions()));
}
n++;
} while ((printAmount /= 10) != 0);
Interlocked.Add(ref _totalBet, amount);
Interlocked.Add(ref _totalPaidOut, result.Won);
var msg = GetText(strs.better_luck);
if (result.Multiplier != 0)
{
await _cs.AddAsync(ctx.User, $"Slot Machine x{result.Multiplier}", amount * result.Multiplier, false, gamble: true).ConfigureAwait(false);
Interlocked.Add(ref _totalPaidOut, amount * result.Multiplier);
if (result.Multiplier == 1)
msg = GetText(strs.slot_single(CurrencySign, 1));
else if (result.Multiplier == 4)
msg = GetText(strs.slot_two(CurrencySign, 4));
else if (result.Multiplier == 10)
msg = GetText(strs.slot_three(10));
else if (result.Multiplier == 30)
msg = GetText(strs.slot_jackpot(30));
}
long ownedAmount;
using (var uow = _db.GetDbContext())
{
ownedAmount = uow.Set<DiscordUser>()
.FirstOrDefault(x => x.UserId == ctx.User.Id)
?.CurrencyAmount ?? 0;
}
using (var imgStream = bgImage.ToStream())
{
await ctx.Channel.SendFileAsync(imgStream, "result.png", ctx.User.Mention + " " + msg + $"\n`{GetText(strs.slot_bet)}:`{amount} `{GetText(strs.won)}:` {amount * result.Multiplier}{CurrencySign}").ConfigureAwait(false);
}
}
}
finally
{
var _ = Task.Run(async () =>
{
await Task.Delay(1000).ConfigureAwait(false);
_runningUsers.Remove(ctx.User.Id);
});
}
using (var bgImage = Image.Load<Rgba32>(_images.SlotBackground, out var format))
{
var numbers = new int[3];
result.Rolls.CopyTo(numbers, 0);
Color fontColor = _config.Slots.CurrencyFontColor;
bgImage.Mutate(x => x.DrawText(new TextGraphicsOptions
{
TextOptions = new TextOptions()
{
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
WrapTextWidth = 140,
}
}, result.Won.ToString(), _fonts.DottyFont.CreateFont(65), fontColor,
new PointF(227, 92)));
var bottomFont = _fonts.DottyFont.CreateFont(50);
bgImage.Mutate(x => x.DrawText(new TextGraphicsOptions
{
TextOptions = new TextOptions()
{
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
WrapTextWidth = 135,
}
}, amount.ToString(), bottomFont, fontColor,
new PointF(129, 472)));
bgImage.Mutate(x => x.DrawText(new TextGraphicsOptions
{
TextOptions = new TextOptions()
{
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
WrapTextWidth = 135,
}
}, ownedAmount.ToString(), bottomFont, fontColor,
new PointF(325, 472)));
//sw.PrintLap("drew red text");
for (var i = 0; i < 3; i++)
{
using (var img = Image.Load(_images.SlotEmojis[numbers[i]]))
{
bgImage.Mutate(x => x.DrawImage(img, new Point(148 + 105 * i, 217), 1f));
}
}
var msg = GetText(strs.better_luck);
if (result.Multiplier > 0)
{
if (result.Multiplier == 1f)
msg = GetText(strs.slot_single(CurrencySign, 1));
else if (result.Multiplier == 4f)
msg = GetText(strs.slot_two(CurrencySign, 4));
else if (result.Multiplier == 10f)
msg = GetText(strs.slot_three(10));
else if (result.Multiplier == 30f)
msg = GetText(strs.slot_jackpot(30));
}
using (var imgStream = bgImage.ToStream())
{
await ctx.Channel.SendFileAsync(imgStream,
filename: "result.png",
text: Format.Bold(ctx.User.ToString()) + " " + msg).ConfigureAwait(false);
}
}
}
finally
{
var _ = Task.Run(async () =>
{
await Task.Delay(1000).ConfigureAwait(false);
_runningUsers.Remove(ctx.User.Id);
});
}
}
}
}

View File

@@ -317,10 +317,14 @@ namespace NadekoBot.Modules.Gambling
.WithOkColor();
waifuItems
.OrderBy(x => x.Price)
.OrderBy(x => x.Negative)
.ThenBy(x => x.Price)
.Skip(9 * cur)
.Take(9)
.ForEach(x => embed.AddField($"{x.ItemEmoji} {x.Name}", x.Price, true));
.ForEach(x => embed
.AddField($"{(!x.Negative ? string.Empty : "\\💔")} {x.ItemEmoji} {x.Name}",
Format.Bold(x.Price.ToString()) + _config.Currency.Sign,
true));
return embed;
}, waifuItems.Count, 9);

View File

@@ -2,6 +2,7 @@
using Discord.Commands;
using System;
using System.Threading.Tasks;
using NadekoBot.Common;
using NadekoBot.Common.Attributes;
using NadekoBot.Services;
using NadekoBot.Db;
@@ -23,6 +24,7 @@ namespace NadekoBot.Modules.Games
_db = db;
}
[NoPublicBot]
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageMessages)]

View File

@@ -0,0 +1,72 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using NadekoBot.Common;
using NadekoBot.Common.Yml;
using Serilog;
namespace NadekoBot.Modules.Games.Hangman
{
public sealed class DefaultHangmanSource : IHangmanSource
{
private IReadOnlyDictionary<string, HangmanTerm[]> _terms = new Dictionary<string, HangmanTerm[]>();
private readonly Random _rng;
public DefaultHangmanSource()
{
_rng = new NadekoRandom();
Reload();
}
public void Reload()
{
if (!Directory.Exists("data/hangman"))
{
Log.Error("Hangman game won't work. Folder 'data/hangman' is missing.");
return;
}
var qs = new Dictionary<string, HangmanTerm[]>();
foreach (var file in Directory.EnumerateFiles("data/hangman/", "*.yml"))
{
try
{
var data = Yaml.Deserializer.Deserialize<HangmanTerm[]>(File.ReadAllText(file));
qs[Path.GetFileNameWithoutExtension(file).ToLowerInvariant()] = data;
}
catch (Exception ex)
{
Log.Error(ex, "Loading {HangmanFile} failed.", file);
}
}
_terms = qs;
Log.Information("Loaded {HangmanCategoryCount} hangman categories.", qs.Count);
}
public IReadOnlyCollection<string> GetCategories()
=> _terms.Keys.ToList();
public bool GetTerm(string? category, [NotNullWhen(true)] out HangmanTerm? term)
{
if (category is null)
{
var cats = GetCategories();
category = cats.ElementAt(_rng.Next(0, cats.Count));
}
if (_terms.TryGetValue(category, out var terms))
{
term = terms[_rng.Next(0, terms.Length)];
return true;
}
term = null;
return false;
}
}
}

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