Compare commits

..

161 Commits
3.0.8 ... v3

Author SHA1 Message Date
Kwoth
2797e56666 Merge branch 'hokutochen-v3-patch-48466' into 'v3'
updated migration guide with better link

See merge request Kwoth/nadekobot!267
2022-08-31 14:37:43 +00:00
Hokuto Chen
11f27ab67a updated migration guide with better link 2022-08-29 01:29:30 +00:00
Kwoth
c5f8bf1891 Merge branch 'hokutochen-v3-patch-79546' into 'v3'
update migration guide using Hoku's script since old link is dead

See merge request Kwoth/nadekobot!263
2022-08-10 22:27:31 +00:00
Hokuto Chen
66b57482bd update migration guide using Hoku's script since old link is dead 2022-08-10 05:03:12 +00:00
Kwoth
dd9d914943 Merge branch 'hokutochen-v3-patch-15731' into 'v3'
Update bot version stat 3.0.12 to 3.0.13

See merge request Kwoth/nadekobot!233
2022-03-02 21:06:53 +00:00
Hokuto Chen
8b0b1af1f3 Update bot version stat 3.0.12 to 3.0.13 2022-03-02 20:30:54 +00:00
Kwoth
bdff19c718 Update responses.uk-UA.json (POEditor.com) 2022-02-17 16:33:33 +00:00
Kwoth
eec4a91743 Update responses.es-ES.json (POEditor.com) 2022-02-17 16:33:32 +00:00
Kwoth
42caefbd1e Update responses.ru-RU.json (POEditor.com) 2022-02-17 16:33:31 +00:00
Kwoth
3edf435fa9 Update responses.pt-BR.json (POEditor.com) 2022-02-17 16:33:29 +00:00
Kwoth
32da43ad46 Update responses.pl-PL.json (POEditor.com) 2022-02-17 16:33:28 +00:00
Kwoth
3f2d1e128e Update responses.it-IT.json (POEditor.com) 2022-02-17 16:33:26 +00:00
Kwoth
352ced1553 Update responses.id-ID.json (POEditor.com) 2022-02-17 16:33:25 +00:00
Kwoth
17a3fb2c52 Update responses.de-DE.json (POEditor.com) 2022-02-17 16:33:24 +00:00
Kwoth
2235729158 Update responses.fr-FR.json (POEditor.com) 2022-02-17 16:33:23 +00:00
Kwoth
3e4d5d07e5 Update responses.nl-NL.json (POEditor.com) 2022-02-17 16:33:22 +00:00
Kwoth
1857d65b77 Merge branch 'warnpatch' into 'v3'
Weighted warnings now properly apply punishments

See merge request Kwoth/nadekobot!198
2022-02-10 12:52:45 +00:00
Alan Beatty
38da171675 Weighted warnings now properly apply punishments 2022-02-10 12:52:45 +00:00
Kwoth
df076be15c Merge branch 'hokutochen-v3-patch-07454' into 'v3'
modified step 6 of linux release instructions.

See merge request Kwoth/nadekobot!231
2022-02-09 08:43:09 +00:00
Hokuto Chen
190f030b93 modified step 6 of linux release instructions. 2022-02-09 07:31:01 +00:00
Kwoth
411b873c84 Increased .crypto refresh from 1h to 2h to prevent monthly ratelimits 2022-01-22 18:22:35 +01:00
Kwoth
50346fc5b4 Fixed patreon reward bug 2022-01-20 13:26:47 +01:00
Kwoth
e32a65bca4 Merge branch 'hokutochen-v3-patch-72839' into 'v3'
same as !227

See merge request Kwoth/nadekobot!228
2022-01-16 19:25:36 +00:00
Kwoth
ffff539545 Merge branch 'hokutochen-v3-patch-44571' into 'v3'
added quotes to windows default restart command example.

See merge request Kwoth/nadekobot!229
2022-01-16 19:25:08 +00:00
Hokuto Chen
4b3b6e37a9 added quotes to windows default restart command example. 2022-01-16 11:24:31 +00:00
Hokuto Chen
620bcae4bb same as !227 2022-01-16 11:23:40 +00:00
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
Yuno Gasai
717543f6c2 Move plant/pick where they belong 2021-10-19 09:28:55 -04:00
98 changed files with 11966 additions and 4958 deletions

View File

@@ -2,9 +2,98 @@
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
## [3.0.7]
### 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.

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>
@@ -48,8 +63,8 @@ Open Terminal (if you're on an installation with a window manager) and navigate
5. Copy the creds.yml template
- `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)
- `nano creds.yml`
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`
@@ -244,3 +259,34 @@ This method is similar to the one above, but requires one extra step, with the a
5. Start Nadeko:
- `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

@@ -15,9 +15,9 @@
## Linux
1. In order to migrate a bot hosted on **Linux**, first update your current version to the latest 2.x version using the 2.x installer, run the bot, and make sure it works. Then:
- Run the **old** installer with `cd ~ && wget -N https://github.com/Kwoth/NadekoBot-BashScript/raw/1.9/linuxAIO.sh && bash linuxAIO.sh`
- Run option **1** again
1. In order to migrate a bot hosted on **Linux**, first **BACKUP** then update your current version to the latest **2.x** version using the **2.x** installer, run the bot, and make sure it works. Then:
- Run the **old** installer with `cd ~ && wget -N https://gitlab.com/hokutochen/becausekwoth/-/raw/main/linuxAIO.sh && bash linuxAIO.sh`
- Run option **2**
- You **MUST** Run the bot now to ensure database is ready for migration
- Type `.stats` and ensure the version is `2.46.5` or later
- Stop the bot

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**
@@ -38,7 +38,7 @@
- Click on **`DOWNLOAD`** at the lower right
![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.
- 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.
@@ -69,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!**
@@ -83,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
@@ -129,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.
@@ -69,6 +75,7 @@ For Windows (Updater), add this to your `creds.yml`
```yml
RestartCommand:
Cmd: "NadekoBot.exe"
args: "{0}"
```
For Windows (Source), Linux or OSX, add this to your `creds.yml`
@@ -92,11 +99,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
@@ -156,8 +162,8 @@ osuApiKey: 4c8c8fdffdsfdsfsdfsfa33f3f3140a7d93320d6
# cmd: dotnet
# args: "NadekoBot.dll -- {0}"
# Windows default
# cmd: NadekoBot.exe
# args: {0}
# cmd: "NadekoBot.exe"
# args: "{0}"
restartCommand:
cmd:
args:

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

@@ -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()

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

@@ -73,11 +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;
[Comment(@"Api key obtained on https://rapidapi.com (go to MyApps -> Add New App -> Enter Name -> Application key)")]
public string RapidApiKey { get; set; }
@@ -121,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)")]

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,12 +12,11 @@ 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; }
Creds.VotesSettings Votes { get; }

View File

@@ -6,7 +6,7 @@ namespace NadekoBot.Common
public class ImageUrls
{
[Comment("DO NOT CHANGE")]
public int Version { get; set; } = 2;
public int Version { get; set; } = 3;
public CoinData Coins { get; set; }
public Uri[] Currency { get; set; }
@@ -27,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

@@ -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

@@ -1,4 +1,5 @@
using NadekoBot.Db.Models;
using System;
using NadekoBot.Db.Models;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Discord;
@@ -168,7 +169,7 @@ VALUES ({userId}, {name}, {discrim}, {avatarId}, {amount}, 0);
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

@@ -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

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

View File

@@ -60,6 +60,8 @@ namespace NadekoBot.Services.Database
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)
{
@@ -196,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
@@ -362,6 +370,21 @@ namespace NadekoBot.Services.Database
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,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

@@ -340,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")
@@ -1967,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");
@@ -2189,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)
@@ -2536,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");

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

@@ -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,29 +60,32 @@ namespace NadekoBot.Modules.Administration.Services
Forgiven = false,
Reason = reason,
Moderator = modName,
Weight = weight,
};
int warnings = 1;
long previousCount;
List<WarningPunishment> ps;
using (var uow = _db.GetDbContext())
{
ps = uow.GuildConfigsForId(guildId, set => set.Include(x => x.WarnPunishments))
.WarnPunishments;
warnings += uow
.Warnings
.ForId(guildId, userId)
.Where(w => !w.Forgiven && w.UserId == userId)
.Count();
previousCount = uow.Warnings.ForId(guildId, userId)
.Where(w => !w.Forgiven && w.UserId == userId)
.Sum(x => x.Weight);
uow.Warnings.Add(warn);
uow.SaveChanges();
await uow.SaveChangesAsync();
}
var p = ps.FirstOrDefault(x => x.Count == warnings);
var totalCount = previousCount + weight;
var p = ps.Where(x => x.Count > previousCount && x.Count <= totalCount)
.OrderByDescending(x => x.Count)
.FirstOrDefault();
if (p != null)
if (p is not null)
{
var user = await guild.GetUserAsync(userId).ConfigureAwait(false);
if (user is null)
@@ -95,6 +101,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 +177,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 +501,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 +518,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));
}
}
@@ -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

@@ -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,7 @@ 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
@@ -20,6 +21,7 @@ namespace NadekoBot.Modules.Gambling.Common
Generation = new GenerationConfig();
Timely = new TimelyConfig();
Decay = new DecayConfig();
Slots = new SlotsConfig();
}
[Comment(@"DO NOT CHANGE")]
@@ -64,6 +66,9 @@ Set 0 for unlimited")]
[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
@@ -273,6 +278,12 @@ Example: If a waifu is worth 1000, and she receives a negative gift worth 100, h
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]
public sealed partial class WaifuItemModel
{

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

@@ -68,9 +68,18 @@ namespace NadekoBot.Modules.Gambling.Services
{
ModifyConfig(c =>
{
c.Version = 3;
c.VoteReward = 100;
});
}
if (_data.Version < 4)
{
ModifyConfig(c =>
{
c.Version = 4;
});
}
}
}
}

View File

@@ -19,6 +19,7 @@ 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
{
@@ -186,6 +187,8 @@ namespace NadekoBot.Modules.Gambling
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()
@@ -194,9 +197,11 @@ namespace NadekoBot.Modules.Gambling
VerticalAlignment = VerticalAlignment.Center,
WrapTextWidth = 140,
}
}, result.Won.ToString(), _fonts.DottyFont.CreateFont(65), SixLabors.ImageSharp.Color.Red,
}, 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()
@@ -205,7 +210,7 @@ namespace NadekoBot.Modules.Gambling
VerticalAlignment = VerticalAlignment.Center,
WrapTextWidth = 135,
}
}, amount.ToString(), _fonts.DottyFont.CreateFont(50), SixLabors.ImageSharp.Color.Red,
}, amount.ToString(), bottomFont, fontColor,
new PointF(129, 472)));
bgImage.Mutate(x => x.DrawText(new TextGraphicsOptions
@@ -216,7 +221,7 @@ namespace NadekoBot.Modules.Gambling
VerticalAlignment = VerticalAlignment.Center,
WrapTextWidth = 135,
}
}, ownedAmount.ToString(), _fonts.DottyFont.CreateFont(50), SixLabors.ImageSharp.Color.Red,
}, ownedAmount.ToString(), bottomFont, fontColor,
new PointF(325, 472)));
//sw.PrintLap("drew red text");

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

@@ -104,7 +104,8 @@ namespace NadekoBot.Modules.Games.Hangman
CurrentPhase = Phase.Ended;
return GetState(GuessResult.Win);
}
_correct.Add(charGuess);
return GetState(GuessResult.Guess);
}

View File

@@ -763,7 +763,7 @@ namespace NadekoBot.Modules.Music
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[RequireUserPermission(GuildPermission.Administrator)]
[UserPerm(GuildPerm.Administrator)]
public async Task MusicQuality()
{
var quality = await _service.GetMusicQualityAsync(ctx.Guild.Id);
@@ -772,7 +772,7 @@ namespace NadekoBot.Modules.Music
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[RequireUserPermission(GuildPermission.Administrator)]
[UserPerm(GuildPerm.Administrator)]
public async Task MusicQuality(QualityPreset preset)
{
await _service.SetMusicQualityAsync(ctx.Guild.Id, preset);

View File

@@ -195,7 +195,7 @@ namespace NadekoBot.Modules.Music.Services
public Task<IUserMessage?> SendToOutputAsync(ulong guildId, IEmbedBuilder embed)
{
if (_outputChannels.TryGetValue(guildId, out var chan))
return (chan.Default ?? chan.Override).EmbedAsync(embed);
return (chan.Override ?? chan.Default).EmbedAsync(embed);
return Task.FromResult<IUserMessage?>(null);
}

View File

@@ -2,6 +2,7 @@
using System.Linq;
using System.Net.Http;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
@@ -13,10 +14,11 @@ namespace NadekoBot.Modules.Nsfw.Common
{
}
public override async Task<List<DapiImageObject>> DownloadImagesAsync(string[] tags, int page, bool isExplicit = false, CancellationToken cancel = default)
public override async Task<List<DapiImageObject>> DownloadImagesAsync(string[] tags, int page,
bool isExplicit = false, CancellationToken cancel = default)
{
var tagString = ImageDownloaderHelper.GetTagString(tags, isExplicit);
var uri = $"http://gelbooru.com/index.php?page=dapi&s=post&json=1&q=index&limit=100" +
var uri = $"https://gelbooru.com/index.php?page=dapi&s=post&json=1&q=index&limit=100" +
$"&tags={tagString}&pid={page}";
using var req = new HttpRequestMessage(HttpMethod.Get, uri);
using var res = await _http.SendAsync(req, cancel).ConfigureAwait(false);
@@ -24,12 +26,18 @@ namespace NadekoBot.Modules.Nsfw.Common
var resString = await res.Content.ReadAsStringAsync(cancel);
if (string.IsNullOrWhiteSpace(resString))
return new();
var images = JsonSerializer.Deserialize<List<DapiImageObject>>(resString, _serializerOptions);
if (images is null)
var images = JsonSerializer.Deserialize<GelbooruResponse>(resString, _serializerOptions);
if (images is null or { Post: null })
return new();
return images.Where(x => x.FileUrl is not null).ToList();
return images.Post.Where(x => x.FileUrl is not null).ToList();
}
}
public class GelbooruResponse
{
[JsonPropertyName("post")]
public List<DapiImageObject> Post { get; set; }
}
}

View File

@@ -24,7 +24,7 @@ namespace NadekoBot.Modules.Nsfw.Common
return new();
return images
.Where(img => !string.IsNullOrWhiteSpace(img.Directory) && !string.IsNullOrWhiteSpace(img.Image))
.Where(img => !string.IsNullOrWhiteSpace(img.Image))
.ToList();
}
}

View File

@@ -3,7 +3,7 @@
public class Rule34Object : IImageData
{
public string Image { get; init; }
public string Directory { get; init; }
public int Directory { get; init; }
public string Tags { get; init; }
public int Score { get; init; }

View File

@@ -0,0 +1,12 @@
using System.Text.Json;
namespace SystemTextJsonSamples
{
public class LowerCaseNamingPolicy : JsonNamingPolicy
{
public static LowerCaseNamingPolicy Default = new LowerCaseNamingPolicy();
public override string ConvertName(string name) =>
name.ToLower();
}
}

View File

@@ -20,7 +20,7 @@ namespace NadekoBot.Modules.Searches
var (crypto, nearest) = await _service.GetCryptoData(name).ConfigureAwait(false);
if (nearest != null)
if (nearest is not null)
{
var embed = _eb.Create()
.WithTitle(GetText(strs.crypto_not_found))

View File

@@ -0,0 +1,16 @@
using System.Linq;
using System.Threading.Tasks;
using LinqToDB.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using NadekoBot.Services.Database.Models;
namespace NadekoBot.Modules.Searches
{
public static class AtlExtensions
{
public static Task<AutoTranslateChannel> GetByChannelId(this IQueryable<AutoTranslateChannel> set, ulong channelId)
=> set
.Include(x => x.Users)
.FirstOrDefaultAsyncEF(x => x.ChannelId == channelId);
}
}

View File

@@ -35,6 +35,9 @@ namespace NadekoBot.Modules.Searches.Services
name = name.ToUpperInvariant();
var cryptos = await CryptoData().ConfigureAwait(false);
if (cryptos is null)
return (null, null);
var crypto = cryptos
?.FirstOrDefault(x => x.Id.ToUpperInvariant() == name || x.Name.ToUpperInvariant() == name
|| x.Symbol.ToUpperInvariant() == name);
@@ -42,7 +45,8 @@ namespace NadekoBot.Modules.Searches.Services
(CryptoResponseData Elem, int Distance)? nearest = null;
if (crypto is null)
{
nearest = cryptos.Select(x => (x, Distance: x.Name.ToUpperInvariant().LevenshteinDistance(name)))
nearest = cryptos
.Select(x => (x, Distance: x.Name.ToUpperInvariant().LevenshteinDistance(name)))
.OrderBy(x => x.Distance)
.Where(x => x.Distance <= 2)
.FirstOrDefault();
@@ -68,18 +72,17 @@ namespace NadekoBot.Modules.Searches.Services
{
try
{
using (var _http = _httpFactory.CreateClient())
{
var strData = await _http.GetStringAsync(new Uri($"https://pro-api.coinmarketcap.com/v1/cryptocurrency/listings/latest?" +
$"CMC_PRO_API_KEY={_creds.CoinmarketcapApiKey}" +
$"&start=1" +
$"&limit=500" +
$"&convert=USD"));
using var _http = _httpFactory.CreateClient();
var strData = await _http.GetStringAsync(
$"https://pro-api.coinmarketcap.com/v1/cryptocurrency/listings/latest?" +
$"CMC_PRO_API_KEY={_creds.CoinmarketcapApiKey}" +
$"&start=1" +
$"&limit=5000" +
$"&convert=USD");
JsonConvert.DeserializeObject<CryptoResponse>(strData); // just to see if its' valid
JsonConvert.DeserializeObject<CryptoResponse>(strData); // just to see if its' valid
return strData;
}
return strData;
}
catch (Exception ex)
{
@@ -87,7 +90,7 @@ namespace NadekoBot.Modules.Searches.Services
return default;
}
}, "", TimeSpan.FromHours(1));
}, "", TimeSpan.FromHours(2));
return JsonConvert.DeserializeObject<CryptoResponse>(fullStrData).Data;
}

View File

@@ -0,0 +1,14 @@
using System.Collections.Generic;
using System.Threading.Tasks;
namespace NadekoBot.Modules.Searches
{
public interface ITranslateService
{
public Task<string> Translate(string source, string target, string text = null);
Task<bool> ToggleAtl(ulong guildId, ulong channelId, bool autoDelete);
IEnumerable<string> GetLanguages();
Task<bool?> RegisterUserAsync(ulong userId, ulong channelId, string @from, string to);
Task<bool> UnregisterUser(ulong channelId, ulong userId);
}
}

View File

@@ -1,10 +1,6 @@
using Discord;
using Discord.WebSocket;
using Microsoft.EntityFrameworkCore;
using NadekoBot.Common;
using NadekoBot.Common;
using NadekoBot.Modules.Searches.Common;
using NadekoBot.Services;
using NadekoBot.Services.Database.Models;
using NadekoBot.Extensions;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
@@ -13,18 +9,14 @@ using SixLabors.ImageSharp.Drawing.Processing;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using AngleSharp.Html.Dom;
using AngleSharp.Html.Parser;
using NadekoBot.Db;
using NadekoBot.Modules.Administration;
using Serilog;
using HorizontalAlignment = SixLabors.Fonts.HorizontalAlignment;
using Image = SixLabors.ImageSharp.Image;
@@ -34,71 +26,31 @@ namespace NadekoBot.Modules.Searches.Services
public class SearchesService : INService
{
private readonly IHttpClientFactory _httpFactory;
private readonly DiscordSocketClient _client;
private readonly IGoogleApiService _google;
private readonly DbService _db;
private readonly IImageCache _imgs;
private readonly IDataCache _cache;
private readonly FontProvider _fonts;
private readonly IBotCredentials _creds;
private readonly IEmbedBuilderService _eb;
private readonly NadekoRandom _rng;
public ConcurrentDictionary<ulong, bool> TranslatedChannels { get; } = new ConcurrentDictionary<ulong, bool>();
// (userId, channelId)
public ConcurrentDictionary<(ulong UserId, ulong ChannelId), string> UserLanguages { get; } = new ConcurrentDictionary<(ulong, ulong), string>();
public List<WoWJoke> WowJokes { get; } = new List<WoWJoke>();
public List<MagicItem> MagicItems { get; } = new List<MagicItem>();
private readonly List<string> _yomamaJokes;
public SearchesService(DiscordSocketClient client, IGoogleApiService google,
DbService db, Bot bot, IDataCache cache, IHttpClientFactory factory,
FontProvider fonts, IBotCredentials creds, IEmbedBuilderService eb)
public SearchesService(IGoogleApiService google,
IDataCache cache,
IHttpClientFactory factory,
FontProvider fonts,
IBotCredentials creds)
{
_httpFactory = factory;
_client = client;
_google = google;
_db = db;
_imgs = cache.LocalImages;
_cache = cache;
_fonts = fonts;
_creds = creds;
_eb = eb;
_rng = new NadekoRandom();
//translate commands
_client.MessageReceived += (msg) =>
{
var _ = Task.Run(async () =>
{
try
{
if (!(msg is SocketUserMessage umsg))
return;
if (!TranslatedChannels.TryGetValue(umsg.Channel.Id, out var autoDelete))
return;
var key = (umsg.Author.Id, umsg.Channel.Id);
if (!UserLanguages.TryGetValue(key, out string langs))
return;
var text = await Translate(langs, umsg.Resolve(TagHandling.Ignore))
.ConfigureAwait(false);
if (autoDelete)
try { await umsg.DeleteAsync().ConfigureAwait(false); } catch { }
await umsg.Channel.SendConfirmAsync(_eb, $"{umsg.Author.Mention} `:` "
+ text.Replace("<@ ", "<@", StringComparison.InvariantCulture)
.Replace("<@! ", "<@!", StringComparison.InvariantCulture)).ConfigureAwait(false);
}
catch { }
});
return Task.CompletedTask;
};
//joke commands
if (File.Exists("data/wowjokes.json"))
{
@@ -340,19 +292,6 @@ namespace NadekoBot.Modules.Searches.Services
_rng.Next(1, max).ToString("000") + ".png";
}
public async Task<string> Translate(string langs, string text = null)
{
if (string.IsNullOrWhiteSpace(text))
throw new ArgumentException("Text is empty or null", nameof(text));
var langarr = langs.ToLowerInvariant().Split('>');
if (langarr.Length != 2)
throw new ArgumentException("Langs does not have 2 parts separated by a >", nameof(langs));
var from = langarr[0];
var to = langarr[1];
text = text?.Trim();
return (await _google.Translate(text, from, to).ConfigureAwait(false)).SanitizeMentions(true);
}
private readonly object yomamaLock = new object();
private int yomamaJokeIndex = 0;
public Task<string> GetYomamaJoke()

View File

@@ -0,0 +1,235 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using Discord;
using Discord.Net;
using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Extensions;
using NadekoBot.Services;
namespace NadekoBot.Modules.Searches
{
public sealed class TranslateService : ITranslateService, ILateExecutor, IReadyExecutor, INService
{
private readonly IGoogleApiService _google;
private readonly DbService _db;
private readonly IEmbedBuilderService _eb;
private readonly Bot _bot;
private readonly ConcurrentDictionary<ulong, bool> _atcs = new();
private readonly ConcurrentDictionary<ulong, ConcurrentDictionary<ulong, (string From, string To)>> _users = new();
public TranslateService(IGoogleApiService google,
DbService db,
IEmbedBuilderService eb,
Bot bot)
{
_google = google;
_db = db;
_eb = eb;
_bot = bot;
}
public async Task OnReadyAsync()
{
var ctx = _db.GetDbContext();
var guilds = _bot.AllGuildConfigs.Select(x => x.GuildId).ToList();
var cs = await ctx.AutoTranslateChannels
.Include(x => x.Users)
.Where(x => guilds.Contains(x.GuildId))
.ToListAsyncEF();
foreach (var c in cs)
{
_atcs[c.ChannelId] = c.AutoDelete;
_users[c.ChannelId] = new(c.Users.ToDictionary(x => x.UserId, x => (x.Source.ToLower(), x.Target.ToLower())));
}
}
public async Task LateExecute(IGuild guild, IUserMessage msg)
{
if (string.IsNullOrWhiteSpace(msg.Content))
return;
if (msg is IUserMessage { Channel: ITextChannel tch } um)
{
if (!_atcs.TryGetValue(tch.Id, out var autoDelete))
return;
if (!_users.TryGetValue(tch.Id, out var users)
|| !users.TryGetValue(um.Author.Id, out var langs))
return;
var output = await _google.Translate(msg.Content, langs.From, langs.To);
if (string.IsNullOrWhiteSpace(output)
|| msg.Content.Equals(output, StringComparison.InvariantCultureIgnoreCase))
return;
var embed = _eb.Create()
.WithOkColor();
if (autoDelete)
{
embed
.WithAuthor(um.Author.ToString(), um.Author.GetAvatarUrl())
.AddField(langs.From, um.Content)
.AddField(langs.To, output);
await tch.EmbedAsync(embed);
try
{
await um.DeleteAsync();
}
catch (HttpException ex) when (ex.HttpCode == HttpStatusCode.Forbidden)
{
_atcs.TryUpdate(tch.Id, false, true);
}
return;
}
await um.ReplyAsync(embed: embed
.AddField(langs.To, output)
.Build(),
allowedMentions: AllowedMentions.None);
}
}
public async Task<string> Translate(string source, string target, string text = null)
{
if (string.IsNullOrWhiteSpace(text))
throw new ArgumentException("Text is empty or null", nameof(text));
var res = await _google.Translate(text, source, target).ConfigureAwait(false);
return res.SanitizeMentions(true);
}
public async Task<bool> ToggleAtl(ulong guildId, ulong channelId, bool autoDelete)
{
var ctx = _db.GetDbContext();
var old = await ctx.AutoTranslateChannels
.ToLinqToDBTable()
.FirstOrDefaultAsyncLinqToDB(x => x.ChannelId == channelId);
if (old is null)
{
ctx.AutoTranslateChannels
.Add(new()
{
GuildId = guildId,
ChannelId = channelId,
AutoDelete = autoDelete,
});
await ctx.SaveChangesAsync();
_atcs[channelId] = autoDelete;
_users[channelId] = new();
return true;
}
// if autodelete value is different, update the autodelete value
// instead of disabling
if (old.AutoDelete != autoDelete)
{
old.AutoDelete = autoDelete;
await ctx.SaveChangesAsync();
_atcs[channelId] = autoDelete;
return true;
}
await ctx.AutoTranslateChannels
.ToLinqToDBTable()
.DeleteAsync(x => x.ChannelId == channelId);
await ctx.SaveChangesAsync();
_atcs.TryRemove(channelId, out _);
_users.TryRemove(channelId, out _);
return false;
}
private void UpdateUser(ulong channelId, ulong userId, string from, string to)
{
var dict = _users.GetOrAdd(channelId, new ConcurrentDictionary<ulong, (string, string)>());
dict[userId] = (from, to);
}
public async Task<bool?> RegisterUserAsync(ulong userId, ulong channelId, string from, string to)
{
if (!_google.Languages.ContainsKey(from) || !_google.Languages.ContainsKey(to))
return null;
var ctx = _db.GetDbContext();
var ch = await ctx.AutoTranslateChannels
.GetByChannelId(channelId);
if (ch is null)
return null;
var user = ch.Users
.FirstOrDefault(x => x.UserId == userId);
if (user is null)
{
ch.Users.Add(user = new()
{
Source = from,
Target = to,
UserId = userId,
});
await ctx.SaveChangesAsync();
UpdateUser(channelId, userId, from, to);
return true;
}
// if it's different from old settings, update
if (user.Source != from || user.Target != to)
{
user.Source = from;
user.Target = to;
await ctx.SaveChangesAsync();
UpdateUser(channelId, userId, from, to);
return true;
}
return await UnregisterUser(channelId, userId);
}
public async Task<bool> UnregisterUser(ulong channelId, ulong userId)
{
var ctx = _db.GetDbContext();
var rows = await ctx.AutoTranslateUsers
.ToLinqToDBTable()
.DeleteAsync(x => x.UserId == userId &&
x.Channel.ChannelId == channelId);
if (_users.TryGetValue(channelId, out var inner))
inner.TryRemove(userId, out _);
await ctx.SaveChangesAsync();
return rows > 0;
}
public IEnumerable<string> GetLanguages() => _google.Languages.Select(x => x.Key);
}
}

View File

@@ -2,35 +2,29 @@
using Discord.Commands;
using NadekoBot.Extensions;
using System.Threading.Tasks;
using System.Linq;
using NadekoBot.Common.Attributes;
using NadekoBot.Services;
using NadekoBot.Modules.Searches.Services;
namespace NadekoBot.Modules.Searches
{
public partial class Searches
{
[Group]
public class TranslateCommands : NadekoSubmodule
public class TranslateCommands : NadekoSubmodule<ITranslateService>
{
private readonly SearchesService _searches;
private readonly IGoogleApiService _google;
public TranslateCommands(SearchesService searches, IGoogleApiService google)
{
_searches = searches;
_google = google;
}
[NadekoCommand, Aliases]
public async Task Translate(string langs, [Leftover] string text = null)
public async Task Translate(string from, string to, [Leftover] string text)
{
try
{
await ctx.Channel.TriggerTypingAsync().ConfigureAwait(false);
var translation = await _searches.Translate(langs, text).ConfigureAwait(false);
await SendConfirmAsync(GetText(strs.translation) + " " + langs, translation).ConfigureAwait(false);
var translation = await _service.Translate(from.ToLower(), to.ToLower(), text).ConfigureAwait(false);
var embed = _eb.Create(ctx)
.WithOkColor()
.AddField(from, text, false)
.AddField(to, translation, false);
await ctx.Channel.EmbedAsync(embed);
}
catch
{
@@ -38,27 +32,6 @@ namespace NadekoBot.Modules.Searches
}
}
//[NadekoCommand, Usage, Description, Aliases]
//[OwnerOnly]
//public async Task Obfuscate([Leftover] string txt)
//{
// var lastItem = "en";
// foreach (var item in _google.Languages.Except(new[] { "en" }).Where(x => x.Length < 4))
// {
// var txt2 = await _searches.Translate(lastItem + ">" + item, txt);
// await ctx.Channel.EmbedAsync(_eb.Create()
// .WithOkColor()
// .WithTitle(lastItem + ">" + item)
// .AddField("Input", txt)
// .AddField("Output", txt2));
// txt = txt2;
// await Task.Delay(500);
// lastItem = item;
// }
// txt = await _searches.Translate(lastItem + ">en", txt);
// await SendConfirmAsync("Final output:\n\n" + txt);
//}
public enum AutoDeleteAutoTranslate
{
Del,
@@ -68,56 +41,52 @@ namespace NadekoBot.Modules.Searches
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
[BotPerm(ChannelPerm.ManageMessages)]
[OwnerOnly]
public async Task AutoTranslate(AutoDeleteAutoTranslate autoDelete = AutoDeleteAutoTranslate.Nodel)
{
var channel = (ITextChannel)ctx.Channel;
if (autoDelete == AutoDeleteAutoTranslate.Del)
{
_searches.TranslatedChannels.AddOrUpdate(channel.Id, true, (key, val) => true);
await ReplyConfirmLocalizedAsync(strs.atl_ad_started).ConfigureAwait(false);
return;
}
if (_searches.TranslatedChannels.TryRemove(channel.Id, out _))
{
await ReplyConfirmLocalizedAsync(strs.atl_stopped).ConfigureAwait(false);
return;
}
if (_searches.TranslatedChannels.TryAdd(channel.Id, autoDelete == AutoDeleteAutoTranslate.Del))
var toggle = await _service.ToggleAtl(ctx.Guild.Id, ctx.Channel.Id, autoDelete == AutoDeleteAutoTranslate.Del);
if (toggle)
{
await ReplyConfirmLocalizedAsync(strs.atl_started).ConfigureAwait(false);
}
else
{
await ReplyConfirmLocalizedAsync(strs.atl_stopped).ConfigureAwait(false);
}
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
public async Task AutoTransLang([Leftover] string langs = null)
public async Task AutoTransLang()
{
var ucp = (ctx.User.Id, ctx.Channel.Id);
if (string.IsNullOrWhiteSpace(langs))
if (await _service.UnregisterUser(ctx.Channel.Id, ctx.User.Id))
{
if (_searches.UserLanguages.TryRemove(ucp, out langs))
await ReplyConfirmLocalizedAsync(strs.atl_removed).ConfigureAwait(false);
await ReplyConfirmLocalizedAsync(strs.atl_removed).ConfigureAwait(false);
}
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
public async Task AutoTransLang(string from, string to)
{
var succ = await _service.RegisterUserAsync(ctx.User.Id,
ctx.Channel.Id,
from.ToLower(),
to.ToLower());
if (succ is null)
{
await ReplyErrorLocalizedAsync(strs.atl_not_enabled);
return;
}
var langarr = langs.ToLowerInvariant().Split('>');
if (langarr.Length != 2)
return;
var from = langarr[0];
var to = langarr[1];
if (!_google.Languages.Contains(from) || !_google.Languages.Contains(to))
if (succ is false)
{
await ReplyErrorLocalizedAsync(strs.invalid_lang).ConfigureAwait(false);
return;
}
_searches.UserLanguages.AddOrUpdate(ucp, langs, (key, val) => langs);
await ReplyConfirmLocalizedAsync(strs.atl_set(from, to));
}
@@ -125,7 +94,7 @@ namespace NadekoBot.Modules.Searches
[RequireContext(ContextType.Guild)]
public async Task Translangs()
{
await ctx.Channel.SendTableAsync(_google.Languages, str => $"{str,-15}", 3).ConfigureAwait(false);
await ctx.Channel.SendTableAsync(_service.GetLanguages(), str => $"{str,-15}", 3).ConfigureAwait(false);
}
}

View File

@@ -1,23 +1,134 @@
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace NadekoBot.Modules.Utility.Common.Patreon
{
public class PatreonData
public sealed class Attributes
{
public JObject[] Included { get; set; }
public JObject[] Data { get; set; }
public PatreonDataLinks Links { get; set; }
[JsonPropertyName("full_name")]
public string FullName { get; set; }
[JsonPropertyName("is_follower")]
public bool IsFollower { get; set; }
[JsonPropertyName("last_charge_date")]
public DateTime LastChargeDate { get; set; }
[JsonPropertyName("last_charge_status")]
public string LastChargeStatus { get; set; }
[JsonPropertyName("lifetime_support_cents")]
public int LifetimeSupportCents { get; set; }
[JsonPropertyName("currently_entitled_amount_cents")]
public int CurrentlyEntitledAmountCents { get; set; }
[JsonPropertyName("patron_status")]
public string PatronStatus { get; set; }
}
public class PatreonDataLinks
public sealed class Data
{
public string first { get; set; }
public string next { get; set; }
[JsonPropertyName("id")]
public string Id { get; set; }
[JsonPropertyName("type")]
public string Type { get; set; }
}
public class PatreonUserAndReward
public sealed class Address
{
public PatreonUser User { get; set; }
public PatreonPledge Reward { get; set; }
[JsonPropertyName("data")]
public Data Data { get; set; }
}
// public sealed class CurrentlyEntitledTiers
// {
// [JsonPropertyName("data")]
// public List<Datum> Data { get; set; }
// }
// public sealed class Relationships
// {
// [JsonPropertyName("address")]
// public Address Address { get; set; }
//
// // [JsonPropertyName("currently_entitled_tiers")]
// // public CurrentlyEntitledTiers CurrentlyEntitledTiers { get; set; }
// }
public sealed class PatreonResponse
{
[JsonPropertyName("data")]
public List<PatreonMember> Data { get; set; }
[JsonPropertyName("included")]
public List<PatreonUser> Included { get; set; }
[JsonPropertyName("links")]
public PatreonLinks Links { get; set; }
}
public sealed class PatreonLinks
{
[JsonPropertyName("next")]
public string Next { get; set; }
}
public sealed class PatreonUser
{
[JsonPropertyName("attributes")]
public PatreonUserAttributes Attributes { get; set; }
[JsonPropertyName("id")]
public string Id { get; set; }
// public string Type { get; set; }
}
public sealed class PatreonUserAttributes
{
[JsonPropertyName("social_connections")]
public PatreonSocials SocialConnections { get; set; }
}
public sealed class PatreonSocials
{
[JsonPropertyName("discord")]
public DiscordSocial Discord { get; set; }
}
public sealed class DiscordSocial
{
[JsonPropertyName("user_id")]
public string UserId { get; set; }
}
public sealed class PatreonMember
{
[JsonPropertyName("attributes")]
public Attributes Attributes { get; set; }
[JsonPropertyName("relationships")]
public Relationships Relationships { get; set; }
[JsonPropertyName("type")]
public string Type { get; set; }
}
public sealed class Relationships
{
[JsonPropertyName("user")]
public PatreonRelationshipUser User { get; set; }
}
public sealed class PatreonRelationshipUser
{
[JsonPropertyName("data")]
public PatreonUserData Data { get; set; }
}
public sealed class PatreonUserData
{
[JsonPropertyName("id")]
public string Id { get; set; }
}
}

View File

@@ -1,62 +0,0 @@
namespace NadekoBot.Modules.Utility.Common.Patreon
{
public class Attributes
{
public int amount_cents { get; set; }
public string created_at { get; set; }
public object declined_since { get; set; }
public bool is_twitch_pledge { get; set; }
public bool patron_pays_fees { get; set; }
public int? pledge_cap_cents { get; set; }
}
public class Address
{
public object data { get; set; }
}
public class Data
{
public string id { get; set; }
public string type { get; set; }
}
public class Links
{
public string related { get; set; }
}
public class Creator
{
public Data data { get; set; }
public Links links { get; set; }
}
public class Patron
{
public Data data { get; set; }
public Links links { get; set; }
}
public class Reward
{
public Data data { get; set; }
public Links links { get; set; }
}
public class Relationships
{
public Address address { get; set; }
public Creator creator { get; set; }
public Patron patron { get; set; }
public Reward reward { get; set; }
}
public class PatreonPledge
{
public Attributes attributes { get; set; }
public string id { get; set; }
public Relationships relationships { get; set; }
public string type { get; set; }
}
}

View File

@@ -1,64 +0,0 @@
namespace NadekoBot.Modules.Utility.Common.Patreon
{
public class DiscordConnection
{
public string user_id { get; set; }
}
public class SocialConnections
{
public object deviantart { get; set; }
public DiscordConnection discord { get; set; }
public object facebook { get; set; }
public object spotify { get; set; }
public object twitch { get; set; }
public object twitter { get; set; }
public object youtube { get; set; }
}
public class UserAttributes
{
public string about { get; set; }
public string created { get; set; }
public object discord_id { get; set; }
public string email { get; set; }
public object facebook { get; set; }
public object facebook_id { get; set; }
public string first_name { get; set; }
public string full_name { get; set; }
public int gender { get; set; }
public bool has_password { get; set; }
public string image_url { get; set; }
public bool is_deleted { get; set; }
public bool is_nuked { get; set; }
public bool is_suspended { get; set; }
public string last_name { get; set; }
public SocialConnections social_connections { get; set; }
public int status { get; set; }
public string thumb_url { get; set; }
public object twitch { get; set; }
public string twitter { get; set; }
public string url { get; set; }
public string vanity { get; set; }
public object youtube { get; set; }
}
public class Campaign
{
public Data data { get; set; }
public Links links { get; set; }
}
public class UserRelationships
{
public Campaign campaign { get; set; }
}
public class PatreonUser
{
public UserAttributes attributes { get; set; }
public string id { get; set; }
public UserRelationships relationships { get; set; }
public string type { get; set; }
}
}

View File

@@ -32,35 +32,40 @@ namespace NadekoBot.Modules.Utility
var channel = (ITextChannel)ctx.Channel;
guildName = guildName?.ToUpperInvariant();
SocketGuild guild;
if (string.IsNullOrWhiteSpace(guildName))
guild = (SocketGuild)channel.Guild;
else
guild = _client.Guilds.FirstOrDefault(g => g.Name.ToUpperInvariant() == guildName.ToUpperInvariant());
if (guild is null)
return;
var ownername = guild.GetUser(guild.OwnerId);
var textchn = guild.TextChannels.Count();
var voicechn = guild.VoiceChannels.Count();
var textchn = guild.TextChannels.Count;
var voicechn = guild.VoiceChannels.Count;
var channels = $@"{GetText(strs.text_channels(textchn))}
{GetText(strs.voice_channels(voicechn))}";
var createdAt = new DateTime(2015, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc).AddMilliseconds(guild.Id >> 22);
var features = string.Join("\n", guild.Features);
var features = string.Join(", ", guild.Features);
if (string.IsNullOrWhiteSpace(features))
features = "-";
var embed = _eb.Create()
.WithAuthor(GetText(strs.server_info))
.WithTitle(guild.Name)
.AddField(GetText(strs.id), guild.Id.ToString(), true)
.AddField(GetText(strs.owner), ownername.ToString(), true)
.AddField(GetText(strs.members), guild.MemberCount.ToString(), true)
.AddField(GetText(strs.text_channels), textchn.ToString(), true)
.AddField(GetText(strs.voice_channels), voicechn.ToString(), true)
.AddField(GetText(strs.channels), channels, true)
.AddField(GetText(strs.created_at), $"{createdAt:dd.MM.yyyy HH:mm}", true)
.AddField(GetText(strs.region), guild.VoiceRegionId.ToString(), true)
.AddField(GetText(strs.roles), (guild.Roles.Count - 1).ToString(), true)
.AddField(GetText(strs.features), features, true)
.AddField(GetText(strs.features), features)
.WithOkColor();
if (Uri.IsWellFormedUriString(guild.IconUrl, UriKind.Absolute))
embed.WithThumbnailUrl(guild.IconUrl);
if (guild.Emotes.Any())
{
embed.AddField(GetText(strs.custom_emojis) + $"({guild.Emotes.Count})",

View File

@@ -6,6 +6,7 @@ using NadekoBot.Extensions;
using Discord;
using NadekoBot.Common.Attributes;
using NadekoBot.Modules.Utility.Services;
using Serilog;
namespace NadekoBot.Modules.Utility
{
@@ -25,8 +26,12 @@ namespace NadekoBot.Modules.Utility
[RequireContext(ContextType.DM)]
public async Task ClaimPatreonRewards()
{
if (string.IsNullOrWhiteSpace(_creds.PatreonAccessToken))
if (string.IsNullOrWhiteSpace(_creds.Patreon.AccessToken))
{
Log.Warning("In order to use patreon reward commands, " +
"you need to specify CampaignId and AccessToken in creds.yml");
return;
}
if (DateTime.UtcNow.Day < 5)
{

View File

@@ -7,10 +7,13 @@ using NadekoBot.Db.Models;
using NadekoBot.Extensions;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using NadekoBot.Common.Yml;
using NadekoBot.Services;
using NadekoBot.Services.Database.Models;
using NadekoBot.Db;
using YamlDotNet.Serialization;
namespace NadekoBot.Modules.Utility
{
@@ -20,10 +23,12 @@ namespace NadekoBot.Modules.Utility
public class QuoteCommands : NadekoSubmodule
{
private readonly DbService _db;
private readonly IHttpClientFactory _http;
public QuoteCommands(DbService db)
public QuoteCommands(DbService db, IHttpClientFactory http)
{
_db = db;
_http = http;
}
[NadekoCommand, Aliases]
@@ -203,7 +208,7 @@ namespace NadekoBot.Modules.Utility
[RequireContext(ContextType.Guild)]
public async Task QuoteDelete(int id)
{
var isAdmin = ((IGuildUser)ctx.Message.Author).GuildPermissions.ManageMessages;
var hasManageMessages = ((IGuildUser)ctx.Message.Author).GuildPermissions.ManageMessages;
var success = false;
string response;
@@ -211,7 +216,7 @@ namespace NadekoBot.Modules.Utility
{
var q = uow.Quotes.GetById(id);
if ((q?.GuildId != ctx.Guild.Id) || (!isAdmin && q.AuthorId != ctx.Message.Author.Id))
if ((q?.GuildId != ctx.Guild.Id) || (!hasManageMessages && q.AuthorId != ctx.Message.Author.Id))
{
response = GetText(strs.quotes_remove_none);
}
@@ -248,6 +253,140 @@ namespace NadekoBot.Modules.Utility
await ReplyConfirmLocalizedAsync(strs.quotes_deleted(Format.Bold(keyword.SanitizeAllMentions()))).ConfigureAwait(false);
}
public class ExportedQuote
{
public static ExportedQuote FromModel(Quote quote)
=> new ExportedQuote()
{
Id = ((kwum)quote.Id).ToString(),
An = quote.AuthorName,
Aid = quote.AuthorId,
Txt = quote.Text
};
public string Id { get; set; }
public string An { get; set; }
public ulong Aid { get; set; }
public string Txt { get; set; }
}
private const string _prependExport =
@"# Keys are keywords, Each key has a LIST of quotes in the following format:
# - id: Alphanumeric id used for commands related to the quote. (Note, when using .quotesimport, a new id will be generated.)
# an: Author name
# aid: Author id
# txt: Quote text
";
private static readonly ISerializer _exportSerializer = new SerializerBuilder()
.WithEventEmitter(args => new MultilineScalarFlowStyleEmitter(args))
.WithNamingConvention(YamlDotNet.Serialization.NamingConventions.CamelCaseNamingConvention.Instance)
.WithIndentedSequences()
.ConfigureDefaultValuesHandling(DefaultValuesHandling.OmitDefaults)
.DisableAliases()
.Build();
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
public async Task QuotesExport()
{
IEnumerable<Quote> quotes;
using (var uow = _db.GetDbContext())
{
quotes = uow.Quotes
.GetForGuild(ctx.Guild.Id)
.ToList();
}
var crsDict = quotes
.GroupBy(x => x.Keyword)
.ToDictionary(x => x.Key, x => x.Select(ExportedQuote.FromModel));
var text = _prependExport + _exportSerializer
.Serialize(crsDict)
.UnescapeUnicodeCodePoints();
await using var stream = await text.ToStream();
await ctx.Channel.SendFileAsync(stream, "quote-export.yml", text: null);
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
[Ratelimit(300)]
#if GLOBAL_NADEKO
[OwnerOnly]
#endif
public async Task QuotesImport([Leftover]string input = null)
{
input = input?.Trim();
_ = ctx.Channel.TriggerTypingAsync();
if (input is null)
{
var attachment = ctx.Message.Attachments.FirstOrDefault();
if (attachment is null)
{
await ReplyErrorLocalizedAsync(strs.expr_import_no_input);
return;
}
using var client = _http.CreateClient();
input = await client.GetStringAsync(attachment.Url);
if (string.IsNullOrWhiteSpace(input))
{
await ReplyErrorLocalizedAsync(strs.expr_import_no_input);
return;
}
}
var succ = await ImportCrsAsync(ctx.Guild.Id, input);
if (!succ)
{
await ReplyErrorLocalizedAsync(strs.expr_import_invalid_data);
return;
}
await ctx.OkAsync();
}
public async Task<bool> ImportCrsAsync(ulong guildId, string input)
{
Dictionary<string, List<ExportedQuote>> data;
try
{
data = Yaml.Deserializer.Deserialize<Dictionary<string, List<ExportedQuote>>>(input);
if (data.Sum(x => x.Value.Count) == 0)
return false;
}
catch
{
return false;
}
await using var uow = _db.GetDbContext();
foreach (var entry in data)
{
var keyword = entry.Key;
await uow.Quotes
.AddRangeAsync(entry.Value
.Where(quote => !string.IsNullOrWhiteSpace(quote.Txt))
.Select(quote => new Quote()
{
GuildId = guildId,
Keyword = keyword,
Text = quote.Txt,
AuthorId = quote.Aid,
AuthorName = quote.An,
}));
}
await uow.SaveChangesAsync();
return true;
}
}
}
}

View File

@@ -80,21 +80,50 @@ namespace NadekoBot.Modules.Utility
}
}
public enum Server
{
Server = int.MinValue,
Srvr = int.MinValue,
Serv = int.MinValue,
S = int.MinValue,
}
[NadekoCommand, Aliases]
public async Task RemindList(int page = 1)
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
[Priority(0)]
public Task RemindList(Server _, int page = 1)
=> RemindList(page, true);
[NadekoCommand, Aliases]
[Priority(1)]
public Task RemindList(int page = 1)
=> RemindList(page, false);
private async Task RemindList(int page, bool isServer)
{
if (--page < 0)
return;
var embed = _eb.Create()
.WithOkColor()
.WithTitle(GetText(strs.reminder_list));
.WithTitle(GetText(isServer ? strs.reminder_server_list : strs.reminder_list));
List<Reminder> rems;
using (var uow = _db.GetDbContext())
{
rems = uow.Reminders.RemindersFor(ctx.User.Id, page)
.ToList();
if (isServer)
{
rems = uow.Reminders
.RemindersForServer(ctx.Guild.Id, page)
.ToList();
}
else
{
rems = uow.Reminders
.RemindersFor(ctx.User.Id, page)
.ToList();
}
}
if (rems.Any())
@@ -121,18 +150,33 @@ namespace NadekoBot.Modules.Utility
}
[NadekoCommand, Aliases]
public async Task RemindDelete(int index)
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
[Priority(0)]
public Task RemindDelete(Server _, int index)
=> RemindDelete(index, true);
[NadekoCommand, Aliases]
[Priority(1)]
public Task RemindDelete(int index)
=> RemindDelete(index, false);
private async Task RemindDelete(int index, bool isServer)
{
if (--index < 0)
return;
var embed = _eb.Create();
Reminder rem = null;
using (var uow = _db.GetDbContext())
{
var rems = uow.Reminders.RemindersFor(ctx.User.Id, index / 10)
.ToList();
var rems = isServer
? uow.Reminders
.RemindersForServer(ctx.Guild.Id, index / 10)
.ToList()
: uow.Reminders
.RemindersFor(ctx.User.Id, index / 10)
.ToList();
var pageIndex = index % 10;
if (rems.Count > pageIndex)
{

View File

@@ -2,17 +2,23 @@
using NadekoBot.Services;
using NadekoBot.Services.Database.Models;
using NadekoBot.Modules.Utility.Common.Patreon;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using Discord;
using NadekoBot.Modules.Gambling.Services;
using NadekoBot.Extensions;
using Serilog;
using StackExchange.Redis;
using JsonSerializer = System.Text.Json.JsonSerializer;
namespace NadekoBot.Modules.Utility.Services
{
@@ -20,98 +26,211 @@ namespace NadekoBot.Modules.Utility.Services
{
private readonly SemaphoreSlim getPledgesLocker = new SemaphoreSlim(1, 1);
private PatreonUserAndReward[] _pledges;
private readonly Timer _updater;
private readonly SemaphoreSlim claimLockJustInCase = new SemaphoreSlim(1, 1);
public TimeSpan Interval { get; } = TimeSpan.FromMinutes(3);
private readonly IBotCredentials _creds;
private readonly DbService _db;
private readonly ICurrencyService _currency;
private readonly GamblingConfigService _gamblingConfigService;
private readonly ConnectionMultiplexer _redis;
private readonly IBotCredsProvider _credsProvider;
private readonly IHttpClientFactory _httpFactory;
private readonly IEmbedBuilderService _eb;
private readonly DiscordSocketClient _client;
public DateTime LastUpdate { get; private set; } = DateTime.UtcNow;
public PatreonRewardsService(IBotCredentials creds, DbService db,
ICurrencyService currency, IHttpClientFactory factory, IEmbedBuilderService eb,
DiscordSocketClient client, GamblingConfigService gamblingConfigService)
public PatreonRewardsService(
DbService db,
ICurrencyService currency,
IHttpClientFactory factory,
IEmbedBuilderService eb,
DiscordSocketClient client,
GamblingConfigService gamblingConfigService,
ConnectionMultiplexer redis,
IBotCredsProvider credsProvider)
{
_creds = creds;
_db = db;
_currency = currency;
_gamblingConfigService = gamblingConfigService;
_redis = redis;
_credsProvider = credsProvider;
_httpFactory = factory;
_eb = eb;
_client = client;
if (client.ShardId == 0)
_updater = new Timer(async _ => await RefreshPledges().ConfigureAwait(false),
_updater = new Timer(async _ => await RefreshPledges(_credsProvider.GetCreds()).ConfigureAwait(false),
null, TimeSpan.Zero, Interval);
}
public async Task RefreshPledges()
private DateTime LastAccessTokenUpdate(IBotCredentials creds)
{
if (string.IsNullOrWhiteSpace(_creds.PatreonAccessToken)
|| string.IsNullOrWhiteSpace(_creds.PatreonAccessToken))
return;
var db = _redis.GetDatabase();
var val = db.StringGet($"{creds.RedisKey()}_patreon_update");
if (val == default)
return DateTime.MinValue;
var lastTime = DateTime.FromBinary((long)val);
return lastTime;
}
private sealed class PatreonRefreshData
{
[JsonPropertyName("access_token")]
public string AccessToken { get; set; }
[JsonPropertyName("refresh_token")]
public string RefreshToken { get; set; }
[JsonPropertyName("expires_in")]
public long ExpiresIn { get; set; }
[JsonPropertyName("scope")]
public string Scope { get; set; }
[JsonPropertyName("token_type")]
public string TokenType { get; set; }
}
private async Task<bool> UpdateAccessToken(IBotCredentials creds)
{
Log.Information("Updating patreon access token...");
try
{
using var http = _httpFactory.CreateClient();
var res = await http.PostAsync($"https://www.patreon.com/api/oauth2/token" +
$"?grant_type=refresh_token" +
$"&refresh_token={creds.Patreon.RefreshToken}" +
$"&client_id={creds.Patreon.ClientId}" +
$"&client_secret={creds.Patreon.ClientSecret}",
new StringContent(string.Empty));
res.EnsureSuccessStatusCode();
var data = await res.Content.ReadFromJsonAsync<PatreonRefreshData>();
if (data is null)
throw new("Invalid patreon response.");
_credsProvider.ModifyCredsFile(oldData =>
{
oldData.Patreon.AccessToken = data.AccessToken;
oldData.Patreon.RefreshToken = data.RefreshToken;
});
var db = _redis.GetDatabase();
await db.StringSetAsync($"{creds.RedisKey()}_patreon_update", DateTime.UtcNow.ToBinary());
return true;
}
catch (Exception ex)
{
Log.Error("Failed updating patreon access token: {ErrorMessage}", ex.ToString());
return false;
}
}
private bool HasPatreonCreds(IBotCredentials creds)
{
var _1 = creds.Patreon.ClientId;
var _2 = creds.Patreon.ClientSecret;
var _4 = creds.Patreon.RefreshToken;
return !(string.IsNullOrWhiteSpace(_1)
|| string.IsNullOrWhiteSpace(_2)
|| string.IsNullOrWhiteSpace(_4));
}
public async Task RefreshPledges(IBotCredentials creds)
{
if (DateTime.UtcNow.Day < 5)
return;
if (string.IsNullOrWhiteSpace(creds.Patreon.CampaignId))
return;
var lastUpdate = LastAccessTokenUpdate(creds);
var now = DateTime.UtcNow;
if (lastUpdate.Year != now.Year
|| lastUpdate.Month != now.Month
|| string.IsNullOrWhiteSpace(creds.Patreon.AccessToken))
{
// if the user has the necessary patreon creds
// and the access token expired or doesn't exist
// -> update access token
if (!HasPatreonCreds(creds))
return;
var success = await UpdateAccessToken(creds);
if (!success)
return;
}
LastUpdate = DateTime.UtcNow;
await getPledgesLocker.WaitAsync().ConfigureAwait(false);
try
{
var rewards = new List<PatreonPledge>();
var members = new List<PatreonMember>();
var users = new List<PatreonUser>();
using (var http = _httpFactory.CreateClient())
{
http.DefaultRequestHeaders.Clear();
http.DefaultRequestHeaders.Add("Authorization", "Bearer " + _creds.PatreonAccessToken);
var data = new PatreonData()
{
Links = new PatreonDataLinks()
{
next = $"https://api.patreon.com/oauth2/api/campaigns/{_creds.PatreonCampaignId}/pledges"
}
};
http.DefaultRequestHeaders.TryAddWithoutValidation("Authorization",
$"Bearer {creds.Patreon.AccessToken}");
var page = $"https://www.patreon.com/api/oauth2/v2/campaigns/{creds.Patreon.CampaignId}/members" +
"?fields%5Bmember%5D=full_name,currently_entitled_amount_cents" +
"&fields%5Buser%5D=social_connections" +
"&include=user";
PatreonResponse data = null;
do
{
var res = await http.GetStringAsync(data.Links.next)
.ConfigureAwait(false);
data = JsonConvert.DeserializeObject<PatreonData>(res);
var pledgers = data.Data.Where(x => x["type"].ToString() == "pledge");
rewards.AddRange(pledgers.Select(x => JsonConvert.DeserializeObject<PatreonPledge>(x.ToString()))
.Where(x => x.attributes.declined_since is null));
if (data.Included != null)
{
users.AddRange(data.Included
.Where(x => x["type"].ToString() == "user")
.Select(x => JsonConvert.DeserializeObject<PatreonUser>(x.ToString())));
}
} while (!string.IsNullOrWhiteSpace(data.Links.next));
var res = await http.GetStringAsync(page).ConfigureAwait(false);
data = JsonSerializer.Deserialize<PatreonResponse>(res);
if (data is null)
break;
members.AddRange(data.Data);
users.AddRange(data.Included);
} while (!string.IsNullOrWhiteSpace(page = data?.Links?.Next));
}
var toSet = rewards.Join(users, (r) => r.relationships?.patron?.data?.id, (u) => u.id, (x, y) => new PatreonUserAndReward()
{
User = y,
Reward = x,
}).ToArray();
_pledges = toSet;
foreach (var pledge in _pledges)
{
var userIdStr = pledge.User.attributes?.social_connections?.discord?.user_id;
if (userIdStr != null && ulong.TryParse(userIdStr, out var userId))
var userData = members.Join(users,
(m) => m.Relationships.User.Data.Id,
(u) => u.Id,
(m, u) => new
{
await ClaimReward(userId);
}
PatreonUserId = m.Relationships.User.Data.Id,
UserId = ulong.TryParse(u.Attributes?.SocialConnections?.Discord?.UserId ?? string.Empty,
out var userId)
? userId
: 0,
EntitledTo = m.Attributes.CurrentlyEntitledAmountCents,
})
.Where(x => x is
{
UserId: not 0,
EntitledTo: > 0
})
.ToList();
foreach (var pledge in userData)
{
await ClaimReward(pledge.UserId, pledge.PatreonUserId, pledge.EntitledTo);
}
}
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.Unauthorized)
{
Log.Warning("Patreon credentials invalid or expired. I will try to refresh them during the next run");
var db = _redis.GetDatabase();
await db.KeyDeleteAsync($"{creds.RedisKey()}_patreon_update");
}
catch (Exception ex)
{
Log.Warning(ex, "Error refreshing patreon pledges");
@@ -120,83 +239,75 @@ namespace NadekoBot.Modules.Utility.Services
{
getPledgesLocker.Release();
}
}
public async Task<int> ClaimReward(ulong userId)
public async Task<int> ClaimReward(ulong userId, string patreonUserId, int cents)
{
await claimLockJustInCase.WaitAsync().ConfigureAwait(false);
var settings = _gamblingConfigService.Data;
var now = DateTime.UtcNow;
try
{
var datas = _pledges?.Where(x => x.User.attributes?.social_connections?.discord?.user_id == userId.ToString())
?? Enumerable.Empty<PatreonUserAndReward>();
var eligibleFor = (int)(cents * settings.PatreonCurrencyPerCent);
var totalAmount = 0;
foreach (var data in datas)
using (var uow = _db.GetDbContext())
{
var amount = (int)(data.Reward.attributes.amount_cents * settings.PatreonCurrencyPerCent);
var users = uow.Set<RewardedUser>();
var usr = await users.FirstOrDefaultAsync(x => x.PatreonUserId == patreonUserId);
using (var uow = _db.GetDbContext())
if (usr is null)
{
var users = uow.Set<RewardedUser>();
var usr = users.FirstOrDefault(x => x.PatreonUserId == data.User.id);
if (usr is null)
users.Add(new RewardedUser()
{
users.Add(new RewardedUser()
{
PatreonUserId = data.User.id,
LastReward = now,
AmountRewardedThisMonth = amount,
});
PatreonUserId = patreonUserId,
LastReward = now,
AmountRewardedThisMonth = eligibleFor,
});
await uow.SaveChangesAsync();
await uow.SaveChangesAsync();
await _currency.AddAsync(userId, "Patreon reward - new", amount, gamble: true);
totalAmount += amount;
Log.Information($"Sending new currency reward to {userId}");
await SendMessageToUser(userId, $"Thank you for your pledge! " +
$"You've been awarded **{amount}**{settings.Currency.Sign} !");
continue;
}
if (usr.LastReward.Month != now.Month)
{
usr.LastReward = now;
usr.AmountRewardedThisMonth = amount;
await uow.SaveChangesAsync();
await _currency.AddAsync(userId, "Patreon reward - recurring", amount, gamble: true);
totalAmount += amount;
Log.Information($"Sending recurring currency reward to {userId}");
await SendMessageToUser(userId, $"Thank you for your continued support! " +
$"You've been awarded **{amount}**{settings.Currency.Sign} for this month's support!");
continue;
}
if (usr.AmountRewardedThisMonth < amount)
{
var toAward = amount - usr.AmountRewardedThisMonth;
usr.LastReward = now;
usr.AmountRewardedThisMonth = amount;
await uow.SaveChangesAsync();
await _currency.AddAsync(userId, "Patreon reward - update", toAward, gamble: true);
totalAmount += toAward;
Log.Information($"Sending updated currency reward to {userId}");
await SendMessageToUser(userId, $"Thank you for increasing your pledge! " +
$"You've been awarded an additional **{toAward}**{settings.Currency.Sign} !");
continue;
}
await _currency.AddAsync(userId, "Patreon reward - new", eligibleFor, gamble: true);
Log.Information($"Sending new currency reward to {userId}");
await SendMessageToUser(userId, $"Thank you for your pledge! " +
$"You've been awarded **{eligibleFor}**{settings.Currency.Sign} !");
return eligibleFor;
}
}
return totalAmount;
if (usr.LastReward.Month != now.Month)
{
usr.LastReward = now;
usr.AmountRewardedThisMonth = eligibleFor;
await uow.SaveChangesAsync();
await _currency.AddAsync(userId, "Patreon reward - recurring", eligibleFor, gamble: true);
Log.Information($"Sending recurring currency reward to {userId}");
await SendMessageToUser(userId, $"Thank you for your continued support! " +
$"You've been awarded **{eligibleFor}**{settings.Currency.Sign} for this month's support!");
return eligibleFor;
}
if (usr.AmountRewardedThisMonth < eligibleFor)
{
var toAward = eligibleFor - usr.AmountRewardedThisMonth;
usr.LastReward = now;
usr.AmountRewardedThisMonth = eligibleFor;
await uow.SaveChangesAsync();
await _currency.AddAsync(userId, "Patreon reward - update", toAward, gamble: true);
Log.Information($"Sending updated currency reward to {userId}");
await SendMessageToUser(userId, $"Thank you for increasing your pledge! " +
$"You've been awarded an additional **{toAward}**{settings.Currency.Sign} !");
return toAward;
}
return 0;
}
}
finally
{

View File

@@ -227,6 +227,9 @@ namespace NadekoBot.Modules.Utility.Services
private async Task RescanUser(IGuildUser user, StreamRoleSettings setting, IRole addRole = null)
{
if (user.IsBot)
return;
var g = (StreamingGame)user.Activities
.FirstOrDefault(a => a is StreamingGame &&
(string.IsNullOrWhiteSpace(setting.Keyword)
@@ -240,7 +243,7 @@ namespace NadekoBot.Modules.Utility.Services
{
try
{
addRole = addRole ?? user.Guild.GetRole(setting.AddRoleId);
addRole ??= user.Guild.GetRole(setting.AddRoleId);
if (addRole is null)
{
await StopStreamRole(user.Guild).ConfigureAwait(false);
@@ -249,9 +252,12 @@ namespace NadekoBot.Modules.Utility.Services
}
//check if he doesn't have addrole already, to avoid errors
if (!user.RoleIds.Contains(setting.AddRoleId))
if (!user.RoleIds.Contains(addRole.Id))
{
await user.AddRoleAsync(addRole).ConfigureAwait(false);
Log.Information("Added stream role to user {0} in {1} server", user.ToString(), user.Guild.ToString());
Log.Information("Added stream role to user {0} in {1} server", user.ToString(),
user.Guild.ToString());
}
}
catch (HttpException ex) when (ex.HttpCode == System.Net.HttpStatusCode.Forbidden)
{

View File

@@ -9,12 +9,16 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using NadekoBot.Common.Replacements;
using NadekoBot.Services;
using Serilog;
using SystemTextJsonSamples;
using JsonSerializer = Newtonsoft.Json.JsonSerializer;
namespace NadekoBot.Modules.Utility
{
@@ -25,15 +29,18 @@ namespace NadekoBot.Modules.Utility
private readonly IStatsService _stats;
private readonly IBotCredentials _creds;
private readonly DownloadTracker _tracker;
private readonly IHttpClientFactory _httpFactory;
public Utility(DiscordSocketClient client, ICoordinator coord,
IStatsService stats, IBotCredentials creds, DownloadTracker tracker)
IStatsService stats, IBotCredentials creds, DownloadTracker tracker,
IHttpClientFactory httpFactory)
{
_client = client;
_coord = coord;
_stats = stats;
_creds = creds;
_tracker = tracker;
_httpFactory = httpFactory;
}
[NadekoCommand, Aliases]
@@ -101,7 +108,9 @@ namespace NadekoBot.Modules.Utility
await ctx.Channel.TriggerTypingAsync().ConfigureAwait(false);
await _tracker.EnsureUsersDownloadedAsync(ctx.Guild).ConfigureAwait(false);
var users = await ctx.Guild.GetUsersAsync();
var users = await ctx.Guild.GetUsersAsync(
CacheMode.CacheOnly
);
var roleUsers = users
.Where(u => role is null ? u.RoleIds.Count == 1 : u.RoleIds.Contains(role.Id))
.Select(u => $"`{u.Id, 18}` {u}")
@@ -115,9 +124,9 @@ namespace NadekoBot.Modules.Utility
if (pageUsers.Count == 0)
return _eb.Create().WithOkColor().WithDescription(GetText(strs.no_user_on_this_page));
return _eb.Create().WithOkColor()
.WithTitle(GetText(strs.inrole_list(Format.Bold(role?.Name ?? "No Role") + $" - {roleUsers.Length}")))
.WithTitle(GetText(strs.inrole_list(Format.Bold(role?.Name ?? "No Role"), roleUsers.Length)))
.WithDescription(string.Join("\n", pageUsers));
}, roleUsers.Length, 20).ConfigureAwait(false);
}
@@ -278,6 +287,60 @@ namespace NadekoBot.Modules.Utility
await ctx.Channel.SendMessageAsync(result.TrimTo(2000)).ConfigureAwait(false);
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[BotPerm(GuildPerm.ManageEmojis)]
[UserPerm(GuildPerm.ManageEmojis)]
[Priority(2)]
public Task EmojiAdd(string name, Emote emote)
=> EmojiAdd(name, emote.Url);
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[BotPerm(GuildPerm.ManageEmojis)]
[UserPerm(GuildPerm.ManageEmojis)]
[Priority(1)]
public Task EmojiAdd(Emote emote)
=> EmojiAdd(emote.Name, emote.Url);
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[BotPerm(GuildPerm.ManageEmojis)]
[UserPerm(GuildPerm.ManageEmojis)]
[Priority(0)]
public async Task EmojiAdd(string name, string url = null)
{
name = name.Trim(':');
url ??= ctx.Message.Attachments.FirstOrDefault()?.Url;
if (url is null)
return;
using var http = _httpFactory.CreateClient();
var res = await http.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);
if (!res.IsImage() || res.GetImageSize() is null or > 262_144)
{
await ReplyErrorLocalizedAsync(strs.invalid_emoji_link);
return;
}
await using var imgStream = await res.Content.ReadAsStreamAsync();
Emote em;
try
{
em = await ctx.Guild.CreateEmoteAsync(name, new(imgStream));
}
catch (Exception ex)
{
Log.Warning(ex, "Error adding emoji on server {GuildId}", ctx.Guild.Id);
await ReplyErrorLocalizedAsync(strs.emoji_add_error);
return;
}
await ConfirmLocalizedAsync(strs.emoji_added(em.ToString()));
}
[NadekoCommand, Aliases]
[OwnerOnly]
public async Task ListServers(int page = 1)
@@ -304,7 +367,48 @@ namespace NadekoBot.Modules.Utility
await ctx.Channel.EmbedAsync(embed);
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
public Task ShowEmbed(ulong messageId)
=> ShowEmbed((ITextChannel)ctx.Channel, messageId);
private static readonly JsonSerializerOptions _showEmbedSerializerOptions = new JsonSerializerOptions()
{
WriteIndented = true,
IgnoreNullValues = true,
PropertyNamingPolicy = LowerCaseNamingPolicy.Default
};
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
public async Task ShowEmbed(ITextChannel ch, ulong messageId)
{
var user = (IGuildUser)ctx.User;
var perms = user.GetPermissions(ch);
if (!perms.ReadMessageHistory || !perms.ViewChannel)
{
await ReplyErrorLocalizedAsync(strs.insuf_perms_u);
return;
}
var msg = await ch.GetMessageAsync(messageId);
if (msg is null)
{
await ReplyErrorLocalizedAsync(strs.msg_not_found);
return;
}
var embed = msg.Embeds.FirstOrDefault();
if (embed is null)
{
await ReplyErrorLocalizedAsync(strs.not_found);
return;
}
var json = SmartEmbedText.FromEmbed(embed, msg.Content).ToJson(_showEmbedSerializerOptions);
await SendConfirmAsync(Format.Sanitize(json).Replace("](", "]\\("));
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
@@ -375,6 +479,8 @@ namespace NadekoBot.Modules.Utility
New
}
// [NadekoCommand, Usage, Description, Aliases]
// [RequireContext(ContextType.Guild)]
// public async Task CreateMyInvite(CreateInviteType type = CreateInviteType.Any)

View File

@@ -746,27 +746,6 @@ namespace NadekoBot.Modules.Xp.Services
guildRank);
}
public static (int Level, int LevelXp, int LevelRequiredXp) GetLevelData(UserXpStats stats)
{
var baseXp = XpService.XP_REQUIRED_LVL_1;
var required = baseXp;
var totalXp = 0;
var lvl = 1;
while (true)
{
required = (int) (baseXp + baseXp / 4.0 * (lvl - 1));
if (required + totalXp > stats.Xp)
break;
totalXp += required;
lvl++;
}
return (lvl - 1, stats.Xp - totalXp, required);
}
public bool ToggleExcludeServer(ulong id)
{
using (var uow = _db.GetDbContext())

View File

@@ -1,4 +1,4 @@
using Discord;
using Discord;
using Discord.Commands;
using Discord.WebSocket;
using NadekoBot.Common.Attributes;
@@ -39,6 +39,8 @@ namespace NadekoBot.Modules.Xp
}
[NadekoCommand, Aliases]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
public async Task XpRewsReset()
{
var reply = await PromptUserConfirmAsync(_eb.Create()
@@ -276,7 +278,7 @@ namespace NadekoBot.Modules.Xp
}
else
{
await ReplyConfirmLocalizedAsync(strs.excluded(Format.Bold(channel.ToString())));
await ReplyConfirmLocalizedAsync(strs.not_excluded(Format.Bold(channel.ToString())));
}
}

View File

@@ -9,29 +9,29 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AngleSharp" Version="0.16.0" />
<PackageReference Include="AWSSDK.S3" Version="3.7.1.19" />
<PackageReference Include="AngleSharp" Version="0.16.1" />
<PackageReference Include="AWSSDK.S3" Version="3.7.7.3" />
<PackageReference Include="Cloneable" Version="1.3.0" />
<PackageReference Include="CodeHollow.FeedReader" Version="1.2.1" />
<PackageReference Include="CodeHollow.FeedReader" Version="1.2.2" />
<PackageReference Include="CommandLineParser" Version="2.8.0" />
<PackageReference Include="Discord.Net" Version="2.4.0" />
<PackageReference Include="CoreCLR-NCalc" Version="2.2.92" />
<PackageReference Include="Google.Apis.Urlshortener.v1" Version="1.41.1.138" />
<PackageReference Include="Google.Apis.YouTube.v3" Version="1.53.0.2378" />
<PackageReference Include="Google.Apis.YouTube.v3" Version="1.55.0.2449" />
<PackageReference Include="Google.Apis.Customsearch.v1" Version="1.49.0.2084" />
<PackageReference Include="Google.Protobuf" Version="3.17.3" />
<PackageReference Include="Grpc.Net.ClientFactory" Version="2.38.0" />
<PackageReference Include="Grpc.Tools" Version="2.38.1">
<PackageReference Include="Google.Protobuf" Version="3.19.1" />
<PackageReference Include="Grpc.Net.ClientFactory" Version="2.41.0" />
<PackageReference Include="Grpc.Tools" Version="2.42.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Html2Markdown" Version="4.0.0.427" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="5.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.8">
<PackageReference Include="Html2Markdown" Version="5.0.0.468" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="5.0.13" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.13">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.13" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="5.0.0" />
@@ -44,15 +44,13 @@
<PackageReference Include="Microsoft.SyndicationFeed.ReaderWriter" Version="1.0.2" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="Scrutor" Version="3.3.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="4.0.0" />
<PackageReference Include="Serilog.Sinks.Seq" Version="5.0.1" />
<PackageReference Include="SixLabors.ImageSharp" Version="1.0.3" />
<PackageReference Include="Serilog.Sinks.Console" Version="4.0.1" />
<PackageReference Include="Serilog.Sinks.Seq" Version="5.1.0" />
<PackageReference Include="SixLabors.ImageSharp" Version="1.0.4" />
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="1.0.0-beta0010" />
<PackageReference Include="StackExchange.Redis" Version="2.2.62" />
<PackageReference Include="VideoLibrary" Version="3.1.2" />
<PackageReference Include="StackExchange.Redis" Version="2.2.88" />
<PackageReference Include="YamlDotNet" Version="11.2.1" />
<PackageReference Include="YoutubeExplode" Version="6.0.5" />
<PackageReference Include="linq2db.EntityFrameworkCore" Version="5.4.0" />
<PackageReference Include="linq2db.EntityFrameworkCore" Version="5.9.0" />
</ItemGroup>
<ItemGroup>

View File

@@ -8,14 +8,16 @@ using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Channels;
using System.Threading.Tasks;
using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Db;
using NadekoBot.Modules.Administration;
using Serilog;
namespace NadekoBot.Services
{
public class GreetSettingsService : INService
public class GreetSettingsService : INService, IReadyExecutor
{
private readonly DbService _db;
@@ -51,6 +53,17 @@ namespace NadekoBot.Services
_client.GuildMemberUpdated += ClientOnGuildMemberUpdated;
}
public async Task OnReadyAsync()
{
while (true)
{
var (conf, user, compl) = await _greetDmQueue.Reader.ReadAsync();
var res = await GreetDmUserInternal(conf, user);
compl.TrySetResult(res);
await Task.Delay(2000);
}
}
private Task ClientOnGuildMemberUpdated(SocketGuildUser oldUser, SocketGuildUser newUser)
{
@@ -240,17 +253,48 @@ namespace NadekoBot.Services
}
}
private async Task<bool> GreetDmUser(GreetSettings conf, IDMChannel channel, IGuildUser user)
private readonly Channel<(GreetSettings, IGuildUser, TaskCompletionSource<bool>)> _greetDmQueue =
Channel.CreateBounded<(GreetSettings, IGuildUser, TaskCompletionSource<bool>)>(new BoundedChannelOptions(60)
{
// The limit of 60 users should be only hit when there's a raid. In that case
// probably the best thing to do is to drop newest (raiding) users
FullMode = BoundedChannelFullMode.DropNewest
});
private async Task<bool> GreetDmUser(GreetSettings conf, IGuildUser user)
{
var completionSource = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
await _greetDmQueue.Writer.WriteAsync((conf, user, completionSource));
return await completionSource.Task;
}
private async Task<bool> GreetDmUserInternal(GreetSettings conf, IGuildUser user)
{
var rep = new ReplacementBuilder()
.WithDefault(user, channel, (SocketGuild)user.Guild, _client)
.Build();
var text = SmartText.CreateFrom(conf.DmGreetMessageText);
rep.Replace(text);
try
{
await channel.SendAsync(text).ConfigureAwait(false);
var rep = new ReplacementBuilder()
.WithUser(user)
.WithServer(_client, (SocketGuild)user.Guild)
.Build();
var text = SmartText.CreateFrom(conf.DmGreetMessageText);
text = rep.Replace(text);
if (text is SmartPlainText pt)
{
text = new SmartEmbedText() { PlainText = pt.Text };
}
((SmartEmbedText)text).Footer = new()
{
Text = $"This message was sent from {user.Guild} server.", IconUrl = user.Guild.IconUrl
};
var ch = await user.GetOrCreateDMChannelAsync();
if (ch is null)
return false;
await ch.SendAsync(text);
}
catch
{
@@ -301,12 +345,7 @@ namespace NadekoBot.Services
if (conf.SendDmGreetMessage)
{
var channel = await user.GetOrCreateDMChannelAsync().ConfigureAwait(false);
if (channel != null)
{
await GreetDmUser(conf, channel, user);
}
await GreetDmUser(conf, user);
}
}
catch
@@ -477,10 +516,10 @@ namespace NadekoBot.Services
return GreetUsers(conf, channel, user);
}
public Task<bool> GreetDmTest(IDMChannel channel, IGuildUser user)
public Task<bool> GreetDmTest(IGuildUser user)
{
var conf = GetOrAddSettingsForGuild(user.GuildId);
return GreetDmUser(conf, channel, user);
return GreetDmUser(conf, user);
}
#endregion

View File

@@ -1,15 +1,17 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace NadekoBot.Services
{
public interface ICoordinator
{
bool RestartBot();
void Die();
void Die(bool graceful);
bool RestartShard(int shardId);
IList<ShardStatus> GetAllShardStatuses();
int GetGuildCount();
Task Reload();
}
public class ShardStatus

View File

@@ -7,7 +7,7 @@ namespace NadekoBot.Services
{
public interface IGoogleApiService : INService
{
IEnumerable<string> Languages { get; }
IReadOnlyDictionary<string, string> Languages { get; }
Task<IEnumerable<string>> GetVideoLinksByKeywordAsync(string keywords, int count = 1);
Task<IEnumerable<(string Name, string Id, string Url)>> GetVideoInfosByKeywordAsync(string keywords, int count = 1);

View File

@@ -14,7 +14,6 @@ namespace NadekoBot.Services
IReadOnlyList<byte[]> Dice { get; }
IReadOnlyList<byte[]> SlotEmojis { get; }
IReadOnlyList<byte[]> SlotNumbers { get; }
IReadOnlyList<byte[]> Currency { get; }
byte[] SlotBackground { get; }

View File

@@ -10,7 +10,14 @@ using Serilog;
namespace NadekoBot.Services
{
public sealed class BotCredsProvider
public interface IBotCredsProvider
{
public void Reload();
public IBotCredentials GetCreds();
public void ModifyCredsFile(Action<Creds> func);
}
public sealed class BotCredsProvider : IBotCredsProvider
{
private readonly int? _totalShards;
private const string _credsFileName = "creds.yml";
@@ -27,7 +34,7 @@ namespace NadekoBot.Services
private readonly object reloadLock = new object();
private void Reload()
public void Reload()
{
lock (reloadLock)
{
@@ -102,6 +109,19 @@ namespace NadekoBot.Services
Reload();
}
public void ModifyCredsFile(Action<Creds> func)
{
var ymlData = File.ReadAllText(_credsFileName);
var creds = Yaml.Deserializer.Deserialize<Creds>(ymlData);
func(creds);
ymlData = Yaml.Serializer.Serialize(creds);
File.WriteAllText(_credsFileName, ymlData);
Reload();
}
/// <summary>
/// Checks if there's a V2 credentials file present, loads it if it exists,
/// converts it to new model, and saves it to YAML. Also backs up old credentials to credentials.json.bak
@@ -157,6 +177,6 @@ namespace NadekoBot.Services
}
public Creds GetCreds() => _creds;
public IBotCredentials GetCreds() => _creds;
}
}

View File

@@ -1,4 +1,4 @@
using Google;
using Google;
using Google.Apis.Customsearch.v1;
using Google.Apis.Services;
using Google.Apis.Urlshortener.v1;
@@ -220,14 +220,14 @@ namespace NadekoBot.Services
req.Fields = "items(image(contextLink,thumbnailLink),link)";
req.SearchType = CseResource.ListRequest.SearchTypeEnum.Image;
req.Start = new NadekoRandom().Next(0, 20);
req.Safe = CseResource.ListRequest.SafeEnum.Active;
var search = await req.ExecuteAsync().ConfigureAwait(false);
return new ImageResult(search.Items[0].Image, search.Items[0].Link);
}
public IEnumerable<string> Languages => _languageDictionary.Keys.OrderBy(x => x);
private readonly Dictionary<string, string> _languageDictionary = new Dictionary<string, string>() {
public IReadOnlyDictionary<string, string> Languages { get; } = new Dictionary<string, string>() {
{ "afrikaans", "af"},
{ "albanian", "sq"},
{ "arabic", "ar"},
@@ -365,8 +365,8 @@ namespace NadekoBot.Services
await Task.Yield();
string text;
if (!_languageDictionary.ContainsKey(sourceLanguage) ||
!_languageDictionary.ContainsKey(targetLanguage))
if (!Languages.ContainsKey(sourceLanguage) ||
!Languages.ContainsKey(targetLanguage))
throw new ArgumentException(nameof(sourceLanguage) + "/" + nameof(targetLanguage));
@@ -385,8 +385,8 @@ namespace NadekoBot.Services
private string ConvertToLanguageCode(string language)
{
_languageDictionary.TryGetValue(language, out var mode);
Languages.TryGetValue(language, out var mode);
return mode;
}
}
}
}

View File

@@ -36,7 +36,6 @@ namespace NadekoBot.Services
Dice,
SlotBg,
SlotEmojis,
SlotNumbers,
Currency,
RategirlMatrix,
RategirlDot,
@@ -57,9 +56,6 @@ namespace NadekoBot.Services
public IReadOnlyList<byte[]> SlotEmojis
=> GetByteArrayData(ImageKeys.SlotEmojis);
public IReadOnlyList<byte[]> SlotNumbers
=> GetByteArrayData(ImageKeys.SlotNumbers);
public IReadOnlyList<byte[]> Currency
=> GetByteArrayData(ImageKeys.Currency);
@@ -157,20 +153,7 @@ namespace NadekoBot.Services
"https://cdn.nadeko.bot/slots/3.png",
"https://cdn.nadeko.bot/slots/4.png",
"https://cdn.nadeko.bot/slots/5.png"
}.Map(x => new Uri(x)),
Numbers = new[]
{
"https://cdn.nadeko.bot/other/slots/numbers/0.png",
"https://cdn.nadeko.bot/other/slots/numbers/1.png",
"https://cdn.nadeko.bot/other/slots/numbers/2.png",
"https://cdn.nadeko.bot/other/slots/numbers/3.png",
"https://cdn.nadeko.bot/other/slots/numbers/4.png",
"https://cdn.nadeko.bot/other/slots/numbers/5.png",
"https://cdn.nadeko.bot/other/slots/numbers/6.png",
"https://cdn.nadeko.bot/other/slots/numbers/7.png",
"https://cdn.nadeko.bot/other/slots/numbers/8.png",
"https://cdn.nadeko.bot/other/slots/numbers/9.png"
}.Map(x => new Uri(x)),
}.Map(x => new Uri(x))
},
Xp = new ImageUrls.XpData()
{
@@ -183,6 +166,14 @@ namespace NadekoBot.Services
File.WriteAllText(_imagesPath, Yaml.Serializer.Serialize(newData));
}
}
// removed numbers from slots
var localImageUrls = Yaml.Deserializer.Deserialize<ImageUrls>(File.ReadAllText(_imagesPath));
if (localImageUrls.Version == 2)
{
localImageUrls.Version = 3;
File.WriteAllText(_imagesPath, Yaml.Serializer.Serialize(localImageUrls));
}
}
public async Task Reload()
@@ -207,9 +198,6 @@ namespace NadekoBot.Services
case ImageKeys.SlotEmojis:
await Load(key, ImageUrls.Slots.Emojis);
break;
case ImageKeys.SlotNumbers:
await Load(key, ImageUrls.Slots.Numbers);
break;
case ImageKeys.Currency:
await Load(key, ImageUrls.Currency);
break;

View File

@@ -39,11 +39,11 @@ namespace NadekoBot.Services
return true;
}
public void Die()
public void Die(bool graceful)
{
_coordClient.Die(new DieRequest()
{
Graceful = false
Graceful = graceful
});
}
@@ -79,6 +79,11 @@ namespace NadekoBot.Services
return res.Statuses.Sum(x => x.GuildCount);
}
public async Task Reload()
{
await _coordClient.ReloadAsync(new());
}
public Task OnReadyAsync()
{
Task.Run(async () =>

View File

@@ -36,7 +36,7 @@ namespace NadekoBot.Services
return true;
}
public void Die()
public void Die(bool graceful = false)
{
Environment.Exit(5);
}
@@ -64,5 +64,10 @@ namespace NadekoBot.Services
{
return _client.Guilds.Count;
}
public Task Reload()
{
return Task.CompletedTask;
}
}
}

View File

@@ -1,4 +1,4 @@
using Discord;
using Discord;
using Discord.WebSocket;
using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Extensions;
@@ -20,7 +20,7 @@ namespace NadekoBot.Services
private readonly IBotCredentials _creds;
private readonly DateTime _started;
public const string BotVersion = "3.0.8";
public const string BotVersion = "3.0.13";
public string Author => "Kwoth#2452";
public string Library => "Discord.Net";
public double MessagesPerSecond => MessageCounter / GetUptime().TotalSeconds;

View File

@@ -22,10 +22,12 @@ using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using NadekoBot.Common.Attributes;
using Color = Discord.Color;
using JsonSerializer = System.Text.Json.JsonSerializer;
namespace NadekoBot.Extensions
{
@@ -286,8 +288,8 @@ namespace NadekoBot.Extensions
public static async Task<IEnumerable<IGuildUser>> GetMembersAsync(this IRole role) =>
(await role.Guild.GetUsersAsync(CacheMode.CacheOnly).ConfigureAwait(false)).Where(u => u.RoleIds.Contains(role.Id)) ?? Enumerable.Empty<IGuildUser>();
public static string ToJson<T>(this T any, Formatting formatting = Formatting.Indented) =>
JsonConvert.SerializeObject(any, formatting);
public static string ToJson<T>(this T any, JsonSerializerOptions options = null) =>
JsonSerializer.Serialize(any, options);
/// <summary>
/// Adds fallback fonts to <see cref="TextOptions"/>
@@ -357,7 +359,7 @@ namespace NadekoBot.Extensions
public static bool IsImage(this HttpResponseMessage msg, out string mimeType)
{
mimeType = msg.Content.Headers.ContentType.MediaType;
mimeType = msg.Content.Headers.ContentType?.MediaType;
if (mimeType == "image/png"
|| mimeType == "image/jpeg"
|| mimeType == "image/gif")

View File

@@ -31,11 +31,9 @@ votes:
# Patreon auto reward system settings.
# go to https://www.patreon.com/portal -> my clients -> create client
patreon:
# Access token. You have to manually update this 1st of each month by refreshing the token on https://patreon.com/portal
clientId:
accessToken: ''
# Unused atm
refreshToken: ''
# Unused atm
clientSecret: ''
# 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)
campaignId: ''
@@ -76,8 +74,8 @@ osuApiKey:
# cmd: dotnet
# args: "NadekoBot.dll -- {0}"
# Windows default
# cmd: NadekoBot.exe
# args: {0}
# cmd: "NadekoBot.exe"
# args: "{0}""
restartCommand:
cmd:
args:

View File

@@ -720,6 +720,9 @@ removeperm:
showemojis:
- showemojis
- se
emojiadd:
- emojiadd
- ea
deckshuffle:
- deckshuffle
- dsh
@@ -1260,3 +1263,13 @@ imageonlychannel:
- imageonlychannel
- imageonly
- imagesonly
coordreload:
- coordreload
quotesexport:
- quotesexport
- qexport
quotesimport:
- quotesimport
- qimport
showembed:
- showembed

View File

@@ -1,5 +1,5 @@
# DO NOT CHANGE
version: 2
version: 4
# Currency settings
currency:
# What is the emoji/character which represents the currency
@@ -240,3 +240,7 @@ patreonCurrencyPerCent: 1
# Currency reward per vote.
# This will work only if you've set up VotesApi and correct credentials for topgg and/or discords voting
voteReward: 100
# Slot config
slots:
# Hex value of the color which the numbers on the slot image will have.
currencyFontColor: ff0000

View File

@@ -1,5 +1,5 @@
# DO NOT CHANGE
version: 2
version: 3
coins:
heads:
- https://cdn.nadeko.bot/coins/heads3.png
@@ -36,15 +36,4 @@ slots:
- https://cdn.nadeko.bot/slots/3.png
- https://cdn.nadeko.bot/slots/4.png
- https://cdn.nadeko.bot/slots/5.png
numbers:
- https://cdn.nadeko.bot/other/slots/numbers/0.png
- https://cdn.nadeko.bot/other/slots/numbers/1.png
- https://cdn.nadeko.bot/other/slots/numbers/2.png
- https://cdn.nadeko.bot/other/slots/numbers/3.png
- https://cdn.nadeko.bot/other/slots/numbers/4.png
- https://cdn.nadeko.bot/other/slots/numbers/5.png
- https://cdn.nadeko.bot/other/slots/numbers/6.png
- https://cdn.nadeko.bot/other/slots/numbers/7.png
- https://cdn.nadeko.bot/other/slots/numbers/8.png
- https://cdn.nadeko.bot/other/slots/numbers/9.png
bg: https://cdn.nadeko.bot/slots/slots_bg.png

View File

@@ -406,13 +406,15 @@ remind:
- "me 1d5h Do something"
- "#general 1m Start now!"
reminddelete:
desc: "Deletes a reminder on the specified index."
desc: "Deletes a reminder on the specified index. You can specify 'server' option if you're an Administrator, and you want to delete a reminder on this server created by someone else. "
args:
- "3"
- "server 2"
remindlist:
desc: "Lists all reminders you created. Paginated."
desc: "Lists all reminders you created. You can specify 'server' option if you're an Administrator to list all reminders created on this server. Paginated."
args:
- "1"
- "server 2"
serverinfo:
desc: "Shows info about the server the bot is on. If no server is supplied, it defaults to current one."
args:
@@ -609,7 +611,7 @@ quoteid:
args:
- "123456"
quotedelete:
desc: "Deletes a quote with the specified ID. You have to be either server Administrator or the creator of the quote to delete it."
desc: "Deletes a quote with the specified ID. You have to either have the Manage Messages permission or be the creator of the quote to delete it."
args:
- "123456"
draw:
@@ -1141,9 +1143,9 @@ butts:
args:
- ""
translate:
desc: "Translates from>to text. From the given language to the destination language."
desc: "Translates text from the given language to the destination language."
args:
- "en>fr Hello"
- "en fr Hello"
translangs:
desc: "Lists the valid languages for translation."
args:
@@ -1195,6 +1197,17 @@ showemojis:
desc: "Shows a name and a link to every SPECIAL emoji in the message."
args:
- "A message full of SPECIAL emojis"
emojiadd:
desc: |-
Adds the specified emoji to this server.
You can specify a name before the emoji to add it under a different name.
You can specify a name followed by an image link to add a new emoji from an image.
You can omit imageUrl and instead upload the image as an attachment.
Image size has to be below 256KB.
args:
- ":someonesCustomEmoji:"
- "MyEmojiName :someonesCustomEmoji:"
- "owoNice https://cdn.discordapp.com/emojis/587930873811173386.png?size=128"
deckshuffle:
desc: "Reshuffles all cards back into the deck."
args:
@@ -1293,7 +1306,7 @@ poll:
autotranslang:
desc: "Sets your source and target language to be used with `{0}at`. Specify no parameters to remove previously set value."
args:
- "en>fr"
- "en fr"
autotranslate:
desc: "Starts automatic translation of all messages by users who set their `{0}atl` in this channel. You can set \"del\" parameter to automatically delete all translated user messages."
args:
@@ -1565,6 +1578,14 @@ crsexport:
desc: "Exports custom reactions from the current server (or global custom reactions in DMs) into a .yml file"
args:
- ""
quotesimport:
desc: "Upload the file or send the raw .yml data with this command to import all quotes from the specified string or file into the current server."
args:
- "<upload .yml file>"
quotesexport:
desc: "Exports quotes from the current server into a .yml file"
args:
- ""
aliaslist:
desc: "Shows the list of currently set aliases. Paginated."
args:
@@ -1585,9 +1606,12 @@ warnlogall:
- ""
- "2"
warn:
desc: "Warns a user."
desc: |-
Warns a user with an optional reason.
You can specify a warning weight integer before the user. For example, 3 would mean that this warning counts as 3 warnings.
args:
- "@Someone Very rude person"
- "3 @Someone Very rude person"
scadd:
desc: "Adds a command to the list of commands which will be executed automatically in the current channel, in the order they were added in, by the bot when it startups up."
args:
@@ -2052,7 +2076,7 @@ rollduel:
- "50 @Someone"
- "@Challenger"
reactionroles:
desc: "Specify role names and server emojis with which they're represented, the bot will then add those emojis to the previous message in the channel, and users will be able to get the roles by clicking on the emoji. You can set 'excl' as the parameter before the reactions and roles to make them exclusive. You can have up to 5 of these enabled on one server at a time. Optionally you can specify target message if you don't want it to be the previous one."
desc: "Specify role names and server emojis with which they're represented, the bot will then add those emojis to the previous message in the channel, and users will be able to get the roles by clicking on the emoji. You can set 'excl' as the parameter before the reactions and roles to make them exclusive. You can have up to 10 of these enabled on one server at a time. Optionally you can specify target message if you don't want it to be the previous one."
args:
- "Gamer :SomeServerEmoji: Streamer :Other: Watcher :Other2:"
- "excl Horde :Horde: Alliance :Alliance:"
@@ -2124,4 +2148,13 @@ imageonlychannel:
Toggles whether the channel only allows images.
Users who send more than a few non-image messages will be banned from using the channel.
args:
- ""
- ""
coordreload:
desc: "Reloads coordinator config"
args:
- ""
showembed:
desc: "Prints the json equivalent of the embed of the message specified by its Id."
args:
- "820022733172121600"
- "#some-channel 820022733172121600"

View File

@@ -593,8 +593,6 @@
"log_all": "Alle Events werden nun in diesem Channel geloggt.",
"log_disabled": "Protokollierung ausgeschaltet.",
"log_events": "Protokoll Ereignisse die du Abonnieren kannst:",
"log_ignore": "Protokollierung wird {0} ignorieren",
"log_not_ignore": "Protokollierung wird {0} nicht ignorieren",
"log_stop": "Protokollierung des {0} Ereignisses wurde gestoppt.",
"msg_not_found": "Nachricht wurde nicht gefunden.",
"time_too_long": "Die spezifizierte Zeit ist zu lang.",
@@ -953,5 +951,29 @@
"empty_page": "Diese Seite ist leer.",
"pages": "Seiten",
"favorites": "Favoriten",
"tags": "Stichworte"
"tags": "Stichworte",
"invalid_emoji_link": "",
"emoji_add_error": "",
"emoji_added": "",
"boost_on": "",
"boost_off": "",
"boostmsg_cur": "",
"boostmsg_enable": "",
"boostmsg_new": "",
"boostdel_off": "",
"boostdel_on": "",
"log_ignored_channels": "",
"log_ignored_users": "",
"log_ignore_user": "",
"log_not_ignore_user": "",
"log_ignore_chan": "",
"log_not_ignore_chan": "",
"streams_cleared": "",
"warn_weight": "",
"warn_count": "",
"mass_ban_in_progress": "",
"mass_ban_completed": "",
"reminder_server_list": "",
"imageonly_enable": "",
"imageonly_disable": ""
}

View File

@@ -9,6 +9,9 @@
"crr_reset": "Custom reaction with id {0} will no longer add reactions.",
"crr_set": "Custom reaction with id {0} will add following reactions to the response message: {1}",
"invalid_emojis": "All emojis you've specified are invalid.",
"invalid_emoji_link": "Specified link is either not an image or exceeds 256KB.",
"emoji_add_error": "Error adding emoji. You either ran out of emoji slots, or image size is inadequate.",
"emoji_added": "Added a new emoji: {0}",
"fw_cleared": "Removed all filtered words and filtered words channel settings.",
"aliases_cleared": "All {0} aliases on this server have been removed.",
"no_results": "No results found.",
@@ -447,6 +450,7 @@
"atl_set": "Your auto-translate language has been set to {0}>{1}",
"atl_started": "Started automatic translation of messages on this channel.",
"atl_stopped": "Stopped automatic translation of messages on this channel.",
"atl_not_enabled": "Automatic translation is not enabled on this channel or you've provided an invalid language.",
"bad_input_format": "Bad input format, or something went wrong.",
"card_not_found": "Couldn't find that card.",
"catfact": "fact",
@@ -552,7 +556,7 @@
"error": "Error",
"features": "Features",
"index_out_of_range": "Index out of range.",
"inrole_list": "List of users in {0} role",
"inrole_list": "List of users in {0} role ({1})",
"joined_discord": "Joined Discord",
"joined_server": "Joined server",
"listservers": "ID: {0}\nMembers: {1}\nOwner ID: {2}",
@@ -602,12 +606,13 @@
"shard": "Shard",
"showemojis": "**Name:** {0} **Link:** {1}",
"showemojis_none": "No special emojis found.",
"text_channels": "Text channels",
"channels": "Channels",
"text_channels": "Text: {0}",
"voice_channels": "Voice: {0}",
"uptime": "Uptime",
"userid": "{0} of the user {1} is {2}",
"roleid": "{0} of the role {1} is {2}",
"users": "Users",
"voice_channels": "Voice channels",
"current_poll_results": "Current poll results",
"poll_already_running": "Poll is already running on this server.",
"poll_created": "📃 {0} has created a poll",
@@ -662,6 +667,8 @@
"warning_clear_fail": "Warning not cleared. Either the warning at that index doesn't exist, or it has already been cleared.",
"warning_cleared": "Warning {0} has been cleared for {1}.",
"warnings_none": "No warning on this page.",
"warn_weight": "Weight: {0}",
"warn_count": "{0} current, {1} total",
"warnlog_for": "Warnlog for {0}",
"warnpl_none": "No punishments set.",
"warn_expire_set_delete": "Warnings will be deleted after {0} days.",
@@ -677,7 +684,7 @@
"clpa_obsolete": ":tada: **Patreon currency rewards are now automatic!** :tada:\nThis command is now obsolete.\nIf you did not receive your reward for this month's pledge, below are some of the reasons as to why that might be.",
"clpa_fail_already": "Maybe you've already received your reward for this month. You can receive rewards only once a month unless you increase your pledge.\nYou can check it by using `.curtrs` command.",
"clpa_fail_already_title": "Already rewarded",
"clpa_fail_conn": "Your discord account might not be connected to Patreon. If you are unsure what that means, or don't know how to connect it - you have to go to [Patreon account settings page](https://patreon.com/settings/account) and click 'Connect to discord' button.",
"clpa_fail_conn": "Your discord account might not be connected to Patreon. If you are unsure what that means, or don't know how to connect it - you have to go to [Patreon account settings page](https://www.patreon.com/settings/apps) and click 'Connect to discord' button.",
"clpa_fail_conn_title": "Discord account not connected",
"clpa_fail_sup": "In order to be eligible for the reward, you must support the project on patreon. You can use {0} command to get the link.",
"clpa_fail_sup_title": "Not supporting",
@@ -914,6 +921,7 @@
"reaction_role_removed": "Removed ReactionRole message #{0}",
"reaction_roles_full": "You've reached the limit on ReactionRole messages. You have to delete some.",
"reminder_list": "List of reminders",
"reminder_server_list": "List of server reminders",
"reminder_deleted": "Reminder #{0} was deleted.",
"reminder_not_exist": "Reminder at that index does not exist.",
"reminders_none": "No reminder on this page.",

View File

@@ -177,7 +177,7 @@
"cleverbot_enabled": "Cleverbot activado en este canal.",
"curgen_disabled": "La generación de moneda ha sido desactivada en este canal.",
"curgen_enabled": "La generación de moneda ha sido habilitada en este canal.",
"curgen_pl": "¡{0} {1} han aparecido!",
"curgen_pl": "¡{0} {1} ha aparecido!",
"curgen_sn": "¡Ha aparecido {0}!",
"game_started": "Juego iniciado",
"hangman_game_started": "Juego del ahorcado iniciado",
@@ -593,8 +593,6 @@
"log_all": "Registrando todos los eventos en este canal.",
"log_disabled": "Registros desactivados.",
"log_events": "Registrar los eventos que puedes seguir en:",
"log_ignore": "Los registros ignorarán {0}",
"log_not_ignore": "Los registros no ignorarán {0}",
"log_stop": "Se detuvo el registro del evento {0}.",
"msg_not_found": "Mensaje no encontrado.",
"time_too_long": "El tiempo especificado es mucho.",
@@ -805,7 +803,7 @@
"warn_expire_set_clear": "Las advertencias se reiniciarán cada {0} días.",
"warn_expire_reset": "Las advertencias ya no expirarán.",
"warn_punish_set_timed": "Aplicaré el castigo {0} por {2} a los usuarios con {1} advertencias.",
"clpa_obsolete": ":tada: **¡Las recompensas de Patreon ahora son automáticas!** :tada:\nEste comando está obsoleto.\nSi no recibiste tu recompensa de este mes, abajo tienes alguna de las posibles razones.",
"clpa_obsolete": ":tada: **¡Las recompensas de Patreon ahora son automáticas!** :tada:\nEste comando está obsoleto.\nSi no recibiste tu recompensa de este mes, abajo tienes algunas de las posibles razones.",
"time_new": "Tiempo",
"timezone_db_api_key": "Necesitas activar tu clave de API de TimezoneDB. Puedes hacerlo haciendo click en el link que recibiste en tu e-mail con tu clave de API.",
"rolehoist_enabled": "El rol {0} ahora se muestra separado de los miembros en línea.",
@@ -934,7 +932,7 @@
"module_page_empty": "No hay módulos en esta pagina",
"module_description_help": "Obtén ayuda con los comandos, descripciones y ejemplos de uso.",
"module_description_gambling": "Apuesta a las tiradas de dados, al blackjack, a las tragamonedas, al lanzamiento de monedas y a otros\n",
"module_description_games": "Juega a la trivia, al nunchi, al colgado, conecta4 y otros juegos.",
"module_description_games": "Juega al trivial, al nunchi, al colgado, conecta4 y a otros juegos.",
"module_description_nsfw": "Comandos NSFW",
"module_description_music": "Reproduce música de YouTube, archivos locales, SoundCloud y radios.",
"module_description_utility": "Administra citas personalizadas, repeticiones de mensajes y revisa datos sobre el servidor",
@@ -953,5 +951,29 @@
"empty_page": "Esta página está vacía.",
"pages": "Páginas",
"favorites": "Favoritos",
"tags": "Etiquetas"
"tags": "Etiquetas",
"invalid_emoji_link": "La liga especificada o no es una imagen, o se excede de los 256KB.",
"emoji_add_error": "Error al agregar emoji. Ya no hay espacio para más, o el tamaño de la imagen no es adecuado.",
"emoji_added": "Se agregó un emoji nuevo: {0}",
"boost_on": "Los anuncios de Aumentos se han activado en este canal.",
"boost_off": "Los anuncios de Aumentos se han desactivado.",
"boostmsg_cur": "Mensaje de Aumento actual: {0}",
"boostmsg_enable": "Active los mensajes de Aumento tecleando {0}",
"boostmsg_new": "Nuevo mensaje de Aumento configurado.",
"boostdel_off": "Se ha desactivado la eliminación automática de mensajes de Aumento.",
"boostdel_on": "Los mensajes de Aumento se eliminarán después de {0} segundos.",
"log_ignored_channels": "Canales ignorados",
"log_ignored_users": "Usuarios ignorados",
"log_ignore_user": "El registro ignorará al usuario {0}",
"log_not_ignore_user": "El registró ya no ignorará al usuario {0}",
"log_ignore_chan": "El registro ignorará el canal {0}",
"log_not_ignore_chan": "El registro ya no ignorará el canal {0}",
"streams_cleared": "Todos los streams seguidos en este servidor se han eliminado.",
"warn_weight": "Peso: {0}",
"warn_count": "Actual {0}, total {1}",
"mass_ban_in_progress": "Corriendo a {0} usuarios...",
"mass_ban_completed": "Se han corrido a {0} usuarios.",
"reminder_server_list": "Lista de recordatorios del servidor",
"imageonly_enable": "Este canal ahora es exclusivo para imágenes.",
"imageonly_disable": "Este canal ya no es exclusivo para imágenes."
}

View File

@@ -5,26 +5,26 @@
"trigger": "Déclencheur",
"response": "Réponse",
"fw_cleared": "Suppression de tous les mots filtrés et des réglages de mots filtrés du salon.",
"aar_disabled": "Les membres qui rejoignent le serveur ne recevront aucun rôle automatiquement.",
"aar_disabled": "L'**assignation automatique de rôles** aux nouveaux membres est **désactivée**.",
"bandm": "Vous avez été banni du serveur {0}.\nRaison : {1}",
"banned_user": "Membre banni",
"byedel_off": "La suppression automatique des messages de départ a été désactivée.",
"byedel_on": "Les messages de départ seront supprimés après {0} secondes.",
"byedel_on": "Les messages d'au revoir seront supprimés après {0} secondes.",
"byemsg_cur": "Message d'au revoir actuel : {0}",
"byemsg_enable": "Activez les messages de départ en tapant {0}",
"byemsg_new": "Nouveau message de départ réglé.",
"bye_off": "Annonces de départ désactivées.",
"bye_on": "Annonces de départ désactivées dans ce salon.",
"cr": "Le rôle {0} a été créé avec succès",
"byemsg_enable": "Activez les messages d'au revoir en tapant {0}",
"byemsg_new": "Nouveau message d'au revoir réglé.",
"bye_off": "Annonces de d'au revoir désactivées.",
"bye_on": "Annonces d'au revoir désactivées dans ce salon.",
"cr": "Le rôle {0} a été créé avec succès.",
"dr": "Le rôle {0} a été supprimé.",
"createtextchan": "Salon textuel {0} créé.",
"createvoich": "Salon vocal {0} créé.",
"deafen": "Mise en sourdine effectuée.",
"delmsg_off": "Je ne supprimerai plus automatiquement les messages qui auront exécuté avec succès une commande sur ce serveur.",
"delmsg_on": "Je supprimerai automatiquement les messages qui auront exécuté avec succès une commande sur ce serveur.",
"delmsg_channel_off": "Je ne supprimerai jamais automatiquement les messages qui auront exécuté avec succès une commande sur ce salon.",
"delmsg_channel_on": "Je supprimerai toujours automatiquement les messages qui auront exécuté avec succès une commande sur ce salon.",
"delmsg_channel_inherit": "Je supprimerai automatiquement les messages qui auront exécuté avec succès une commande sur ce salon uniquement si l'option est activée sur le serveur.",
"delmsg_off": "Les messages exécutant une commande avec succès ne seront plus supprimés automatiquement sur ce serveur.",
"delmsg_on": "Les messages exécutant une commande avec succès seront supprimés automatiquement sur ce serveur.",
"delmsg_channel_off": "Les messages exécutant une commande avec succès ne seront jamais supprimés sur ce salon.",
"delmsg_channel_on": "Les messages exécutant une commande avec succès seront toujours supprimés automatiquement sur ce salon, en dehors des réglages du serveur.",
"delmsg_channel_inherit": "Les messages exécutant une commande avec succès seront supprimés automatiquement sur ce salon seulement si l'option est activée sur le serveur.",
"deltextchan": "Salon textuel {0} supprimé.",
"delvoich": "Salon vocal {0} supprimé.",
"fwall_start": "Je transmettrai les MPs à tous les propriétaires.",
@@ -48,91 +48,91 @@
"kickdm": "Vous avez été expulsé du serveur {0}.\nRaison : {1}",
"kicked_user": "Membre expulsé",
"lang_list": "Liste des langues",
"lang_set_bot": "Les paramètres régionaux par défaut du bot sont désormais {0} - {1}",
"lang_set_bot_show": "La langage du bot a été réglé sur {0} - {1}",
"lang_set_bot": "La localisation par défaut du bot est désormais {0} - {1}",
"lang_set_bot_show": "Le langage du bot a été réglé sur {0} - {1}",
"lang_set": "La localisation du serveur a été réglée sur {0} - {1}",
"message_sent": "Message envoyé.",
"mute_error": "Je n'ai probablement pas les permissions nécessaires pour ça.",
"mute_error": "Je n'ai certainement pas les permissions nécessaires pour cela.",
"mute_role_set": "Nouveau rôle muet défini.",
"prot_active": "Protections actives",
"prot_disable": "{0} a été **désactivé** sur ce serveur",
"prot_disable": "{0} a été **désactivé** sur ce serveur.",
"prot_enable": "{0} Activé",
"prot_none": "Aucune protection d'activée.",
"raid_cnt": "Le seuil d'utilisateurs doit être entre {0} et {1}",
"raid_stats": "Si {0} ou plus d'utilisateurs rejoignent dans {1} secondes, je les {2}.",
"raid_time": "Le temps doit être entre {0} et {1} secondes.",
"rar": "Suppression de tout les rôles de l'utilisateur {0} avec succès",
"rc": "La couleur de {0} a été changée",
"raid_cnt": "Le seuil d'utilisateurs doit être entre {0} et {1}.",
"raid_stats": "Si {0} utilisateurs ou plus rejoignent en {1} secondes, ils seront {2}.",
"raid_time": "La durée doit être définie entre {0} et {1} secondes.",
"rar": "Tous les rôles de l'utilisateur {0} ont été retirés avec succès.",
"rc": "La couleur de {0} a été changée.",
"renrole": "Rôle renommé.",
"reprm": "Suppression du message de lecture: {0}",
"role_added": "Le rôle {0] a été ajouté à la liste du groupe {1}.",
"reprm": "Suppression du message de lecture: {0}.",
"role_added": "Le rôle {0} a été ajouté à la liste du groupe {1}.",
"role_in_list": "Le rôle {0} est déjà dans la liste.",
"self_assign_already": "Vous avez déjà le rôle {0}",
"self_assign_excl": "Les rôles auto-attribués sont désormais exclusifs !",
"self_assign_already": "Vous disposez déjà du rôle {0}.",
"self_assign_excl": "Les rôles auto-attribués sont désormais exclusifs!",
"self_assign_list": "Il y a {0} rôles auto-attribués",
"self_assign_not": "Ce rôle n'est pas auto-attribuable.",
"self_assign_not_have": "Vous n'avez pas le rôle {0}.",
"self_assign_no_excl": "Les rôles auto-attribués ne sont plus désormais exclusifs !",
"self_assign_perms": "Je suis dans l'incapacité de t'ajouter ce rôle. `Je ne peux pas ajouter de rôles aux propriétaires ou aux autres rôles supérieur à mon rôle dans la hiérarchie des rôles.`",
"self_assign_not_have": "Vous ne disposez pas du rôle {0}.",
"self_assign_no_excl": "Les rôles auto-attribués ne sont désormais plus exclusifs!",
"self_assign_perms": "Je suis dans l'incapacité de t'ajouter ce rôle. `« Je ne peux pas ajouter de rôles aux propriétaires ou aux autres supérieurs au mien dans la hiérarchie des rôles. »`",
"self_assign_rem": "{0} a été supprimé de la liste des rôles auto-attribuable.",
"self_assign_remove": "Vous n'avez plus le rôle {0}.",
"self_assign_success": "Vous avez désormais le rôle {0}.",
"self_assign_remove": "Vous ne disposez plus du rôle {0}.",
"self_assign_success": "Vous disposez désormais du rôle {0}.",
"adsarm_enable": "L'effacement automatique des réponses `{0}iam` et `{0}iamn` a été activé.",
"adsarm_disable": "Le bot ne supprimera plus désormais les réponses `{0}iam` et `{0}iamn`.",
"adsarm_disable": "Le bot ne supprimera plus les réponses `{0}iam` et `{0}iamn`.",
"setrole": "Ajout du rôle {0} à l'utilisateur {1} avec succès.",
"set_channel_name": "Nouveau nom de salon défini.",
"shutting_down": "Arrêt",
"spam_ignore": "{0} ignorera ce salon.",
"spam_not_ignore": "{0} n'ignorera désormais plus ce salon.",
"spam_stats": "Si un utilisateur publie {0} messages identiques à la fois, je le {1}.\n __Canaux Ignorés__: {2}",
"spam_not_ignore": "{0} n'ignorera plus ce salon.",
"spam_stats": "Si un utilisateur publie {0} messages identiques à la fois, il sera {1}.\n __Salons Ignorés__: {2}",
"undeafen": "Mise en sourdine désactivée avec succès.",
"username": "Nom d'utilisateur",
"user_chat_mute": "{0} a été mis en sourdine du tchat.",
"user_chat_unmute": "**La parole a été rétablie** dans le salon textuel pour {0}.",
"user_muted": "{0} a été mise en sourdine du tchat textuel et vocal.",
"user_unmuted": "{0} n'est plus en sourdine dans le tchat textuel et vocal.",
"user_voice_mute": "{0} est maintenant **muet** en vocal.",
"user_voice_unmute": "{0} n'est plus **muet**",
"sbdm": "Vous avez été expulsé du serveur {0}.\nRaison : {1}",
"sb_user": "L'utilisateur a été expulsé",
"better_luck": "Vous aurez plus de chance la prochaine fois ^-^",
"roll": "Tu es tombé sur {0}.",
"br_win": "Félicitations ! Tu gagnes {0} pour être tombé au dessus de {1}",
"flip_guess": "Tu as trouvé ! Tu gagnes {0}",
"flip_results": "Jeté de {0} pièces. {1} faces, {2} piles.",
"user_chat_mute": "{0} a été **rendu muet** dans les salons textuels.",
"user_chat_unmute": "{0} n'est **plus muet** dans les salons textuels.",
"user_muted": "{0} a été **rendu muet** dans les salons textuels et vocaux.",
"user_unmuted": "{0} n'est **plus muet** dans les salons textuels et vocaux.",
"user_voice_mute": "{0} a été **rendu muet** en vocal.",
"user_voice_unmute": "{0} n'est **plus muet** en vocal.",
"sbdm": "Vous avez été radié du serveur {0}.\nRaison : {1}",
"sb_user": "L'utilisateur a été radié.",
"better_luck": "Vous aurez plus de chance la prochaine fois ^_^",
"roll": "Tu as roulé un {0}.",
"br_win": "Félicitations ! Tu as gagné {0} pour avoir roulé au de de {1}",
"flip_guess": "Tu as deviné ! Tu gagnes {0}",
"flip_results": "a jeté {0} pièces. {1} faces, {2} piles.",
"has": "{0} a {1}",
"leaderboard": "Classement",
"mass_award": "Récompense de {0} accordée aux utilisateurs {0} ayant de rôle {2}.",
"mass_award": "Une récompense de {0} sera accordée aux {1} utilisateurs disposant du rôle {2}.",
"max_bet_limit": "Vous ne pouvez pas parier plus de {0}",
"min_bet_limit": "Tu ne peux pas parier moins de '0}",
"not_enough": "Tu n'as pas assez de {0}",
"min_bet_limit": "Vous ne pouvez pas parier moins de {0}",
"not_enough": "Vous n'avez pas assez de {0}",
"raffled_user": "Utilisateur tiré au sort",
"slot_bet": "Pari",
"slot_jackpot": "WOAAHHHHHHH !!! Félicitations !!! x{0}",
"slot_jackpot": "WOAAHHHHHHH !!! Félicitations!!! x{0}",
"slot_single": "Un simple {0}, x{1}",
"slot_three": "Wow ! Chanceux ! Un brelan ! x{0}",
"slot_two": "Bon travail ! Deux {0} - pari x{1}",
"slot_three": "Wow! Chanceux! Un brelan! x{0}",
"slot_two": "Bon travail! Deux {0} - pari x{1}",
"won": "Gagné",
"transactions": "Transactions de l'utilisateur {0}",
"commands_instr": "Taper `{0}h NomCommande` pour voir l'aide de cette commande spécifique. Ex : `{0}h {0}8ball`",
"command_not_found": "Je ne peux pas trouver cette commande. Vérifiez que cette commande existe avant de réessayer.",
"module_not_found": "Ce module n'existe pas.",
"module_not_found_or_cant_exec": "Soit ce module n'existe pas, soit vous ne pouvez exécuter aucunes des commandes dans le module spécifié.",
"module_not_found_or_cant_exec": "Soit ce module n'existe pas, soit vous ne pouvez exécuter aucunes de ces commandes dans le module spécifié.",
"usage": "Usage",
"options": "Options",
"requires": "Requiert",
"blacklisted_tag_list": "Liste des mots-clés en liste noire:",
"blacklisted_tag_add": "Le mot-clé {0} est maintenant sur liste noire.",
"blacklisted_tag_remove": "Le mot-clé {0} n'est désormais plus sur liste noire.",
"blacklisted_tag_list": "Liste des mots-clés sur la liste noire:",
"blacklisted_tag_add": "Le mot-clé *NSFW* «{0}» est intégré à la liste noire.",
"blacklisted_tag_remove": "Le mot-clé *NSFW* «{0}» n'est plus sur la liste noire.",
"animal_race": "Course d'animaux",
"animal_race_failed": "Échec du lancement car il n'y avait pas assez de participants.",
"animal_race_join": "{0} a rejoint en tant que {1}",
"animal_race_join_instr": "Taper `{0}jr` pour rejoindre la course.",
"animal_race_starting": "Début dans {0} secondes ou quand la salle est pleine.",
"animal_race_won": "{0} en tant que {1} a gagné la course !",
"dice_invalid_number": "Nombre spécifié invalide. Vous pouvez jeter {0}-{1} dés à la fois",
"animal_race_starting": "Début dans {0} secondes ou quand la salle sera pleine.",
"animal_race_won": "{0} a gagné la course en tant que {1}!",
"dice_invalid_number": "Nombre spécifié invalide. Vous pouvez jeter {0}-{1} dés à la fois.",
"dice_rolled": "tiré au sort {0}",
"dice_rolled_num": "Dés lancés : {0}",
"dice_rolled_num": "Dés lancés: {0}",
"changes_of_heart": "Changements d'avis",
"claimed_by": "Réclamé par",
"divorces": "Divorces",
@@ -142,59 +142,59 @@
"waifus_none": "Aucunes waifus n'ont encore été réclamées.",
"waifus_top_waifus": "Meilleures Waifu",
"waifu_claimed": "a revendiqué {0} comme sa waifu pour {1} !",
"waifu_divorced_like": "Vous avez divorcé d'une waifu qui vous aimait. Espèce de monstre sans cœur. {0} a reçu {1} en compensation.",
"waifu_egomaniac": "vous ne pouvez pas définir votre affinité pour vous-même, espèce d'égoïste.",
"waifu_divorced_like": "Vous avez divorcé d'une waifu qui vous aimait. Vous n'êtes qu'un monstre sans cœur, {0} a reçu {1} en compensation.",
"waifu_egomaniac": "vous ne pouvez pas tomber amoureux de vous-même, gros narcissique.",
"waifu_fulfilled": "🎉 Leur amour est accompli 🎉\nLa nouvelle valeur de {0} est {1} !",
"waifu_not_enough": "Vous devez payer {0} ou plus pour revendiquer cette waifu !",
"waifu_not_yours": "Cette waifu n'est pas vôtre.",
"waifu_not_yourself": "Vous ne pouvez pas vous revendiquer vous-même.",
"waifu_recent_divorce": "Vous avez divorcé récemment. Vous devez attendre {0} heures et {1} minutes avant de divorcer à nouveau.",
"waifu_recent_divorce": "Vous avez récemment divorcé. Vous devez attendre {0} heures et {1} minutes avant de pouvoir divorcer à nouveau.",
"waifu_transfer_fail": "Vous n'êtes pas le propriétaire de cette waifu.",
"waifu_transfer_success": "Le prétendant de {0} n'est plus {1} mais {2}.",
"waifu_transfer_success": "La propriété de {0} a été transférée de {1} à {2}.",
"nobody": "Personne",
"waifu_divorced_notlike": "Vous avez divorcé d'une waifu qui ne vous aimait pas. Vous avez reçu {0} en compensation.",
"waifu_reset": "Vous stats de waifu ont été remise à zéro.",
"waifu_reset_fail": "Échec de la remise à zéro des stats de waifu. Assurez-vous d'avoir assez de devises.",
"waifu_reset_confirm": "Ceci remettra à zéro vos stats de waifu",
"waifu_reset": "Vos stats de waifu ont été réinitialisée.",
"waifu_reset_fail": "Échec de la réinitialisation des stats de waifu. Assurez-vous d'avoir assez de devises.",
"waifu_reset_confirm": "Cela réinitialisera vos stats de waifu",
"acrophobia": "Acrophobie",
"acro_ended_no_sub": "La partie s'est terminée sans soumissions.",
"acro_no_votes_cast": "Aucun votes émis. La partie s'est terminée avec aucun gagnant.",
"acro_nym_was": "L'acronyme était {0}.",
"acro_running": "Une partie d'acrophobie est déjà en cours dans ce salon.",
"acro_started": "La partie a commencée. Créez une phrase avec l'acronyme suivant: {0}.",
"acro_started_footer": "Vous avez {0} pour faire une soumission.",
"acro_running": "Une partie d'Acrophobia est déjà en cours dans ce salon.",
"acro_started": "La partie a commencé. Créez une phrase avec l'acronyme suivant: {0}.",
"acro_started_footer": "Vous avez {0} secondes pour faire une soumission.",
"acro_vote": "Votez en tapant le numéro de la soumission",
"acro_vote_cast": "{0} a voté !",
"acro_vote_cast": "{0} a voté!",
"acro_winner": "Le gagnant est {0} avec {1} points.",
"acro_winner_only": "{0} est le gagnant puisqu'il est le seul à avoir participé !",
"acro_winner_only": "{0} est le gagnant car il est le seul à avoir participé!",
"question": "Question",
"rps_draw": "C'est un match nul ! Les deux ont choisi {0}",
"rps_win": "{0} a gagné ! {1} bats {2}",
"rps_draw": "C'est un match nul! Les deux ont choisi {0}",
"rps_win": "{0} a gagné! {1} bat {2}",
"submissions_closed": "Soumissions fermées",
"total_average": "Total: {0} Moyenne: {1}",
"category": "Catégorie",
"cleverbot_disabled": "Cleverbot désactivé dans ce salon.",
"cleverbot_enabled": "Cleverbot activé dans ce salon.",
"curgen_disabled": "La génération de devises a été désactivée dans ce salon.",
"curgen_enabled": "La génération de devises a été activée dans ce salon.",
"curgen_pl": "{0} {1} aléatoires sont apparues !",
"curgen_sn": "Une {0} aléatoire est apparue !",
"cleverbot_disabled": "Cleverbot est désactivé sur ce serveur.",
"cleverbot_enabled": "Cleverbot est activé sur ce serveur.",
"curgen_disabled": "La génération de devises a été désactivée sur ce salon.",
"curgen_enabled": "La génération de devises a été activée sur ce salon.",
"curgen_pl": "{0} {1} aléatoires sont apparues!",
"curgen_sn": "Une {0} aléatoire est apparue!",
"game_started": "La partie a commencée",
"hangman_game_started": "La partie de pendu a commencée",
"hangman_running": "Une partie de pendu est déjà en cours dans ce salon",
"hangman_running": "Une partie de pendu est déjà en cours dans ce salon.",
"hangman_types": "Liste des types de termes \"{0}hangman\":",
"no_results": "Aucun résultats.",
"picked": "a choisi {0}",
"planted": "{0} a planté {1}",
"trivia_already_running": "Une partie de Trivial Pursuit est déjà en cours sur ce serveur.",
"trivia_game": "Jeu du Trivial Pursuit",
"trivia_guess": "{0} a trouvé ! La réponse était: {1} ",
"trivia_guess": "{0} a deviné! La réponse était: {1}",
"trivia_none": "Aucun Trivial Pursuit est en cours sur ce serveur",
"trivia_points": "{0} a {1} points",
"trivia_stopping": "La partie s'arrêtera après cette question.",
"trivia_times_up": "Fin du temps imparti ! La réponse correcte était {0}",
"trivia_stopping": "La partie prendra fin après cette question.",
"trivia_times_up": "Fin du temps imparti ! La bonne réponse était {0}",
"ttt_against_yourself": "Vous ne pouvez pas jouer contre vous-même.",
"ttt_already_running": "Une partie de morpion est déjà en cours dans ce salon.",
"ttt_already_running": "Une partie de morpion est déjà en cours sur ce salon.",
"ttt_a_draw": "Égalité !",
"ttt_created": "a créé une partie de morpion.",
"ttt_has_won": "{0} a gagné !",
@@ -202,7 +202,7 @@
"ttt_no_moves": "Aucun coups restants !",
"ttt_time_expired": "Temps écoulé !",
"ttt_users_move": "Au tour de {0}",
"vs": "{0} vs {1}",
"vs": "{0} contre {1}",
"attempting_to_queue": "Tentative d'ajout de {0} morceaux à la file d'attente...",
"fairplay": "Fair-play",
"finished_song": "Morceau fini",
@@ -214,36 +214,36 @@
"no_search_results": "Aucuns résultats de recherche",
"player_queue": "File d'attente du lecteur - Page {0}/{1}",
"playing_song": "Lecture du morceau #{0}",
"playlist_queue_complete": "La file d'attente de la playlist est terminée.",
"queued_song": "Morceau mis en file d'attente",
"playlist_queue_complete": "La file d'attente de la liste de lecture est terminée.",
"queued_song": "Morceau ajouté à la file d'attente",
"removed_song": "Morceau retiré",
"repeating_track": "Répétition de la piste",
"repeating_track": "Le lecteur continuera de jouer la même piste.",
"volume_set": "Volume mis à {0}%",
"acm_disable": "Désactivation de l'utilisation de TOUS LES MODULES dans le salon {0}.",
"acm_enable": "Activation de l'utilisation de TOUS LES MODULES dans le salon {0}.",
"arm_disable": "Désactivation de l'utilisation de TOUS LES MODULES pour le rôle {0}.",
"arm_enable": "Activation de l'utilisation de TOUS LES MODULES pour le rôle {0}.",
"asm_disable": "Désactivation de l'utilisation de TOUS LES MODULES sur ce serveur.",
"asm_enable": "Activation de l'utilisation de TOUS LES MODULES sur ce serveur.",
"aum_disable": "Désactivation de l'utilisation de TOUS LES MODULES pour le membre {0}.",
"aum_enable": "Activation de l'utilisation de TOUS LES MODULES pour le membre {0}.",
"blacklisted": "{0} mis sur liste noire avec les IDs : {1}",
"unblacklisted": "{0} retirés de la liste noire avec les IDs : {1}",
"acm_disable": "Utilisation de TOUS LES MODULES **désactivée** sur le salon {0}.",
"acm_enable": "Utilisation de TOUS LES MODULES **activée** sur le salon {0}.",
"arm_disable": "Utilisation de TOUS LES MODULES **désactivée** pour le rôle {0}.",
"arm_enable": "Utilisation de TOUS LES MODULES **activée** pour le rôle {0}.",
"asm_disable": "Utilisation de TOUS LES MODULES **désactivée** sur ce serveur.",
"asm_enable": "Utilisation de TOUS LES MODULES **activée** sur ce serveur.",
"aum_disable": "Utilisation de TOUS LES MODULES **désactivée** pour le membre {0}.",
"aum_enable": "Utilisation de TOUS LES MODULES **activée** pour le membre {0}.",
"blacklisted": "{0} est inscrit sur la liste noire avec l'ID {1}",
"unblacklisted": "{0} est retiré de la liste noire avec l'ID {1}",
"cmdcd_add": "La commande {0} a désormais un temps de recharge de {1}s",
"cmdcd_cleared": "Commande {0} ne possède plus de cooldown et tous les cooldowns existants ont été supprimés",
"cmdcd_none": "Aucuns temps de recharge pour commande réglé.",
"cx_disable": "Utilisation de {0} {1} interdite dans le salon {2}",
"cx_enable": "Utilisation de {0} {1} autorisé dans le salon {2}",
"filter_word_add": "Mot {0} ajouté à la liste des mots filtrés.",
"cmdcd_cleared": "La commande {0} ne possède plus de temps de recharge et tous ceux existants ont été retirés.",
"cmdcd_none": "Aucun temps de recharge n'est programmé.",
"cx_disable": "L'utilisation de {0} {1} est désactivée sur le salon {2}.",
"cx_enable": "L'utilisation de {0} {1} est autorisée sur le salon {2}.",
"filter_word_add": "Le mot {0} a été ajouté à la liste des mots filtrés.",
"filter_word_list": "Liste des mots filtrés",
"filter_word_remove": "Mot {0} retiré de la liste des mots filtrés.",
"invalid_second_param_between": "Second paramètre invalide. (Doit être un nombre entre {0} et {1})",
"invite_filter_channel_off": "Filtre d'invitation désactivé sur ce salon.",
"filter_word_remove": "Le mot {0} a été retiré de la liste des mots filtrés.",
"invalid_second_param_between": "Le second paramètre est invalide. (Doit être un nombre entre {0} et {1})",
"invite_filter_channel_off": "Le filtre d'invitation est désactivé sur ce salon.",
"invite_filter_channel_on": "Filtre d'invitation activé sur ce salon.",
"invite_filter_server_off": "Filtre d'invitation désactivé sur ce serveur.",
"invite_filter_server_on": "Filtre d'invitation activé sur ce serveur.",
"moved_permission": "Permission {0} déplacée de {1} à {2}",
"perm_not_found": "Impossible de trouver de permission à l'index #{0}",
"perm_not_found": "Impossible de trouver de permission dans l'index #{0}",
"of_command": "commande",
"of_module": "module",
"page": "page {0}",
@@ -251,7 +251,7 @@
"permrole_not_set": "Le rôle de permission n'est pas défini.",
"permrole_reset": "Le rôle de permission est réinitialisé.",
"permrole_changed": "Les utilisateurs ont désormais besoin du rôle {0} pour modifier les permissions.",
"perm_out_of_range": "Aucune permission trouvé sur cet index.",
"perm_out_of_range": "Aucune permission trouvée sur cet index.",
"removed": "permission retirée #{0} - {1}",
"rx_disable": "Usage désactivé de {0} {1} pour {2} rôle.",
"rx_enable": "Usage activé de {0} {1} pour {2} rôle.",
@@ -263,10 +263,10 @@
"ux_enable": "Usage activé de {0} {1} pour {2} utilisateur.",
"verbose_false": "Je n'afficherai plus les alertes de permissions.",
"verbose_true": "J'afficherai dorénavant les alertes de permissions.",
"word_filter_channel_off": "Filtre des mots désactivé dans ce salon.",
"word_filter_channel_on": "Filtre des mots activé sur ce salon.",
"word_filter_server_off": "Filtrage des mots désactivé sur ce serveur.",
"word_filter_server_on": "Filtrage des mots activé sur ce serveur.",
"word_filter_channel_off": "Le filtre du langage est désactivé sur ce salon.",
"word_filter_channel_on": "Le filtre du langage est activé sur ce salon.",
"word_filter_server_off": "Le filtre du langage est désactivé sur ce serveur.",
"word_filter_server_on": "Le filtre du langage est activé sur ce serveur.",
"abilities": "Aptitudes",
"chapters": "Chapitres",
"comic_number": "Bande dessinée #",
@@ -288,8 +288,8 @@
"timezone": "Fuseau horaire",
"min_max": "Min/Max",
"original_url": "Url originale",
"pokemon_ability_none": "Aucune aptitude trouvée",
"pokemon_none": "Aucun pokémon trouvé",
"pokemon_ability_none": "Aucune aptitude trouvée.",
"pokemon_none": "Aucun pokémon trouvé.",
"rating": "Évaluation",
"score": "Score:",
"search_for": "Recherche pour:",
@@ -300,7 +300,7 @@
"temperature": "Température",
"types": "Types",
"url": "Url",
"wind_speed": "Force du vent",
"wind_speed": "Vitesse du vent",
"author": "Auteur",
"botid": "ID du bot",
"channelid": "{0} de ce salon est {1}.",
@@ -329,9 +329,9 @@
"nobody_playing_game": "Personne ne joue à ce jeu.",
"no_topic_set": "Aucun thème réglé",
"owner": "Propriétaire",
"owner_ids": "Identifiant(s) Propriétaire(s)",
"owner_ids": "Identifiant·s Propriétaire·s",
"region": "Région",
"remind": "Je rappellerai à {0} de {1} dans {2} `({3:d.M.yyyy.} at {4:HH:mm})`",
"remind": "Je rappellerai à {0} de {1} dans {2} `(le {3:d.M.yyyy.} à {4:HH:mm}h)`",
"remind_too_long": "Le temps de rappel dépasse le maximum.",
"repeaters_none": "Pas de répétiteur en cours sur ce serveur.",
"repeat_invoke_none": "Aucune répétition de messages trouvé sur ce serveur.",
@@ -339,10 +339,10 @@
"roles": "Rôles",
"serverid": "{0} de ce serveur est {1}",
"server_info": "Info du serveur",
"shard": "Shard",
"shard": "Fragment",
"showemojis_none": "Pas d'emojis spéciaux trouvés.",
"text_channels": "Salons textuels",
"uptime": "Uptime",
"uptime": "Temps de fonctionnement",
"userid": "{0} de l'utilisateur {1} est {2}",
"roleid": "{0} du rôle {1} est {2}",
"voice_channels": "Salons vocaux",
@@ -356,22 +356,22 @@
"pick_pl": "Récupérez les en tapant `{0}pick`",
"pick_sn": "Récupérez la en tapant `{0}pick`",
"sql_confirm_exec": "Confirmez l'exécution de la commande SQL suivante",
"no_vcroles": "Il n'y a pas de rôle pour le salon vocal.",
"user_muted_time": "{0} ne peux plus écrire ni parler pendant {1} minutes.",
"vcrole_added": "Les utilisateurs qui rejoindrons le salon vocal {0} obtiendrons le rôle {1}.",
"vcrole_removed": "Les utilisateurs qui rejoindrons le salon vocal {0} n'obtiendrons plus de rôle.",
"no_vcroles": "Il n'y a pas de rôles pour les salons vocaux.",
"user_muted_time": "{0} a été **rendu muet** à l'écrit et à l'oral pour {1} minutes.",
"vcrole_added": "Les utilisateurs qui rejoindront le salon vocal {0} obtiendront le rôle {1}.",
"vcrole_removed": "Les utilisateurs qui rejoindront le salon vocal {0} n'obtiendront plus de rôle.",
"vc_role_list": "Rôles du salon vocal",
"option_disabled": "L'option {0} est maintenant désactivée pour l'expression avec l'ID {1}.",
"option_enabled": "L'option {0} est désormais activée pour l'expression avec l'ID {1}.",
"channel": "Salons",
"command_text": "Texte de commande",
"option_disabled": "L'option {0} est maintenant **désactivée** pour la réaction personnalisée dont l'ID est {1}.",
"option_enabled": "L'option {0} est désormais **activée** pour la réaction personnalisée dont l'ID est {1}.",
"channel": "Salon",
"command_text": "Commande textuelle",
"moderator": "Modérateur",
"reason": "Raison",
"scadd": "Nouvelle commande de démarrage ajoutée.",
"scrm": "Commande de démarrage retirée avec succès.",
"scrm_fail": "Commande de démarrage non trouvée.",
"server": "Server",
"startcmdlist_none": "Pas de commandes de démarrage sur cette page.",
"server": "Serveur",
"startcmdlist_none": "Aucune commandes de démarrage trouvée sur cette page.",
"startcmds_cleared": "Toutes les commandes de démarrage ont été effacées.",
"unbanned_user": "L'utilisateur {0} à été débanni.",
"user_not_found": "Utilisateur introuvable.",
@@ -380,25 +380,25 @@
"warned_on": "Avertissement envoyé sur le serveur {0}",
"warned_on_by": "Le {0} à {1} par {2}",
"warnings_cleared": "Tous les avertissements ont été effacés pour {0}.",
"warning_clear_fail": "Avertissement non effacé. Soit l'avertissement à cet index n'existe pas, soit il a déjà été effacé.",
"warning_clear_fail": "Avertissement non effacé. Soit l'avertissement sur cet index n'existe pas, soit il a déjà été effacé.",
"warning_cleared": "L'avertissement {0} a été effacé pour {1}.",
"warnings_none": "Aucun avertissement sur cette page.",
"warnlog_for": "Journal d'avertissement pour {0}",
"warnpl_none": "Aucun châtiment réglé.",
"warn_cleared_by": "Effacé par {0}",
"warn_punish_list": "Liste des avertissements de punition",
"warn_punish_rem": "Avoir {0} avertissement ne déclenche plus une punition.",
"warn_punish_set": "J'appliquerais la sanction {0} pour les utilisateurs avec {1} avertissements.",
"cant_apply_punishment": "Je ne peux pas appliquer la sanction. Vérifiez mes permissions ou bien que mon rôle est bien au-dessus.",
"clpa_fail_already": "Vous avez peut-être déjà reçu votre récompense pour ce mois. Vous ne pouvez recevoir des récompenses seulement qu'une fois par mois, sauf si vous augmentez votre engagement.\nVous pouvez vérifier cela avec la commande `.curtrs`.",
"warn_punish_list": "Liste des avertissements punitifs",
"warn_punish_rem": "Avoir {0} avertissements ne déclenche plus une punition.",
"warn_punish_set": "J'appliquerai la sanction {0} pour les utilisateurs avec {1} avertissements.",
"cant_apply_punishment": "Je ne peux pas appliquer la sanction. Veuillez vérifiez mes permissions ainsi que la place de mon rôle, qu'il soit au-dessus des autres dans les paramètres du serveur.",
"clpa_fail_already": "Vous avez peut-être déjà reçu votre récompense pour ce mois. Vous pouvez recevoir des récompenses qu'une seule fois par mois, sauf si vous augmentez votre engagement.\nVous pouvez vérifier cela avec la commande `.curtrs`.",
"clpa_fail_already_title": "Déjà récompensé",
"clpa_fail_conn": "Votre compte Discord n'est peut-être pas connecté à Patreon. Si vous ne savez pas ce que cela signifie ou ne savez pas comment le connecter, vous devez vous rendre sur [Patreon account settings page](https://patreon.com/settings/account) et cliquer sur le bouton 'Connect to discord'.",
"clpa_fail_conn": "Votre compte Discord n'est peut-être pas connecté à Patreon. Si vous ne savez pas ce que cela signifie ou ne savez pas comment le connecter, vous devez vous rendre sur [Patreon account settings page](https://patreon.com/settings/account) et cliquer sur le bouton '« Connect to discord »'.",
"clpa_fail_conn_title": "Compte Discord non connecté",
"clpa_fail_sup": "Pour être éligible à la récompense, vous devez soutenir le projet sur Patreon. Vous pouvez utiliser la commande {0} pour obtenir le lien.",
"clpa_fail_sup_title": "Non supporté",
"clpa_fail_wait": "Vous devez attendre 5-10 minutes après votre engagement (ainsi que connecter votre compte discord à votre patreon) pour que votre devise apparaisse.",
"clpa_fail_wait": "Vous devez attendre 5-10 minutes après votre engagement (ainsi qu'avoir connecté votre compte discord à votre patreon) pour que vos devises apparaissent.",
"clpa_fail_wait_title": "Attendez quelque temps",
"clpa_too_early": "Les récompenses peuvent être réclamées tout les 5 du mois.",
"clpa_too_early": "Les récompenses ne peuvent être réclamées que tous les 5ème jours du mois.",
"shop": "Boutique",
"shop_item_add": "Article ajouté à la boutique",
"shop_none": "Aucun article de boutique trouvé sur cette page.",
@@ -408,39 +408,39 @@
"gvc_disabled": "La fonctionnalité Salon Vocal de Jeu a été désactivée sur ce serveur.",
"gvc_enabled": "{0} est désormais un Salon Vocal de Jeu.",
"not_in_voice": "Vous n'êtes pas dans un salon vocal sur ce serveur.",
"item": "Objet",
"item": "Article",
"out_of_stock": "En rupture de stock",
"random_unique_item": "Objet aléatoire unique.",
"shop_buy_error": "Echec de l'envoi de l'item par DM. Vous avez été remboursé.",
"shop_item_not_found": "Aucun élément de cet index n'a été trouvé.",
"shop_item_purchase": "Item acheté avec succès.",
"shop_item_rm": "Élément de la boutique supprimé",
"shop_buy_error": "Échec de l'envoi de l'article par MP. Vous avez été remboursé.",
"shop_item_not_found": "Aucun article de cet index n'a été trouvé.",
"shop_item_purchase": "Article acheté avec succès.",
"shop_item_rm": "Article supprimé de la boutique.",
"shop_item_wrong_type": "Cette entrée de boutique ne prend pas en charge l'ajout d'articles.",
"shop_list_item_added": "Article ajouté avec succès.",
"shop_list_item_not_unique": "Cet article a déjà été ajouté.",
"shop_purchase": "Acheter sur le serveur {0}",
"shop_role_not_found": "Le rôle qui est vendu n'existe plus.",
"shop_role_not_found": "Le rôle qui se vendait n'existe plus.",
"role_not_found": "Rôle introuvable : {0}",
"shop_role_purchase": "Vous avez acheté le rôle {0}>",
"shop_role_purchase": "Vous avez acheté le rôle {0}.",
"shop_role_purchase_error": "Une erreur est survenue lors de l'assignation du rôle. Votre achat a été remboursé.",
"unique_items_left": "{0} objets uniques restants.",
"unique_items_left": "Il reste {0} articles uniques.",
"blocked_commands": "Commandes bloquées",
"blocked_modules": "Modules bloqués",
"gcmd_add": "La commande {0} a été désactivée sur tout les serveurs.",
"gcmd_remove": "La commande {0} a été activée sur tout les serveurs.",
"gmod_add": "Le module {0} a été désactivé sur tout les serveurs.",
"gmod_remove": "Le module {0} a été activé sur tout les serveurs.",
"lgp_none": "Aucune commande ou module bloqué.",
"cant_read_or_send": "Vous ne pouvez pas lire ou envoyer des messages à ce salon.",
"gcmd_add": "La commande {0} a été **désactivée** sur tous les serveurs.",
"gcmd_remove": "La commande {0} a été **activée** sur tous les serveurs.",
"gmod_add": "Le module {0} a été **désactivé** sur tous les serveurs.",
"gmod_remove": "Le module {0} a été **activé** sur tout les serveurs.",
"lgp_none": "Aucunes commandes ou modules bloqués.",
"cant_read_or_send": "Vous ne pouvez pas lire ou envoyer des messages sur ce salon.",
"prefix_current": "Le préfixe sur ce serveur est {0}",
"defprefix_current": "Le préfixe par défaut du bot est {0}",
"timezone_guild": "Le fuseau horaire de ce serveur est `{0}`",
"timezone_not_found": "Fuseau horaire introuvable. Utilisez la commande \"fuseau horaire\" pour voir la liste des fuseaux horaires disponible.",
"timezones_available": "Fuseaux horaires disponible",
"song_not_found": "Chanson non trouvée",
"timezone_not_found": "Fuseau horaire introuvable. Utilisez la commande \"fuseau horaire\" pour voir la liste des fuseaux horaires disponibles.",
"timezones_available": "Fuseaux horaires disponibles.",
"song_not_found": "Chanson non trouvée.",
"not_found": "Introuvable.",
"verbose_errors_enabled": "Les erreurs de commande non gérées seront désormais affichées dans le tchat.",
"verbose_errors_disabled": "Les erreurs de commande non gérées ne s'afficheront plus dans le tchat.",
"verbose_errors_enabled": "Les commandes mal utilisées afficheront désormais un message d'erreur.",
"verbose_errors_disabled": "Les commandes mal utilisées n'afficheront plus de message d'erreur.",
"perms_reset": "Les permissions pour ce serveur ont été réinitialisées.",
"perm_prevent": "Le numéro d'autorisation # {0} {1} empêche cette action.",
"global_perms_reset": "Les permissions globales ont été réinitialisées.",
@@ -452,60 +452,60 @@
"waifu_gift_shop": "Boutique de cadeaux pour waifu",
"gifts": "Cadeaux",
"connect4_created": "Création d'une partie de Connect4. En attente d'un joueur.",
"connect4_created_bet": "Vous avez créé un partie de Connect4 et misez {0}. Attendez qu'un joueur rejoigne avec le même pari.",
"connect4_created_bet": "Vous avez créé un partie de Connect4 et vous misez {0}. En attente qu'un joueur rejoigne avec le même pari.",
"connect4_player_to_move": "Joueurs à déplacer: {0}",
"connect4_failed_to_start": "La partie de Connect4 n'a pas pu démarrer car personne ne l'a rejoint.",
"connect4_draw": "La partie de Connect4 a fini sur une égalité.",
"connect4_won": "{0} a gagné la partie de Connect4 contre {1}.",
"nunchi_joined": "Vous avez rejoint la partie de Nunchi. {0} utilisateurs ont joints jusqu'à présent.",
"nunchi_ended": "Le partie de Nunchi s'est terminé. {0} gagné",
"nunchi_ended": "Le partie de Nunchi s'est terminé. {0} a gagné",
"nunchi_ended_no_winner": "Le partie de Nunchi s'est terminé sans vainqueur.",
"nunchi_round_ended": "Fin du round de Nunchi. {0} est hors jeu.",
"nunchi_round_ended_boot": "Le round de Nunchi s'est terminé en raison du timeout de certains utilisateurs. Ces utilisateurs sont toujours dans le jeu: {0}",
"nunchi_round_started": "Le round de Nunchi a commencé avec {0} utilisateurs. Commencez à compter à partir du nombre {1}.",
"nunchi_round_ended": "Fin de la manche de Nunchi. {0} est éliminé·e.",
"nunchi_round_ended_boot": "La manche de Nunchi s'est terminé en raison de l'inactivité de certains utilisateurs. Ces utilisateurs sont toujours dans le jeu: {0}",
"nunchi_round_started": "La manche de Nunchi a commencé avec {0} utilisateurs. Commencez à compter à partir du nombre {1}.",
"nunchi_next_number": "Numéro enregistré. Le dernier numéro était {0}.",
"nunchi_failed_to_start": "La partie de Nunchi n'a pas pu démarrer car il n'y avait pas assez de participants.",
"nunchi_created": "Une partie de Nunchi ",
"nunchi_created": "Une partie de Nunchi a été créé. En attente de la participation d'autres utilisateurs.",
"stream_role_enabled": "Lorsqu'un utilisateur du rôle {0} commence à diffuser, je lui donne le rôle {1}.",
"stream_role_disabled": "La fonction de rôle streamer a été désactivée.",
"stream_role_kw_set": "Les streamers nécessitent désormais {0} mot-clé pour recevoir le rôle.",
"stream_role_kw_reset": "Réinitialisation des mots clé du rôle streamer.",
"stream_role_bl_add": "L'utilisateur {0} ne recevra jamais le rôle streamer.",
"stream_role_bl_add_fail": "L'utilisateur {0} est déjà blacklisté.",
"stream_role_bl_rem": "L'utilisateur {0} n'est plus blacklisté.",
"stream_role_bl_rem_fail": "L'utilisateur {0} n'est pas blacklisté.",
"stream_role_wl_add": "L'utilisateur {0} recevra le rôle streamer même s'il n'a pas le mot-clé dans le titre du stream.",
"stream_role_disabled": "La fonction de rôle diffuseur a été **désactivée**.",
"stream_role_kw_set": "Les diffuseurs nécessitent désormais le mot-clé {0} pour recevoir le rôle.",
"stream_role_kw_reset": "Réinitialisation des mots-clés du rôle diffuseur.",
"stream_role_bl_add": "L'utilisateur {0} ne recevra jamais le rôle de diffuseur.",
"stream_role_bl_add_fail": "L'utilisateur {0} est déjà sur la liste noire.",
"stream_role_bl_rem": "L'utilisateur {0} n'est plus sur la liste noire.",
"stream_role_bl_rem_fail": "L'utilisateur {0} n'est pas sur la liste noire.",
"stream_role_wl_add": "L'utilisateur {0} recevra le rôle de diffuseur même s'il manque le mot-clé dans le titre du stream.",
"stream_role_wl_add_fail": "L'utilisateur {0} est déjà sur la liste blanche.",
"stream_role_wl_rem": "L'utilisateur {0} n'est plus sur liste blanche.",
"stream_role_wl_rem_fail": "L'utilisateur {0} n'est pas sur liste blanche.",
"xp_role_reward_add_role": "L'utilisateur qui atteint le niveau {0} recevra le rôle {1}",
"xp_role_reward_remove_role": "L'utilisateur qui atteint le level {0} perdra le rôle {1}",
"cur_reward_cleared": "Atteindre le niveau {0} n'offrera plus aucune {0}.",
"xp_role_reward_add_role": "L'utilisateur qui atteint le niveau {0} recevra le rôle {1}.",
"xp_role_reward_remove_role": "Les utilisateurs qui atteindront le niveau {0} perdront le rôle {1}.",
"cur_reward_cleared": "Atteindre le niveau {0} n'offrera plus aucune {1}.",
"cur_reward_added": "Les utilisateurs atteignants le niveau {0} recevront {1}.",
"level_up_rewards": "Récompenses de monté de niveau",
"xp_receive_role": "Reçoit le rôle {0}.",
"xp_lose_role": "Perd le rôle {0}",
"club_create_error": "Echec de la création du club. Vérifier que vous êtes niveau 5 et plus ou que vous n'êtes pas déjà membre d'un club.",
"club_create_error": "Échec de la création du club. Vérifier que vous êtes au minimum niveau 5 ou que vous n'êtes pas déjà membre d'un club.",
"club_created": "Club {0} créé avec succès !",
"club_not_exists": "Ce club n'existe pas.",
"club_applied": "Vous avez candidaté pour le club {0}.",
"club_apply_error": "Erreur lors de l'envoie de la candidature. Soit vous faites déjà partie d'un club, soit vous ne possédez pas le niveau minimum requis ou sinon vous avez été banni de ce club.",
"club_apply_error": "Erreur lors de l'envoie de la candidature. Soit vous faites déjà partie d'un club, soit vous ne possédez pas le niveau requis ou vous avez été banni de ce club.",
"club_accepted": "L'utilisateur {0} a été accepté dans le club.",
"club_accept_error": "L'utilisateur est introuvable",
"club_left": "Vous avez quitté le club.",
"club_not_in_club": "Vous n'êtes pas dans un club, ou vous essayez de quitter le club dont vous êtes le propriétaire.",
"club_user_kick": "L'utilisateur {0} a été exclu du club {1} .",
"club_user_kick_fail": "Impossible d'exclure l'utilisateur. Vous n'êtes pas le propriétaire du club, où l'utilisateur n'est pas dans votre club.",
"club_user_banned": "L'utilisateur {0} a été banni du club {1}",
"club_user_kick_fail": "Impossible d'exclure l'utilisateur. Vous n'êtes pas le propriétaire du club, ou l'utilisateur n'est pas dans votre club.",
"club_user_banned": "L'utilisateur {0} a été banni du club {1}.",
"club_user_ban_fail": "Échec du bannissement. Vous n'êtes pas le propriétaire, ou cet utilisateur n'est pas dans votre club.",
"club_user_unbanned": "L'utilisateur {0} a été débanni du club {1}.",
"club_user_unban_fail": "Échec du débanissement. Vous n'êtes pas le propriétaire, ou cet utilisateur n'est pas dans votre club.",
"club_level_req_changed": "Changement du niveau requis à {0}",
"club_user_unban_fail": "Échec du débannissement. Vous n'êtes pas le propriétaire, ou cet utilisateur n'est pas dans votre club.",
"club_level_req_changed": "Le niveau requis du club a été changé sur {0}",
"club_level_req_change_error": "Échec du changement du niveau requis.",
"club_desc_updated": "Description du coup mise à jour : {0}",
"club_desc_updated": "La description du club est désormais « {0} ».",
"club_desc_update_failed": "Échec du changement de la description du club.",
"club_disbanded": "Le club {0} a été dissout",
"club_disband_error": "Erreur. Soit vous n'êtes pas dans un club ou soit vous n'êtes pas le propriétaire du club.",
"club_disband_error": "Erreur. Soit vous n'êtes pas dans un club ou vous n'êtes pas le propriétaire du club.",
"club_icon_error": "L'URL de l'image n'est pas valide ou vous n'êtes pas le propriétaire du club.",
"club_icon_set": "Nouvel icône de club défini.",
"club_bans_for": "Liste des bans pour le club {0}",
@@ -518,7 +518,7 @@
"event_duration_footer": "L'événement se fini dans {0} heures.",
"start_event_fail": "Échec de l'événement. Un événement est déjà en cours, où il y a une erreur avec le démarrage de l'événement.",
"event_title": "L'évènement {0} a commencé !",
"poll_closed": "Le sondage Clos !",
"poll_closed": "Le sondage est clos !",
"club_not_exists_owner": "Vous n'êtes ni le propriétaire ni l'administrateur de ce club.",
"club_admin_add": "{0} est désormais administrateur du club.",
"club_admin_remove": "{0} n'est plus administrateur du club.",
@@ -530,10 +530,10 @@
"feed_no_feed": "Vous n'avez souscrit à aucun fil d'actualité sur ce serveur.",
"timely_none": "Le propriétaire de ce bot n'a pas spécifié de récompense récurrente.",
"timely_already_claimed": "Vous avez déjà obtenu votre récompense récurrente. Vous pourrez l'obtenir de nouveau dans {0}.",
"timely": "Vous venez de réclamer vos {0}. Vous pourrez réclamer a nouveau dans {1}h.",
"timely": "Vous venez de réclamer vos {0}. Vous pourrez réclamer à nouveau dans {1}h.",
"timely_set": "Les utilisateurs pourront réclamer {0} toutes les {1}h.",
"timely_set_none": "Les utilisateurs ne pourront réclamer aucune monnaie récurrente.",
"timely_reset": "Tous les utilisateurs peuvent réclamer leur monnaie récurrente de nouveau.",
"timely_set_none": "Les utilisateurs ne pourront réclamer aucune devise récurrente.",
"timely_reset": "Tous les utilisateurs peuvent réclamer leur devise récurrente de nouveau.",
"market_cap": "Capitalisation Boursière",
"volume_24h": "Volume (24h)",
"change_7d_24h": "Change (7j/24h)",
@@ -558,13 +558,13 @@
"bj_joined": "a rejoint la partie de BlackJack",
"no_invites": "Aucune invitation sur cette page.",
"invite_deleted": "L'invitation {0} a été supprimée.",
"deleted": "Réaction personnalisée supprimée",
"deleted": "La réaction personnalisée a été supprimée.",
"insuff_perms": "Permissions insuffisantes. Vous devez être le détenteur du bot pour les réactions personnalisées globales ou administrateur pour celles du serveur.",
"custom_reactions": "Réactions personnalisées",
"new_cust_react": "Nouvelle réaction personnalisée",
"no_found": "Aucune réaction personnalisée trouvée.",
"no_found_id": "Aucune réaction personnalisée trouvée avec cet ID.",
"cleared": "{0} réaction personnalisées ont été supprimées.",
"cleared": "L'ensemble des {0} réactions personnalisées ont été supprimées.",
"crr_reset": "La réaction personnalisée d'ID {0} n'ajoutera désormais plus de réactions.",
"crr_set": "La réaction personnalisée d'ID {0} ajoutera les réactions suivantes au message de réponse : {1}",
"invalid_emojis": "Tous les emojis spécifiés sont invalides.",
@@ -572,9 +572,9 @@
"vcrole_not_found": "Ce salon vocal n'a pas de VcRole.",
"attachments": "Pièces jointes",
"avatar_changed": "Avatar changé",
"banmsg_disabled": "Les messages d'exclusion sont désactivés. Vous pouvez les activer en définissant banmsg autrement que '-'.",
"banmsg_default": "Aucun message d'exclusion défini. La conduite par défaut sera appliquée.",
"banned_pl": "Exclu",
"banmsg_disabled": "Les messages d'exclusion sont désactivés. Vous pouvez les activer en définissant banmsg autrement qu'avec '-'.",
"banmsg_default": "Aucun message de bannissement défini. La conduite par défaut sera appliquée.",
"banned_pl": "banni",
"bot_name": "Le nom du bot a été changé pour {0}",
"bot_status": "Le statut du bot a été changé pour {0}",
"ch_name_change": "Nom de salon modifié",
@@ -588,57 +588,55 @@
"insuf_perms_i": "Mes permissions sont insuffisantes.",
"insuf_perms_u": "Vos permissions sont insuffisantes.",
"lang_set_fail": "Échec de la définition des paramètres régionaux. Revoyez l'aide de cette commande.",
"lang_set_show": "La langue de ce serveur est défini sur {0} - {1}",
"lang_set_show": "La langue de ce serveur est définie sur {0} - {1}",
"log": "Journalisation des évènements de {0} dans ce salon.",
"log_all": "Journalisation de tout les évènements dans ce salon.",
"log_all": "Journalisation de tous les évènements dans ce salon.",
"log_disabled": "Journalisation désactivée.",
"log_events": "Evènements de journalisation auxquels vous pouvez vous abonner :",
"log_ignore": "La journalisation ignorera {0}",
"log_not_ignore": "La journalisation n'ignorera pas {0}",
"log_stop": "La journalisation de l'évènement {0} arrêtée.",
"msg_not_found": "Message introuvable.",
"time_too_long": "Le temps spécifié est trop long.",
"log_vc_moved": "{0} à été déplacé du salon vocal {1} à {2}",
"msg_del": "Message supprimé dans #{0}",
"msg_update": "Message mise-à-jour dans #{0}",
"msg_update": "Message mis à jour dans #{0}",
"muted_pl": "Muets",
"muted_sn": "Muet",
"mute_role": "Rôle muet actuel est {0}",
"mute_perms": "Vous ne pouvez pas mettre en sourdine un utilisateur dont le rôle est au-dessus du vôtre, sauf si vous êtes le propriétaire du serveur.",
"mute_role": "Le rôle muet actuel est {0}",
"mute_perms": "Vous ne pouvez pas rendre muet un utilisateur dont le rôle est au-dessus du vôtre, sauf si vous êtes le propriétaire du serveur.",
"new_msg": "Nouveau message",
"new_nick": "Nouveau surnom",
"new_topic": "Nouveau thème",
"new_topic": "Nouveau sujet",
"nick_change": "Surnom modifié",
"no_shard_id": "Aucune shard avec cet ID n'a été trouvée.",
"nsfw_set_true": "Le NSFW est activé sur ce salon.",
"nsfw_set_false": "Le NSFW est désactivé sur ce salon.",
"no_shard_id": "Aucun fragment avec cet ID n'a été trouvée.",
"nsfw_set_true": "Le NSFW a été **activé** sur ce salon.",
"nsfw_set_false": "Le NSFW a été **désactivé** sur ce salon.",
"old_msg": "Ancien message",
"old_nick": "Ancien surnom",
"old_topic": "Ancien thème",
"prot_cant_use_time": "Vous ne pouvez spécifier de durée pour cette sanction.",
"rar_err": "Échec de la suppression de rôles. Mes permissions sont insuffisantes.",
"old_topic": "Ancien sujet",
"prot_cant_use_time": "Vous ne pouvez pas spécifier de durée pour cette sanction.",
"rar_err": "Échec de la suppression des rôles. Permissions insuffisantes.",
"rc_perms": "Une erreur est survenue suite à une couleur invalide ou des permissions insuffisantes.",
"remrole": "Succès du retrait du rôle {0} de l'utilisateur {1} ",
"remrole": "Le rôle {0} a été retiré de l'utilisateur {1} avec succès.",
"remrole_err": "Échec du retrait du rôle. Permissions insuffisantes.",
"renrole_err": "Échec du renommage du rôle. Permissions insuffisantes.",
"renrole_perms": "Vous ne pouvez pas modifier des rôles supérieurs au votre ou à mon rôle le plus élevé.",
"renrole_perms": "Vous ne pouvez pas modifier des rôles supérieurs au votre, ou mon plus haut.",
"ropl_added": "Ajoutés.",
"ropl_disabled": "Rotation du statut de jeu désactivée.",
"ropl_enabled": "Rotation du statut de jeu activée.",
"ropl_list": "Voici une liste de statuts en rotation :\n{0}",
"ropl_list": "Voici une liste des statuts en rotation :\n{0}",
"ropl_not_set": "Aucun statut en rotation réglé.",
"setrole_err": "Échec de l'ajour de rôle. Mes permissions sont insuffisantes.",
"set_avatar": "Nouvel avatar défini!",
"set_game": "Nouveau jeu défini!",
"set_stream": "Nouveau stream défini!",
"set_topic": "Nouveau thème du salon réglé.",
"shard_reconnecting": "La Shard {0} se reconnecte.\n",
"soft_banned_pl": "ban-soft",
"text_chan_created": "Salon textuel crée.",
"shard_reconnecting": "Le fragment {0} se reconnecte.",
"soft_banned_pl": "radié·e·s",
"text_chan_created": "Salon textuel créé.",
"text_chan_destroyed": "Salon textuel supprimé.",
"duration": "Durée",
"unmuted_sn": "Démuté",
"unable_to_dm_user": "Échec de l'envoi de message par MP.",
"unmuted_sn": "Parole rendue",
"unable_to_dm_user": "Échec de l'envoi du message par MP.",
"username_changed": "Nom de membre changé",
"user_banned": "Utilisateur banni",
"user_joined": "Un utilisateur à rejoint",
@@ -646,21 +644,21 @@
"user_role_add": "Rôle utilisateur ajouté",
"user_role_rem": "Rôle utilisateur retiré",
"user_status_change": "{0} est maintenant {1}",
"user_vjoined": "{0} a rejoint {1} salon vocal.",
"user_vleft": "{0} a quitté {1} salon vocal.",
"user_vjoined": "{0} a rejoint le salon vocal{1}.",
"user_vleft": "{0} a quitté le salon vocal {1}.",
"user_vmoved": "{0} est passé du salon vocal {1} au salon vocal {2}.",
"voice_chan_created": "Salon vocal créé",
"voice_chan_destroyed": "Salon vocal détruit",
"xmuted_text": "Utilisateur {0} du tchat textuel de {1}",
"xmuted_text_and_voice": "Utilisateur {0} du tchat textuel et vocal par {1}",
"xmuted_text": "Utilisateur {0} du salon textuel de {1}",
"xmuted_text_and_voice": "Utilisateur {0} du salon textuel et vocal par {1}",
"xmuted_voice": "Utilisateur {0} du vocal {1}",
"user_unbanned": "Utilisateur débanni",
"presence_updates": "Présence de mise-à-jours",
"presence_updates": "Présence de mises à jour",
"awarded": "donne la récompense de {0} à {1}",
"deck_reshuffled": "Cartes mélangées.",
"flipped": "a retourné {0}.",
"flip_invalid": "Chiffre spécifié invalide. Vous pouvez retourner entre 1 et {0} pièces.",
"cards_left": "{0} cartes restantes dans le jeu",
"cards_left": "{0} cartes restantes dans le jeu.",
"gifted": "a offert {0} à {1}",
"heads": "Face",
"no_more_cards": "Plus de cartes dans le deck.",
@@ -669,11 +667,11 @@
"take_fail": "Impossibilité de prendre {0} de {1} car l'utilisateur n'a pas autant de {2} !",
"commandlist_regen": "Liste de commandes régénérée.",
"desc": "Description",
"cant_dm": "Je ne peux pas te dm. Assurez-toi que tes DM sont ouverts. Allez dans **options (bouton rouage) -> Confidentialité et sécurité -> Autoriser les messages directs des membres du serveur**",
"cant_dm": "Je ne peux pas te dm. Assure-toi que tes MP sont ouverts. Va dans **options (bouton en forme de rouage) -> Confidentialité et sécurité -> Autoriser les messages directs des membres du serveur**",
"donate": "Vous pouvez soutenir le projet NadekoBot sur\nPatreon <{0}> ou\nPaypal<{1}>\nN'oubliez pas de laisser votre nom ou ID Discord dans le message.\n\n**Merci beaucoup**♥️",
"guide": "**Liste de commandes** : <{0}>\n**Des guides et documents d'hébergement peuvent être trouvés ici** : <{1}>",
"list_of_modules": "Liste des modules",
"autohentai_started": "Autohentai commencé. Reposte toutes les {0}sec avec un des tags suivant:\n{1}",
"autohentai_started": "Autohentai commencé. Reposte toutes les {0}sec avec l'un des tags suivant:\n{1}",
"tag": "Tag",
"animal_race_full": "La course est pleine! Départ imminent.",
"animal_race_join_bet": "{0} rejoint en tant que {1} et parie {2} !",
@@ -708,7 +706,7 @@
"set_music_channel": "Je vais maintenant sortir les chansons en cours de lecture, terminées, mises en pause et supprimées sur ce salon.",
"unset_music_channel": "Je vais maintenant afficher les chansons en cours de lecture, terminées, mises en pause et supprimées des lecteurs de musique nouvellement créés dans le salon à partir de laquelle la musique a été lancée.",
"song_moved": "Chanson déplacée",
"to_position": "A la position",
"to_position": "À la position",
"volume_input_invalid": "Le volume doit être entre 0 et 100",
"link_filter_channel_off": "Filtrage des liens désactivé sur ce salon.",
"link_filter_channel_on": "Filtrage des liens activé sur ce salon.",
@@ -717,20 +715,20 @@
"avatar_none": "L'utilisateur {0} n'a pas d'avatar défini.",
"anime_no_fav": "Pas danimé favori",
"atl_ad_started": "Début de la traduction automatique des messages dans ce salon. Les messages de l'utilisateur seront automatiquement supprimés.",
"atl_removed": "Votre langue de traduction automatique a été supprimée.",
"atl_removed": "Votre langue de traduction automatique a été retirée.",
"atl_set": "Votre langue de traduction automatique a été définie sur {0}>{1}",
"atl_started": "Début de la traduction automatique des messages sur cette chaîne.",
"atl_stopped": "Arrêt de la traduction automatique des messages sur ce canal.",
"atl_started": "Début de la traduction automatique des messages sur ce salon.",
"atl_stopped": "Arrêt de la traduction automatique des messages sur ce salon.",
"bad_input_format": "Mauvais format d'entrée, ou quelque chose s'est mal passé.",
"card_not_found": "N'a pas pu trouver cette carte.",
"catfact": "Fait",
"comic_not_found": "Comic introuvé.",
"completed": "Achevé(e)",
"dropped": "Abandonné(e)",
"completed": "Achevé·e",
"dropped": "Abandonné·e",
"error_occured": "Une erreur est survenue.",
"failed_finding_anime": "N'a pas pu trouver l'animu.",
"failed_finding_manga": "N'a pas pu trouver le mango.",
"imdb_fail": "N'a pas pu trouver ce film.",
"failed_finding_anime": "Échec pour trouver cet anime.",
"failed_finding_manga": "Échec pour trouver ce manga.",
"imdb_fail": "Échec pour trouver ce film.",
"invalid_lang": "Langue source ou cible invalide.",
"jokes_not_loaded": "Blagues non chargées.",
"list_of_place_tags": "Liste des {0} tags de lieu",
@@ -742,13 +740,13 @@
"on_hold": "En attente",
"osu_api_key": "Une clé API osu! est demandée.",
"osu_failed": "Échec de récupération de la signature osu!.",
"osu_user_not_found": "Membre introuvé",
"osu_user_not_found": "Membre introuvé.",
"plan_to_watch": "À regarder",
"something_went_wrong": "Quelque chose a mal tourné.",
"specify_search_params": "Veuillez spécifier les paramètres de recherche.",
"store_url": "URL de la boutique",
"streamer_offline": "Streamer {0} est hors-ligne.",
"streamer_online": "Streamer {0} est en ligne avec {0} spectateurs.",
"streamer_online": "Streamer {0} est en ligne avec {1} spectateurs.",
"streams_follow_title": "Streams que vous suivez",
"streams_none": "Vous ne suivez aucun stream sur ce serveur.",
"stream_no": "Il n'y a pas de tel stream.",
@@ -776,7 +774,7 @@
"repeater_created": "Répéteur créé",
"repeater_removed": "Répéteur #{0} supprimé",
"no_roles_on_page": "Aucun rôle sur cette page.",
"no_shards_on_page": "Aucun shard dans cette page.",
"no_shards_on_page": "Aucun fragment dans cette page.",
"presence": "Présence",
"presence_txt": "{0} Serveurs\n{1} Salons Textuels\n{2} Salons Vocaux",
"quotes_deleted": "Suppression de toutes les citations avec le mot-clé {0}.",
@@ -791,19 +789,19 @@
"showemojis": "**Nom:** {0} **Lien:** {1}",
"users": "Membres",
"no_user_on_this_page": "Aucun membre sur cette page.",
"aliases_none": "Aucun psudonyme trouvé",
"aliases_none": "Aucun pseudonyme trouvé",
"alias_added": "Taper {0} sera désormais un alias de {1}.",
"alias_list": "List des pseudonymes",
"alias_list": "Liste des pseudonymes",
"alias_removed": "Le déclencheur {0} n'a plus d'alias.",
"alias_remove_fail": "Le déclencheur {0} n'avait pas d'alias.",
"punishment_unsupported": "Vous ne pouvez pas utiliser {0} action de punition avec cette fonctionnalité.",
"interval": "Intervalle",
"kicked_pl": "Kick",
"autocmd_add": "La commande {0} s'exécutera toutes les {1} dans ce salon.",
"autocmd_add": "La commande {0} s'exécutera toutes les {1} secondes dans ce salon.",
"autocmdlist_none": "Aucune commande automatique sur cette page.",
"warn_expire_set_delete": "Les avertissements seront supprimés après {0} jours.",
"warn_expire_set_clear": "Les avertissements seront effacés après {0} jours.",
"warn_expire_reset": "Les avertissements n'expirent plus.",
"warn_expire_set_clear": "Les avertissements seront annulés après {0} jours.",
"warn_expire_reset": "Les avertissements n'expireront plus.",
"warn_punish_set_timed": "J'appliquerai {0} punition pour {2} aux utilisateurs avec {1} avertissements.",
"clpa_obsolete": ":tada: **Les récompenses en devise Patreon sont désormais automatiques !** :tada:\nCette commande est désormais obsolète.\nSi vous n'avez pas reçu votre récompense pour l'engagement de ce mois, voici quelques-unes des raisons pour lesquelles cela pourrait être le cas.",
"time_new": "Temps",
@@ -818,8 +816,8 @@
"user_nick": "Surnom du membre {0} a été changé pour {1}",
"perm_override": "Les utilisateurs auront besoin de l'autorisation {0} pour exécuter la commande {1}.",
"define_unknown": "N'arrive pas à trouver une définition à ce terme.",
"queue_stopped": "Lecteur en arrêt. Utilisez la commande {0} pour commencer à jouer.",
"removed_song_error": "Chanson sur cet index n'existe pas",
"queue_stopped": "La lecture est arrêtée. Utilisez la commande {0} pour commencer à jouer.",
"removed_song_error": "La chanson n'existe pas sur cet index",
"nunchi_started": "Jeu de Nunchi a commencé avec {0} participants.",
"bot_strings_reloaded": "Les chaînes de caractères du bot ont été mises à jour.",
"xpn_setting_global": "Notifications de montée en niveau Global",
@@ -866,9 +864,9 @@
"leagues_not_found": "Incapable de récupérer les donnes depuis l'API Path of Exile.",
"perm_override_page_none": "Aucune dérogation sur cette page.",
"perm_override_all": "Toutes les dérogations d'autorisation de Discord ont été effacés.",
"perm_override_all_confirm": "Êtes-vous sûr de vouloir supprimer **TOUS** les dérogations d'autorisation de Discord sur ce serveur ? Cette action est irréversible.",
"perm_override_all_confirm": "Êtes-vous sûr de vouloir supprimer **TOUTES** les dérogations d'autorisation de Discord sur ce serveur ? Cette action est irréversible.",
"perm_overrides": "les dérogations d'autorisation de Discord",
"perm_override_reset": "Les dérogations d'autorisation de Discord pour cette commande ont été effacés.",
"perm_override_reset": "Les dérogations d'autorisation de Discord pour cette commande ont été effacées.",
"reset": "Réinitialisation de l'XP",
"reset_server_confirm": "Êtes-vous sûr de vouloir réinitialiser l'XP de tous les utilisateurs du serveur ?",
"reset_user_confirm": "Êtes-vous sûr de vouloir réinitialiser l'XP de l'utilisateur spécifié sur ce serveur ?",
@@ -888,7 +886,7 @@
"aar_roles": "Les rôles suivants sont assignés aux membres qui joignent ce serveur: {0}",
"aar_role_removed": "Les membres ne seront plus assignés {0} au moment de joindre le serveur.",
"remove_roles_pl": "ont eu leurs rôles supprimés",
"role_too_high": "Vous ne pouvez pas utiliser cette commande avec des rôles supérieurs à votre rôle le plus élevé, sauf si vous êtes administrateur de serveur.",
"role_too_high": "Vous ne pouvez pas utiliser cette commande avec des rôles supérieurs au votre, sauf si vous êtes administrateur de serveur.",
"log_vc_joined": "{0} a rejoint le salon vocal {1}",
"log_vc_left": "{0} a quitté le salon vocal {1}",
"protection_not_running": "Protection {0} n'est pas activée",
@@ -903,9 +901,9 @@
"blacklisted_channels": "Salons sur la liste noire",
"blacklisted_servers": "Serveurs sur la liste noire",
"_class": "Classe",
"stream_not_following_any": "Vous ne suivez aucun stream.",
"stream_message_set_all": "Tous les {0} streams suivis ont maintenant le message d'annonce spécifié.",
"repeater_exceed_limit": "Vous ne pouvez avoir plus de {0} rèpètiteurs par serveur.",
"stream_not_following_any": "Vous ne suivez aucune diffusion.",
"stream_message_set_all": "Toutes les {0} diffusions suivies ont maintenant le message d'annonce spécifié.",
"repeater_exceed_limit": "Vous ne pouvez avoir plus de {0} répéteurs par serveur.",
"repeater_remove_fail": "Échec de la suppression du répéteur sur cet index. Soit vous avez spécifié un index non valide, soit le répéteur était en cours d'exécution à ce moment-là, auquel cas, réessayez dans quelques secondes.",
"interval_colon": "Intervalle:",
"executes_in_colon": "S'exécute dans :",
@@ -945,7 +943,7 @@
"module_description_xp": "Gagnez de l'XP en fonction de l'activité de tchat, vérifiez les cartes XP des utilisateurs",
"module_description_missing": "Description manquante pour ce module.",
"obsolete_use": "⚠ Obsolète, utilisez plutôt {0}.",
"purge_user_confirm": "Êtes-vous sûr(e) de vouloir purger {0} de la base des données?",
"purge_user_confirm": "Êtes-vous sûr·e de vouloir purger {0} de la base des données?",
"expr_import_no_input": "Saisie invalide. Aucun chargement de fichier ou saisie de texte trouvé.",
"expr_import_invalid_data": "Impossible d'analyser le fichier. Assurez-vous qu'il s'agisse d'un fichier .yml",
"inv_expire": "Expire dans:",
@@ -953,5 +951,29 @@
"empty_page": "Cette page est vide.",
"pages": "Pages",
"favorites": "Favoris",
"tags": "Tags"
"tags": "Tags",
"invalid_emoji_link": "Le lien spécifié n'est pas une image ou excède 256KB.",
"emoji_add_error": "Erreur lors de l'ajout d'emoji. Soit vous n'avez plus d'emplacements pour emoji, soit la taille de l'image est inadéquate.",
"emoji_added": "Nouveau emoji ajouté: {0}",
"boost_on": "Annonces Boost activées dans ce salon.",
"boost_off": "Annonces Boost désactivées.",
"boostmsg_cur": "Message Boost actuel: {0}",
"boostmsg_enable": "Activez les messages Boost en tapant {0}",
"boostmsg_new": "Nouveau message Boost défini.",
"boostdel_off": "La suppression automatique des messages Boost a été désactivée.",
"boostdel_on": "Les messages Boost seront supprimés après {0} secondes.",
"log_ignored_channels": "Salons ignorés",
"log_ignored_users": "Utilisateurs ignorés",
"log_ignore_user": "La journalisation ignorera le membre {0}",
"log_not_ignore_user": "La journalisation n'ignorera plus le membre {0}",
"log_ignore_chan": "La journalisation ignorera le salon {0}",
"log_not_ignore_chan": "La journalisation n'ignorera plus le salon {0}",
"streams_cleared": "Tous les streams suivis sur ce serveur ont été supprimés.",
"warn_weight": "Poids: {0}",
"warn_count": "{0} actuel·s, {1} total",
"mass_ban_in_progress": "En train de bannir {0} membres...",
"mass_ban_completed": "{0} membres bannis.",
"reminder_server_list": "Liste des rappels du server.",
"imageonly_enable": "Ce salon est maintenant exclusivement pour les images.",
"imageonly_disable": "Ce salon n'est plus exclusivement pour les images."
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +1,16 @@
{
"api_key_missing": "A chave de API necessária para esse comando não foi fornecida.",
"api_key_missing": "Falta a chave API.",
"quote_deleted": "Citação #{0} excluída.",
"redacted_too_long": "O texto é longo demais.",
"trigger": "Gatilho",
"response": "Resposta",
"fw_cleared": "Todas as palavras filtradas foram removidas e suas configurações foram resetadas.",
"aar_disabled": "Usuários que entrarem no servidor não receberão cargos automaticamente.",
"fw_cleared": "Removido todos os filtros e configurações de filtros de palavras do canal.",
"aar_disabled": "**Cargos automáticos** para usuários que entrarem está **desativado**",
"bandm": "Você foi banido do servidor {0}.\nMotivo: {1}",
"banned_user": "Usuário Banido",
"byedel_off": "A exclusão automática de mensagens de despedida foi desativada.",
"byedel_on": "Mensagens de despedida serão excluídas após {0} segundos.",
"byemsg_cur": "Mensagem de despedida atual",
"byemsg_cur": "Mensagem de despedida atual: {0}",
"byemsg_enable": "Digite {0} para ativar a mensagem de despedida.",
"byemsg_new": "Nova mensagem de despedida definida com sucesso.",
"bye_off": "A mensagem de despedida foi desativada.",
@@ -33,22 +33,22 @@
"fwdm_stop": "DMs não serão mais encaminhadas.",
"greetdel_off": "Mensagens de boas-vindas não serão mais automaticamente excluídas.",
"greetdel_on": "Mensagens de boas-vindas serão excluídas após {0} segundos.",
"greetdmmsg_cur": "Mensagem direta de boas-vindas atual",
"greetdmmsg_cur": "Mensagem direta de boas-vindas atual: {0}",
"greetdmmsg_enable": "Digite {0} para ativar a mensagem direta de boas-vindas.",
"greetdmmsg_new": "A mensagem direta de boas-vindas foi definida com sucesso.",
"greetdm_off": "A mensagem direta de boas-vindas foi desativada.",
"greetdm_on": "A mensagem direta de boas-vindas foi ativada.",
"greetmsg_cur": "Mensagem de boas-vindas atual",
"greetmsg_cur": "Mensagem de boas-vindas atual: {0}",
"greetmsg_enable": "Digite {0} para ativar a mensagem de boas-vindas.",
"greetmsg_new": "Mensagem de boas-vindas definida com sucesso.",
"greet_off": "A mensagem de boas-vindas foi desativada.",
"greet_on": "A mensagem de boas-vindas foi ativada nesse canal.",
"hierarchy": "Você não pode usar esse comando em usuários de cargo igual ou superior ao seu na hierarquia de cargos.",
"hierarchy": "Você não pode usar esse comando em usuários de cargo igual ou superior ao seu (ou meu) na hierarquia de cargos.",
"invalid_format": "Input inválido.",
"kickdm": "Você foi expulso do servidor {0}.\nMotivo: {1}",
"kicked_user": "Usuário expulso",
"lang_list": "Lista de idiomas",
"lang_set_bot": "O idioma do bot foi definido para {0} - {1}",
"lang_set_bot": "O idoma do bot foi definido para {0} - {1}",
"lang_set_bot_show": "O idioma do bot está definido como {0} - {1}",
"lang_set": "O idioma do bot no servidor foi definido para {0} - {1}",
"message_sent": "Mensagem enviada.",
@@ -61,7 +61,7 @@
"raid_cnt": "O número de usuários deve estar entre {0} e {1}.",
"raid_stats": "Se {0} ou mais usuários entrarem no servidor em um intervalo de {1} segundos, aplicarei {2} a eles.",
"raid_time": "O tempo deve ser entre {0} e {1} segundos.",
"rar": "Todas os cargos do usuário {0} foram removidos com sucesso.",
"rar": "Todos os cargos do usuário {0} foram removidos com sucesso.",
"rc": "A cor do cargo {0} foi alterada.",
"renrole": "Cargo renomeado.",
"reprm": "Mensagem de status removida: {0}",
@@ -108,9 +108,9 @@
"not_enough": "Você não tem {0} suficiente.",
"raffled_user": "Usuário sorteado",
"slot_bet": "Aposta",
"slot_jackpot": "WOAAHHHHHH!!! Parabéns!!! x{0}",
"slot_jackpot": "UHUUUULLLL!!! Parabéns!!! x{0}",
"slot_single": "Um único {0}, x{1}",
"slot_three": "Whoa! Que sorte! Três do mesmo tipo! x{0}",
"slot_three": "Uhul! Que sorte! Três do mesmo tipo! x{0}",
"slot_two": "Opa! Dois {0} - ganha x{1}",
"won": "Ganhou",
"transactions": "Transações do usuário {0}",
@@ -123,13 +123,13 @@
"requires": "Requer",
"blacklisted_tag_list": "Lista de tags na lista negra:",
"blacklisted_tag_add": "O termo {0} foi adicionado à lista negra.",
"blacklisted_tag_remove": "A tag {0} não está mais na lista negra.",
"blacklisted_tag_remove": "O termo {0} não está mais na lista negra.",
"animal_race": "Corrida de animais",
"animal_race_failed": "Não foi possível iniciar a corrida por não haver participantes suficientes.",
"animal_race_join": "{0} entrou como {1}",
"animal_race_join_instr": "Digite {0}race para participar da corrida.",
"animal_race_join_instr": "Digite {0}jr para participar da corrida.",
"animal_race_starting": "Iniciando em {0} segundos ou quando a sala estiver cheia.",
"animal_race_won": "{0}, como {1}, venceu a corrida!",
"animal_race_won": "{0} como {1} venceu a corrida!",
"dice_invalid_number": "Número inválido. Você pode rolar {0}-{1} dados de uma vez.",
"dice_rolled": "rolou {0}",
"dice_rolled_num": "Dados rolados: {0}",
@@ -149,7 +149,7 @@
"waifu_not_yours": "Essa waifu não lhe pertence.",
"waifu_not_yourself": "Você não pode se reivindicar.",
"waifu_recent_divorce": "Por ter se divorciado recentemente, você deve esperar {0} horas e {1} minutos para se divorciar novamente.",
"waifu_transfer_fail": "Você não é o dono dessa waifu.",
"waifu_transfer_fail": "Você não é o dono desta waifu ou não tem como compra-la.\nSua entrada é inválida.",
"waifu_transfer_success": "A posse de {0} foi transferida de {1} para {2}",
"nobody": "Ninguém",
"waifu_divorced_notlike": "Você se divorciou uma waifu que não gosta de você. Você recebeu {0}.",
@@ -183,8 +183,8 @@
"hangman_game_started": "Jogo da forca iniciado",
"hangman_running": "Já existe um jogo da forca em andamento nesse canal.",
"hangman_types": "Lista dos temas disponíveis no \"{0}hangman\":",
"no_results": "Nenhum resultado.",
"picked": "pegou {0}",
"no_results": "Nenhum resultado encontrado.",
"picked": "coletou {0}",
"planted": "{0} plantou {1}",
"trivia_already_running": "Já há um quiz em andamento nesse servidor.",
"trivia_game": "Quiz",
@@ -217,7 +217,7 @@
"playlist_queue_complete": "Playlist adicionada à fila.",
"queued_song": "Músicas na fila",
"removed_song": "Música removida",
"repeating_track": "Repetindo música",
"repeating_track": "A música ficará sendo repetida.",
"volume_set": "Volume ajustado para {0}%",
"acm_disable": "TODOS OS MÓDULOS foram desativados no canal {0}.",
"acm_enable": "TODOS OS MÓDULOS foram ativados no canal {0}.",
@@ -353,8 +353,8 @@
"poll_voted": "{0} votou.",
"poll_invalid_input": "Input inválido. Você precisa de uma pergunta e pelo menos 2 respostas, todas separadas por `;`.",
"x_votes_cast": "Total de votos: {0}",
"pick_pl": "Pegue digitando {0}",
"pick_sn": "Pegue digitando {0}",
"pick_pl": "Pegue o item digitando `{0}pick`",
"pick_sn": "Pegue digitando `{0}pick`",
"sql_confirm_exec": "Confirme a execução do seguinte comando SQL",
"no_vcroles": "Não há cargos de canal de voz.",
"user_muted_time": "{0} foi **mutado** nos chats de voz e texto por {1} minutos.",
@@ -390,13 +390,13 @@
"warn_punish_rem": "Ter {0} advertências não causará mais uma punição.",
"warn_punish_set": "Usuários que acumularem {1} advertências serão punidos com {0}.",
"cant_apply_punishment": "Não tenho as permissões necessárias para aplicar essa punição.",
"clpa_fail_already": "Talvez você já tenha recebido sua recompensa deste mês. Você só pode recebê-las uma vez a cada mês, a menos que aumente sua doação.",
"clpa_fail_already": "Talvez você já tenha recebido sua recompensa deste mês. Você só pode recebê-las uma vez a cada mês, a menos que aumente sua doação.\nCheque utilizando o comando `.curtrs` .",
"clpa_fail_already_title": "Já foi recompensado",
"clpa_fail_conn": "Sua conta do Discord pode não estar conectada ao Patreon. Se você não sabe o que isso significa ou não sabe como conectá-la, você deve ir à [Página de Configurações de Conta do Patreon](https://patreon.com/settings/account) e clicar no botão 'Connect to Discord'.",
"clpa_fail_conn_title": "Sua conta do Discord não está conectada",
"clpa_fail_sup": "Para receber recompensas, você precisa apoiar o projeto no Patreon. Use o comando {0} para receber o link.",
"clpa_fail_sup_title": "Não é um patrono",
"clpa_fail_wait": "Você precisa esperar algumas horas após o pagamento. Tente novamente mais tarde.",
"clpa_fail_wait": "Você precisa esperar de 5 a 10 minutos após o pagamento.Tente novamente mais tarde (tambem confira se sua conta do patreon está conectada ao seu discord).",
"clpa_fail_wait_title": "Espere um pouco",
"clpa_too_early": "Recompensas podem ser reivindicadas a partir do 5° dia de cada mês",
"shop": "Loja",
@@ -420,7 +420,7 @@
"shop_list_item_not_unique": "Esse item já existe na loja.",
"shop_purchase": "Compra efetuada no servidor {0}",
"shop_role_not_found": "O cargo que está a venda não existe mais.",
"role_not_found": "Cargo não encontrado.",
"role_not_found": "Cargo não encontrado: {0}",
"shop_role_purchase": "Você comprou a cargo {0} com sucesso.",
"shop_role_purchase_error": "Erro ao atribuir o cargo. Sua compra foi reembolsada.",
"unique_items_left": "Restam {0} itens únicos.",
@@ -446,7 +446,7 @@
"global_perms_reset": "Permissões globais foram resetadas.",
"module": "Módulo: {0}",
"hangman_stopped": "O jogo da forca foi encerrado.",
"queue_shuffled": "As músicas tocarão em ordem aleatória.",
"queue_shuffled": "A fila de música está em modo aleatório.",
"warnings_list": "Lista de usuários advertidos no servidor",
"waifu_gift": "Presenteou {1} com {0}",
"waifu_gift_shop": "Loja de presentes",
@@ -498,7 +498,7 @@
"club_user_kick_fail": "Erro ao expulsar. Ou você não é o dono do clube ou o usuário não está no seu clube.",
"club_user_banned": "O usuário {0} foi banido do clube {1}.",
"club_user_ban_fail": "Falha ao banir. Você não é o dono do clube ou o usuário não está, nem se aplicou ao seu clube.",
"club_user_unbanned": "O usuário {0} foi desbanido do seu clube.",
"club_user_unbanned": "Desbanindo o usuário {0} no clube {1}.",
"club_user_unban_fail": "Falha ao desbanir. Você não é o dono do clube ou o usuário não está, nem se aplicou ao seu clube.",
"club_level_req_changed": "O requisito de nível do clube foi alterado para {0}.",
"club_level_req_change_error": "Falha ao alterar o requisito de nível.",
@@ -515,7 +515,7 @@
"self_assign_are_not_exclusive": "Agora usuários poderão escolher quaisquer cargos de qualquer grupo.",
"new_reaction_event": "Clique na reação {0} para receber {1}\nRestam {2}.",
"new_gamestatus_event": "Digite o código secreto em qualquer canal para receber {1}\nRestam {2}.",
"event_duration_footer": "O evento termina em {0}h {1}m",
"event_duration_footer": "O evento está ativo por {0} horas.",
"start_event_fail": "Falha ao iniciar o evento. Já há outro evento em execução ou ocorreu um erro ao iniciá-lo.",
"event_title": "Um evento do tipo {0} foi iniciado!",
"poll_closed": "Votação encerrada!",
@@ -536,8 +536,8 @@
"timely_reset": "Agora, usuários poderão reivindicar recompensas periodicamente.",
"market_cap": "Valor de mercado",
"volume_24h": "Volume (24h)",
"change_7d_24h": "Variação (7d/24h)",
"crypto_not_found": "Não foi possível encontrar uma criptomoeda com esse nome.",
"change_7d_24h": "Variação (7d / 24h)",
"crypto_not_found": "Uma criptomoeda com esse nome não foi encontrada.",
"did_you_mean": "Você quis dizer {0}?",
"self_assign_level_req": "O cargo auto-atribuível {0} agora requer pelo menos nível de servidor {1}.",
"self_assign_not_level": "Esse cargo auto-atribuível requer pelo menos nível de servidor {0}.",
@@ -593,8 +593,6 @@
"log_all": "Registrando todos os eventos nesse canal.",
"log_disabled": "Registros desabilitado.",
"log_events": "Eventos de registro que você pode assinar:",
"log_ignore": "Registros vai ignorar {0}",
"log_not_ignore": "Registros não irá ignorar {0}",
"log_stop": "Parando de registrar evento {0}.",
"msg_not_found": "Mensagem não encontrada.",
"time_too_long": "O tempo que você especificou é muito longo.",
@@ -847,7 +845,7 @@
"stopped": "Repost encerrado.",
"restart_fail": "Você precisa configurar o RestartCommand no arquivo creds.yml",
"restarting": "Reiniciando.",
"edit_fail": "Não existe reação personalizada com essa ID.",
"edit_fail": "Não existe reação personalizada com esse ID.",
"streaming": "Transmitindo",
"rafflecur": "Rifa {0}",
"rafflecur_joined": "O usuário {0} entrou no sorteio",
@@ -953,5 +951,29 @@
"empty_page": "Essa página esta vazia.",
"pages": "Páginas",
"favorites": "Favoritos",
"tags": "Etiquetas"
"tags": "Etiquetas",
"invalid_emoji_link": "O link especificado não retorna para uma imagem ou excede 256KB.",
"emoji_add_error": "Erro ao adicionar emoji. O servidor não possui mais slots de emoji disponíveis ou o tamanho da imagem é incompatível.",
"emoji_added": "Novo emoji adicionado: {0}",
"boost_on": "Os anúncios de impulsionamento foram ativados neste canal.",
"boost_off": "Os anúncios de impulsionamento foram desativados.",
"boostmsg_cur": "Mensagem atual de impulsionamento: {0}",
"boostmsg_enable": "Ative os anúncios de impulsionamento digitando {0}",
"boostmsg_new": "Nova mensagem de impulsionamento definida.",
"boostdel_off": "A deleção automática dos anúncios de impulsionamento foi desativada.",
"boostdel_on": "Anúncios de impulsionamento serão deletados após {0} segundos.",
"log_ignored_channels": "Canais ignorados",
"log_ignored_users": "Usuários ignorados",
"log_ignore_user": "O registro de logs passará a ignorar o usuário {0}",
"log_not_ignore_user": "O registro de logs não vai mais ignorar o usuário {0}",
"log_ignore_chan": "O registro de logs passará a ignorar o canal {0}",
"log_not_ignore_chan": "O registro de logs não vai mais ignorar o canal {0}",
"streams_cleared": "Todas as transmissões seguidas neste servidor foram removidas.",
"warn_weight": "Peso: {0}",
"warn_count": "{0} atual, {1} total",
"mass_ban_in_progress": "Banindo {0} usuários...",
"mass_ban_completed": "{0} usuários banidos.",
"reminder_server_list": "Lista de lembretes do servidor",
"imageonly_enable": "Agora este canal é exclusivo para imagens.",
"imageonly_disable": "Este canal não é mais exclusivo para imagens."
}

View File

@@ -331,7 +331,7 @@
"owner": "Владелец",
"owner_ids": "Идентификаторы владельцев",
"region": "Регион",
"remind": "Я напомню пользователю {0}, чтобы он {1} через {2} `({3:d.M.yyyy} в {4:HH:mm}`",
"remind": "Я напомню пользователю {0}, чтобы он {1} через {2} `({3:d.M.yyyy} в {4:HH:mm})`",
"remind_too_long": "Время напоминания превысило максимум.",
"repeaters_none": "Никаких повторений не включено на этом сервере.",
"repeat_invoke_none": "На этом сервере не найдено никаких повторяющихся сообщений.",
@@ -593,8 +593,6 @@
"log_all": "Регистрация всех событий в этом канале.",
"log_disabled": "Регистрация событий отключена.",
"log_events": "Журнал событий, на которые вы можете подписаться:",
"log_ignore": "Ведение журнала игнорирует {0}",
"log_not_ignore": "Ведение журнала не игнорирует {0}",
"log_stop": "Прекращено ведение журнала события {0}.",
"msg_not_found": "Сообщение не найдено.",
"time_too_long": "Вы указали слишком много времени.",
@@ -953,5 +951,29 @@
"empty_page": "Эта страница пуста.",
"pages": "Страницы",
"favorites": "Любимое",
"tags": "Теги"
"tags": "Теги",
"invalid_emoji_link": "Указанная ссылка либо не является изображением, либо превышает 256 КБ.",
"emoji_add_error": "Ошибка добавления эмодзи. Либо у вас закончились слоты для смайликов, либо размер изображения не отвечающий требованиям.",
"emoji_added": "Добавлен новый эмодзи: {0}",
"boost_on": "На этом канале включены объявления бустов.",
"boost_off": "Буст-объявления отключены.",
"boostmsg_cur": "Текущее буст сообщение: {0}",
"boostmsg_enable": "Включите буст сообщения, набрав {0}",
"boostmsg_new": "Новый набор буст сообщений.",
"boostdel_off": "Автоматическое удаление буст-сообщений отключено.",
"boostdel_on": "Бусте сообщения будут удалены через {0} секунд.",
"log_ignored_channels": "Игнорируемые каналы",
"log_ignored_users": "Игнорируемые пользователи",
"log_ignore_user": "Логирование будет игнорировать пользователя {0}",
"log_not_ignore_user": "Логирование больше не будет игнорировать пользователя {0}",
"log_ignore_chan": "Логирование будет игнорировать канал {0}",
"log_not_ignore_chan": "Логирование больше не будет игнорировать канал {0}",
"streams_cleared": "Все стримы, отслеживаемые на этом сервере, были удалены.",
"warn_weight": "Вес: {0}",
"warn_count": "{0} текущий, {1} всего",
"mass_ban_in_progress": "Блокировка {0} пользователей...",
"mass_ban_completed": "Заблокировано {0} пользователей.",
"reminder_server_list": "Список напоминаний сервера",
"imageonly_enable": "Этот канал теперь только для изображений.",
"imageonly_disable": "Этот канал больше не предназначен только для изображений."
}

View File

@@ -1,30 +1,30 @@
{
"api_key_missing": "Відсутній АРІ ключ.",
"quote_deleted": "Цитата #{0} видалена.",
"redacted_too_long": "Зменшено, тому що це занадто довго.",
"quote_deleted": "Цитату #{0} видалено.",
"redacted_too_long": "Відредаговано, тому що це занадто довго.",
"trigger": "Тригер",
"response": "Відповідь",
"fw_cleared": "Усі фільтровані слова і фільтровані слова налаштування каналу видалені.",
"aar_disabled": "\n**Автопризначення ролі** на приєднання користувача зараз **вимкнена**.",
"bandm": "Ви були забанені на {0} сервері.\nПричина: {1}",
"fw_cleared": "Усі фільтровані слова і фільтровані слова налаштування каналу видалено.",
"aar_disabled": "\n**Авто-призначення ролі** на приєднання користувача зараз **вимкнено**.",
"bandm": "Ви отримали заборону на приєднання до сервер {0}.\nПричина: {1}",
"banned_user": "Користувач заблокований",
"byedel_off": "Автоматичне видалення прощального повідомлення було вимкнене.",
"byedel_on": "Прощальне повідомлення буде видалене через {0} секунд.",
"byemsg_cur": "Поточне прощальне повідомлення: {0}",
"byemsg_enable": "Увімкнене прощальне повідомлення при написанні {0}",
"byemsg_new": "Нова прощальне повідомлення встановлене.",
"bye_off": "Прощальне оголошення вимкнене.",
"byedel_off": "Автоматичне видалення прощального повідомлення було вимкнено.",
"byedel_on": "Прощальне повідомлення буде видалене на протязі {0} секунд.",
"byemsg_cur": "Теперішнє прощальне повідомлення: {0}",
"byemsg_enable": "Увімкнено прощальне повідомлення при написанні {0}",
"byemsg_new": "Нове прощальне повідомлення встановлено.",
"bye_off": "Прощальне оголошення вимкнено.",
"bye_on": "Прощальне повідомлення увімкнене на цьому каналі.",
"cr": "Успішно створена роль {0}",
"dr": "Роль {0} була видалена.",
"dr": "Роль {0} було видалено.",
"createtextchan": "Текстовий канал {0} створено.",
"createvoich": "Голосовий канал {0} створено.",
"deafen": "Успішно заглушений.",
"delmsg_off": "Я не буду більше автоматично видаляти повідомлення яке успішно виконується командою на цьому сервері.",
"delmsg_on": "Я буду автоматично видаляти повідомлення яке успішно виконується командою на цьому сервері.",
"delmsg_channel_off": "Я ніколи не буду автоматично видаляти повідомлення яке успішно виконується командою на цьому сервері.",
"delmsg_channel_on": "Я завжди буду автоматично видаляти повідомлення яке успішно виконується командою на цьому каналі, незважаючи на налаштування сервера.",
"delmsg_channel_inherit": "Я завжди буду автоматично видаляти повідомлення яке успішно виконується командою на цьому каналі, тільки якщо ця можливість увімкнена на сервері.",
"delmsg_off": більше не буду автоматично видаляти повідомлення успішно виконаної команди.",
"delmsg_on": "Я буду автоматично видаляти повідомлення успішно виконаної команди.",
"delmsg_channel_off": "Я ніколи не буду автоматично видаляти повідомлення успішно виконаної команди.",
"delmsg_channel_on": "Я завжди буду автоматично видаляти повідомлення, яке успішно виконаної команди на цьому каналі, незважаючи на налаштування сервера.",
"delmsg_channel_inherit": "Я завжди буду автоматично видаляти повідомлення успішно виконаної команди на цьому каналі, тільки якщо ця можливість увімкнена на сервері.",
"deltextchan": "Текстовий канал {0} видалено.",
"delvoich": "Голосовий канал {0} видалено.",
"fwall_start": "Я відправлятиму ПП усім власникам.",
@@ -43,81 +43,81 @@
"greetmsg_new": "Нове привітальне повідомлення встановлено.",
"greet_off": "Привітальне оголошення вимкнено.",
"greet_on": "Привітальне оголошення увімкнено на цьому каналі.",
"hierarchy": "Ви не можете використовувати цю команду на користувачі з ролью вищою або рівною за Вашу (або мою) у ролевій ієрархії.",
"hierarchy": "Ви не можете використовувати цю команду на користувачі, роль якого вища, однакова з твоєю(моєю) у ролевій ієрархії.",
"invalid_format": "Неправильний формат введення.",
"kickdm": "Ви були виганані з {0} сервера.\nПричина: {1}",
"kickdm": "Вас було вигнано з сервера {0}.\nПричина: {1}",
"kicked_user": "Користувач був вигнаний",
"lang_list": "Список мов",
"lang_set_bot": "Локаль бота по замовчуванню зараз {0} - {1}",
"lang_set_bot_show": "Мова бота встановлено {0} - {1}",
"lang_set": "Локаль Вашого сервера зараз {0} - {1}",
"lang_set_bot": "Поточна мова бота за замовчуванням - {0} - {1} ",
"lang_set_bot_show": "Мова бота встановлена {0} - {1}",
"lang_set": "Поточна мова твого сервера - {0} - {1}",
"message_sent": "Повідомлення відправлено.",
"mute_error": "Я не маю необхідних прав для цього.",
"mute_role_set": "Нова заглушена роль була встановлена.",
"mute_role_set": "Нова роль, яка обмежує можливості була встановлена.",
"prot_active": "Активний захист",
"prot_disable": "{0} був **вимкнений** на цьому сервері.",
"prot_enable": "{0} Увімкнено",
"prot_none": "Захист не увімкнено.",
"raid_cnt": "Поріг користувача повинен бути між {0} і {1}.",
"raid_stats": "Якщо протягом {1} секунд приєднається {0} або більше користувачів, я {2} їх приєднаю.",
"raid_stats": "Якщо протягом {1} секунд приєднається {0} або більше користувачів, я даватиму їм {2} ",
"raid_time": "Час повинен бути між {0} і {1} секундами.",
"rar": "Успішно видалено усі ролі у користувача {0}",
"rc": "Колір ролі {0} був змінений.",
"renrole": "Роль перейменовано",
"reprm": "Вилучено відтворення повідомлення: {0}",
"role_added": "Роль {0} була додана у список груп {1}.",
"role_in_list": "Роль {0} уже у списку.",
"self_assign_already": "У Вас уже є {0} роль.",
"self_assign_excl": "Ролі, призначені самостійно, тепер ексклюзивні!",
"self_assign_list": "Існує {0} ролей, які самостійно призначаються",
"self_assign_not": "Ця роль не призначається самостійно.",
"role_in_list": "Роль {0} уже є у списку.",
"self_assign_already": "У Вас уже є роль {0}.",
"self_assign_excl": "Самопризначені ролі тепер ексклюзивні!",
"self_assign_list": "Існує {0} ролей, які самостійно призначаються.",
"self_assign_not": "Ця роль не може призначатися самостійно.",
"self_assign_not_have": "У вас немає ролі {0}.",
"self_assign_no_excl": "Ролі, призначені самостійно, тепер не є ексклюзивними!",
"self_assign_perms": "Я не можу додати Вам цю роль. `Я не можу додати ролі власникам або інші ролі, вищі за мою роль у ієрархії ролей. ''",
"self_assign_no_excl": "Самопризначені ролі тепер не є ексклюзивними!",
"self_assign_perms": "Я не можу надати вам цієї ролі. `Неможливо надати роль власнику сервера або людям з роллю вищою, ніж моя у ієрархії.`",
"self_assign_rem": "{0} вилучено зі списку ролей, які можна самостійно призначити.",
"self_assign_remove": "Ви більше не маєте ролі {0}.",
"self_assign_success": "Тепер у вас є {0} роль.",
"self_assign_remove": "У вас більше немає ролі {0}.",
"self_assign_success": "Тепер у вас є роль {0}.",
"adsarm_enable": "Увімкнено автоматичне видалення відповідей `{0} iam` та `{0} iamn`.",
"adsarm_disable": "Бот більше не видалятиме відповіді `{0} iam` та `{0} iamn`.",
"setrole": "Успішно додана роль {0} для користувача {1}",
"set_channel_name": "Нове ім'я каналу встановлене.",
"adsarm_disable": "Бот більше не видалятиме відповідь `{0} iam` та `{0} iamn`.",
"setrole": "Успішно додано роль {0} для користувача {1}",
"set_channel_name": "Нове ім'я каналу встановлено.",
"shutting_down": "Вимикаюсь",
"spam_ignore": "{0} ігноруватиме цей канал.",
"spam_not_ignore": "{0} більше не ігноруватиме цей канал.",
"spam_stats": "Якщо користувач публікує {0} однакові повідомлення поспіль, я їх {1} надсилаю.\n __IgnoredChannels__: {2}",
"undeafen": "Успішно не приглушений.",
"spam_stats": "Якщо користувач публікує {0} однакових повідомлень поспіль, я даватиму йому {1}.\n__Канали, що ігноруються__: {2}",
"undeafen": "Надано дозвіл писати.",
"username": "Ім'я користувача",
"user_chat_mute": "{0} став **приглушений** у чаті.",
"user_chat_unmute": "\n{0} став **не приглушений** у чаті.",
"user_muted": "{0} став **приглушений** у текстовому і голосовому чаті.",
"user_unmuted": "{0} став **не приглушений** у текстовому і голосовому чаті.",
"user_voice_mute": "**голос приглушено** у {0}.",
"user_voice_unmute": "**голос не приглушений** у {0}.",
"sbdm": "Вас було мягко забанено на {0} сервері.\nПричина: {1}",
"sb_user": "Користувач мягко забанений.",
"better_luck": "Удачі наступного разу ^_^",
"user_chat_mute": "{0} **заборонено писати** в чаті.",
"user_chat_unmute": "\n{0} тепер **дозволено писати** у чаті.",
"user_muted": "{0} тепер **має можливість писати та розмовляти** у текстовому і голосовому каналі.",
"user_unmuted": "{0} тепер **заборонено писати та розмовляти** у текстовому і голосовому каналах.",
"user_voice_mute": "**заборонено розмовляти** учаснику {0}.",
"user_voice_unmute": "**дозволено розмовляти** учаснику {0}.",
"sbdm": "Вас було тимчасово заблоковано на сервері {0}.\nПричина: {1}",
"sb_user": "Користувач тимчасово заблокований.",
"better_luck": "Нехай пощастить наступного разу ^_^",
"roll": "Ви прокрутили {0}.",
"br_win": "Вітаємо! Ви виграли {0}, викинувши більше {1}",
"flip_guess": "Ви здогадалися! Ви виграли {0}",
"flip_results": "Викинуто {0} монету. {1} орел, {2} решка.",
"flip_guess": "Овва! Ви здогадались та перемогли! {0}",
"flip_results": "Викинуто монету {0}. {1} орел, {2} решка.",
"has": "{0} має {1}",
"leaderboard": "Таблиця лідерів",
"mass_award": "Нагороджені {0} користувачі {1} з ролі {2}.",
"max_bet_limit": "Ви не можете поставити більше ніж {0}",
"min_bet_limit": "Ви не можете поставити менше ніж {0}",
"not_enough": "У вас не вистарчає {0}",
"not_enough": "Вам не достатньо {0}",
"raffled_user": "Розіграв користувач",
"slot_bet": "Ставка",
"slot_jackpot": "УРАААААААААА!!! Вітаю!!! х{0}",
"slot_single": "Один {0}, х{1}",
"slot_three": "Воу! Вдалий! Три підряд! х{0}",
"slot_three": "Оце Так! Пощастило! Три одразу! х{0}",
"slot_two": "Хороша робота! Два {0} - ставка х{1}",
"won": "Перемога",
"transactions": "Транзакція користувача {0}",
"commands_instr": "Введіть `{0}h CommandName`, щоб побачити довідку для цієї заданої команди. наприклад `{0}h {0}8ball`",
"command_not_found": "Я не можу знайти цю команду. Перш ніж повторити спробу, перевірте наявність команди.",
"module_not_found": "Такого модуля не існує.",
"module_not_found_or_cant_exec": "Або модуль не існує, або ви не можете запустити жодну з команд у зазначеному модулі.",
"module_not_found_or_cant_exec": "Модуль не існує, або ви не можете запустити жодну з команд у зазначеному модулі.",
"usage": "Використання",
"options": "Налаштування",
"requires": "Вимоги",
@@ -125,14 +125,14 @@
"blacklisted_tag_add": "Тег NSFW {0} тепер внесено до чорного списку.",
"blacklisted_tag_remove": "Тег NSFW {0} більше не входить у чорний список.",
"animal_race": "Гонки тварин",
"animal_race_failed": "Не вдалося розпочати, оскільки бракує учасників.",
"animal_race_failed": "Не вдалося розпочати, оскільки бракує учасників. ",
"animal_race_join": "{0} приєднався як {1}",
"animal_race_join_instr": "Щоб приєднатися до перегонів, введіть {0}jr.",
"animal_race_starting": "Починається через {0} секунди або коли кімната заповнена.",
"animal_race_won": "{0} як {1} Виграв гонку!",
"dice_invalid_number": "Вказано недійсний номер. Ви можете кидати кості від {0} - {1} одночасно.",
"dice_rolled": "викинуто {0}",
"dice_rolled_num": "кості викинули: {0}",
"dice_rolled_num": "Кубик викинув: {0}",
"changes_of_heart": "Передумав",
"claimed_by": "Прийнято",
"divorces": "Розлучення",
@@ -142,59 +142,59 @@
"waifus_none": "Ніяких вайфу не було поки прийнято.",
"waifus_top_waifus": "Найкращі Вайфу",
"waifu_claimed": "прийнято {0} як вайфу для {1}!",
"waifu_divorced_like": "Ви розлучились з вайфу якій Ви подобаєтесь. Ви безсердечний монстер. {0} получив {1} в якості компенсації.",
"waifu_egomaniac": "Ви не можете встановити спорідненість до себе, Ви егоманьяк.",
"waifu_fulfilled": "🎉 Їхня любов сповнилася! 🎉\nНове значення {0} - {1}!",
"waifu_not_enough": "Ви повинні заплатити {0} чи більше щоб прийняти вайфу!",
"waifu_not_yours": "Ця вайфу не Ваша.",
"waifu_divorced_like": "Ви розійшлися з вайфу якій ви подобались. Ви безсердечні монстер. {0} отримав {1} в якості компенсації.",
"waifu_egomaniac": "Ти не можеш бути спорідненим, ти его-маньяк.",
"waifu_fulfilled": "🎉 Їхнє кохання взаємне! 🎉\nНове значення {0} - {1}!",
"waifu_not_enough": "Ви повинні заплатити {0} чи більше, щоб купити цю вайфу! ",
"waifu_not_yours": "Ця вайфу не твоя.",
"waifu_not_yourself": "Ви не можете претендувати на себе.",
"waifu_recent_divorce": "Ви розлучились недавно. Ви повинні почекати {0} годин і {1} хвилин щоб розлучитись знову.",
"waifu_transfer_fail": "Ви або не є власником цієї вайфу, або не можете собі цього дозволити. В іншому випадку введення недійсне.",
"waifu_transfer_success": "Претензія на {0} перенесена з {1} на {2}",
"waifu_recent_divorce": "Ви розійшлися нещодавно. Ви повинні почекати {0} годин і {1} хвилин щоб розлучитись знову.",
"waifu_transfer_fail": "Ви не є власником цієї вайфу, або не можете собі цього дозволити. В іншому випадку - введення недійсне.",
"waifu_transfer_success": "Претендування на {0} перенесено з {1} на {2}",
"nobody": "Ніхто",
"waifu_divorced_notlike": "Ви розлучилися з вайфу, якій Ви не подобаєтесь. Ви отримали {0} назад.",
"waifu_reset": "Статистика вашої вайфу були скинуті.",
"waifu_reset_fail": "Не вдалось скинути статистику вайфу. Впевніться що у Вас достатньо грошей.",
"waifu_reset_confirm": "Це скине статистику Вашої вайфу",
"waifu_divorced_notlike": "Ви розійшлися з вайфу, якій ви не подобаєтесь. Ти повернув собі {0} назад.",
"waifu_reset": "Статистика вашої вайфу були скинуто.",
"waifu_reset_fail": "Не вдалось скинути статистику вайфу. Переконайтеся, що у вас достатньо коштів.",
"waifu_reset_confirm": "Це скине статистику вашої вайфу.",
"acrophobia": "Акрофобія",
"acro_ended_no_sub": "Гра закінчилася без подань.",
"acro_no_votes_cast": "Не подано голосів. Гра закінчилася без переможця.",
"acro_ended_no_sub": "Гра закінчилася без подач.",
"acro_no_votes_cast": "Не подано голоси. Гра закінчилася без переможця.",
"acro_nym_was": "Акронім був {0}.",
"acro_running": "Акрофобія уже запущена на цьому каналі.",
"acro_started": "Гра почалась. Створіть речення з наступними акронімами: {0}.",
"acro_started_footer": "У вас {0} секунд щоб зробити подання.",
"acro_vote": "Голосуйте написавши номер подання",
"acro_started_footer": "У вас {0} секунд, щоб зробити подачу.",
"acro_vote": "Голосуйте написавши номер подачі",
"acro_vote_cast": "{0} віддав свій голос!",
"acro_winner": "Переможець {0} з {1} очками.",
"acro_winner_only": "{0} переможець будучи єдиним хто зробив подання!",
"acro_winner_only": "{0} переможець, адже він єдиний, хто зробив подачу!",
"question": "Питання",
"rps_draw": "Це нічия! Обидва обрали {0}",
"rps_win": "{0} виграв! {1} переміг {2}",
"submissions_closed": "Подання закрито",
"submissions_closed": "Подачу закрито",
"total_average": "Всього: {0} В середньому: {1}",
"category": "Категорія",
"cleverbot_disabled": "Розумний бот вимкнений на цьому сервері.",
"cleverbot_enabled": "Розумний бот увімкнений на цьому сервері.",
"curgen_disabled": "Генерація валюти була вимкнена на цьому сервері.",
"curgen_enabled": "Генерація валюти була увімкнена на цьому сервері.",
"curgen_pl": "{0} рандомний {1} появився!",
"curgen_sn": "Рандомний {0} появився!",
"game_started": "Гра розпочата",
"curgen_pl": "{0} рандомний {1} по!",
"curgen_sn": "Рандомний {0} з'явився!",
"game_started": "Гру розпочато",
"hangman_game_started": "Шибеник розпочався",
"hangman_running": "Шибеник наразі розпочався на цьому каналі.",
"hangman_types": "Список \"{0}hangman\" правил пишуть:",
"no_results": "Немає результатів",
"picked": "вибрано {0}",
"planted": "{0} встановлено {1}",
"trivia_already_running": "Дрібниці наразі розпочався на цьому каналі.",
"trivia_already_running": "\"Дрібниці\" наразі розпочато на цьому каналі.",
"trivia_game": "Дрібниці",
"trivia_guess": "{0} вгадав це! Відповідь була: {1}",
"trivia_none": "Дрібниці не запущена на цьому сервері.",
"trivia_guess": "{0} вгадав! Відповідь була: {1}",
"trivia_none": "\"Дрібниці\" не запущена на цьому сервері.",
"trivia_points": "{0} має {1} очків",
"trivia_stopping": "Зупинившись після цього питання.",
"trivia_times_up": "Час вийшов! Правильною відповіддю було {0}",
"trivia_stopping": "Зупинитись після цього питання.",
"trivia_times_up": "Час вийшов! Правильна відповідь {0}",
"ttt_against_yourself": "Ви не можете грати проти себе.",
"ttt_already_running": "Хрестики-нулики наразі розпочався на цьому сервері.",
"ttt_already_running": "Хрестики-нулики наразі розпочато на цьому сервері.",
"ttt_a_draw": "Нічия",
"ttt_created": "створив гру в Хрестики-нулики.",
"ttt_has_won": "{0} виграв!",
@@ -206,7 +206,7 @@
"attempting_to_queue": "Спроба поставити в чергу {0} пісень у черзі ...",
"fairplay": "Чесна гра",
"finished_song": "Пісня завершилась",
"id": "Id",
"id": "ID",
"invalid_input": "Не вірне введення.",
"must_be_in_voice": "Ви повинні бути в голосовому каналі на цьому сервері.",
"name": "Ім'я",
@@ -225,25 +225,25 @@
"arm_enable": "Увімкнене використання ВСІХ МОДУЛІВ для ролі {0}.",
"asm_disable": "Вимкнене використання ВСІХ МОДУЛІВ на цьому сервері.",
"asm_enable": "Увімкнене використання ВСІХ МОДУЛІВ на цьому сервері.",
"aum_disable": "Вимкнене використання ВСІХ МОДУЛІВ для користувача {0}.",
"aum_enable": "Увімкнене використання ВСІХ МОДУЛІВ для користувача {0}.",
"blacklisted": "Добавлено у чорний список {0} з ID {1}",
"aum_disable": "Вимкнено використання ВСІХ МОДУЛІВ для користувача {0}.",
"aum_enable": "Увімкнено використання ВСІХ МОДУЛІВ для користувача {0}.",
"blacklisted": "Додано у чорний список {0} з ID {1}",
"unblacklisted": "Вилучено з чорного списку {0} з ID {1}",
"cmdcd_add": "Команда {0} має {1} секунд відновлення.",
"cmdcd_cleared": "Команда {0} не має відновлення і всі існуючі відновлення були очищені.",
"cmdcd_add": "Команда {0} має {1} секунд для відновлення.",
"cmdcd_cleared": "Команда {0} не має відновлення, і всі існуючі відновлення були очищені.",
"cmdcd_none": "Відновлення команд не встановлено.",
"cx_disable": "Вимкнено використання {0} {1} на каналі {2}.",
"cx_enable": "Увімкнено використання {0} {1} на каналі {2}.",
"filter_word_add": "Додане слово {0} в список фільтрованих слів.",
"filter_word_add": "Додано слово {0} в список фільтрованих слів.",
"filter_word_list": "Список фільтрованих слів",
"filter_word_remove": "Вилучено слово {0} з списку фільтрованих слів.",
"invalid_second_param_between": "Не вірний другий параметер. (Повинен бути числом між {0} і {1})",
"invite_filter_channel_off": "Фільтр запрошень вимкнений на цьому каналі.",
"invite_filter_channel_on": "Фільтр запрошень увімкнений на цьому каналі.",
"invite_filter_server_off": "Фільтр запрошень вимкнений на цьому сервері.",
"invite_filter_server_on": "Фільтр запрошень вимкнений на цьому сервері.",
"moved_permission": "Перенесені дозволи {0} з #{1} до #{2}",
"perm_not_found": "Не можу знайти дозволи на індексі #{0}",
"filter_word_remove": "Вилучено слово {0} зі списку фільтрованих слів.",
"invalid_second_param_between": "Не вірний другий параметер. (Повинно бути числом між {0} і {1})",
"invite_filter_channel_off": "Фільтрацію запрошень **вимкнено** на цьому каналі. ",
"invite_filter_channel_on": "Фільтрацію запрошень **увімкнено** на цьому каналі. ",
"invite_filter_server_off": "Фільтрацію запрошень **вимкнено** на цьому сервері.",
"invite_filter_server_on": "Фільтрацію запрошень **увімкнено** на цьому сервері.",
"moved_permission": "Дозвіл {0} перенесено з #{1} до #{2}",
"perm_not_found": "Не можу знайти дозвіл з індексом #{0}",
"of_command": "команда",
"of_module": "модуль",
"page": "Сторінка {0}",
@@ -251,22 +251,22 @@
"permrole_not_set": "Роль дозволу не встановлена.",
"permrole_reset": "Роль дозволу скинута.",
"permrole_changed": "Тепер для редагування дозволів користувачам потрібна роль {0}.",
"perm_out_of_range": "Дозволи не знайдені на цьому індексі.",
"removed": "вилучений дозвіл #{0} - {1}",
"rx_disable": "Виключене використання {0} {1} для {2} ролі.",
"rx_enable": "Увімкнене використання {0} {1} для {2} ролі.",
"perm_out_of_range": "Не знайдено дозвіл з вказаним дозволом.",
"removed": " дозвіл видалено #{0} - {1}",
"rx_disable": "Вимкнено використання {0} {1} для ролі {2}.",
"rx_enable": "Увімкнено використання {0} {1} для ролі {2}.",
"sec": "сек.",
"sx_disable": "Вимкнене використання {0} {1} на цьому сервері.",
"sx_enable": "Увімкнене використання {0} {1} на цьому сервері.",
"uneditable": "не підлягає редагуванню",
"ux_disable": "Вимкнене використання {0} {1} для користувача {2}.",
"ux_enable": "Увімкнене використання {0} {1} для користувача {2}.",
"sx_disable": "Вимкнено використання {0} {1} на цьому сервері.",
"sx_enable": "Увімкнено використання {0} {1} на цьому сервері.",
"uneditable": "не редагується",
"ux_disable": "Вимкнено використання {0} {1} для користувача {2}.",
"ux_enable": "Увімкнено використання {0} {1} для користувача {2}.",
"verbose_false": "Я більше не буду показувати попередження про дозволи.",
"verbose_true": "Тепер я буду показувати попередження про дозволи.",
"word_filter_channel_off": "Фільтрування слів вимкнена на цьому каналі.",
"word_filter_channel_on": "Фільтрування слів увімкнена на цьому каналі.",
"word_filter_server_off": "Фільтрування слів вимкнено на цьому сервері.",
"word_filter_server_on": "ФІльтрування слів увімкнено на цьому сервері.",
"word_filter_channel_off": "Фільтрація слів вимкнена на цьому каналі.",
"word_filter_channel_on": "Фільтрація слів увімкнена на цьому каналі.",
"word_filter_server_off": "Фільтрація слів вимкнена на цьому сервері.",
"word_filter_server_on": "ФІльтрація слів увімкнена на цьому сервері.",
"abilities": "Здібності",
"chapters": "Розділи",
"comic_number": "Комікс #",
@@ -289,7 +289,7 @@
"min_max": "Мін/Макс",
"original_url": "Оригінальна URL-адреса",
"pokemon_ability_none": "Здатності не знайдено.",
"pokemon_none": "Покемонів не знайдено.",
"pokemon_none": "Покемон не знайдено.",
"rating": "Рейтинг",
"score": "Оцінка:",
"search_for": "Шукати:",
@@ -310,7 +310,7 @@
"convertlist": "Одиниці виміру, які можуть бути використані конвертером",
"convert_not_found": "Не можливо конвертувати {0} до {1}: одиниця виміру не знайдена",
"convert_type_error": "Не можливо конвертувати {0} до {2}: типи одиниць виміру не одинакові",
"created_at": "Створено о",
"created_at": "Створено",
"created_by": "Створено {0}",
"custom_emojis": "Кастомні емодзі",
"error": "Помилка",
@@ -340,7 +340,7 @@
"serverid": "{0} цього сервера {1}",
"server_info": "Інформація про сервер",
"shard": "Черепок",
"showemojis_none": "Спеціальні емодзі не знайдені.",
"showemojis_none": "Спеціальні емодзі не знайдено.",
"text_channels": "Текстовий канал",
"uptime": "Час роботи",
"userid": "{0} користувача {1} є {2}",
@@ -353,11 +353,11 @@
"poll_voted": "{0} проголосував.",
"poll_invalid_input": "Не вірне введення. Ви повині мати питання і хочаб дві відповіді, розділенні за допомогою `;`.",
"x_votes_cast": "{0} всього голосів.",
"pick_pl": "Підніми їх написавши `{0}pick`",
"pick_pl": "Підніми їх, написавши `{0}pick`",
"pick_sn": "Підніми це написавши `{0}pick`",
"sql_confirm_exec": "Підтвердьте виконання наведеної нижче команди SQL",
"no_vcroles": "Ролі голосового каналу відсутні.",
"user_muted_time": "{0} був **заглушений** від текстового і голосового чату на {1} хвилин.",
"user_muted_time": "{0} тепер **заборонено писати і розмовляти** в текстовому і голосового каналах на {1} хвилин.",
"vcrole_added": "Користувачі що приєднаються до {0} голосового каналу получать {1} роль.",
"vcrole_removed": "Користувачі що приєднаються до {0} голосового каналу більше не получать роль.",
"vc_role_list": "Ролі голосового каналу",
@@ -373,7 +373,7 @@
"server": "Сервер",
"startcmdlist_none": "Команд запуску немає на цій сторінці.",
"startcmds_cleared": "Очищені всі команди запуску.",
"unbanned_user": "Користувач {0} був розбанений.",
"unbanned_user": "Користувача {0} розблоковано.",
"user_not_found": "Користувач не знайдений",
"user_warned": "Користувач {0} був попереджений.",
"user_warned_and_punished": "Користувач {0} був попереджений і {1} покарання було застосоване.",
@@ -390,13 +390,13 @@
"warn_punish_rem": "Маючи {0} попереджень більше не каратиметься покаранням.",
"warn_punish_set": "Я застосую {0} покарання для користувача з {1} попередженнями.",
"cant_apply_punishment": "Не можу застосувати покарання. Будь ласка переконайтесь що у вас є усі потрібні дозволи, і що моя роль вище всіх інших у настройках сервера.",
"clpa_fail_already": "Можливо Ви уже получили нагороду цього місяця. Ви можете получити нагороду лише раз в місць, якщо не збільшите свою заставу.\nВи можете перевірити використавши команду `.curtrs`.",
"clpa_fail_already": "Можливо ви вже отримували нагороду цього місяця. Ви можете отримати нагороду лише раз у місяць, якщо не збільшиш свою заставу.\nТи можеш перевірити, використавши команду `.curtrs`.",
"clpa_fail_already_title": "Вже нагороджений",
"clpa_fail_conn": "Ваш обліковий запис Discord може не бути підключеним до Patreon. Якщо ви не впевнені, що це означає, або не знаєте, як це підключити - вам потрібно перейти на [сторінку налаштувань облікового запису Patreon] (https://patreon.com/settings/account) і натиснути кнопку «Підключитися до Discord'y» .",
"clpa_fail_conn": "Ваш обліковий запис Discord може не бути підключеним до Patreon. Якщо ви не впевнені, що це значить, або не знаєте як це підключити - вам потрібно перейти на [сторінку налаштувань облікового запису Patreon] (https://patreon.com/settings/account) і натиснути кнопку «Підключитися до Discord'y» .",
"clpa_fail_conn_title": "Discord аккаунт не підключенний",
"clpa_fail_sup": "Щоб мати право на винагороду, ви повинні підтримати проект на patreon. Ви можете використати команду {0}, щоб отримати посилання.",
"clpa_fail_sup": "Щоб мати право на винагороду, ви повинні підтримати проєкт на patreon. Ви можете використати команду {0}, щоб отримати посилання.",
"clpa_fail_sup_title": "Не підтримується",
"clpa_fail_wait": "Вам потрібно зачекати 5-10 хвилин після того, як ви зробите свою заставу (і під’єднаєте свій обліковий запис розбрату до свого патріона), щоб ваша валюта з’явилася.",
"clpa_fail_wait": "Вам потрібно зачекати 5-10 хвилин після того, як підтвердите свою передплату (і під’єднаєте свій обліковий запис дискорд до свого патреону), щоб ваша валюта з’явилася.",
"clpa_fail_wait_title": "Зачекайте деякий час",
"clpa_too_early": "Нагорода може бути прийняти до або після 5того числа кожного місяця.",
"shop": "Магазин",
@@ -412,7 +412,7 @@
"out_of_stock": "Не має в наявності.",
"random_unique_item": "Рандомний унікальний предмет.",
"shop_buy_error": "Помилка відправки предмета. Вам повернуто кошти.",
"shop_item_not_found": "Не знайдені предмети з таким індексом.",
"shop_item_not_found": "Не знайдено предмети з таким індексом.",
"shop_item_purchase": "Купівля предмета успішна.",
"shop_item_rm": "Предмет з магазину вилучений",
"shop_item_wrong_type": "Цей запис про магазин не підтримує додавання товару.",
@@ -489,7 +489,7 @@
"club_created": "Клуб {0} успішно створенний!",
"club_not_exists": "Такого клубу не існує.",
"club_applied": "Ви подали заявку на членство в клубі {0}.",
"club_apply_error": "Помилка застосування. Ви або вже є членом клубу, або не відповідаєте вимогам мінімального рівня, або вам заборонили цей клуб.",
"club_apply_error": "Помилка застосування. Ви вже є членом клубу, або не відповідаєте вимогам мінімального рівня, також можливо вам заборонено вхід у цей клуб.",
"club_accepted": "Прийнято користувача {0} до клубу.",
"club_accept_error": "Користувач не знайдений",
"club_left": "Ви покинули клуб",
@@ -498,7 +498,7 @@
"club_user_kick_fail": "Помилка вигнання. Ви або не є власником клубу, або цього користувача немає у вашому клубі.",
"club_user_banned": "Користувачу {0} заборонено відвідувати клуб {1}.",
"club_user_ban_fail": "Не вдалося заборонити. Ви або не є власником клубу, або цей користувач не перебуває у вашому клубі або не подає до нього звернення.",
"club_user_unbanned": "Незаблокований користувач {0} у клубі {1}.",
"club_user_unbanned": "Не заблокований користувач {0} у клубі {1}.",
"club_user_unban_fail": "Не вдалося скасувати заборону. Ви або не є власником клубу, або цей користувач не перебуває у вашому клубі або не подає до нього звернення.",
"club_level_req_changed": "Вимогу щодо рівня клубу змінено на {0}",
"club_level_req_change_error": "Не вдалося змінити вимогу до рівня.",
@@ -567,14 +567,14 @@
"cleared": "Всі {0} кастомні реакції на цьому сервері були видалені.",
"crr_reset": "Кастомна реакція з цим id {0} більше не додаватиме реакції.",
"crr_set": "Кастомна реакція з цим id {0} буде додавати наступне повідомлення до відповіді: {1}",
"invalid_emojis": "Усі вказані вами емодзі недійсні.",
"invalid_emojis": "Усі вказані емодзі недійсні.",
"aliases_cleared": "Усі {0} псевдоніми на цьому сервері видалено.",
"vcrole_not_found": "Цей голосовий канал не має VcRole.",
"attachments": "Вкладення",
"avatar_changed": "Аватар зміненно",
"banmsg_disabled": "Повідомлення про бан вимкнено. Ви можете включити їх, встановивши banmsg на щось інше, ніж '-'.",
"banmsg_disabled": "Сповіщення про бан вимкнено. Ви можете увімкнути його, встановивши banmsg на щось інше, ніж '-'.",
"banmsg_default": "Повідомлення про бан не встановлено. Буде використано поведінку за замовчуванням.",
"banned_pl": "забанений",
"banned_pl": "заблокований",
"bot_name": "Ім'я бота змінене на {0}",
"bot_status": "Статус бота змінений на {0}",
"ch_name_change": "Ім'я каналу змінене",
@@ -587,24 +587,22 @@
"images_loading": "Зображення будуть завантажені протягом декількох секунд. ",
"insuf_perms_i": "У мене недостатньо дозволів.",
"insuf_perms_u": "У Вас недостатні дозволи.",
"lang_set_fail": "Не вдалося налаштувати локаль. Поверніться до допомоги цієї команди.",
"lang_set_fail": "Не вдалося налаштувати поточну мову. Повернися в команду-помічник.",
"lang_set_show": "Мова цього сервера встановлена на {0} - {1}",
"log": "Запис події {0} у цей канал.",
"log_all": "Запис усіх подій на цьому каналі.",
"log_disabled": "Запис подій вимкнено.",
"log_events": "Журнал подій, на які можна підписатися:",
"log_ignore": "Запис подій ігноруватиметься {0}",
"log_not_ignore": "Запис подій не ігноруватиметься {0}",
"log_stop": "Зупинено запис подій {0}.",
"msg_not_found": "Повідомлення не знайдено.",
"time_too_long": "Вказаний Вами час надто довгий.",
"time_too_long": "Вказаний тобою час надто довгий.",
"log_vc_moved": "{0} переміщено з {1} на {2} голосовий канал",
"msg_del": "Повідомлення видалено в #{0}",
"msg_update": "Повідомлення оновлено в #{0}",
"muted_pl": "Заглушений",
"muted_sn": "Заглушений",
"mute_role": "поточна заглушена роль {0}",
"mute_perms": "Ви не можете ігнорувати користувача, у ролі якого вище вашої найвищої ролі в ієрархії ролей, якщо ви не є власником сервера.",
"muted_sn": "Заглушений ",
"mute_role": "поточна роль-заборона {0}",
"mute_perms": "Ви не можете заборонити писати користувачу, роль якого вища вашої в ієрархії ролей, якщо ви не є власником сервера.",
"new_msg": "Нове повідомлення",
"new_nick": "Новий нік",
"new_topic": "Нова тема",
@@ -633,14 +631,14 @@
"set_stream": "Новий стрім встановлений!",
"set_topic": "Нова тема каналу встановлена.",
"shard_reconnecting": "Черепок {0} перепідключення.",
"soft_banned_pl": "мягкий-бан",
"soft_banned_pl": "тимчасовий бан",
"text_chan_created": "Текстовий канал створений.",
"text_chan_destroyed": "Текстовий канал знищений.",
"duration": "Тривалість",
"unmuted_sn": "Не заглушений",
"unable_to_dm_user": "ПП користувачу провалилось.",
"username_changed": "Ім'я користувача змінено",
"user_banned": "Користувач забанений",
"user_banned": "Користувач заблокований",
"user_joined": "Користувач приєднався",
"user_left": "Користувач покинув",
"user_role_add": "Роль користувача додана",
@@ -654,7 +652,7 @@
"xmuted_text": "Користувач {0} з текстового чата {1}",
"xmuted_text_and_voice": "Користувач {0} з текстового і голосового чату {1}",
"xmuted_voice": "Користувач {0} з голосового чату {1}",
"user_unbanned": "Користувач розбанений",
"user_unbanned": "Користувач розблокований",
"presence_updates": "Оновлення присутності",
"awarded": "нагороджений {0} до {1}",
"deck_reshuffled": "Колода перетасована.",
@@ -670,10 +668,10 @@
"commandlist_regen": "Список команд відновлено.",
"desc": "Опис",
"cant_dm": "Я не можу надіслати ПП. Переконайтеся, що Ваші ПП відкриті. Перейдіть до опцій ** (кнопка шестерні) -> Конфіденційність та безпека -> Дозволити прямі повідомлення від членів сервера **",
"donate": "Ви можете підтримати проект NadekoBot на\nПатреон <{0}> або\nPaypal <{1}>\nНе забудьте залишити у повідомленні своє ім’я або ідентифікатор Discord.\n\n** Дякую ** ♥ ",
"donate": "Ви можете підтримати проєкт NadekoBot на\nПатреон <{0}> або\nPaypal <{1}>\nНе забудьте залишити у повідомленні своє ім’я або ідентифікатор Discord.\n\n** Дякую ** ♥ ",
"guide": "** Список команд **: <{0}>\n** Посібники та документи щодо розміщення можна знайти тут **: <{1}>",
"list_of_modules": "Список модулів",
"autohentai_started": "Автохентай почався. Повторно публікуйте кожні {0} з одним із таких тегів:\n{1}",
"autohentai_started": "Авто-хентай почався. Повторно публікуйте кожні {0} з одним із таких тегів:\n{1}",
"tag": "Тег",
"animal_race_full": "Гонка повна! Починаєм негайно.",
"animal_race_join_bet": "{0} приєднався як {1} і зробив ставку {2}!",
@@ -684,9 +682,9 @@
"waifu_affinity_already": "ваша близкість уже налаштована на цю вайфу, або ви намагаєтесь видалити свою спорідненість, не маючи її.",
"waifu_affinity_changed": "змінили свою близкість з {0} на {1}.\n\n*Це морально сумнівно.*🤔",
"waifu_affinity_cooldown": "Вам потрібно почекати {0} годин і {1} хвилин, щоб знову змінити свою близькість.",
"waifu_affinity_reset": "Ваша близкість скинута. У тебе більше немає людини, яка тобі подобається.",
"waifu_affinity_reset": "Ваша близкість скинута. У вас більше немає людини, яка вам подобається.",
"waifu_affinity_set": "хоче бути вайфу {0}. Ой <3",
"waifu_isnt_cheap": "Жодна вайфу не така дешева. Ви повинні сплатити щонайменше {0}, щоб отримати вайфу, навіть якщо їх фактична вартість нижча.",
"waifu_isnt_cheap": "Жодної вайфу за вказаною ціною немає. Ви повинні сплатити щонайменше {0}, щоб отримати вайфу, навіть якщо їх фактична вартість нижча.",
"waifu_reset_price": "Ціна: {0}",
"animal_race_already_started": "Гонка Тварин вже запущена.",
"failed_loading_question": "Не вдалося завантажити запитання.",
@@ -719,7 +717,7 @@
"atl_ad_started": "Почався автоматичний переклад повідомлень на цьому каналі. Повідомлення користувачів будуть автоматично видалені.",
"atl_removed": "Вашу мову автоперекладу вилучено.",
"atl_set": "Вашу мову автоматичного перекладу встановлено на {0}> {1}",
"atl_started": "Почався автоматичний переклад повідомлень на цьому каналі.",
"atl_started": "Розпочато автоматичний переклад повідомлень на цьому каналі.",
"atl_stopped": "Зупинено автоматичний переклад повідомлень на цьому каналі.",
"bad_input_format": "Неправильний формат введення або щось пішло не так.",
"card_not_found": "Не вдалося знайти цю картку.",
@@ -729,7 +727,7 @@
"dropped": "Кинуто",
"error_occured": "Виникла помилка.",
"failed_finding_anime": "Не вдалося знайти це аніму.",
"failed_finding_manga": "Не вдалося знайти цю мангу.",
"failed_finding_manga": "Не вдалося знайти цю манґу.",
"imdb_fail": "Не вдалося знайти цей фільм.",
"invalid_lang": "Недійсне джерело або ціль мови.",
"jokes_not_loaded": "Жарти не загружені.",
@@ -805,9 +803,9 @@
"warn_expire_set_clear": "Попередження буде очищено через {0} днів.",
"warn_expire_reset": "Термін дії попереджень більше не припиняється.",
"warn_punish_set_timed": "Я застосую {0} покарання для {2} до користувачів із попередженнями {1}.",
"clpa_obsolete": ": tada: **Винагороди валюти Patreon тепер автоматичні!**: tada:\nЦя команда тепер застаріла.\nЯкщо ви не отримали винагороду за заставу цього місяця, нижче наведено деякі причини, чому це могло бути.",
"clpa_obsolete": ": tada: **Винагороди валюти Patreon тепер автоматичні!**: tada:\nЦя команда тепер застаріла.\nЯкщо ви не отримали винагороду за внесок цього місяця, нижче наведено деякі причини, чому це могло бути.",
"time_new": "Час",
"timezone_db_api_key": "Вам потрібно активувати ключ API TimezoneDB. Ви можете зробити це, натиснувши посилання, яке ви отримали в електронному листі, з ключем API.",
"timezone_db_api_key": "Вам потрібно активувати ключ API TimezoneDB. Ви можете зробити це, натиснувши посилання, яке ви отримали в електронному листі з ключем API.",
"rolehoist_enabled": "Роль {0} тепер відображається окремо від користувачів у мережі.",
"rolehoist_disabled": "Роль {0} більше не відображається окремо від користувачів у мережі.",
"shop_role_already_bought": "Ви вже купили цю роль.",
@@ -818,7 +816,7 @@
"user_nick": "Псевдонім користувача {0} змінено на {1}",
"perm_override": "Для виконання команди {1} користувачам потрібен дозвіл {0}.",
"define_unknown": "Не можу знайти визначення цього терміну.",
"queue_stopped": "Плеєр зупинений. Використовуйте команду {0}, щоб почати відтворення.",
"queue_stopped": "Плеєр зупинено. Використовуйте команду {0}, щоб почати відтворення.",
"removed_song_error": "Пісня в цьому індексі не існує",
"nunchi_started": "Гра Нунчі розпочалася з {0} учасниками.",
"bot_strings_reloaded": "Строки ботів були перезавантажені.",
@@ -858,7 +856,7 @@
"failed_finding_novel": "Не можу знайти цю новелу. Переконайтеся, що ви ввели точне повне ім’я та воно існує на сайті novelupdates.com",
"roll_duel_challenge": "{1} викликав на дуель {2}",
"roll_duel": "Дуель",
"roll_duel_no_funds": "Або Вам, або вашому опонентові не вистачає коштів.",
"roll_duel_no_funds": "Вам, або вашому опонентові не вистачає коштів.",
"roll_duel_timeout": "Виклик на дуель не прийнято.",
"roll_duel_already_challenged": "Цей користувач уже викликав Вас на дуель.",
"account_not_found": "Цей обліковий запис не існує або має статус приватного.",
@@ -892,32 +890,32 @@
"log_vc_joined": "{0} приєднався до {1} голосового каналу",
"log_vc_left": "{0} покинув {1} голосовий канал",
"protection_not_running": "Захист {0} не ввімкнено.",
"anti_alt_status": "Будь -який користувач, що приєднується до сервера з обліковим записом, не старшим за {0}, до нього застосує таку дію: {1}\nНаразі покараний: {2}",
"anti_alt_status": "Будь-який користувач, що приєднується до сервера з обліковим записом, не старшим за {0}, до нього застосовується така дія: {1}\nНаразі покараних: {2}",
"mass_take": "Взято {0} від {1} користувачів у ролі {2}.",
"_8ball": "Магічна куля",
"repeating_none": "Плеєр зупиниться, коли буде досягнуто кінця черги.",
"repeating_queue": "Плеєр повторить чергу.",
"current_music_quality": "Наразі встановлена якість музики: {0}",
"music_quality_set": "Якість музики встановлено на {0}. Щоб зміни вступили в силу, вам доведеться знищити та перезапустити музичний плеєр.",
"music_quality_set": "Якість музики встановлено на {0}. Щоб зміни вступили в силу, вам доведеться очистити та перезапустити музичний плеєр.",
"blacklisted_users": "Користувачі з чорного списку",
"blacklisted_channels": "Канали з чорного списку",
"blacklisted_servers": "Сервера з чорного списку",
"_class": "Клас",
"stream_not_following_any": "Ви не стежите за будь-якими стрімами.",
"stream_not_following_any": "Ви не стежите ні за якими стрімами.",
"stream_message_set_all": "Усі {0} стріми, за якими стежать, тепер мають зазначене повідомлення-оголошення.",
"repeater_exceed_limit": "На одному сервері не може бути більше {0} повторювачів.",
"repeater_remove_fail": "Не вдалося видалити повторювач для цього індексу. Або ви вказали недійсний індекс, або ретранслятор на той момент перебував у стані виконання, в цьому випадку повторіть спробу за кілька секунд.",
"repeater_remove_fail": "Не вдалося видалити повторювач для цього індексу. Ви вказали не дійсний індекс, або ретранслятор на той момент перебував у стані виконання, в цьому випадку повторіть спробу за кілька секунд.",
"interval_colon": "Інтервал:",
"executes_in_colon": "Виконується в:",
"message_colon": "Повідомлення:",
"no_redundant": "Не буде опубліковано повторюване повідомлення.",
"remind_invalid": "Недійсний формат нагадування. Нагадування повинно мати ціль, таймер і причину. Перевірте список команд.",
"user_voice_mute_time": "Користувач {0} ** вимкнено голос ** протягом {1} хвилин.",
"user_chat_mute_time": "{0} ** вимкнено чат ** протягом {1} хвилин.",
"acrm_fail": "Автоматична команда не знайдена у вказаному індексі.",
"warns_dont_expire": "Попередження не встановлені.",
"no_redundant": "Повідомлення, яке повторюється не буде публікуватися. ",
"remind_invalid": "Не дійсний формат нагадування. Нагадування повинно мати ціль, таймер і причину. Перевірте список команд.",
"user_voice_mute_time": "Користувачу {0} **заборонено розмовляти** протягом {1} хвилин.",
"user_chat_mute_time": "{0} **заборонено писати ** протягом {1} хвилин.",
"acrm_fail": "Авто-команду не знайдено у вказаному індексі.",
"warns_dont_expire": "Попередження не встановлено.",
"warns_expire_in": "Термін дії попереджень закінчується через {0} днів.",
"not_with_bot_in_voice": "Ви не в тому ж голосовому каналі, що і бот.",
"not_with_bot_in_voice": "Ви повинні перебувати в одному каналі разом з ботом.",
"waifu_gift_not_exist": "Цей предмет вайфу не існує",
"fans": "Шанувальники ({0})",
"config_edit_fail": "Не вдалося налаштувати {0} значення {1}",
@@ -925,26 +923,26 @@
"config_prop_not_found": "Ресурс {0} не знайдено в конфігурації {1}",
"config_list": "Список конфігурацій",
"level_req": "Вимагається рівень.",
"xprewsreset_confirm": "Ви впевнені, що хочете видалити ВСІ нагороди за підвищення рівня ХР з цього сервера? Ця дія є незворотною.",
"xp_role_reward_cleared": "Досягнення рівня {0} більше не винагороджує роль.",
"xprewsreset_confirm": "Ви впевнені, що хочете видалити ВСІ нагороди за підвищення рівня ХР на цьому сервері? Ця дія є незворотною. ",
"xp_role_reward_cleared": "Досягнення {0} рівня більше не винагороджує роллю. ",
"club_name_too_long": "Назва клубу задовга.",
"club_user_not_in_club": "{0} не в клубі.",
"reaction_cant_access": "Я не можу отримати доступ до реакції {0}. Ви можете використовувати лише емоції з серверів, на яких я перебуваю.",
"club_user_not_in_club": "{0} не є у клубі.",
"reaction_cant_access": "Я не можу отримати доступ до реакції {0}. Ви можете використовувати лише емоджі з серверів, на яких я перебуваю.",
"module_footer": "{0}cmds {1}",
"module_page_empty": "На цій сторінці немає модуля.",
"module_description_help": "Отримайте довідку з команд, описи та приклади використання",
"module_description_gambling": "Робіть ставку на кидки костей, блекджек, слоти, монети та інші",
"module_description_games": "Грайте в дрібниці, нунчі, шибеника, з'єднай4 та інші ігри",
"module_description_gambling": "Робіть ставку на кидки кубиків, блекджек, слоти, монети та інші",
"module_description_games": "Грайте в гру: дрібниці, нунчі, шибеника, з'єднай4 та інші ігри",
"module_description_nsfw": "Команди NSFW.",
"module_description_music": "Відтворюйте музику з YouTube, локальні файли soundcloud та радіопотоки",
"module_description_music": "Відтворюйте музику з YouTube, локальні файли, soundcloud та радіопотоки",
"module_description_utility": "Керуйте власними цитатами, повторюваними повідомленнями та перевіряйте факти про сервер",
"module_description_administration": "Модеруйте, карайте користувачів, налаштовуйте самостійно призначені ролі та вітайте повідомлення",
"module_description_administration": "Модеруйте, карайте користувачів, налаштовуйте самостійно призначені ролі та вітальні повідомлення",
"module_description_customreactions": "Налаштуйте власні відповіді бота на певні слова чи фрази",
"module_description_permissions": "Налаштуйте дозволи для команд, фільтруйте слова та налаштуйте час відновлення",
"module_description_searches": "Шукайте жарти, зображення тварин, аніме та мангу",
"module_description_searches": "Шукайте жарти, зображення тварин, аніме та манґу",
"module_description_xp": "Отримайте ХР на основі активності в чаті, перевірте картки ХР користувачів",
"module_description_missing": "Опис цього модуля відсутній.",
"obsolete_use": "⚠ Застаріло, замість цього використовуйте {0}.",
"obsolete_use": "⚠ Застаріло, замість цього використовуй {0}.",
"purge_user_confirm": "Ви впевнені, що хочете видалити {0} з бази даних?",
"expr_import_no_input": "Неправильні дані. Не знайдено дійсного файлу для завантаження чи введення тексту.",
"expr_import_invalid_data": "Не вдалося проаналізувати файл. Переконайтеся, що це дійсний .yml файл",
@@ -953,5 +951,29 @@
"empty_page": "Ця сторінка пуста.",
"pages": "Сторінки",
"favorites": "Улюблене",
"tags": "Теги"
"tags": "Теги",
"invalid_emoji_link": "Вказане посилання не є зображенням, або перевищує ліміт 256 КБ.",
"emoji_add_error": "Помилка додавання емоджі. У вас вичерпано слоти для емоджі, або невідповідний розмір.",
"emoji_added": "Додано новий смайлик: {0}",
"boost_on": "Увімкнено буст-сповіщення на цьому каналі.",
"boost_off": "Буст-сповіщення вимкнено.",
"boostmsg_cur": "Поточне буст-сповіщення: {0} ",
"boostmsg_enable": "Увімкність буст-сповіщення ввівши {0}",
"boostmsg_new": "Нове буст-сповіщення. ",
"boostdel_off": "Автоматичне видалення буст-сповіщень вимкнено. ",
"boostdel_on": "Буст-сповіщення буде видалятися через {0} секунд. ",
"log_ignored_channels": "Ігноровані канали",
"log_ignored_users": "Ігноровані користувачі",
"log_ignore_user": "Журнал ігноруватиме користувача {0}",
"log_not_ignore_user": "Журнал більше не ігноруватиме користувача {0}",
"log_ignore_chan": "Журнал ігноруватиме канал {0}",
"log_not_ignore_chan": "Журнал більше не ігноруватиме канал {0}",
"streams_cleared": "Усі стріми, за якими ви слідкували, на цьому сервері видалено.",
"warn_weight": "Вага: {0} ",
"warn_count": "{0} поточно, {1} всього",
"mass_ban_in_progress": "Заблокувати {0} користувачів...",
"mass_ban_completed": "Заблоковано {0} користувачів. ",
"reminder_server_list": "Список нагадувань серверу",
"imageonly_enable": "Цей канал доступний лише для зображень.",
"imageonly_disable": "Цей канал більше не є лише для зображень. "
}