Merge pull request #50 from StudyLions/rewrite

This commit is contained in:
Interitio
2023-09-24 12:26:32 +03:00
committed by GitHub
78 changed files with 1575 additions and 975 deletions

View File

@@ -11,6 +11,8 @@ ALSO_READ = config/emojis.conf, config/secrets.conf, config/gui.conf
asset_path = assets
support_guild =
[ENDPOINTS]
guild_log =
gem_transaction =
@@ -19,8 +21,10 @@ gem_transaction =
log_file = bot.log
general_log =
error_log =
critical_log =
error_log = %(general_log)
critical_log = %(general_log)
warning_log = %(general_log)
warning_prefix =
error_prefix =
critical_prefix =

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-09-13 08:47+0300\n"
"POT-Creation-Date: 2023-09-24 12:21+0300\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -18,41 +18,41 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
#: src/modules/pomodoro/timer.py:51
#: src/modules/pomodoro/timer.py:52
msgctxt "timer|stage:break|name"
msgid "BREAK"
msgstr ""
#: src/modules/pomodoro/timer.py:52
#: src/modules/pomodoro/timer.py:53
msgctxt "timer|stage:focus|name"
msgid "FOCUS"
msgstr ""
#: src/modules/pomodoro/timer.py:158
#: src/modules/pomodoro/timer.py:160
#, possible-python-brace-format
msgctxt "timer|webhook|name"
msgid "{bot_name} Pomodoro"
msgstr ""
#: src/modules/pomodoro/timer.py:162
#: src/modules/pomodoro/timer.py:164
msgctxt "timer|webhook|audit_reason"
msgid "Pomodoro Notifications"
msgstr ""
#: src/modules/pomodoro/timer.py:173
#: src/modules/pomodoro/timer.py:175
msgctxt "timer|webhook|error:insufficient_permissions"
msgid ""
"I require the `MANAGE_WEBHOOKS` permission to send pomodoro notifications "
"here!"
msgstr ""
#: src/modules/pomodoro/timer.py:232
#: src/modules/pomodoro/timer.py:234
#, possible-python-brace-format
msgctxt "timer|default_base_name"
msgid "Timer {pattern}"
msgstr ""
#: src/modules/pomodoro/timer.py:406
#: src/modules/pomodoro/timer.py:408
#, possible-python-brace-format
msgctxt "timer|kicked_message"
msgid ""
@@ -64,20 +64,20 @@ msgid_plural ""
msgstr[0] ""
msgstr[1] ""
#: src/modules/pomodoro/timer.py:499
#: src/modules/pomodoro/timer.py:501
#, possible-python-brace-format
msgctxt "timer|status|stage:focus|statusline"
msgid "{channel} is now in **FOCUS**! Good luck, **BREAK** starts {timestamp}"
msgstr ""
#: src/modules/pomodoro/timer.py:504
#: src/modules/pomodoro/timer.py:506
#, possible-python-brace-format
msgctxt "timer|status|stage:break|statusline"
msgid ""
"{channel} is now on **BREAK**! Take a rest, **FOCUS** starts {timestamp}"
msgstr ""
#: src/modules/pomodoro/timer.py:536
#: src/modules/pomodoro/timer.py:538
#, possible-python-brace-format
msgctxt "timer|status|warningline"
msgid ""
@@ -85,13 +85,13 @@ msgid ""
"next stage."
msgstr ""
#: src/modules/pomodoro/timer.py:555
#: src/modules/pomodoro/timer.py:557
#, possible-python-brace-format
msgctxt "timer|status|stopped:auto"
msgid "Timer stopped! Join {channel} to start the timer."
msgstr ""
#: src/modules/pomodoro/timer.py:560
#: src/modules/pomodoro/timer.py:562
msgctxt "timer|status|stopped:manual"
msgid "Timer stopped! Press `Start` to restart the timer."
msgstr ""
@@ -116,34 +116,34 @@ msgctxt "dash:stats|dropdown|placeholder"
msgid "Pomodoro Timer Panel"
msgstr ""
#: src/modules/pomodoro/cog.py:82
#: src/modules/pomodoro/cog.py:96
msgctxt "cmd_check:ready|failed"
msgid ""
"I am currently restarting! The Pomodoro timers will be unavailable until I "
"have restarted. Thank you for your patience!"
msgstr ""
#: src/modules/pomodoro/cog.py:322
#: src/modules/pomodoro/cog.py:336
msgctxt "cmd:timer"
msgid "timer"
msgstr ""
#: src/modules/pomodoro/cog.py:323
#: src/modules/pomodoro/cog.py:337
msgctxt "cmd:timer|desc"
msgid "Show your current (or selected) pomodoro timer."
msgstr ""
#: src/modules/pomodoro/cog.py:326
#: src/modules/pomodoro/cog.py:340
msgctxt "cmd:timer|param:channel"
msgid "timer_channel"
msgstr ""
#: src/modules/pomodoro/cog.py:331
#: src/modules/pomodoro/cog.py:345
msgctxt "cmd:timer|param:channel|desc"
msgid "Select a timer to display (by selecting the timer voice channel)"
msgstr ""
#: src/modules/pomodoro/cog.py:353 src/modules/pomodoro/cog.py:423
#: src/modules/pomodoro/cog.py:367 src/modules/pomodoro/cog.py:438
#, possible-python-brace-format
msgctxt "cmd:timer|error:no_timers|desc"
msgid ""
@@ -152,7 +152,7 @@ msgid ""
"rent a private room with {room_cmd} and create one yourself!"
msgstr ""
#: src/modules/pomodoro/cog.py:367
#: src/modules/pomodoro/cog.py:381
#, possible-python-brace-format
msgctxt "cmd:timer|error:no_channel|desc"
msgid ""
@@ -161,7 +161,7 @@ msgid ""
"list the available timers in this server."
msgstr ""
#: src/modules/pomodoro/cog.py:380
#: src/modules/pomodoro/cog.py:394
#, possible-python-brace-format
msgctxt "cmd:timer|error:no_timer_in_channel"
msgid ""
@@ -169,17 +169,17 @@ msgid ""
"Use {timers_cmd} to list the available timers in this server."
msgstr ""
#: src/modules/pomodoro/cog.py:396
#: src/modules/pomodoro/cog.py:411
msgctxt "cmd:timers"
msgid "timers"
msgstr ""
#: src/modules/pomodoro/cog.py:397
#: src/modules/pomodoro/cog.py:412
msgctxt "cmd:timers|desc"
msgid "List the available pomodoro timer rooms."
msgstr ""
#: src/modules/pomodoro/cog.py:436
#: src/modules/pomodoro/cog.py:451
#, possible-python-brace-format
msgctxt "cmd:timer|error:no_visible_timers|desc"
msgid ""
@@ -188,13 +188,13 @@ msgid ""
"with {room_cmd} and create one yourself!"
msgstr ""
#: src/modules/pomodoro/cog.py:449
#: src/modules/pomodoro/cog.py:464
#, possible-python-brace-format
msgctxt "cmd:timers|embed:timer_list|title"
msgid "Pomodoro Timer Rooms in **{guild}**"
msgstr ""
#: src/modules/pomodoro/cog.py:458
#: src/modules/pomodoro/cog.py:473
#, possible-python-brace-format
msgctxt "cmd:timers|status:stopped_auto"
msgid ""
@@ -202,7 +202,7 @@ msgid ""
"Join {channel} to restart it."
msgstr ""
#: src/modules/pomodoro/cog.py:464
#: src/modules/pomodoro/cog.py:479
#, possible-python-brace-format
msgctxt "cmd:timers|status:stopped_manual"
msgid ""
@@ -210,7 +210,7 @@ msgid ""
"Join {channel} and press `Start` to start it!"
msgstr ""
#: src/modules/pomodoro/cog.py:471
#: src/modules/pomodoro/cog.py:486
#, possible-python-brace-format
msgctxt "cmd:timers|status:running_focus"
msgid ""
@@ -218,7 +218,7 @@ msgid ""
"Currently **focusing**, with break starting {timestamp}"
msgstr ""
#: src/modules/pomodoro/cog.py:477
#: src/modules/pomodoro/cog.py:492
#, possible-python-brace-format
msgctxt "cmd:timers|status:running_break"
msgid ""
@@ -226,78 +226,78 @@ msgid ""
"Currently **resting**, with focus starting {timestamp}"
msgstr ""
#: src/modules/pomodoro/cog.py:491
#: src/modules/pomodoro/cog.py:506
msgctxt "cmd:pomodoro"
msgid "pomodoro"
msgstr ""
#: src/modules/pomodoro/cog.py:492
#: src/modules/pomodoro/cog.py:507
msgctxt "cmd:pomodoro|desc"
msgid "Create and configure pomodoro timer rooms."
msgstr ""
#: src/modules/pomodoro/cog.py:499
#: src/modules/pomodoro/cog.py:514
msgctxt "cmd:pomodoro_create"
msgid "create"
msgstr ""
#: src/modules/pomodoro/cog.py:502
#: src/modules/pomodoro/cog.py:517
msgctxt "cmd:pomodoro_create|desc"
msgid "Create a new Pomodoro timer. Requires manage channel permissions."
msgstr ""
#: src/modules/pomodoro/cog.py:506
#: src/modules/pomodoro/cog.py:521
msgctxt "cmd:pomodoro_create|param:channel"
msgid "timer_channel"
msgstr ""
#: src/modules/pomodoro/cog.py:512
#: src/modules/pomodoro/cog.py:527
msgctxt "cmd:pomodoro_create|param:channel|desc"
msgid ""
"Voice channel to create the timer in. (Defaults to your current channel, or "
"makes a new one.)"
msgstr ""
#: src/modules/pomodoro/cog.py:557
#: src/modules/pomodoro/cog.py:572
msgctxt "cmd:pomodoro_create|new_channel|error:your_insufficient_perms|title"
msgid "Could not create pomodoro voice channel!"
msgstr ""
#: src/modules/pomodoro/cog.py:561
#: src/modules/pomodoro/cog.py:576
msgctxt "cmd:pomodoro_create|new_channel|error:your_insufficient_perms"
msgid ""
"No `timer_channel` was provided, and you lack the 'Manage Channels` "
"permission required to create a new timer room!"
msgstr ""
#: src/modules/pomodoro/cog.py:572
#: src/modules/pomodoro/cog.py:587
msgctxt "cmd:pomodoro_create|new_channel|error:my_insufficient_perms|title"
msgid "Could not create pomodoro voice channel!"
msgstr ""
#: src/modules/pomodoro/cog.py:576
#: src/modules/pomodoro/cog.py:591
msgctxt "cmd:pomodoro_create|new_channel|error:my_insufficient_perms|desc"
msgid ""
"No `timer_channel` was provided, and I lack the 'Manage Channels' permission "
"required to create a new voice channel."
msgstr ""
#: src/modules/pomodoro/cog.py:587
#: src/modules/pomodoro/cog.py:602
msgctxt "cmd:pomodoro_create|new_channel|default_name"
msgid "Timer"
msgstr ""
#: src/modules/pomodoro/cog.py:591
#: src/modules/pomodoro/cog.py:606
msgctxt "cmd:pomodoro_create|new_channel|audit_reason"
msgid "Creating Pomodoro Voice Channel"
msgstr ""
#: src/modules/pomodoro/cog.py:600
#: src/modules/pomodoro/cog.py:615
msgctxt "cmd:pomodoro_create|new_channel|error:channel_create_failed|title"
msgid "Could not create pomodoro voice channel!"
msgstr ""
#: src/modules/pomodoro/cog.py:604
#: src/modules/pomodoro/cog.py:619
msgctxt "cmd:pomodoro_create|new_channel|error:channel_create_failed|desc"
msgid ""
"Failed to create a new pomodoro voice channel due to an unknown Discord "
@@ -305,13 +305,13 @@ msgid ""
"the `timer_channel` argument of this command."
msgstr ""
#: src/modules/pomodoro/cog.py:621
#: src/modules/pomodoro/cog.py:636
#, possible-python-brace-format
msgctxt "cmd:pomodoro_create|add_timer|error:timer_exists"
msgid "A timer already exists in {channel}! Reconfigure it with {edit_cmd}."
msgstr ""
#: src/modules/pomodoro/cog.py:635
#: src/modules/pomodoro/cog.py:650
#, possible-python-brace-format
msgctxt "cmd:pomodoro_create|add_timer|error:your_insufficient_perms"
msgid ""
@@ -319,43 +319,43 @@ msgid ""
"timer there!"
msgstr ""
#: src/modules/pomodoro/cog.py:684
#: src/modules/pomodoro/cog.py:699
msgctxt "cmd:pomodoro_create|response:success|content"
msgid "Timer created successfully! Use the panel below to reconfigure."
msgstr ""
#: src/modules/pomodoro/cog.py:690
#: src/modules/pomodoro/cog.py:705
msgctxt "cmd:pomodoro_destroy"
msgid "destroy"
msgstr ""
#: src/modules/pomodoro/cog.py:693
#: src/modules/pomodoro/cog.py:708
msgctxt "cmd:pomodoro_destroy|desc"
msgid "Remove a pomodoro timer from a voice channel."
msgstr ""
#: src/modules/pomodoro/cog.py:697
#: src/modules/pomodoro/cog.py:712
msgctxt "cmd:pomodoro_destroy|param:channel"
msgid "timer_channel"
msgstr ""
#: src/modules/pomodoro/cog.py:700
#: src/modules/pomodoro/cog.py:715
msgctxt "cmd:pomodoro_destroy|param:channel"
msgid "Select a timer voice channel to remove the timer from."
msgstr ""
#: src/modules/pomodoro/cog.py:718
#: src/modules/pomodoro/cog.py:733
msgctxt "cmd:pomodoro_destroy|error:no_timer"
msgid "This channel doesn't have an attached pomodoro timer!"
msgstr ""
#: src/modules/pomodoro/cog.py:731
#: src/modules/pomodoro/cog.py:746
msgctxt "cmd:pomodoro_destroy|error:insufficient_perms|owned"
msgid ""
"You need to be an administrator or own this channel to remove this timer!"
msgstr ""
#: src/modules/pomodoro/cog.py:740
#: src/modules/pomodoro/cog.py:755
#, possible-python-brace-format
msgctxt "cmd:pomodoro_destroy|error:insufficient_perms|notowned"
msgid ""
@@ -363,38 +363,38 @@ msgid ""
"this timer!"
msgstr ""
#: src/modules/pomodoro/cog.py:751
#: src/modules/pomodoro/cog.py:766
#, possible-python-brace-format
msgctxt "cmd:pomdoro_destroy|response:success|description"
msgid "Timer successfully removed from {channel}."
msgstr ""
#: src/modules/pomodoro/cog.py:757
#: src/modules/pomodoro/cog.py:772
msgctxt "cmd:pomodoro_edit"
msgid "edit"
msgstr ""
#: src/modules/pomodoro/cog.py:760
#: src/modules/pomodoro/cog.py:775
msgctxt "cmd:pomodoro_edit|desc"
msgid "Reconfigure a pomodoro timer."
msgstr ""
#: src/modules/pomodoro/cog.py:764
#: src/modules/pomodoro/cog.py:779
msgctxt "cmd:pomodoro_edit|param:channel"
msgid "timer_channel"
msgstr ""
#: src/modules/pomodoro/cog.py:770
#: src/modules/pomodoro/cog.py:785
msgctxt "cmd:pomodoro_edit|param:channel|desc"
msgid "Select a timer voice channel to reconfigure."
msgstr ""
#: src/modules/pomodoro/cog.py:811
#: src/modules/pomodoro/cog.py:826
msgctxt "cmd:pomodoro_edit|error:no_timer"
msgid "This channel doesn't have an attached pomodoro timer to edit!"
msgstr ""
#: src/modules/pomodoro/cog.py:824
#: src/modules/pomodoro/cog.py:839
msgctxt "cmd:pomodoro_edit|error:insufficient_perms|role:other"
msgid ""
"Insufficient permissions to modifiy this timer!\n"
@@ -402,28 +402,28 @@ msgid ""
"manager role."
msgstr ""
#: src/modules/pomodoro/cog.py:845
#: src/modules/pomodoro/cog.py:860
msgctxt "cmd:pomodoro_edit|error:insufficient_permissions|role_needed:admin"
msgid "You need to be a guild admin to modify this option!"
msgstr ""
#: src/modules/pomodoro/cog.py:850
#: src/modules/pomodoro/cog.py:865
msgctxt "cmd:pomodoro_edit|error:insufficient_permissions|role_needed:owner"
msgid "You need to be a channel owner or guild admin to modify this option!"
msgstr ""
#: src/modules/pomodoro/cog.py:855
#: src/modules/pomodoro/cog.py:870
msgctxt "cmd:pomodoro_edit|error:insufficient_permissions|role_needed:manager"
msgid ""
"You need to be a guild admin or have the manager role to modify this option!"
msgstr ""
#: src/modules/pomodoro/cog.py:891
#: src/modules/pomodoro/cog.py:906
msgctxt "cmd:configure_pomodoro"
msgid "pomodoro"
msgstr ""
#: src/modules/pomodoro/cog.py:892
#: src/modules/pomodoro/cog.py:907
msgctxt "cmd:configure_pomodoro|desc"
msgid "Configure Pomodoro Timer System"
msgstr ""

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-09-13 08:47+0300\n"
"POT-Creation-Date: 2023-09-24 12:21+0300\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -84,7 +84,7 @@ msgctxt "cmd:userconfig_language|button:reset|label"
msgid "Reset"
msgstr ""
#: src/babel/cog.py:251
#: src/babel/cog.py:252
#, possible-python-brace-format
msgctxt "acmpl:language|no_match"
msgid "No supported languages matching {partial}"
@@ -219,162 +219,172 @@ msgctxt "guildset:locale|response"
msgid "You have set the guild language to {lang}."
msgstr ""
#: src/babel/enums.py:42
msgctxt "localenames|locale:en-US"
msgid "American English"
msgstr ""
#: src/babel/enums.py:43
msgctxt "localenames|locale:en-GB"
msgid "British English"
msgctxt "localenames|locale:id"
msgid "Indonesian"
msgstr ""
#: src/babel/enums.py:44
msgctxt "localenames|locale:bg"
msgid "Bulgarian"
msgstr ""
#: src/babel/enums.py:45
msgctxt "localenames|locale:zh-CN"
msgid "Chinese"
msgstr ""
#: src/babel/enums.py:46
msgctxt "localenames|locale:zh-TW"
msgid "Taiwan Chinese"
msgstr ""
#: src/babel/enums.py:47
msgctxt "localenames|locale:hr"
msgid "Croatian"
msgstr ""
#: src/babel/enums.py:48
msgctxt "localenames|locale:cs"
msgid "Czech"
msgstr ""
#: src/babel/enums.py:49
msgctxt "localenames|locale:da"
msgid "Danish"
msgstr ""
#: src/babel/enums.py:50
msgctxt "localenames|locale:nl"
msgid "Dutch"
msgstr ""
#: src/babel/enums.py:51
msgctxt "localenames|locale:fi"
msgid "Finnish"
msgstr ""
#: src/babel/enums.py:52
msgctxt "localenames|locale:fr"
msgid "French"
msgstr ""
#: src/babel/enums.py:53
#: src/babel/enums.py:45
msgctxt "localenames|locale:de"
msgid "German"
msgstr ""
#: src/babel/enums.py:54
msgctxt "localenames|locale:el"
msgid "Greek"
#: src/babel/enums.py:46
msgctxt "localenames|locale:en-GB"
msgid "English, UK"
msgstr ""
#: src/babel/enums.py:55
msgctxt "localenames|locale:hi"
msgid "Hindi"
#: src/babel/enums.py:47
msgctxt "localenames|locale:en-US"
msgid "English, US"
msgstr ""
#: src/babel/enums.py:56
msgctxt "localenames|locale:hu"
msgid "Hungarian"
#: src/babel/enums.py:48
msgctxt "localenames|locale:es-ES"
msgid "Spanish"
msgstr ""
#: src/babel/enums.py:57
#: src/babel/enums.py:49
msgctxt "localenames|locale:fr"
msgid "French"
msgstr ""
#: src/babel/enums.py:50
msgctxt "localenames|locale:hr"
msgid "Croatian"
msgstr ""
#: src/babel/enums.py:51
msgctxt "localenames|locale:it"
msgid "Italian"
msgstr ""
#: src/babel/enums.py:58
msgctxt "localenames|locale:ja"
msgid "Japanese"
msgstr ""
#: src/babel/enums.py:59
msgctxt "localenames|locale:ko"
msgid "Korean"
msgstr ""
#: src/babel/enums.py:60
#: src/babel/enums.py:52
msgctxt "localenames|locale:lt"
msgid "Lithuanian"
msgstr ""
#: src/babel/enums.py:61
#: src/babel/enums.py:53
msgctxt "localenames|locale:hu"
msgid "Hungarian"
msgstr ""
#: src/babel/enums.py:54
msgctxt "localenames|locale:nl"
msgid "Dutch"
msgstr ""
#: src/babel/enums.py:55
msgctxt "localenames|locale:no"
msgid "Norwegian"
msgstr ""
#: src/babel/enums.py:62
#: src/babel/enums.py:56
msgctxt "localenames|locale:pl"
msgid "Polish"
msgstr ""
#: src/babel/enums.py:63
#: src/babel/enums.py:57
msgctxt "localenames|locale:pt-BR"
msgid "Brazil Portuguese"
msgid "Portuguese, Brazilian"
msgstr ""
#: src/babel/enums.py:64
#: src/babel/enums.py:58
msgctxt "localenames|locale:ro"
msgid "Romanian"
msgid "Romanian, Romania"
msgstr ""
#: src/babel/enums.py:65
msgctxt "localenames|locale:ru"
msgid "Russian"
#: src/babel/enums.py:59
msgctxt "localenames|locale:fi"
msgid "Finnish"
msgstr ""
#: src/babel/enums.py:66
msgctxt "localenames|locale:es-ES"
msgid "Spain Spanish"
msgstr ""
#: src/babel/enums.py:67
#: src/babel/enums.py:60
msgctxt "localenames|locale:sv-SE"
msgid "Swedish"
msgstr ""
#: src/babel/enums.py:68
msgctxt "localenames|locale:th"
msgid "Thai"
msgstr ""
#: src/babel/enums.py:69
msgctxt "localenames|locale:tr"
msgid "Turkish"
msgstr ""
#: src/babel/enums.py:70
msgctxt "localenames|locale:uk"
msgid "Ukrainian"
msgstr ""
#: src/babel/enums.py:71
#: src/babel/enums.py:61
msgctxt "localenames|locale:vi"
msgid "Vietnamese"
msgstr ""
#: src/babel/enums.py:62
msgctxt "localenames|locale:tr"
msgid "Turkish"
msgstr ""
#: src/babel/enums.py:63
msgctxt "localenames|locale:cs"
msgid "Czech"
msgstr ""
#: src/babel/enums.py:64
msgctxt "localenames|locale:el"
msgid "Greek"
msgstr ""
#: src/babel/enums.py:65
msgctxt "localenames|locale:bg"
msgid "Bulgarian"
msgstr ""
#: src/babel/enums.py:66
msgctxt "localenames|locale:ru"
msgid "Russian"
msgstr ""
#: src/babel/enums.py:67
msgctxt "localenames|locale:uk"
msgid "Ukrainian"
msgstr ""
#: src/babel/enums.py:68
msgctxt "localenames|locale:hi"
msgid "Hindi"
msgstr ""
#: src/babel/enums.py:69
msgctxt "localenames|locale:th"
msgid "Thai"
msgstr ""
#: src/babel/enums.py:70
msgctxt "localenames|locale:zh-CN"
msgid "Chinese, China"
msgstr ""
#: src/babel/enums.py:71
msgctxt "localenames|locale:ja"
msgid "Japanese"
msgstr ""
#: src/babel/enums.py:72
msgctxt "localenames|locale:zh-TW"
msgid "Chinese, Taiwan"
msgstr ""
#: src/babel/enums.py:73
msgctxt "localenames|locale:ko"
msgid "Korean"
msgstr ""
#: src/babel/enums.py:78
msgctxt "localenames|locale:he"
msgid "Hebrew"
msgstr ""
#: src/babel/enums.py:73
msgctxt "localenames|locale:he_IL"
msgid "Hebrew (Israel)"
#: src/babel/enums.py:79
msgctxt "localenames|locale:he-IL"
msgid "Hebrew"
msgstr ""
#: src/babel/enums.py:80
msgctxt "localenames|locale:test"
msgid "Test Language"
msgstr ""

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-09-13 08:47+0300\n"
"POT-Creation-Date: 2023-09-24 12:21+0300\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-09-13 08:47+0300\n"
"POT-Creation-Date: 2023-09-24 12:21+0300\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-09-13 08:47+0300\n"
"POT-Creation-Date: 2023-09-24 12:21+0300\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-09-13 08:47+0300\n"
"POT-Creation-Date: 2023-09-24 12:21+0300\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -17,74 +17,74 @@ msgstr ""
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: 8bit\n"
#: src/modules/sysadmin/exec_cog.py:257
#: src/modules/sysadmin/exec_cog.py:269
msgctxt "ward:sys_admin|failed"
msgid "You must be a bot owner to do this!"
msgstr ""
#: src/modules/sysadmin/exec_cog.py:262
#: src/modules/sysadmin/exec_cog.py:274
msgid "async"
msgstr ""
#: src/modules/sysadmin/exec_cog.py:263
#: src/modules/sysadmin/exec_cog.py:275
msgid "Execute arbitrary code with Exec"
msgstr ""
#: src/modules/sysadmin/exec_cog.py:325
#: src/modules/sysadmin/exec_cog.py:337
msgctxt "command"
msgid "eval"
msgstr ""
#: src/modules/sysadmin/exec_cog.py:326
#: src/modules/sysadmin/exec_cog.py:338
msgctxt "command:eval"
msgid "Execute arbitrary code with Eval"
msgstr ""
#: src/modules/sysadmin/exec_cog.py:329
#: src/modules/sysadmin/exec_cog.py:341
msgctxt "command:eval|param:string"
msgid "Code to evaluate."
msgstr ""
#: src/modules/sysadmin/exec_cog.py:336
#: src/modules/sysadmin/exec_cog.py:348
msgctxt "command"
msgid "asyncall"
msgstr ""
#: src/modules/sysadmin/exec_cog.py:337
#: src/modules/sysadmin/exec_cog.py:349
msgctxt "command:asyncall|desc"
msgid "Execute arbitrary code on all shards."
msgstr ""
#: src/modules/sysadmin/exec_cog.py:340
#: src/modules/sysadmin/exec_cog.py:352
msgctxt "command:asyncall|param:string"
msgid "Cross-shard code to execute. Cannot reference ctx!"
msgstr ""
#: src/modules/sysadmin/exec_cog.py:341
#: src/modules/sysadmin/exec_cog.py:353
msgctxt "command:asyncall|param:target"
msgid "Target shard app name, see autocomplete for options."
msgstr ""
#: src/modules/sysadmin/exec_cog.py:384
#: src/modules/sysadmin/exec_cog.py:396
msgid "reload"
msgstr ""
#: src/modules/sysadmin/exec_cog.py:385
#: src/modules/sysadmin/exec_cog.py:397
msgid "Reload a given LionBot extension. Launches an ExecUI."
msgstr ""
#: src/modules/sysadmin/exec_cog.py:388
#: src/modules/sysadmin/exec_cog.py:400
msgid "Name of the extension to reload. See autocomplete for options."
msgstr ""
#: src/modules/sysadmin/exec_cog.py:389
#: src/modules/sysadmin/exec_cog.py:401
msgid "Whether to force an extension reload even if it doesn't exist."
msgstr ""
#: src/modules/sysadmin/exec_cog.py:425
#: src/modules/sysadmin/exec_cog.py:437
msgid "shutdown"
msgstr ""
#: src/modules/sysadmin/exec_cog.py:426
#: src/modules/sysadmin/exec_cog.py:438
msgid "Shutdown (or restart) the client."
msgstr ""

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-09-13 08:47+0300\n"
"POT-Creation-Date: 2023-09-24 12:21+0300\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-09-13 08:47+0300\n"
"POT-Creation-Date: 2023-09-24 12:21+0300\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-09-13 08:47+0300\n"
"POT-Creation-Date: 2023-09-24 12:21+0300\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -27,45 +27,47 @@ msgctxt "settype:coin|parse|error:notinteger"
msgid "The coin quantity must be a positive integer!"
msgstr ""
#: src/core/setting_types.py:54
#: src/core/setting_types.py:55
#, possible-python-brace-format
msgctxt "settype:coin|parse|error:too_large"
msgid "Provided number of coins was too high!"
msgid "You cannot set this to more than {coin}**{max}**!"
msgstr ""
#: src/core/setting_types.py:60
msgctxt "settype:coin|parse|error:too_large"
msgid "Provided number of coins was too low!"
#: src/core/setting_types.py:63
#, possible-python-brace-format
msgctxt "settype:coin|parse|error:too_small"
msgid "You cannot set this to less than {coin}**{min}**!"
msgstr ""
#: src/core/setting_types.py:71
#: src/core/setting_types.py:75
#, possible-python-brace-format
msgctxt "settype:coin|formatted"
msgid "{coin}**{amount}**"
msgstr ""
#: src/core/setting_types.py:87
#: src/core/setting_types.py:91
msgctxt "settype:message|accepts"
msgid "JSON formatted raw message data"
msgstr ""
#: src/core/setting_types.py:102
#: src/core/setting_types.py:106
msgctxt "settype:message|download|error:not_json"
msgid "The attached message data is not a JSON file!"
msgstr ""
#: src/core/setting_types.py:107
#: src/core/setting_types.py:111
msgctxt "settype:message|download|error:size"
msgid "The attached message data is too large!"
msgstr ""
#: src/core/setting_types.py:116
#: src/core/setting_types.py:120
msgctxt "settype:message|download|error:decoding"
msgid ""
"Could not decode the message data. Please ensure it is saved with the "
"`UTF-8` encoding."
msgstr ""
#: src/core/setting_types.py:173
#: src/core/setting_types.py:177
#, possible-python-brace-format
msgctxt "settype:message|error_suffix"
msgid ""
@@ -73,7 +75,7 @@ msgid ""
"({link})."
msgstr ""
#: src/core/setting_types.py:185
#: src/core/setting_types.py:189
#, possible-python-brace-format
msgctxt "settype:message|error:invalid_json"
msgid ""
@@ -81,34 +83,34 @@ msgid ""
"`{error}`"
msgstr ""
#: src/core/setting_types.py:193
#: src/core/setting_types.py:197
msgctxt "settype:message|error:json_missing_keys"
msgid ""
"Message data must be a JSON object with at least one of the following "
"fields: `content`, `embed`, `embeds`"
msgstr ""
#: src/core/setting_types.py:202
#: src/core/setting_types.py:206
msgctxt "settype:message|error:json_embed_type"
msgid "`embed` field must be a valid JSON object."
msgstr ""
#: src/core/setting_types.py:210
#: src/core/setting_types.py:214
msgctxt "settype:message|error:json_embeds_type"
msgid "`embeds` field must be a list."
msgstr ""
#: src/core/setting_types.py:217
#: src/core/setting_types.py:221
msgctxt "settype:message|error:json_embed_embeds"
msgid "Message data cannot include both `embed` and `embeds`."
msgstr ""
#: src/core/setting_types.py:225
#: src/core/setting_types.py:229
msgctxt "settype:message|error:json_content_type"
msgid "`content` field must be a string."
msgstr ""
#: src/core/setting_types.py:241
#: src/core/setting_types.py:245
#, possible-python-brace-format
msgctxt "ui:settype:message|error:embed_conversion"
msgid ""
@@ -116,7 +118,7 @@ msgid ""
"**Error:** `{exception}`"
msgstr ""
#: src/core/setting_types.py:269
#: src/core/setting_types.py:273
msgctxt "settype:message|format:too_long"
msgid "Too long to display! See Preview."
msgstr ""

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-09-13 08:47+0300\n"
"POT-Creation-Date: 2023-09-24 12:21+0300\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -44,7 +44,7 @@ msgstr ""
#: src/modules/member_admin/settingui.py:213
msgctxt "ui:memberadmin|embed|title"
msgid "Member Admin Configuration Panel"
msgid "Greetings and Initial Roles Panel"
msgstr ""
#: src/modules/member_admin/settingui.py:257
@@ -67,39 +67,39 @@ msgctxt "dash:member_admin|section:initial_roles|name"
msgid "Initial Roles ({commands[configure welcome]})"
msgstr ""
#: src/modules/member_admin/cog.py:234
#: src/modules/member_admin/cog.py:239
msgctxt "cmd:resetmember"
msgid "resetmember"
msgstr ""
#: src/modules/member_admin/cog.py:237
#: src/modules/member_admin/cog.py:242
msgctxt "cmd:resetmember|desc"
msgid "Reset (server-associated) member data for the target member or user."
msgstr ""
#: src/modules/member_admin/cog.py:241
#: src/modules/member_admin/cog.py:246
msgctxt "cmd:resetmember|param:target"
msgid "member_to_reset"
msgstr ""
#: src/modules/member_admin/cog.py:242
#: src/modules/member_admin/cog.py:247
msgctxt "cmd:resetmember|param:saved_roles"
msgid "saved_roles"
msgstr ""
#: src/modules/member_admin/cog.py:247
#: src/modules/member_admin/cog.py:252
msgctxt "cmd:resetmember|param:target|desc"
msgid "Choose the member (current or past) you want to reset."
msgstr ""
#: src/modules/member_admin/cog.py:251
#: src/modules/member_admin/cog.py:256
msgctxt "cmd:resetmember|param:saved_roles|desc"
msgid ""
"Clear the saved roles for this member, so their past roles are not restored "
"on rejoin."
msgstr ""
#: src/modules/member_admin/cog.py:278
#: src/modules/member_admin/cog.py:283
#, possible-python-brace-format
msgctxt "cmd:resetmember|reset:saved_roles|success"
msgid ""
@@ -107,17 +107,17 @@ msgid ""
"roles if they rejoin."
msgstr ""
#: src/modules/member_admin/cog.py:286
#: src/modules/member_admin/cog.py:291
msgctxt "cmd:resetmember|error:nothing_to_do"
msgid "No reset operation selected, nothing to do."
msgstr ""
#: src/modules/member_admin/cog.py:302
#: src/modules/member_admin/cog.py:307
msgctxt "cmd:configure_welcome"
msgid "welcome"
msgstr ""
#: src/modules/member_admin/cog.py:305
#: src/modules/member_admin/cog.py:310
msgctxt "cmd:configure_welcome|desc"
msgid "Configure new member greetings and roles."
msgstr ""

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-09-13 08:47+0300\n"
"POT-Creation-Date: 2023-09-24 12:21+0300\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-09-13 08:47+0300\n"
"POT-Creation-Date: 2023-09-24 12:21+0300\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-09-13 08:47+0300\n"
"POT-Creation-Date: 2023-09-24 12:21+0300\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-09-13 08:47+0300\n"
"POT-Creation-Date: 2023-09-24 12:21+0300\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-09-13 08:47+0300\n"
"POT-Creation-Date: 2023-09-24 12:21+0300\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -32,17 +32,22 @@ msgctxt "cmd:configure_ranks|param:rank_type|choice:message"
msgid "Message"
msgstr ""
#: src/modules/ranks/cog.py:406
#: src/modules/ranks/cog.py:495
msgctxt "event:rank_update|embed:notify"
msgid "New Activity Rank Attained!"
msgstr ""
#: src/modules/ranks/cog.py:516
#: src/modules/ranks/cog.py:601
msgctxt "rank_refresh|error:cannot_chunk|desc"
msgid "Could not retrieve member list from Discord. Please try again later."
msgstr ""
#: src/modules/ranks/cog.py:614
msgctxt "rank_refresh|error:roles_dne|desc"
msgid "Some ranks have invalid or deleted roles! Please remove them first."
msgstr ""
#: src/modules/ranks/cog.py:526
#: src/modules/ranks/cog.py:624
#, possible-python-brace-format
msgctxt "rank_refresh|error:unassignable_roles|desc"
msgid ""
@@ -50,36 +55,36 @@ msgid ""
"{roles}"
msgstr ""
#: src/modules/ranks/cog.py:596
#: src/modules/ranks/cog.py:694
msgctxt "rank_refresh|remove_roles|audit"
msgid "Removing invalid rank role."
msgstr ""
#: src/modules/ranks/cog.py:610
#: src/modules/ranks/cog.py:708
#, possible-python-brace-format
msgctxt "rank_refresh|remove_roles|small_error"
msgid "*Could not remove ranks from {member}*"
msgstr ""
#: src/modules/ranks/cog.py:617
#: src/modules/ranks/cog.py:715
msgctxt "rank_refresh|remove_roles|error:too_many_issues"
msgid ""
"Too many issues occurred while removing ranks! Please check my permissions "
"and try again in a few minutes."
msgstr ""
#: src/modules/ranks/cog.py:631
#: src/modules/ranks/cog.py:729
msgctxt "rank_refresh|add_roles|audit"
msgid "Adding rank role from refresh"
msgstr ""
#: src/modules/ranks/cog.py:645
#: src/modules/ranks/cog.py:743
#, possible-python-brace-format
msgctxt "rank_refresh|add_roles|small_error"
msgid "*Could not add {role} to {member}*"
msgstr ""
#: src/modules/ranks/cog.py:652
#: src/modules/ranks/cog.py:750
msgctxt "rank_refresh|add_roles|error:too_many_issues"
msgid ""
"Too many issues occurred while adding ranks! Please check my permissions and "
@@ -87,22 +92,22 @@ msgid ""
msgstr ""
#. ---------- Commands ----------
#: src/modules/ranks/cog.py:677
#: src/modules/ranks/cog.py:775
msgctxt "cmd:ranks"
msgid "ranks"
msgstr ""
#: src/modules/ranks/cog.py:709
#: src/modules/ranks/cog.py:807
msgctxt "cmd:configure_ranks"
msgid "ranks"
msgstr ""
#: src/modules/ranks/cog.py:710
#: src/modules/ranks/cog.py:808
msgctxt "cmd:configure_ranks|desc"
msgid "Configure Activity Ranks"
msgstr ""
#: src/modules/ranks/cog.py:770
#: src/modules/ranks/cog.py:868
#, possible-python-brace-format
msgctxt ""
"cmd:configure_ranks|response:updated|setting:notification|withdm_withchannel"
@@ -111,20 +116,20 @@ msgid ""
"otherwise to {channel}"
msgstr ""
#: src/modules/ranks/cog.py:776
#: src/modules/ranks/cog.py:874
msgctxt ""
"cmd:configure_ranks|response:updated|setting:notification|withdm_nochannel"
msgid "Rank update notifications will be sent via **direct message**."
msgstr ""
#: src/modules/ranks/cog.py:782
#: src/modules/ranks/cog.py:880
#, possible-python-brace-format
msgctxt ""
"cmd:configure_ranks|response:updated|setting:notification|nodm_withchannel"
msgid "Rank update notifications will be sent to {channel}."
msgstr ""
#: src/modules/ranks/cog.py:787
#: src/modules/ranks/cog.py:885
msgctxt ""
"cmd:configure_ranks|response:updated|setting:notification|nodm_nochannel"
msgid "Members will not be notified when their activity rank updates."
@@ -346,31 +351,31 @@ msgctxt "guildset:dm_ranks|response:false"
msgid "I will never direct message members upon rank advancement."
msgstr ""
#: src/modules/ranks/ui/preview.py:74
#: src/modules/ranks/ui/preview.py:75
msgctxt "ui:rank_preview|button:edit|error:role_deleted"
msgid ""
"The role underlying this rank no longer exists! Please select a new role "
"from the role menu."
msgstr ""
#: src/modules/ranks/ui/preview.py:81
#: src/modules/ranks/ui/preview.py:82
msgctxt "ui:rank_preview|button:edit|error:role_not_assignable"
msgid ""
"I do not have permission to edit the underlying role! Please select a new "
"role from the role menu, or ensure my top role is above the selected role."
msgstr ""
#: src/modules/ranks/ui/preview.py:90
#: src/modules/ranks/ui/preview.py:91
msgctxt "ui:rank_preview|button:edit|error|title"
msgid "Failed to edit rank!"
msgstr ""
#: src/modules/ranks/ui/preview.py:108
#: src/modules/ranks/ui/preview.py:109
msgctxt "ui:rank_preview|button:edit|label"
msgid "Edit"
msgstr ""
#: src/modules/ranks/ui/preview.py:139
#: src/modules/ranks/ui/preview.py:142
#, possible-python-brace-format
msgctxt "ui:rank_preview|button:delete|response:success|description|with_role"
msgid ""
@@ -378,24 +383,24 @@ msgid ""
"the role."
msgstr ""
#: src/modules/ranks/ui/preview.py:144
#: src/modules/ranks/ui/preview.py:147
#, possible-python-brace-format
msgctxt "ui:rank_preview|button:delete|response:success|description|no_role"
msgid "You have deleted the rank {mention}."
msgstr ""
#: src/modules/ranks/ui/preview.py:150
#: src/modules/ranks/ui/preview.py:153
msgctxt "ui:rank_preview|button:delete|response:success|title"
msgid "Rank Deleted"
msgstr ""
#: src/modules/ranks/ui/preview.py:160
#: src/modules/ranks/ui/preview.py:163
msgctxt ""
"ui:rank_preview|button:delete|response:success|button:delete_role|label"
msgid "Delete Role"
msgstr ""
#: src/modules/ranks/ui/preview.py:176
#: src/modules/ranks/ui/preview.py:179
#, possible-python-brace-format
msgctxt ""
"ui:rank_preview|button:delete|response:success|button:delete_role|response:"
@@ -405,7 +410,7 @@ msgid ""
"unknown error."
msgstr ""
#: src/modules/ranks/ui/preview.py:182
#: src/modules/ranks/ui/preview.py:185
#, possible-python-brace-format
msgctxt ""
"ui:rank_preview|button:delete|response:success|button:delete_role|response:"
@@ -413,37 +418,24 @@ msgctxt ""
msgid "You have deleted the rank **{name}** along with the underlying role."
msgstr ""
#: src/modules/ranks/ui/preview.py:199
#: src/modules/ranks/ui/preview.py:202
msgctxt "ui:rank_preview|button:delete|label"
msgid "Delete Rank"
msgstr ""
#: src/modules/ranks/ui/preview.py:219
#, possible-python-brace-format
msgctxt "ui:rank_preview|menu:roles|error:above_caller"
msgid ""
"You have insufficient permissions to assign {mention} as a rank role! You "
"may only manage roles below your top role."
msgstr ""
#: src/modules/ranks/ui/preview.py:225
msgctxt "ui:rank_preview|menu:roles|error:above_caller|title"
msgid "Insufficient permissions!"
msgstr ""
#: src/modules/ranks/ui/preview.py:241
#: src/modules/ranks/ui/preview.py:232
msgctxt "ui:rank_preview|menu:roles|error:not_assignable|suberror:is_default"
msgid "The @everyone role cannot be removed, and cannot be a rank!"
msgstr ""
#: src/modules/ranks/ui/preview.py:246
#: src/modules/ranks/ui/preview.py:237
msgctxt "ui:rank_preview|menu:roles|error:not_assignable|suberror:is_managed"
msgid ""
"The role is managed by another application or integration, and cannot be a "
"rank!"
msgstr ""
#: src/modules/ranks/ui/preview.py:251
#: src/modules/ranks/ui/preview.py:242
msgctxt ""
"ui:rank_preview|menu:roles|error:not_assignable|suberror:no_permissions"
msgid ""
@@ -451,121 +443,121 @@ msgid ""
"manage ranks!"
msgstr ""
#: src/modules/ranks/ui/preview.py:256
#: src/modules/ranks/ui/preview.py:247
msgctxt "ui:rank_preview|menu:roles|error:not_assignable|suberror:above_me"
msgid ""
"This role is above my top role in the role hierarchy, so I cannot add or "
"remove it!"
msgstr ""
#: src/modules/ranks/ui/preview.py:262
#: src/modules/ranks/ui/preview.py:253
msgctxt "ui:rank_preview|menu:roles|error:not_assignable|suberror:other"
msgid "I am not able to manage the selected role, so it cannot be a rank!"
msgstr ""
#: src/modules/ranks/ui/preview.py:268
#: src/modules/ranks/ui/preview.py:259
msgctxt "ui:rank_preview|menu:roles|error:not_assignable|title"
msgid "Could not update rank!"
msgstr ""
#: src/modules/ranks/ui/preview.py:278
#: src/modules/ranks/ui/preview.py:269
msgctxt "ui:rank_preview|menu:roles|placeholder"
msgid "Update Rank Role"
msgstr ""
#: src/modules/ranks/ui/preview.py:290
#: src/modules/ranks/ui/preview.py:281
msgctxt "ui:rank_preview|embed|title"
msgid "Rank Information"
msgstr ""
#: src/modules/ranks/ui/preview.py:297
#: src/modules/ranks/ui/preview.py:288
msgctxt "ui:rank_preview|embed|field:role|name"
msgid "Role"
msgstr ""
#: src/modules/ranks/ui/preview.py:304
#: src/modules/ranks/ui/preview.py:295
msgctxt "ui:rank_preview|embed|field:required|name"
msgid "Required"
msgstr ""
#: src/modules/ranks/ui/preview.py:311
#: src/modules/ranks/ui/preview.py:302
msgctxt "ui:rank_preview|embed|field:reward|name"
msgid "Reward"
msgstr ""
#: src/modules/ranks/ui/preview.py:320
#: src/modules/ranks/ui/preview.py:311
msgctxt "ui:rank_preview|embed|field:message"
msgid "Congratulatory Message"
msgstr ""
#: src/modules/ranks/ui/refresh.py:125
#: src/modules/ranks/ui/refresh.py:134
msgctxt "ui:refresh_ranks|embed|title:errored"
msgid "Could not refresh the server ranks!"
msgstr ""
#: src/modules/ranks/ui/refresh.py:133
#: src/modules/ranks/ui/refresh.py:142
msgctxt "ui:refresh_ranks|embed|title:done"
msgid "Rank refresh complete!"
msgstr ""
#: src/modules/ranks/ui/refresh.py:139
#: src/modules/ranks/ui/refresh.py:148
msgctxt "ui:refresh_ranks|embed|title:working"
msgid "Refreshing your server ranks, please wait."
msgstr ""
#: src/modules/ranks/ui/refresh.py:157
#: src/modules/ranks/ui/refresh.py:166
#, possible-python-brace-format
msgctxt "ui:refresh_ranks|embed|line:ranks"
msgid "**Loading server ranks:** {emoji}"
msgstr ""
#: src/modules/ranks/ui/refresh.py:167
#: src/modules/ranks/ui/refresh.py:176
#, possible-python-brace-format
msgctxt "ui:refresh_ranks|embed|line:members"
msgid "**Loading server members:** {emoji}"
msgstr ""
#: src/modules/ranks/ui/refresh.py:177
#: src/modules/ranks/ui/refresh.py:186
#, possible-python-brace-format
msgctxt "ui:refresh_ranks|embed|line:roles"
msgid "**Loading rank roles:** {emoji}"
msgstr ""
#: src/modules/ranks/ui/refresh.py:187
#: src/modules/ranks/ui/refresh.py:196
#, possible-python-brace-format
msgctxt "ui:refresh_ranks|embed|line:compute"
msgid "**Computing correct ranks:** {emoji}"
msgstr ""
#: src/modules/ranks/ui/refresh.py:198
#: src/modules/ranks/ui/refresh.py:207
msgctxt "ui:refresh_ranks|embed|field:remove|name"
msgid "Removing invalid rank roles from members"
msgstr ""
#: src/modules/ranks/ui/refresh.py:202
#: src/modules/ranks/ui/refresh.py:211
#, possible-python-brace-format
msgctxt "ui:refresh_ranks|embed|field:remove|value"
msgid "{progress} {done}/{total} removed"
msgstr ""
#: src/modules/ranks/ui/refresh.py:213
#: src/modules/ranks/ui/refresh.py:222
#, possible-python-brace-format
msgctxt "ui:refresh_ranks|embed|line:remove"
msgid "**Removed invalid ranks:** {done}/{target}"
msgstr ""
#: src/modules/ranks/ui/refresh.py:221
#: src/modules/ranks/ui/refresh.py:230
msgctxt "ui:refresh_ranks|embed|field:add|name"
msgid "Giving members their rank roles"
msgstr ""
#: src/modules/ranks/ui/refresh.py:225
#: src/modules/ranks/ui/refresh.py:234
#, possible-python-brace-format
msgctxt "ui:refresh_ranks|embed|field:add|value"
msgid "{progress} {done}/{total} given"
msgstr ""
#: src/modules/ranks/ui/refresh.py:236
#: src/modules/ranks/ui/refresh.py:245
#, possible-python-brace-format
msgctxt "ui:refresh_ranks|embed|line:add"
msgid "**Updated member ranks:** {done}/{target}"
@@ -621,67 +613,54 @@ msgctxt "dash:rank|dropdown|placeholder"
msgid "Activity Rank Panel"
msgstr ""
#: src/modules/ranks/ui/overview.py:94
#: src/modules/ranks/ui/overview.py:95
msgctxt "ui:rank_overview|button:auto|label"
msgid "Auto Create"
msgstr ""
#: src/modules/ranks/ui/overview.py:109
#: src/modules/ranks/ui/overview.py:110
msgctxt "ui:rank_overview|button:refresh|label"
msgid "Refresh Member Ranks"
msgstr ""
#: src/modules/ranks/ui/overview.py:121
#: src/modules/ranks/ui/overview.py:122
msgctxt "ui:rank_overview|button:clear|confirm"
msgid "Are you sure you want to **delete all activity ranks** in this server?"
msgstr ""
#: src/modules/ranks/ui/overview.py:126
#: src/modules/ranks/ui/overview.py:127
msgctxt "ui:rank_overview|button:clear|confirm|button:yes"
msgid "Yes, clear ranks"
msgstr ""
#: src/modules/ranks/ui/overview.py:132
#: src/modules/ranks/ui/overview.py:133
msgctxt "ui:rank_overview|button:clear|confirm|button:no"
msgid "Cancel"
msgstr ""
#: src/modules/ranks/ui/overview.py:148
#: src/modules/ranks/ui/overview.py:149
msgctxt "ui:rank_overview|button:clear|label"
msgid "Clear Ranks"
msgstr ""
#: src/modules/ranks/ui/overview.py:178
#: src/modules/ranks/ui/overview.py:179
msgctxt "ui:rank_overview|button:create|label"
msgid "Create Rank"
msgstr ""
#: src/modules/ranks/ui/overview.py:194
#, possible-python-brace-format
msgctxt "ui:rank_overview|menu:roles|error:above_caller"
msgid ""
"You have insufficient permissions to assign {mention} as a rank role! You "
"may only manage roles below your top role."
msgstr ""
#: src/modules/ranks/ui/overview.py:200
msgctxt "ui:rank_overview|menu:roles|error:above_caller|title"
msgid "Insufficient permissions!"
msgstr ""
#: src/modules/ranks/ui/overview.py:233
#: src/modules/ranks/ui/overview.py:222
msgctxt "ui:rank_overview|menu:roles|error:not_assignable|suberror:is_default"
msgid "The @everyone role cannot be removed, and cannot be a rank!"
msgstr ""
#: src/modules/ranks/ui/overview.py:238
#: src/modules/ranks/ui/overview.py:227
msgctxt "ui:rank_overview|menu:roles|error:not_assignable|suberror:is_managed"
msgid ""
"The role is managed by another application or integration, and cannot be a "
"rank!"
msgstr ""
#: src/modules/ranks/ui/overview.py:243
#: src/modules/ranks/ui/overview.py:232
msgctxt ""
"ui:rank_overview|menu:roles|error:not_assignable|suberror:no_permissions"
msgid ""
@@ -689,45 +668,71 @@ msgid ""
"manage ranks!"
msgstr ""
#: src/modules/ranks/ui/overview.py:248
#: src/modules/ranks/ui/overview.py:237
msgctxt "ui:rank_overview|menu:roles|error:not_assignable|suberror:above_me"
msgid ""
"This role is above my top role in the role hierarchy, so I cannot add or "
"remove it!"
msgstr ""
#: src/modules/ranks/ui/overview.py:254
#: src/modules/ranks/ui/overview.py:243
msgctxt "ui:rank_overview|menu:roles|error:not_assignable|suberror:other"
msgid "I am not able to manage the selected role, so it cannot be a rank!"
msgstr ""
#: src/modules/ranks/ui/overview.py:260
#: src/modules/ranks/ui/overview.py:249
msgctxt "ui:rank_overview|menu:roles|error:not_assignable|title"
msgid "Could not create rank!"
msgstr ""
#: src/modules/ranks/ui/overview.py:284
#: src/modules/ranks/ui/overview.py:273
msgctxt "ui:rank_overview|menu:roles|placeholder"
msgid "Create from role"
msgstr ""
#: src/modules/ranks/ui/overview.py:301
#: src/modules/ranks/ui/overview.py:290
msgctxt "ui:rank_overview|menu:ranks|placeholder"
msgid "View or edit rank"
msgstr ""
#: src/modules/ranks/ui/overview.py:387
#: src/modules/ranks/ui/overview.py:376
msgctxt "ui:rank_overview|embed:noranks|desc"
msgid ""
"No activity ranks have been set up!\n"
"Press 'AUTO' to automatically create a standard heirachy of voice | text | "
"xp ranks, or select a role or press Create below!"
msgstr ""
#: src/modules/ranks/ui/overview.py:384
#, possible-python-brace-format
msgctxt "ui:rank_overview|embed|title|type:voice"
msgid "Voice Ranks in {guild_name}"
msgstr ""
#: src/modules/ranks/ui/overview.py:389
#, possible-python-brace-format
msgctxt "ui:rank_overview|embed|title|type:xp"
msgid "XP ranks in {guild_name}"
msgstr ""
#: src/modules/ranks/ui/overview.py:394
#, possible-python-brace-format
msgctxt "ui:rank_overview|embed|title|type:message"
msgid "Message ranks in {guild_name}"
msgstr ""
#: src/modules/ranks/ui/overview.py:406
msgctxt "ui:rank_overview|embed|field:note|name"
msgid "Note"
msgstr ""
#: src/modules/ranks/ui/overview.py:393
#: src/modules/ranks/ui/overview.py:412
#, possible-python-brace-format
msgctxt "ui:rank_overview|embed|field:note|value:with_season"
msgid "Ranks are determined by activity since {timestamp}."
msgstr ""
#: src/modules/ranks/ui/overview.py:400
#: src/modules/ranks/ui/overview.py:419
#, possible-python-brace-format
msgctxt "ui:rank_overview|embed|field:note|value:without_season"
msgid ""
@@ -736,7 +741,7 @@ msgid ""
"ranks) set the `season_start` with {stats_cmd}"
msgstr ""
#: src/modules/ranks/ui/overview.py:407
#: src/modules/ranks/ui/overview.py:426
msgctxt "ui:rank_overview|embed|field:note|value|voice_addendum"
msgid ""
"Also note that ranks will only be updated when a member leaves a tracked "
@@ -744,32 +749,6 @@ msgid ""
"members manually."
msgstr ""
#: src/modules/ranks/ui/overview.py:415
msgctxt "ui:rank_overview|embed:noranks|desc"
msgid ""
"No activity ranks have been set up!\n"
"Press 'AUTO' to automatically create a standard heirachy of voice | text | "
"xp ranks, or select a role or press Create below!"
msgstr ""
#: src/modules/ranks/ui/overview.py:423
#, possible-python-brace-format
msgctxt "ui:rank_overview|embed|title|type:voice"
msgid "Voice Ranks in {guild_name}"
msgstr ""
#: src/modules/ranks/ui/overview.py:428
#, possible-python-brace-format
msgctxt "ui:rank_overview|embed|title|type:xp"
msgid "XP ranks in {guild_name}"
msgstr ""
#: src/modules/ranks/ui/overview.py:433
#, possible-python-brace-format
msgctxt "ui:rank_overview|embed|title|type:message"
msgid "Message ranks in {guild_name}"
msgstr ""
#: src/modules/ranks/ui/editor.py:33
msgctxt "ui:rank_editor|input:role_name|label"
msgid "Role Name"

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-09-13 08:47+0300\n"
"POT-Creation-Date: 2023-09-24 12:21+0300\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-09-13 08:47+0300\n"
"POT-Creation-Date: 2023-09-24 12:21+0300\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -349,13 +349,18 @@ msgctxt "cmd:rolemenu_edit|parse:template|success:custom"
msgid "Now using a custom menu message."
msgstr ""
#: src/modules/rolemenus/cog.py:994
#: src/modules/rolemenus/cog.py:986
msgctxt "cmd:rolemenu_edit|parse:custom_message|success"
msgid "Custom menu message updated."
msgstr ""
#: src/modules/rolemenus/cog.py:1001
#, possible-python-brace-format
msgctxt "cmd:rolemenu_edit|repost|success"
msgid "The role menu is now available at {message}"
msgstr ""
#: src/modules/rolemenus/cog.py:1005
#: src/modules/rolemenus/cog.py:1012
#, possible-python-brace-format
msgctxt "cmd:rolemenu_edit|repost|error:forbidden"
msgid ""
@@ -363,7 +368,7 @@ msgid ""
"permission in {channel}."
msgstr ""
#: src/modules/rolemenus/cog.py:1010
#: src/modules/rolemenus/cog.py:1017
#, possible-python-brace-format
msgctxt "cmd:rolemenu_edit|repost|error:unknown"
msgid ""
@@ -371,40 +376,40 @@ msgid ""
"**Error:** `{exception}`"
msgstr ""
#: src/modules/rolemenus/cog.py:1044
#: src/modules/rolemenus/cog.py:1051
msgctxt "cmd:rolemenu_delete"
msgid "delmenu"
msgstr ""
#: src/modules/rolemenus/cog.py:1047
#: src/modules/rolemenus/cog.py:1054
msgctxt "cmd:rolemenu_delete|desc"
msgid "Delete a role menu."
msgstr ""
#: src/modules/rolemenus/cog.py:1051
#: src/modules/rolemenus/cog.py:1058
msgctxt "cmd:rolemenu_delete|param:name"
msgid "menu"
msgstr ""
#: src/modules/rolemenus/cog.py:1056
#: src/modules/rolemenus/cog.py:1063
msgctxt "cmd:rolemenu_delete|param:name|desc"
msgid "Name of the rolemenu to delete."
msgstr ""
#: src/modules/rolemenus/cog.py:1071
#: src/modules/rolemenus/cog.py:1078
msgctxt "cmd:rolemenu_delete|error:author_perms"
msgid ""
"You need the `MANAGE_ROLES` permission in order to manage the server role "
"menus."
msgstr ""
#: src/modules/rolemenus/cog.py:1094
#: src/modules/rolemenus/cog.py:1101
#, possible-python-brace-format
msgctxt "cmd:rolemenu_delete|error:menu_not_found"
msgid "This server does not have a role menu called `{name}`!"
msgstr ""
#: src/modules/rolemenus/cog.py:1102
#: src/modules/rolemenus/cog.py:1109
#, possible-python-brace-format
msgctxt "cmd:rolemenu_delete|confirm|title"
msgid ""
@@ -412,272 +417,272 @@ msgid ""
"reversible!"
msgstr ""
#: src/modules/rolemenus/cog.py:1107
#: src/modules/rolemenus/cog.py:1114
msgctxt "cmd:rolemenu_delete|confirm|button:yes"
msgid "Yes, Delete Now"
msgstr ""
#: src/modules/rolemenus/cog.py:1112
#: src/modules/rolemenus/cog.py:1119
msgctxt "cmd:rolemenu_delete|confirm|button:no"
msgid "No, Cancel"
msgstr ""
#: src/modules/rolemenus/cog.py:1137
#: src/modules/rolemenus/cog.py:1144
#, possible-python-brace-format
msgctxt "cmd:rolemenu_delete|success|desc"
msgid "Successfully deleted the menu **{name}**"
msgstr ""
#: src/modules/rolemenus/cog.py:1145
#: src/modules/rolemenus/cog.py:1152
msgctxt "cmd:rolemenu_addrole"
msgid "addrole"
msgstr ""
#: src/modules/rolemenus/cog.py:1148
#: src/modules/rolemenus/cog.py:1155
msgctxt "cmd:rolemenu_addrole|desc"
msgid "Add a new role to an existing role menu."
msgstr ""
#: src/modules/rolemenus/cog.py:1153
#: src/modules/rolemenus/cog.py:1160
msgctxt "cmd:rolemenu_addrole|param:menu"
msgid "menu"
msgstr ""
#: src/modules/rolemenus/cog.py:1156
#: src/modules/rolemenus/cog.py:1163
msgctxt "cmd:rolemenu_addrole|param:role"
msgid "role"
msgstr ""
#: src/modules/rolemenus/cog.py:1167
#: src/modules/rolemenus/cog.py:1174
msgctxt "cmd:rolemenu_addrole|param:menu|desc"
msgid "Name of the menu to add a role to"
msgstr ""
#: src/modules/rolemenus/cog.py:1171
#: src/modules/rolemenus/cog.py:1178
msgctxt "cmd:rolemenu_addrole|param:role|desc"
msgid "Role to add to the menu"
msgstr ""
#: src/modules/rolemenus/cog.py:1179
#: src/modules/rolemenus/cog.py:1186
msgctxt "cmd:rolemenu_addrole|param:duration|desc"
msgid "Lifetime of the role after selection in minutes."
msgstr ""
#: src/modules/rolemenus/cog.py:1227
#: src/modules/rolemenus/cog.py:1234
#, possible-python-brace-format
msgctxt "cmd:rolemenu_addrole|error:menu_not_found"
msgid "This server does not have a role menu called `{name}`!"
msgstr ""
#: src/modules/rolemenus/cog.py:1312
#: src/modules/rolemenus/cog.py:1319
msgctxt "cmd:rolemenu_addrole|success:create|title"
msgid "Added Menu Role"
msgstr ""
#: src/modules/rolemenus/cog.py:1316
#: src/modules/rolemenus/cog.py:1323
#, possible-python-brace-format
msgctxt "cmd:rolemenu_addrole|success:create|desc"
msgid "Add the role {role} to the menu **{menu}**."
msgstr ""
#: src/modules/rolemenus/cog.py:1334
#: src/modules/rolemenus/cog.py:1341
msgctxt "cmd:rolemenu_addrole|success:edit|title"
msgid "Menu Role updated"
msgstr ""
#: src/modules/rolemenus/cog.py:1346
#: src/modules/rolemenus/cog.py:1353
#, possible-python-brace-format
msgctxt "cmd:rolemenu_addrole|error:role_exists"
msgid "The role {role} is already selectable from the menu **{menu}**"
msgstr ""
#: src/modules/rolemenus/cog.py:1364
#: src/modules/rolemenus/cog.py:1371
msgctxt "cmd:rolemenu_addrole|success|error:reaction|name"
msgid "Note"
msgstr ""
#: src/modules/rolemenus/cog.py:1376
#: src/modules/rolemenus/cog.py:1383
msgctxt "cmd:rolemenu_addrole|success|button:editor|label"
msgid "Edit Menu"
msgstr ""
#: src/modules/rolemenus/cog.py:1393
#: src/modules/rolemenus/cog.py:1400
msgctxt "cmd:rolemenu_editrole"
msgid "editrole"
msgstr ""
#: src/modules/rolemenus/cog.py:1396
#: src/modules/rolemenus/cog.py:1403
msgctxt "cmd:rolemenu_editrole|desc"
msgid "Edit role options in an existing role menu."
msgstr ""
#: src/modules/rolemenus/cog.py:1401
#: src/modules/rolemenus/cog.py:1408
msgctxt "cmd:rolemenu_editrole|param:menu"
msgid "menu"
msgstr ""
#: src/modules/rolemenus/cog.py:1404
#: src/modules/rolemenus/cog.py:1411
msgctxt "cmd:rolemenu_editrole|param:menu_role"
msgid "menu_role"
msgstr ""
#: src/modules/rolemenus/cog.py:1407
#: src/modules/rolemenus/cog.py:1414
msgctxt "cmd:rolemenu_editrole|param:role"
msgid "new_role"
msgstr ""
#: src/modules/rolemenus/cog.py:1418
#: src/modules/rolemenus/cog.py:1425
msgctxt "cmd:rolemenu_editrole|param:menu|desc"
msgid "Name of the menu to edit the role for"
msgstr ""
#: src/modules/rolemenus/cog.py:1422
#: src/modules/rolemenus/cog.py:1429
msgctxt "cmd:rolemenu_editrole|param:menu_role|desc"
msgid "Label, name, or mention of the menu role to edit."
msgstr ""
#: src/modules/rolemenus/cog.py:1426
#: src/modules/rolemenus/cog.py:1433
msgctxt "cmd:rolemenu_editrole|param:role|desc"
msgid "New server role this menu role should give."
msgstr ""
#: src/modules/rolemenus/cog.py:1434
#: src/modules/rolemenus/cog.py:1441
msgctxt "cmd:rolemenu_editrole|param:duration|desc"
msgid "Lifetime of the role after selection in minutes."
msgstr ""
#: src/modules/rolemenus/cog.py:1475
#: src/modules/rolemenus/cog.py:1482
#, possible-python-brace-format
msgctxt "cmd:rolemenu_editrole|error:menu_not_found"
msgid "This server does not have a role menu called `{name}`!"
msgstr ""
#: src/modules/rolemenus/cog.py:1503
#: src/modules/rolemenus/cog.py:1510
#, possible-python-brace-format
msgctxt "cmd:rolemenu_editrole|error:role_not_found"
msgid "The menu **{menu}** does not have the role **{name}**"
msgstr ""
#: src/modules/rolemenus/cog.py:1569
#: src/modules/rolemenus/cog.py:1576
msgctxt "cmd:rolemenu_editrole|success|title"
msgid "Role menu role updated"
msgstr ""
#: src/modules/rolemenus/cog.py:1584
#: src/modules/rolemenus/cog.py:1591
msgctxt "cmd:rolemenu_editrole|success|error:reaction|name"
msgid "Warning!"
msgstr ""
#: src/modules/rolemenus/cog.py:1609
#: src/modules/rolemenus/cog.py:1616
msgctxt "cmd:rolemenu_delrole"
msgid "delrole"
msgstr ""
#: src/modules/rolemenus/cog.py:1612
#: src/modules/rolemenus/cog.py:1619
msgctxt "cmd:rolemenu_delrole|desc"
msgid "Remove a role from a role menu."
msgstr ""
#: src/modules/rolemenus/cog.py:1616
#: src/modules/rolemenus/cog.py:1623
msgctxt "cmd:rolemenu_delrole|param:menu"
msgid "menu"
msgstr ""
#: src/modules/rolemenus/cog.py:1617
#: src/modules/rolemenus/cog.py:1624
msgctxt "cmd:rolemenu_delrole|param:menu_role"
msgid "menu_role"
msgstr ""
#: src/modules/rolemenus/cog.py:1622
#: src/modules/rolemenus/cog.py:1629
msgctxt "cmd:rolemenu_delrole|param:menu|desc"
msgid "Name of the menu to delete the role from."
msgstr ""
#: src/modules/rolemenus/cog.py:1626
#: src/modules/rolemenus/cog.py:1633
msgctxt "cmd:rolemenu_delrole|param:menu_role|desc"
msgid "Name, label, or mention of the role to delete."
msgstr ""
#: src/modules/rolemenus/cog.py:1644
#: src/modules/rolemenus/cog.py:1651
msgctxt "cmd:rolemenu_delrole|error:author_perms"
msgid ""
"You need the `MANAGE_ROLES` permission in order to manage the server role "
"menus."
msgstr ""
#: src/modules/rolemenus/cog.py:1668
#: src/modules/rolemenus/cog.py:1675
#, possible-python-brace-format
msgctxt "cmd:rolemenu_delrole|error:menu_not_found"
msgid "This server does not have a role menu called `{name}`!"
msgstr ""
#: src/modules/rolemenus/cog.py:1696
#: src/modules/rolemenus/cog.py:1703
#, possible-python-brace-format
msgctxt "cmd:rolemenu_delrole|error:role_not_found"
msgid "The menu **{menu}** does not have the role **{name}**"
msgstr ""
#: src/modules/rolemenus/cog.py:1713
#: src/modules/rolemenus/cog.py:1720
#, possible-python-brace-format
msgctxt "cmd:rolemenu_delrole|success"
msgid "The role **{name}** was successfully removed from the menu **{menu}**."
msgstr ""
#: src/modules/rolemenus/roleoptions.py:54
#: src/modules/rolemenus/roleoptions.py:57
msgctxt "roleset:role"
msgid "role"
msgstr ""
#: src/modules/rolemenus/roleoptions.py:57
#: src/modules/rolemenus/roleoptions.py:60
msgctxt "roleset:role|desc"
msgid "The role associated to this menu item."
msgstr ""
#: src/modules/rolemenus/roleoptions.py:61
#: src/modules/rolemenus/roleoptions.py:64
msgctxt "roleset:role|long_desc"
msgid "The role given when this menu item is selected in the role menu."
msgstr ""
#: src/modules/rolemenus/roleoptions.py:74
#: src/modules/rolemenus/roleoptions.py:77
#, possible-python-brace-format
msgctxt "roleset:role|set_response:set"
msgid "This menu item will now give the role {role}."
msgstr ""
#: src/modules/rolemenus/roleoptions.py:82
#: src/modules/rolemenus/roleoptions.py:88
msgctxt "roleset:label"
msgid "label"
msgstr ""
#: src/modules/rolemenus/roleoptions.py:85
#: src/modules/rolemenus/roleoptions.py:91
msgctxt "roleset:label|desc"
msgid "A short button label for this role."
msgstr ""
#: src/modules/rolemenus/roleoptions.py:90
#: src/modules/rolemenus/roleoptions.py:96
msgctxt "roleset:label|long_desc"
msgid ""
"A short name for this role, to be displayed in button labels, dropdown "
"titles, and some menu layouts. By default uses the Discord role name."
msgstr ""
#: src/modules/rolemenus/roleoptions.py:104
#: src/modules/rolemenus/roleoptions.py:110
#, possible-python-brace-format
msgctxt "roleset:role|set_response"
msgid "This menu role is now called `{value}`."
msgstr ""
#: src/modules/rolemenus/roleoptions.py:112
#: src/modules/rolemenus/roleoptions.py:118
msgctxt "roleset:emoji"
msgid "emoji"
msgstr ""
#: src/modules/rolemenus/roleoptions.py:115
#: src/modules/rolemenus/roleoptions.py:121
msgctxt "roleset:emoji|desc"
msgid "The emoji associated with this role."
msgstr ""
#: src/modules/rolemenus/roleoptions.py:119
#: src/modules/rolemenus/roleoptions.py:125
msgctxt "roleset:emoji|long_desc"
msgid ""
"The role emoji is used for the reaction (in reaction role menus), and "
@@ -685,110 +690,120 @@ msgid ""
"The emoji is also displayed next to the role in most menu templates."
msgstr ""
#: src/modules/rolemenus/roleoptions.py:157
#: src/modules/rolemenus/roleoptions.py:165
#, possible-python-brace-format
msgctxt "roleset:emoji|error:test_emoji"
msgid "The selected emoji `{emoji}` is invalid or has been deleted."
msgstr ""
#: src/modules/rolemenus/roleoptions.py:168
#: src/modules/rolemenus/roleoptions.py:176
#, possible-python-brace-format
msgctxt "roleset:emoji|set_response:set"
msgid "The menu role emoji is now {emoji}."
msgstr ""
#: src/modules/rolemenus/roleoptions.py:173
#: src/modules/rolemenus/roleoptions.py:181
msgctxt "roleset:emoji|set_response:unset"
msgid "The menu role emoji has been removed."
msgstr ""
#: src/modules/rolemenus/roleoptions.py:181
#: src/modules/rolemenus/roleoptions.py:189
msgctxt "roleset:description"
msgid "description"
msgstr ""
#: src/modules/rolemenus/roleoptions.py:184
#: src/modules/rolemenus/roleoptions.py:192
msgctxt "roleset:description|desc"
msgid "A longer description of this role."
msgstr ""
#: src/modules/rolemenus/roleoptions.py:189
#: src/modules/rolemenus/roleoptions.py:197
msgctxt "roleset:description|long_desc"
msgid ""
"The description is displayed under the role label in dropdown style menus. "
"It may also be used as a substitution key in custom role selection responses."
msgstr ""
#: src/modules/rolemenus/roleoptions.py:205
#: src/modules/rolemenus/roleoptions.py:213
msgctxt "roleset:description|set_response:set"
msgid "The role description has been set."
msgstr ""
#: src/modules/rolemenus/roleoptions.py:210
#: src/modules/rolemenus/roleoptions.py:218
msgctxt "roleset:description|set_response:unset"
msgid "The role description has been removed."
msgstr ""
#: src/modules/rolemenus/roleoptions.py:218
#: src/modules/rolemenus/roleoptions.py:226
msgctxt "roleset:price"
msgid "price"
msgstr ""
#: src/modules/rolemenus/roleoptions.py:221
msgctxt "roleset:price|desc"
msgid "Price of the role, in LionCoins."
msgstr ""
#: src/modules/rolemenus/roleoptions.py:225
msgctxt "roleset:price|long_desc"
msgid "How much the role costs when selected, in LionCoins."
msgstr ""
#: src/modules/rolemenus/roleoptions.py:229
msgctxt "roleset:price|desc"
msgid "Price of the role, in LionCoins. May be negative."
msgstr ""
#: src/modules/rolemenus/roleoptions.py:233
msgctxt "roleset:price|long_desc"
msgid ""
"How many LionCoins should be deducted from a member's account when they "
"equip this role through this menu.\n"
"The price may be negative, in which case the member will instead be rewarded "
"coins when they equip the role."
msgstr ""
#: src/modules/rolemenus/roleoptions.py:240
msgctxt "roleset:price|accepts"
msgid "Amount of coins that the role costs."
msgid "Amount of coins that the role costs when equipped."
msgstr ""
#: src/modules/rolemenus/roleoptions.py:242
#: src/modules/rolemenus/roleoptions.py:254
#, possible-python-brace-format
msgctxt "roleset:price|set_response:set"
msgid "This role will now cost {price} to equip."
msgctxt "roleset:price|set_response:positive"
msgid "Equipping this role will now cost {coin}**{price}**."
msgstr ""
#: src/modules/rolemenus/roleoptions.py:247
msgctxt "roleset:price|set_response:unset"
msgid "This role will now be free to equip from this role menu."
#: src/modules/rolemenus/roleoptions.py:259
msgctxt "roleset:price|set_response:zero"
msgid "Equipping this role is now free."
msgstr ""
#: src/modules/rolemenus/roleoptions.py:255
#: src/modules/rolemenus/roleoptions.py:264
#, possible-python-brace-format
msgctxt "roleset:price|set_response:negative"
msgid "Equipping this role will now reward {coin}**{price}**."
msgstr ""
#: src/modules/rolemenus/roleoptions.py:272
msgctxt "roleset:duration"
msgid "duration"
msgstr ""
#: src/modules/rolemenus/roleoptions.py:258
#: src/modules/rolemenus/roleoptions.py:275
msgctxt "roleset:duration|desc"
msgid "Lifetime of the role after selection"
msgstr ""
#: src/modules/rolemenus/roleoptions.py:262
#: src/modules/rolemenus/roleoptions.py:279
msgctxt "roleset:duration|long_desc"
msgid ""
"Allows creation of 'temporary roles' which expire a given time after being "
"equipped. Refunds will not be given upon expiry."
msgstr ""
#: src/modules/rolemenus/roleoptions.py:267
#: src/modules/rolemenus/roleoptions.py:284
msgctxt "roleset:duration|notset"
msgid "Forever."
msgstr ""
#: src/modules/rolemenus/roleoptions.py:280
#: src/modules/rolemenus/roleoptions.py:297
#, possible-python-brace-format
msgctxt "roleset:duration|set_response:set"
msgid "This role will now expire after {duration}."
msgstr ""
#: src/modules/rolemenus/roleoptions.py:285
#: src/modules/rolemenus/roleoptions.py:302
msgctxt "roleset:duration|set_response:unset"
msgid "This role will no longer expire after being selected."
msgstr ""
@@ -893,25 +908,31 @@ msgctxt "rolemenu|deselect|success:refund|desc"
msgid "You have removed **{role}**, and been refunded {coin} **{amount}**."
msgstr ""
#: src/modules/rolemenus/rolemenu.py:540
#: src/modules/rolemenus/rolemenu.py:541
#, possible-python-brace-format
msgctxt "rolemenu|deselect|success:negrefund|desc"
msgid "You have removed **{role}**, and have lost {coin} **{amount}**."
msgstr ""
#: src/modules/rolemenus/rolemenu.py:546
#, possible-python-brace-format
msgctxt "rolemenu|deselect|success:norefund|desc"
msgid "You have unequipped **{role}**."
msgstr ""
#: src/modules/rolemenus/rolemenu.py:554
#: src/modules/rolemenus/rolemenu.py:560
#, possible-python-brace-format
msgctxt "rolemenu|select|error:required_role"
msgid "You need to have the **{role}** role to use this!"
msgid "You need to have the role **{role}** required to use this menu!"
msgstr ""
#: src/modules/rolemenus/rolemenu.py:568
#: src/modules/rolemenus/rolemenu.py:574
#, possible-python-brace-format
msgctxt "rolemenu|select|error:max_obtainable"
msgid "You already have the maximum of {obtainable} roles from this menu!"
msgstr ""
#: src/modules/rolemenus/rolemenu.py:582
#: src/modules/rolemenus/rolemenu.py:588
#, possible-python-brace-format
msgctxt "rolemenu|select|error:insufficient_funds"
msgid ""
@@ -919,41 +940,41 @@ msgid ""
"**{balance}**!"
msgstr ""
#: src/modules/rolemenus/rolemenu.py:598
#: src/modules/rolemenus/rolemenu.py:604
msgctxt "rolemenu|select|error:perms"
msgid "I don't have enough permissions to give you this role!"
msgstr ""
#: src/modules/rolemenus/rolemenu.py:605
#: src/modules/rolemenus/rolemenu.py:611
msgctxt "rolemenu|select|error:discord"
msgid ""
"An unknown error occurred while assigning your role! Please try again later."
msgstr ""
#: src/modules/rolemenus/rolemenu.py:647
#: src/modules/rolemenus/rolemenu.py:653
msgctxt "rolemenu|select|success|title"
msgid "Role equipped"
msgstr ""
#: src/modules/rolemenus/rolemenu.py:653
#: src/modules/rolemenus/rolemenu.py:659
#, possible-python-brace-format
msgctxt "rolemenu|select|success:purchase|desc"
msgid "You have purchased the role **{role}** for {coin}**{amount}**"
msgstr ""
#: src/modules/rolemenus/rolemenu.py:658
#: src/modules/rolemenus/rolemenu.py:664
#, possible-python-brace-format
msgctxt "rolemenu|select|success:nopurchase|desc"
msgid "You have equipped the role **{role}**"
msgstr ""
#: src/modules/rolemenus/rolemenu.py:664
#: src/modules/rolemenus/rolemenu.py:670
#, possible-python-brace-format
msgctxt "rolemenu|select|expires_at"
msgid "The role will expire at {timestamp}."
msgstr ""
#: src/modules/rolemenus/rolemenu.py:717
#: src/modules/rolemenus/rolemenu.py:724
#, possible-python-brace-format
msgctxt "rolemenu|content:reactions"
msgid "[Click here]({jump_link}) to jump back."
@@ -1200,251 +1221,251 @@ msgctxt "ui:menu_editor|button:bulk_edit|modal|title"
msgid "Menu Options"
msgstr ""
#: src/modules/rolemenus/ui/menueditor.py:215
#: src/modules/rolemenus/ui/menueditor.py:218
msgctxt "ui:menu_editor|button:bulk_edit|label"
msgid "Bulk Edit"
msgstr ""
#: src/modules/rolemenus/ui/menueditor.py:235
#: src/modules/rolemenus/ui/menueditor.py:238
msgctxt "ui:menu_editor|button:sticky|label"
msgid "Toggle Sticky"
msgstr ""
#: src/modules/rolemenus/ui/menueditor.py:259
#: src/modules/rolemenus/ui/menueditor.py:262
msgctxt "ui:menu_editor|button:refunds|label"
msgid "Toggle Refunds"
msgstr ""
#: src/modules/rolemenus/ui/menueditor.py:289
#: src/modules/rolemenus/ui/menueditor.py:292
msgctxt "ui:menu_editor|menu:reqroles|placeholder"
msgid "Select Required Role"
msgstr ""
#: src/modules/rolemenus/ui/menueditor.py:308
#: src/modules/rolemenus/ui/menueditor.py:311
msgctxt "ui:menu_editor|button:modify_roles|label"
msgid "Modify Roles"
msgstr ""
#: src/modules/rolemenus/ui/menueditor.py:337
#: src/modules/rolemenus/ui/menueditor.py:340
msgctxt "ui:menu_editor|role_editor|modal|title"
msgid "Edit Menu Role"
msgstr ""
#: src/modules/rolemenus/ui/menueditor.py:411
#: src/modules/rolemenus/ui/menueditor.py:416
msgctxt "ui:menu_editor|menu:add_roles|error:too_many_reactions"
msgid "Too many roles! Reaction role menus cannot exceed `20` roles."
msgstr ""
#: src/modules/rolemenus/ui/menueditor.py:416
#: src/modules/rolemenus/ui/menueditor.py:421
msgctxt "ui:menu_editor|menu:add_roles|error:too_many_roles"
msgid "Too many roles! Role menus cannot have more than `25` roles."
msgstr ""
#: src/modules/rolemenus/ui/menueditor.py:455
#: src/modules/rolemenus/ui/menueditor.py:460
msgctxt "ui:menu_editor|menu:add_roles|placeholder"
msgid "Add Roles"
msgstr ""
#: src/modules/rolemenus/ui/menueditor.py:481
#: src/modules/rolemenus/ui/menueditor.py:486
msgctxt "ui:menu_editor|menu:edit_roles|placeholder"
msgid "Edit Roles"
msgstr ""
#: src/modules/rolemenus/ui/menueditor.py:514
#: src/modules/rolemenus/ui/menueditor.py:524
msgctxt "ui:menu_editor|menu:del_role|placeholder"
msgid "Remove Roles"
msgstr ""
#: src/modules/rolemenus/ui/menueditor.py:538
#: src/modules/rolemenus/ui/menueditor.py:548
msgctxt "ui:menu_editor|button:style|error:non-managed"
msgid ""
"Cannot change the style of a menu attached to a message I did not send! "
"Please repost first."
msgstr ""
#: src/modules/rolemenus/ui/menueditor.py:551
#: src/modules/rolemenus/ui/menueditor.py:561
msgctxt "ui:menu_editor|button:style|label"
msgid "Menu Style"
msgstr ""
#: src/modules/rolemenus/ui/menueditor.py:576
#: src/modules/rolemenus/ui/menueditor.py:586
msgctxt "ui:menu_editor|menu:style|error:too_many_reactions"
msgid ""
"Too many roles! The Reaction style is limited to `20` roles (Discord "
"limitation)."
msgstr ""
#: src/modules/rolemenus/ui/menueditor.py:586
#: src/modules/rolemenus/ui/menueditor.py:596
msgctxt "ui:menu_editor|menu:style|error:incomplete_emojis"
msgid ""
"Cannot switch to the Reaction Role Style! Every role needs to have a "
"distinct emoji first."
msgstr ""
#: src/modules/rolemenus/ui/menueditor.py:602
#: src/modules/rolemenus/ui/menueditor.py:614
msgctxt "ui:menu_editor|menu:style|placeholder"
msgid "Select Menu Style"
msgstr ""
#: src/modules/rolemenus/ui/menueditor.py:606
#: src/modules/rolemenus/ui/menueditor.py:618
msgctxt "ui:menu_editor|menu:style|option:reaction|label"
msgid "Reaction Roles"
msgstr ""
#: src/modules/rolemenus/ui/menueditor.py:609
#: src/modules/rolemenus/ui/menueditor.py:621
msgctxt "ui:menu_editor|menu:style|option:reaction|desc"
msgid "Roles are represented compactly as clickable reactions on a message."
msgstr ""
#: src/modules/rolemenus/ui/menueditor.py:615
#: src/modules/rolemenus/ui/menueditor.py:627
msgctxt "ui:menu_editor|menu:style|option:button|label"
msgid "Button Menu"
msgstr ""
#: src/modules/rolemenus/ui/menueditor.py:618
#: src/modules/rolemenus/ui/menueditor.py:630
msgctxt "ui:menu_editor|menu:style|option:button|desc"
msgid ""
"Roles are represented in 5 rows of 5 buttons, each with an emoji and label."
msgstr ""
#: src/modules/rolemenus/ui/menueditor.py:624
#: src/modules/rolemenus/ui/menueditor.py:636
msgctxt "ui:menu_editor|menu:style|option:dropdown|label"
msgid "Dropdown Menu"
msgstr ""
#: src/modules/rolemenus/ui/menueditor.py:627
#: src/modules/rolemenus/ui/menueditor.py:639
msgctxt "ui:menu_editor|menu:style|option:dropdown|desc"
msgid "Roles are selectable from a dropdown menu below the message."
msgstr ""
#: src/modules/rolemenus/ui/menueditor.py:697
#: src/modules/rolemenus/ui/menueditor.py:709
msgctxt "ui:menu_editor|menu:template|placeholder"
msgid "Select Message Template"
msgstr ""
#: src/modules/rolemenus/ui/menueditor.py:707
#: src/modules/rolemenus/ui/menueditor.py:719
msgctxt "ui:menu_editor|menu:template|option:custom|label"
msgid "Custom Message"
msgstr ""
#: src/modules/rolemenus/ui/menueditor.py:712
#: src/modules/rolemenus/ui/menueditor.py:724
msgctxt "ui:menu_editor|menu:template|option:custom|description"
msgid "Entirely custom menu message (opens an interactive editor)."
msgstr ""
#: src/modules/rolemenus/ui/menueditor.py:729
#: src/modules/rolemenus/ui/menueditor.py:741
msgctxt "ui:menu_editor|button:delete|confirm|title"
msgid "Are you sure you want to delete this menu? This is not reversible!"
msgstr ""
#: src/modules/rolemenus/ui/menueditor.py:734
#: src/modules/rolemenus/ui/menueditor.py:746
msgctxt "ui:menu_editor|button:delete|confirm|button:yes"
msgid "Yes, Delete Now"
msgstr ""
#: src/modules/rolemenus/ui/menueditor.py:739
#: src/modules/rolemenus/ui/menueditor.py:751
msgctxt "ui:menu_editor|button:delete|confirm|button:no"
msgid "No, Go Back"
msgstr ""
#: src/modules/rolemenus/ui/menueditor.py:756
#: src/modules/rolemenus/ui/menueditor.py:768
msgctxt "ui:menu_editor|button:delete|label"
msgid "Delete Menu"
msgstr ""
#: src/modules/rolemenus/ui/menueditor.py:813
#: src/modules/rolemenus/ui/menueditor.py:825
msgctxt "ui:menu_editor|button:edit_msg|label"
msgid "Edit Message"
msgstr ""
#: src/modules/rolemenus/ui/menueditor.py:844
#: src/modules/rolemenus/ui/menueditor.py:851
msgctxt "ui:menu_editor|button:preview|label"
msgid "Preview"
msgstr ""
#: src/modules/rolemenus/ui/menueditor.py:860
#: src/modules/rolemenus/ui/menueditor.py:867
msgctxt "ui:menu_editor|button:repost|widget:repost|menu:channel|placeholder"
msgid "Select New Channel"
msgstr ""
#: src/modules/rolemenus/ui/menueditor.py:878
#: src/modules/rolemenus/ui/menueditor.py:885
msgctxt "ui:menu_editor|button:repost|widget:repost|error:perms|title"
msgid "Insufficient Permissions!"
msgstr ""
#: src/modules/rolemenus/ui/menueditor.py:882
#: src/modules/rolemenus/ui/menueditor.py:889
msgctxt "ui:menu_editor|button:repost|eidget:repost|error:perms|desc"
msgid "I lack the `EMBED_LINKS` or `SEND_MESSAGES` permission in this channel."
msgstr ""
#: src/modules/rolemenus/ui/menueditor.py:895
#: src/modules/rolemenus/ui/menueditor.py:902
#, possible-python-brace-format
msgctxt "ui:menu_editor|button:repost|widget:repost|error:post_failed"
msgid ""
"An error ocurred while posting to {channel}. Do I have sufficient "
"permissions?"
"An unknown error ocurred while posting to {channel}!\n"
"**Error:** `{exception}`"
msgstr ""
#: src/modules/rolemenus/ui/menueditor.py:907
#: src/modules/rolemenus/ui/menueditor.py:915
msgctxt "ui:menu_editor|button:repost|widget:repost|success|title"
msgid "Role Menu Moved"
msgstr ""
#: src/modules/rolemenus/ui/menueditor.py:913
#: src/modules/rolemenus/ui/menueditor.py:921
#, possible-python-brace-format
msgctxt "ui:menu_editor|button:repost|widget:repost|success|desc:general"
msgid "The role menu `{name}` is now available at {message_link}."
msgstr ""
#: src/modules/rolemenus/ui/menueditor.py:927
#: src/modules/rolemenus/ui/menueditor.py:935
msgctxt "ui:menu_editor|button:repost|widget:repost|success|desc:reactions"
msgid "Please check the message reactions are correct."
msgstr ""
#: src/modules/rolemenus/ui/menueditor.py:940
#: src/modules/rolemenus/ui/menueditor.py:948
msgctxt "ui:menu_editor|button:repost|widget:repost|title"
msgid "Repost Role Menu"
msgstr ""
#: src/modules/rolemenus/ui/menueditor.py:944
#: src/modules/rolemenus/ui/menueditor.py:952
msgctxt "ui:menu_editor|button:repost|widget:repost|description"
msgid "Please select the channel to which you want to resend this menu."
msgstr ""
#: src/modules/rolemenus/ui/menueditor.py:959
#: src/modules/rolemenus/ui/menueditor.py:967
msgctxt "ui:menu_editor|button:repost|label:repost"
msgid "Repost"
msgstr ""
#: src/modules/rolemenus/ui/menueditor.py:964
#: src/modules/rolemenus/ui/menueditor.py:972
msgctxt "ui:menu_editor|button:repost|label:post"
msgid "Post"
msgstr ""
#: src/modules/rolemenus/ui/menueditor.py:974
#: src/modules/rolemenus/ui/menueditor.py:982
msgctxt "ui:menu_editor|embed|title"
msgid "Role Menu Editor"
msgstr ""
#: src/modules/rolemenus/ui/menueditor.py:983
#: src/modules/rolemenus/ui/menueditor.py:991
#, possible-python-brace-format
msgctxt "ui:menu_editor|embed|description|jump_text:attached"
msgid "Members may use this menu from {jump_url}"
msgstr ""
#: src/modules/rolemenus/ui/menueditor.py:988
#: src/modules/rolemenus/ui/menueditor.py:996
msgctxt "ui:menu_editor|embed|description|jump_text:unattached"
msgid ""
"This menu is not currently active!\n"
"Make it available by clicking `Post` below."
msgstr ""
#: src/modules/rolemenus/ui/menueditor.py:1002
#: src/modules/rolemenus/ui/menueditor.py:1010
msgctxt "ui:menu_editor|embed|field:tips|name"
msgid "Command Tips"
msgstr ""
#: src/modules/rolemenus/ui/menueditor.py:1006
#: src/modules/rolemenus/ui/menueditor.py:1014
#, possible-python-brace-format
msgctxt "ui:menu_editor|embed|field:tips|value"
msgid ""
@@ -1454,12 +1475,12 @@ msgid ""
"{editrole} to edit role options."
msgstr ""
#: src/modules/rolemenus/ui/menueditor.py:1046
#: src/modules/rolemenus/ui/menueditor.py:1054
msgctxt "ui:menu_editor|error:invald_emoji|title"
msgid "Invalid emoji encountered."
msgstr ""
#: src/modules/rolemenus/ui/menueditor.py:1050
#: src/modules/rolemenus/ui/menueditor.py:1058
#, possible-python-brace-format
msgctxt "ui:menu_editor|error:invalid_emoji|desc"
msgid ""

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-09-13 08:47+0300\n"
"POT-Creation-Date: 2023-09-24 12:21+0300\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-09-13 08:47+0300\n"
"POT-Creation-Date: 2023-09-24 12:21+0300\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -18,25 +18,25 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
#: src/modules/schedule/cog.py:429
#: src/modules/schedule/cog.py:478
msgctxt "create_booking|error:no_lobby"
msgid ""
"This server has not set a `session_lobby`, so the scheduled session system "
"is disabled!"
msgstr ""
#: src/modules/schedule/cog.py:441
#: src/modules/schedule/cog.py:490
msgctxt "create_booking|error:no_member"
msgid "An unknown Discord error occurred. Please try again in a few minutes."
msgstr ""
#: src/modules/schedule/cog.py:449
#: src/modules/schedule/cog.py:498
msgctxt "create_booking|error:blacklisted"
msgid ""
"You have been blacklisted from the scheduled session system in this server."
msgstr ""
#: src/modules/schedule/cog.py:460
#: src/modules/schedule/cog.py:509
#, possible-python-brace-format
msgctxt "create_booking|error:insufficient_balance"
msgid ""
@@ -48,22 +48,22 @@ msgid_plural ""
msgstr[0] ""
msgstr[1] ""
#: src/modules/schedule/cog.py:474
#: src/modules/schedule/cog.py:523
msgctxt "create_booking|error:already_booked"
msgid "One or more requested timeslots are already booked!"
msgstr ""
#: src/modules/schedule/cog.py:677
#: src/modules/schedule/cog.py:726
msgctxt "cmd:schedule"
msgid "schedule"
msgstr ""
#: src/modules/schedule/cog.py:680
#: src/modules/schedule/cog.py:729
msgctxt "cmd:schedule|desc"
msgid "View and manage your scheduled session."
msgstr ""
#: src/modules/schedule/cog.py:708
#: src/modules/schedule/cog.py:757
#, possible-python-brace-format
msgctxt "cmd:schedule|cancel_booking|error:parse_slot"
msgid ""
@@ -71,25 +71,25 @@ msgid ""
"from the autocomplete options."
msgstr ""
#: src/modules/schedule/cog.py:716
#: src/modules/schedule/cog.py:765
#, possible-python-brace-format
msgctxt "cmd:schedule|cancel_booking|error:not_booked"
msgid "Could not cancel {time} booking because it is not booked!"
msgstr ""
#: src/modules/schedule/cog.py:725
#: src/modules/schedule/cog.py:774
#, possible-python-brace-format
msgctxt "cmd:schedule|cancel_booking|error:too_soon"
msgid "Cannot cancel {time} booking because it is running or starting soon!"
msgstr ""
#: src/modules/schedule/cog.py:738
#: src/modules/schedule/cog.py:787
#, possible-python-brace-format
msgctxt "cmd:schedule|cancel_booking|success"
msgid "Successfully cancelled your booking at {time}."
msgstr ""
#: src/modules/schedule/cog.py:751
#: src/modules/schedule/cog.py:800
#, possible-python-brace-format
msgctxt "cmd:schedule|create_booking|error:parse_slot"
msgid ""
@@ -97,30 +97,30 @@ msgid ""
"from the autocomplete options."
msgstr ""
#: src/modules/schedule/cog.py:759
#: src/modules/schedule/cog.py:808
#, possible-python-brace-format
msgctxt "cmd:schedule|create_booking|error:already_booked"
msgid "You have already booked a scheduled session for {time}."
msgstr ""
#: src/modules/schedule/cog.py:768
#: src/modules/schedule/cog.py:817
#, possible-python-brace-format
msgctxt "cmd:schedule|create_booking|error:too_soon"
msgid "Cannot book session at {time} because it is running or starting soon!"
msgstr ""
#: src/modules/schedule/cog.py:780
#: src/modules/schedule/cog.py:829
#, possible-python-brace-format
msgctxt "cmd:schedule|create_booking|success"
msgid "You have successfully scheduled a session at {time}."
msgstr ""
#: src/modules/schedule/cog.py:847
#: src/modules/schedule/cog.py:896
msgctxt "cmd:configure_schedule"
msgid "schedule"
msgstr ""
#: src/modules/schedule/cog.py:850
#: src/modules/schedule/cog.py:899
msgctxt "cmd:configure_schedule|desc"
msgid "Configure Scheduled Session system"
msgstr ""
@@ -841,7 +841,7 @@ msgid ""
"`MANAGE_WEBHOOKS` permission."
msgstr ""
#: src/modules/schedule/core/session.py:274
#: src/modules/schedule/core/session.py:280
#, possible-python-brace-format
msgctxt "session|prepare|error:room_permissions"
msgid ""
@@ -850,7 +850,7 @@ msgid ""
"`VIEW_CHANNEL` permissions."
msgstr ""
#: src/modules/schedule/core/session.py:317
#: src/modules/schedule/core/session.py:330
#, possible-python-brace-format
msgctxt "session|open|error:room_permissions"
msgid ""
@@ -859,57 +859,57 @@ msgid ""
"`VIEW_CHANNEL` permissions."
msgstr ""
#: src/modules/schedule/core/session.py:358
#: src/modules/schedule/core/session.py:371
#, possible-python-brace-format
msgctxt "session|status|title"
msgid "Session {start} - {end}"
msgstr ""
#: src/modules/schedule/core/session.py:369
#: src/modules/schedule/core/session.py:382
msgctxt "session|status|desc:cancelled"
msgid ""
"I cancelled this scheduled session because I was unavailable. All members "
"who booked the session have been refunded."
msgstr ""
#: src/modules/schedule/core/session.py:376
#: src/modules/schedule/core/session.py:389
msgctxt "session|status|desc:no_members"
msgid "*No members scheduled this session.*"
msgstr ""
#: src/modules/schedule/core/session.py:382
#: src/modules/schedule/core/session.py:395
#, possible-python-brace-format
msgctxt "session|status:preparing|desc:has_members"
msgid "Starting {start}"
msgstr ""
#: src/modules/schedule/core/session.py:385
#: src/modules/schedule/core/session.py:398
msgctxt "session|status:preparing|field:members"
msgid "Members"
msgstr ""
#: src/modules/schedule/core/session.py:392
#: src/modules/schedule/core/session.py:405
#, possible-python-brace-format
msgctxt "session|status:running|desc:has_members"
msgid "Finishing {start}"
msgstr ""
#: src/modules/schedule/core/session.py:426
#: src/modules/schedule/core/session.py:439
msgctxt "session|status:running|field:waiting"
msgid "Waiting For"
msgstr ""
#: src/modules/schedule/core/session.py:432
#: src/modules/schedule/core/session.py:445
msgctxt "session|status:running|field:attending"
msgid "Attending"
msgstr ""
#: src/modules/schedule/core/session.py:438
#: src/modules/schedule/core/session.py:451
msgctxt "session|status:running|field:attended"
msgid "Attended"
msgstr ""
#: src/modules/schedule/core/session.py:463
#: src/modules/schedule/core/session.py:476
#, possible-python-brace-format
msgctxt "session|status:finished|desc:everyone_att"
msgid ""
@@ -917,7 +917,7 @@ msgid ""
"**{reward} + {bonus}**!"
msgstr ""
#: src/modules/schedule/core/session.py:474
#: src/modules/schedule/core/session.py:487
#, possible-python-brace-format
msgctxt "session|status:finished|desc:some_att"
msgid ""
@@ -927,7 +927,7 @@ msgid ""
"without refund!*"
msgstr ""
#: src/modules/schedule/core/session.py:486
#: src/modules/schedule/core/session.py:499
msgctxt "session|status:finished|desc:some_att"
msgid ""
"No-one attended this session! No-one received rewards.\n"
@@ -935,12 +935,12 @@ msgid ""
"without refund!*"
msgstr ""
#: src/modules/schedule/core/session.py:492
#: src/modules/schedule/core/session.py:505
msgctxt "session|status:finished|field:attended"
msgid "Attended"
msgstr ""
#: src/modules/schedule/core/session.py:497
#: src/modules/schedule/core/session.py:510
msgctxt "session|status:finished|field:missing"
msgid "Missing"
msgstr ""

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-09-13 08:47+0300\n"
"POT-Creation-Date: 2023-09-24 12:21+0300\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -84,91 +84,91 @@ msgctxt "settype:role|accepts"
msgid "A role name or id"
msgstr ""
#: src/settings/setting_types.py:427
#: src/settings/setting_types.py:430
#, possible-python-brace-format
msgctxt "settype:role|parse|error:not_found"
msgid "Role `{string}` could not be found in this guild!"
msgstr ""
#: src/settings/setting_types.py:492
#: src/settings/setting_types.py:495
msgctxt "settype:bool|accepts"
msgid "Enabled/Disabled"
msgstr ""
#: src/settings/setting_types.py:497
#: src/settings/setting_types.py:500
msgctxt "settype:bool|parse:truthy_values"
msgid "enabled|yes|true|on|enable|1"
msgstr ""
#: src/settings/setting_types.py:501
#: src/settings/setting_types.py:504
msgctxt "settype:bool|parse:falsey_values"
msgid "disabled|no|false|off|disable|0"
msgstr ""
#: src/settings/setting_types.py:506
#: src/settings/setting_types.py:509
msgctxt "settype:bool|output:true"
msgid "On"
msgstr ""
#: src/settings/setting_types.py:507
#: src/settings/setting_types.py:510
msgctxt "settype:bool|output:false"
msgid "Off"
msgstr ""
#: src/settings/setting_types.py:508
#: src/settings/setting_types.py:511
msgctxt "settype:bool|output:none"
msgid "Not Set"
msgstr ""
#: src/settings/setting_types.py:619
#: src/settings/setting_types.py:622
msgctxt "settype:integer|accepts"
msgid "An integer"
msgstr ""
#: src/settings/setting_types.py:682
#: src/settings/setting_types.py:685
msgctxt "settype:emoji|desc"
msgid "Unicode or custom emoji"
msgstr ""
#: src/settings/setting_types.py:754
#: src/settings/setting_types.py:757
msgctxt "settype:guildid|accepts"
msgid "Any Snowflake ID"
msgstr ""
#: src/settings/setting_types.py:823
#: src/settings/setting_types.py:826
msgctxt "settype:timezone|accepts"
msgid "A timezone name from the 'tz database' (e.g. 'Europe/London')"
msgstr ""
#: src/settings/setting_types.py:893
#: src/settings/setting_types.py:896
msgctxt "settype:timezone|summary_table|field:supported|key"
msgid "Supported"
msgstr ""
#: src/settings/setting_types.py:897
#: src/settings/setting_types.py:900
#, possible-python-brace-format
msgctxt "settype:timezone|summary_table|field:supported|value"
msgid "Any timezone from the [tz database]({link})."
msgstr ""
#: src/settings/setting_types.py:914
#: src/settings/setting_types.py:917
#, possible-python-brace-format
msgctxt "set_type:timezone|acmpl|no_matching"
msgid "No timezones matching '{input}'!"
msgstr ""
#: src/settings/setting_types.py:927
#: src/settings/setting_types.py:930
#, possible-python-brace-format
msgctxt "set_type:timezone|acmpl|choice"
msgid "{tz} (Currently {now})"
msgstr ""
#: src/settings/setting_types.py:957
#: src/settings/setting_types.py:960
msgctxt "settype:timestamp|accepts"
msgid "A timestamp in the form YYYY-MM-DD HH:MM"
msgstr ""
#: src/settings/setting_types.py:986
#: src/settings/setting_types.py:989
#, possible-python-brace-format
msgctxt "settype:timestamp|parse|error:invalid"
msgid ""
@@ -176,43 +176,43 @@ msgid ""
"format."
msgstr ""
#: src/settings/setting_types.py:1017
#: src/settings/setting_types.py:1020
msgctxt "settype:raw|accepts"
msgid "Anything"
msgstr ""
#: src/settings/setting_types.py:1070
#: src/settings/setting_types.py:1073
msgctxt "settype:enum|accepts"
msgid "A valid option."
msgstr ""
#: src/settings/setting_types.py:1120
#: src/settings/setting_types.py:1123
#, possible-python-brace-format
msgctxt "settype:enum|parse|error:not_found"
msgid "`{provided}` is not a valid option!"
msgstr ""
#: src/settings/setting_types.py:1168
#: src/settings/setting_types.py:1171
msgctxt "settype:duration|accepts"
msgid "A number of days, hours, minutes, and seconds, e.g. `2d 4h 10s`."
msgstr ""
#: src/settings/setting_types.py:1349
#: src/settings/setting_types.py:1352
msgctxt "settype:channel_list|accepts"
msgid "Comma separated list of channel ids."
msgstr ""
#: src/settings/setting_types.py:1360
#: src/settings/setting_types.py:1363
msgctxt "settype:role_list|accepts"
msgid "Comma separated list of role ids."
msgstr ""
#: src/settings/setting_types.py:1376
#: src/settings/setting_types.py:1379
msgctxt "settype:stringlist|accepts"
msgid "Comma separated strings."
msgstr ""
#: src/settings/setting_types.py:1387
#: src/settings/setting_types.py:1390
msgctxt "settype:guildidlist|accepts"
msgid "Comma separated list of guild ids."
msgstr ""

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-09-13 08:47+0300\n"
"POT-Creation-Date: 2023-09-24 12:21+0300\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -32,27 +32,27 @@ msgctxt "cmd:shop|desc"
msgid "Purchase coloures, roles, and other goodies with LionCoins."
msgstr ""
#: src/modules/shop/cog.py:124
#: src/modules/shop/cog.py:125
msgctxt "cmd:shop_open"
msgid "open"
msgstr ""
#: src/modules/shop/cog.py:125
#: src/modules/shop/cog.py:126
msgctxt "cmd:shop_open|desc"
msgid "Open the server shop."
msgstr ""
#: src/modules/shop/cog.py:151
#: src/modules/shop/cog.py:153
msgctxt "cmd:shop_open|error:no_shops"
msgid "There is nothing to buy!"
msgstr ""
#: src/modules/shop/cog.py:213
#: src/modules/shop/cog.py:215
msgctxt "ui:stores|button:close|label"
msgid "Close"
msgstr ""
#: src/modules/shop/cog.py:220
#: src/modules/shop/cog.py:222
msgctxt "ui:stores|button:close|response|title"
msgid "Shop Closed"
msgstr ""

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-09-13 08:47+0300\n"
"POT-Creation-Date: 2023-09-24 12:21+0300\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -17,58 +17,58 @@ msgstr ""
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: 8bit\n"
#: src/modules/statistics/cog.py:42
#: src/modules/statistics/cog.py:43
msgctxt "cmd:me"
msgid "me"
msgstr ""
#: src/modules/statistics/cog.py:45
#: src/modules/statistics/cog.py:46
msgctxt "cmd:me|desc"
msgid "Display your personal profile and summary statistics."
msgstr ""
#: src/modules/statistics/cog.py:55
#: src/modules/statistics/cog.py:56
msgctxt "cmd:stats"
msgid "stats"
msgstr ""
#: src/modules/statistics/cog.py:58
#: src/modules/statistics/cog.py:59
msgctxt "cmd:stats|desc"
msgid "Weekly and monthly statistics for your recent activity."
msgstr ""
#: src/modules/statistics/cog.py:71
#: src/modules/statistics/cog.py:72
msgctxt "cmd:leaderboard"
msgid "leaderboard"
msgstr ""
#: src/modules/statistics/cog.py:74
#: src/modules/statistics/cog.py:75
msgctxt "cmd:leaderboard|desc"
msgid "Server leaderboard."
msgstr ""
#: src/modules/statistics/cog.py:89
#: src/modules/statistics/cog.py:90
#, possible-python-brace-format
msgctxt "cmd:leaderboard|chunking|desc"
msgid "Requesting server member list from Discord, please wait {loading}"
msgstr ""
#: src/modules/statistics/cog.py:108
#: src/modules/statistics/cog.py:113
msgctxt "cmd:configure_statistics"
msgid "statistics"
msgstr ""
#: src/modules/statistics/cog.py:109
#: src/modules/statistics/cog.py:114
msgctxt "cmd:configure_statistics|desc"
msgid "Statistics configuration panel"
msgstr ""
#: src/modules/statistics/cog.py:112
#: src/modules/statistics/cog.py:117
msgctxt "cmd:configure_statistics|param:season_start"
msgid "season_start"
msgstr ""
#: src/modules/statistics/cog.py:117
#: src/modules/statistics/cog.py:122
msgctxt "cmd:configure_statistics|param:season_start|desc"
msgid ""
"Time from which to start counting activity for rank badges and season "
@@ -613,101 +613,108 @@ msgid ""
"again to revert."
msgstr ""
#: src/modules/statistics/ui/leaderboard.py:250
#: src/modules/statistics/ui/leaderboard.py:253
msgctxt "ui:leaderboard|menu:stats|placeholder"
msgid "Select Activity Type"
msgstr ""
#: src/modules/statistics/ui/leaderboard.py:259
#: src/modules/statistics/ui/leaderboard.py:262
msgctxt "ui:leaderboard|menu:stats|item:voice"
msgid "Voice Activity"
msgstr ""
#: src/modules/statistics/ui/leaderboard.py:270
#: src/modules/statistics/ui/leaderboard.py:273
msgctxt "ui:leaderboard|menu:stats|item:study"
msgid "Study Statistics"
msgstr ""
#: src/modules/statistics/ui/leaderboard.py:281
#: src/modules/statistics/ui/leaderboard.py:284
msgctxt "ui:leaderboard|menu:stats|item:message"
msgid "Message Activity"
msgstr ""
#: src/modules/statistics/ui/leaderboard.py:292
#: src/modules/statistics/ui/leaderboard.py:295
msgctxt "ui:leaderboard|menu;stats|item:anki"
msgid "Anki Cards Reviewed"
msgstr ""
#: src/modules/statistics/ui/leaderboard.py:346
#: src/modules/statistics/ui/leaderboard.py:349
msgctxt "ui:leaderboard|button:season|label"
msgid "This Season"
msgstr ""
#: src/modules/statistics/ui/leaderboard.py:350
#: src/modules/statistics/ui/leaderboard.py:353
msgctxt "ui:leaderboard|button:day|label"
msgid "Today"
msgstr ""
#: src/modules/statistics/ui/leaderboard.py:354
#: src/modules/statistics/ui/leaderboard.py:357
msgctxt "ui:leaderboard|button:week|label"
msgid "This Week"
msgstr ""
#: src/modules/statistics/ui/leaderboard.py:358
#: src/modules/statistics/ui/leaderboard.py:361
msgctxt "ui:leaderboard|button:month|label"
msgid "This Month"
msgstr ""
#: src/modules/statistics/ui/leaderboard.py:362
#: src/modules/statistics/ui/leaderboard.py:365
msgctxt "ui:leaderboard|button:alltime|label"
msgid "All Time"
msgstr ""
#: src/modules/statistics/ui/leaderboard.py:366
#: src/modules/statistics/ui/leaderboard.py:369
msgctxt "ui:leaderboard|button:jump|label"
msgid "Jump"
msgstr ""
#: src/modules/statistics/ui/leaderboard.py:381
#: src/modules/statistics/ui/leaderboard.py:384
msgctxt "ui:leaderboard|button:jump|input:title"
msgid "Jump to page"
msgstr ""
#: src/modules/statistics/ui/leaderboard.py:385
#: src/modules/statistics/ui/leaderboard.py:388
msgctxt "ui:leaderboard|button:jump|input:question"
msgid "Page number to jump to"
msgstr ""
#: src/modules/statistics/ui/leaderboard.py:396
#: src/modules/statistics/ui/leaderboard.py:399
msgctxt "ui:leaderboard|button:jump|error:invalid_page"
msgid "Invalid page number, please try again!"
msgstr ""
#: src/modules/statistics/ui/leaderboard.py:442
#: src/modules/statistics/ui/leaderboard.py:443
msgctxt "ui:leaderboard|chunk_warning"
msgid ""
"**Note:** Could not retrieve member list from Discord, so some members may "
"be missing. Try again in a minute!"
msgstr ""
#: src/modules/statistics/ui/leaderboard.py:450
#, possible-python-brace-format
msgctxt "ui:leaderboard|since"
msgid "Counting statistics since {timestamp}"
msgstr ""
#: src/modules/statistics/ui/leaderboard.py:453
#: src/modules/statistics/ui/leaderboard.py:463
#, possible-python-brace-format
msgctxt "ui:leaderboard|mode:voice|message:empty|desc"
msgid "There has been no voice activity since {timestamp}"
msgstr ""
#: src/modules/statistics/ui/leaderboard.py:458
#: src/modules/statistics/ui/leaderboard.py:468
#, possible-python-brace-format
msgctxt "ui:leaderboard|mode:text|message:empty|desc"
msgid "There has been no message activity since {timestamp}"
msgstr ""
#: src/modules/statistics/ui/leaderboard.py:463
#: src/modules/statistics/ui/leaderboard.py:473
#, possible-python-brace-format
msgctxt "ui:leaderboard|mode:anki|message:empty|desc"
msgid "There have been no Anki cards reviewed since {timestamp}"
msgstr ""
#: src/modules/statistics/ui/leaderboard.py:472
#: src/modules/statistics/ui/leaderboard.py:482
msgctxt "ui:leaderboard|message:empty|title"
msgid "Leaderboard Empty!"
msgstr ""

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-09-13 08:47+0300\n"
"POT-Creation-Date: 2023-09-24 12:21+0300\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-09-13 08:47+0300\n"
"POT-Creation-Date: 2023-09-24 12:21+0300\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-09-13 08:47+0300\n"
"POT-Creation-Date: 2023-09-24 12:21+0300\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-09-13 08:47+0300\n"
"POT-Creation-Date: 2023-09-24 12:21+0300\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-09-13 08:47+0300\n"
"POT-Creation-Date: 2023-09-24 12:21+0300\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-09-13 08:47+0300\n"
"POT-Creation-Date: 2023-09-24 12:21+0300\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-09-13 08:47+0300\n"
"POT-Creation-Date: 2023-09-24 12:21+0300\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-09-13 08:47+0300\n"
"POT-Creation-Date: 2023-09-24 12:21+0300\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-09-13 08:47+0300\n"
"POT-Creation-Date: 2023-09-24 12:21+0300\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-09-13 08:47+0300\n"
"POT-Creation-Date: 2023-09-24 12:21+0300\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-09-13 08:47+0300\n"
"POT-Creation-Date: 2023-09-24 12:21+0300\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -17,68 +17,68 @@ msgstr ""
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: 8bit\n"
#: src/wards.py:79
#: src/wards.py:83
msgctxt "ward:sys_admin|failed"
msgid "You must be a bot owner to do this!"
msgstr ""
#: src/wards.py:95
#: src/wards.py:99
msgctxt "ward:high_management|failed"
msgid "You must have the `ADMINISTRATOR` permission in this server to do this!"
msgstr ""
#: src/wards.py:111
#: src/wards.py:115
msgctxt "ward:low_management|failed"
msgid "You must have the `MANAGE_GUILD` permission in this server to do this!"
msgstr ""
#: src/wards.py:123
#: src/wards.py:127
msgctxt "ward:moderator|failed"
msgid ""
"You must have the configured moderator role, or `MANAGE_GUILD` permissions "
"to do this."
msgstr ""
#: src/wards.py:149
#: src/wards.py:153
#, possible-python-brace-format
msgctxt "ward:equippable_role|error:bot_managed"
msgid "I cannot manage {role} because it is managed by another bot!"
msgstr ""
#: src/wards.py:156
#: src/wards.py:160
#, possible-python-brace-format
msgctxt "ward:equippable_role|error:integration"
msgid "I cannot manage {role} because it is managed by a server integration."
msgstr ""
#: src/wards.py:163
#: src/wards.py:167
msgctxt "ward:equippable_role|error:default_role"
msgid "I cannot manage the server's default role."
msgstr ""
#: src/wards.py:170
#: src/wards.py:174
msgctxt "ward:equippable_role|error:no_perms"
msgid "I need the `MANAGE_ROLES` permission before I can manage roles!"
msgstr ""
#: src/wards.py:177
#: src/wards.py:181
#, possible-python-brace-format
msgctxt "ward:equippable_role|error:my_top_role"
msgid "I cannot assign or remove {role} because it is above my top role!"
msgstr ""
#: src/wards.py:184
#: src/wards.py:188
#, possible-python-brace-format
msgctxt "ward:equippable_role|error:not_assignable"
msgid "I don't have sufficient permissions to assign or remove {role}."
msgstr ""
#: src/wards.py:192
#: src/wards.py:196
msgctxt "ward:equippable_role|error:actor_perms"
msgid "You need the `MANAGE_ROLES` permission before you can configure roles!"
msgstr ""
#: src/wards.py:199
#: src/wards.py:203
#, possible-python-brace-format
msgctxt "ward:equippable_role|error:actor_top_role"
msgid "You cannot configure {role} because it is above your top role!"

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-09-13 08:47+0300\n"
"POT-Creation-Date: 2023-09-24 12:21+0300\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"

View File

@@ -3,7 +3,7 @@ cachetools==4.2.2
configparser==5.0.2
discord.py
iso8601==0.1.16
psycopg
psycopg[pool]
pytz==2021.1
topggpy
psutil

View File

@@ -22,7 +22,7 @@ async def shard_snapshot():
snap = ShardSnapshot(
guild_count=len(bot.guilds),
voice_count=sum(len(channel.members) for guild in bot.guilds for channel in guild.voice_channels),
member_count=sum(len(guild.members) for guild in bot.guilds),
member_count=sum(guild.member_count for guild in bot.guilds),
user_count=len(set(m.id for guild in bot.guilds for m in guild.members))
)
return snap

View File

@@ -230,14 +230,15 @@ class BabelCog(LionCog):
supported = self.bot.translator.supported_locales
formatted = []
for locale in supported:
name = locale_names.get(locale.replace('_', '-'), None)
if name:
localestr = f"{locale} ({t(name)})"
names = locale_names.get(locale.replace('_', '-'), None)
if names:
local_name, native_name = names
localestr = f"{native_name} ({t(local_name)})"
else:
localestr = locale
formatted.append((locale, localestr))
matching = {item for item in formatted if partial in item[1]}
matching = {item for item in formatted if partial in item[1] or partial in item[0]}
if matching:
choices = [
appcmds.Choice(name=localestr, value=locale)

View File

@@ -38,37 +38,44 @@ class LocaleMap(Enum):
hebrew = 'he-IL'
# Original Discord names
locale_names = {
'en-US': _p('localenames|locale:en-US', "American English"),
'en-GB': _p('localenames|locale:en-GB', "British English"),
'bg': _p('localenames|locale:bg', "Bulgarian"),
'zh-CN': _p('localenames|locale:zh-CN', "Chinese"),
'zh-TW': _p('localenames|locale:zh-TW', "Taiwan Chinese"),
'hr': _p('localenames|locale:hr', "Croatian"),
'cs': _p('localenames|locale:cs', "Czech"),
'da': _p('localenames|locale:da', "Danish"),
'nl': _p('localenames|locale:nl', "Dutch"),
'fi': _p('localenames|locale:fi', "Finnish"),
'fr': _p('localenames|locale:fr', "French"),
'de': _p('localenames|locale:de', "German"),
'el': _p('localenames|locale:el', "Greek"),
'hi': _p('localenames|locale:hi', "Hindi"),
'hu': _p('localenames|locale:hu', "Hungarian"),
'it': _p('localenames|locale:it', "Italian"),
'ja': _p('localenames|locale:ja', "Japanese"),
'ko': _p('localenames|locale:ko', "Korean"),
'lt': _p('localenames|locale:lt', "Lithuanian"),
'no': _p('localenames|locale:no', "Norwegian"),
'pl': _p('localenames|locale:pl', "Polish"),
'pt-BR': _p('localenames|locale:pt-BR', "Brazil Portuguese"),
'ro': _p('localenames|locale:ro', "Romanian"),
'ru': _p('localenames|locale:ru', "Russian"),
'es-ES': _p('localenames|locale:es-ES', "Spain Spanish"),
'sv-SE': _p('localenames|locale:sv-SE', "Swedish"),
'th': _p('localenames|locale:th', "Thai"),
'tr': _p('localenames|locale:tr', "Turkish"),
'uk': _p('localenames|locale:uk', "Ukrainian"),
'vi': _p('localenames|locale:vi', "Vietnamese"),
'he': _p('localenames|locale:he', "Hebrew"),
'he-IL': _p('localenames|locale:he_IL', "Hebrew (Israel)"),
'id': (_p('localenames|locale:id', "Indonesian"), "Bahasa Indonesia"),
'da': (_p('localenames|locale:da', "Danish"), "Dansk"),
'de': (_p('localenames|locale:de', "German"), "Deutsch"),
'en-GB': (_p('localenames|locale:en-GB', "English, UK"), "English, UK"),
'en-US': (_p('localenames|locale:en-US', "English, US"), "English, US"),
'es-ES': (_p('localenames|locale:es-ES', "Spanish"), "Español"),
'fr': (_p('localenames|locale:fr', "French"), "Français"),
'hr': (_p('localenames|locale:hr', "Croatian"), "Hrvatski"),
'it': (_p('localenames|locale:it', "Italian"), "Italiano"),
'lt': (_p('localenames|locale:lt', "Lithuanian"), "Lietuviškai"),
'hu': (_p('localenames|locale:hu', "Hungarian"), "Magyar"),
'nl': (_p('localenames|locale:nl', "Dutch"), "Nederlands"),
'no': (_p('localenames|locale:no', "Norwegian"), "Norsk"),
'pl': (_p('localenames|locale:pl', "Polish"), "Polski"),
'pt-BR': (_p('localenames|locale:pt-BR', "Portuguese, Brazilian"), "Português do Brasil"),
'ro': (_p('localenames|locale:ro', "Romanian, Romania"), "Română"),
'fi': (_p('localenames|locale:fi', "Finnish"), "Suomi"),
'sv-SE': (_p('localenames|locale:sv-SE', "Swedish"), "Svenska"),
'vi': (_p('localenames|locale:vi', "Vietnamese"), "Tiếng Việt"),
'tr': (_p('localenames|locale:tr', "Turkish"), "Türkçe"),
'cs': (_p('localenames|locale:cs', "Czech"), "Čeština"),
'el': (_p('localenames|locale:el', "Greek"), "Ελληνικά"),
'bg': (_p('localenames|locale:bg', "Bulgarian"), "български"),
'ru': (_p('localenames|locale:ru', "Russian"), "Pусский"),
'uk': (_p('localenames|locale:uk', "Ukrainian"), "Українська"),
'hi': (_p('localenames|locale:hi', "Hindi"), "हिन्दी"),
'th': (_p('localenames|locale:th', "Thai"), "ไทย"),
'zh-CN': (_p('localenames|locale:zh-CN', "Chinese, China"), "中文"),
'ja': (_p('localenames|locale:ja', "Japanese"), "日本語"),
'zh-TW': (_p('localenames|locale:zh-TW', "Chinese, Taiwan"), "繁體中文"),
'ko': (_p('localenames|locale:ko', "Korean"), "한국어"),
}
# More names for languages not supported by Discord
locale_names |= {
'he': (_p('localenames|locale:he', "Hebrew"), "Hebrew"),
'he-IL': (_p('localenames|locale:he-IL', "Hebrew"), "Hebrew"),
'ceaser': (_p('localenames|locale:test', "Test Language"), "dfbtfs"),
}

View File

@@ -43,9 +43,9 @@ class LocaleSetting(StringSetting):
if data is None:
formatted = t(_p('settype:locale|formatted:unset', "Unset"))
else:
name = locale_names.get(data, None)
if name:
formatted = f"`{data} ({t(name)})`"
if data in locale_names:
local_name, native_name = locale_names[data]
formatted = f"`{native_name} ({t(local_name)})`"
else:
formatted = f"`{data}`"
return formatted

View File

@@ -47,33 +47,43 @@ class LeoBabel(Translator):
Initialise the gettext translators for the supported_locales.
"""
self.read_supported()
missing = []
loaded = []
for locale in self.supported_locales:
for domain in self.supported_domains:
if locale == SOURCE_LOCALE:
continue
try:
translator = gettext.translation(domain, "locales/", languages=[locale])
loaded.append(f"Loaded translator for <locale: {locale}> <domain: {domain}>")
except OSError:
# Presume translation does not exist
logger.warning(f"Could not load translator for supported <locale: {locale}> <domain: {domain}>")
pass
else:
logger.debug(f"Loaded translator for <locale: {locale}> <domain: {domain}>")
self.translators[locale][domain] = translator
missing.append(f"Could not load translator for supported <locale: {locale}> <domain: {domain}>")
translator = null
self.translators[locale][domain] = translator
if missing:
logger.warning('\n'.join(("Missing Translators:", *missing)))
if loaded:
logger.debug('\n'.join(("Loaded Translators:", *loaded)))
async def unload(self):
self.translators.clear()
def get_translator(self, locale, domain):
if locale == SOURCE_LOCALE:
return null
translator = self.translators[locale].get(domain, None)
if translator is None:
logger.warning(
f"Translator missing for requested <locale: {locale}> and <domain: {domain}>. Setting NullTranslator."
)
self.translators[locale][domain] = null
translator = null
elif locale in self.supported_locales and domain in self.supported_domains:
translator = self.translators[locale].get(domain, None)
if translator is None:
# This should never really happen because we already loaded the supported translators
logger.warning(
f"Translator missing for supported <locale: {locale}> "
"and <domain: {domain}>. Setting NullTranslator."
)
translator = self.translators[locale][domain] = null
else:
# Unsupported
translator = null
return translator

View File

@@ -9,6 +9,7 @@ from meta import LionBot, conf, sharding, appname, shard_talk
from meta.app import shardname
from meta.logger import log_context, log_action_stack, setup_main_logger
from meta.context import ctx_bot
from meta.monitor import ComponentMonitor, StatusLevel, ComponentStatus
from data import Database
@@ -21,7 +22,7 @@ for name in conf.config.options('LOGGING_LEVELS', no_defaults=True):
logging.getLogger(name).setLevel(conf.logging_levels[name])
setup_main_logger()
logging_queue = setup_main_logger()
logger = logging.getLogger(__name__)
@@ -29,6 +30,25 @@ logger = logging.getLogger(__name__)
db = Database(conf.data['args'])
async def _data_monitor() -> ComponentStatus:
"""
Component monitor callback for the database.
"""
data = {
'stats': str(db.pool.get_stats())
}
if not db.pool._opened:
level = StatusLevel.WAITING
info = "(WAITING) Database Pool is not opened."
elif db.pool._closed:
level = StatusLevel.ERRORED
info = "(ERROR) Database Pool is closed."
else:
level = StatusLevel.OKAY
info = "(OK) Database Pool statistics: {stats}"
return ComponentStatus(level, info, info, data)
async def main():
log_action_stack.set(("Initialising",))
logger.info("Initialising StudyLion")
@@ -69,9 +89,13 @@ async def main():
shard_count=sharding.shard_count,
help_command=None,
proxy=conf.bot.get('proxy', None),
translator=translator
translator=translator,
chunk_guilds_at_startup=False,
) as lionbot:
ctx_bot.set(lionbot)
lionbot.system_monitor.add_component(
ComponentMonitor('Database', _data_monitor)
)
try:
log_context.set(f"APP: {appname}")
logger.info("StudyLion initialised, starting!", extra={'action': 'Starting'})

View File

@@ -49,16 +49,20 @@ class CoinSetting(IntegerSetting):
if num > cls._max:
t = ctx_translator.get().t
raise UserInputError(t(_p(
'settype:coin|parse|error:too_large',
"Provided number of coins was too high!"
))) from None
raise UserInputError(
t(_p(
'settype:coin|parse|error:too_large',
"You cannot set this to more than {coin}**{max}**!"
)).format(coin=conf.emojis.coin, max=cls._max)
) from None
elif num < cls._min:
t = ctx_translator.get().t
raise UserInputError(t(_p(
'settype:coin|parse|error:too_large',
"Provided number of coins was too low!"
))) from None
raise UserInputError(
t(_p(
'settype:coin|parse|error:too_small',
"You cannot set this to less than {coin}**{min}**!"
)).format(coin=conf.emojis.coin, min=cls._min)
) from None
return num

Submodule src/gui updated: b781f7f9f2...ba9ace6ced

View File

@@ -12,6 +12,8 @@ from aiohttp import ClientSession
from data import Database
from utils.lib import tabulate
from gui.errors import RenderingException
from babel.translator import ctx_locale
from .config import Conf
from .logger import logging_context, log_context, log_action_stack, log_wrap, set_logging_context
@@ -19,6 +21,7 @@ from .context import context
from .LionContext import LionContext
from .LionTree import LionTree
from .errors import HandledException, SafeCancellation
from .monitor import SystemMonitor, ComponentMonitor, StatusLevel, ComponentStatus
if TYPE_CHECKING:
from core import CoreCog
@@ -46,9 +49,40 @@ class LionBot(Bot):
self.core: Optional['CoreCog'] = None
self.translator = translator
self.system_monitor = SystemMonitor()
self.monitor = ComponentMonitor('LionBot', self._monitor_status)
self.system_monitor.add_component(self.monitor)
self._locks = WeakValueDictionary()
self._running_events = set()
async def _monitor_status(self):
if self.is_closed():
level = StatusLevel.ERRORED
info = "(ERROR) Websocket is closed"
data = {}
elif self.is_ws_ratelimited():
level = StatusLevel.WAITING
info = "(WAITING) Websocket is ratelimited"
data = {}
elif not self.is_ready():
level = StatusLevel.STARTING
info = "(STARTING) Not yet ready"
data = {}
else:
level = StatusLevel.OKAY
info = (
"(OK) "
"Logged in with {guild_count} guilds, "
", websocket latency {latency}, and {events} running events."
)
data = {
'guild_count': len(self.guilds),
'latency': self.latency,
'events': len(self._running_events),
}
return ComponentStatus(level, info, info, data)
async def setup_hook(self) -> None:
log_context.set(f"APP: {self.application_id}")
await self.app_ipc.connect()
@@ -204,13 +238,23 @@ class LionBot(Bot):
pass
except asyncio.TimeoutError:
pass
except RenderingException as e:
logger.info(f"Command failed due to RenderingException: {repr(e)}")
embed = self.tree.rendersplat(e)
try:
await ctx.error_reply(embed=embed)
except discord.HTTPException:
pass
except Exception as e:
logger.exception(
f"Caught an unknown CommandInvokeError while executing: {cmd_str}",
extra={'action': 'BotError', 'with_ctx': True}
)
error_embed = discord.Embed(title="Something went wrong!")
error_embed = discord.Embed(
title="Something went wrong!",
colour=discord.Colour.dark_red()
)
error_embed.description = (
"An unexpected error occurred while processing your command!\n"
"Our development team has been notified, and the issue will be addressed soon.\n"
@@ -225,11 +269,12 @@ class LionBot(Bot):
details['cmd'] = f"`{ctx.command.qualified_name}`"
if ctx.author:
details['author'] = f"`{ctx.author.id}` -- `{ctx.author}`"
details['locale'] = f"`{ctx_locale.get()}`"
if ctx.guild:
details['guild'] = f"`{ctx.guild.id}` -- `{ctx.guild.name}`"
details['my_guild_perms'] = f"`{ctx.guild.me.guild_permissions.value}`"
if ctx.author:
ownerstr = ' (owner)' if ctx.author == ctx.guild.owner else ''
ownerstr = ' (owner)' if ctx.author.id == ctx.guild.owner_id else ''
details['author_guild_perms'] = f"`{ctx.author.guild_permissions.value}{ownerstr}`"
if ctx.channel.type is discord.enums.ChannelType.private:
details['channel'] = "`Direct Message`"
@@ -246,7 +291,7 @@ class LionBot(Bot):
try:
await ctx.error_reply(embed=error_embed)
except Exception:
except discord.HTTPException:
pass
finally:
exception.original = HandledException(exception.original)
@@ -270,3 +315,29 @@ class LionBot(Bot):
def add_command(self, command):
if not hasattr(command, '_placeholder_group_'):
super().add_command(command)
def request_chunking_for(self, guild):
if not guild.chunked:
return asyncio.create_task(
self._connection.chunk_guild(guild, wait=False, cache=True),
name=f"Background chunkreq for {guild.id}"
)
async def on_interaction(self, interaction: discord.Interaction):
"""
Adds the interaction author to guild cache if appropriate.
This gets run a little bit late, so it is possible the interaction gets handled
without the author being in case.
"""
guild = interaction.guild
user = interaction.user
if guild is not None and user is not None and isinstance(user, discord.Member):
if not guild.get_member(user.id):
guild._add_member(user)
if guild is not None and not guild.chunked:
# Getting an interaction in the guild is a good enough reason to request chunking
logger.info(
f"Unchunked guild <gid: {guild.id}> requesting chunking after interaction."
)
self.request_chunking_for(guild)

View File

@@ -6,6 +6,7 @@ from typing import Optional, TYPE_CHECKING
import discord
from discord.enums import ChannelType
from discord.ext.commands import Context
from babel.translator import ctx_locale
if TYPE_CHECKING:
from .LionBot import LionBot
@@ -35,6 +36,7 @@ FlatContext = namedtuple(
'interaction',
'guild',
'author',
'channel',
'alias',
'prefix',
'failed')
@@ -78,6 +80,7 @@ class LionContext(Context['LionBot']):
parts['alias'] = f"\"{self.invoked_with}\""
if self.command_failed:
parts['failed'] = self.command_failed
parts['locale'] = f"\"{ctx_locale.get()}\""
return "<LionContext: {}>".format(
' '.join(f"{name}={value}" for name, value in parts.items())

View File

@@ -8,9 +8,12 @@ from discord.enums import InteractionType
from discord.app_commands.namespace import Namespace
from utils.lib import tabulate
from gui.errors import RenderingException
from babel.translator import ctx_locale
from .logger import logging_context, set_logging_context, log_wrap, log_action_stack
from .errors import SafeCancellation
from .config import conf
logger = logging.getLogger(__name__)
@@ -29,17 +32,36 @@ class LionTree(CommandTree):
except SafeCancellation:
# Assume this has already been handled
pass
except RenderingException as e:
logger.info(f"Tree interaction failed due to rendering exception: {repr(e)}")
embed = self.rendersplat(e)
await self.error_reply(interaction, embed)
except Exception:
logger.exception(f"Unhandled exception in interaction: {interaction}", extra={'action': 'TreeError'})
if not interaction.is_expired():
splat = self.bugsplat(interaction, error)
try:
if interaction.response.is_done():
await interaction.followup.send(embed=splat, ephemeral=True)
else:
await interaction.response.send_message(embed=splat, ephemeral=True)
except discord.HTTPException:
pass
embed = self.bugsplat(interaction, error)
await self.error_reply(interaction, embed)
async def error_reply(self, interaction, embed):
if not interaction.is_expired():
try:
if interaction.response.is_done():
await interaction.followup.send(embed=embed, ephemeral=True)
else:
await interaction.response.send_message(embed=embed, ephemeral=True)
except discord.HTTPException:
pass
def rendersplat(self, e: RenderingException):
embed = discord.Embed(
title="Resource Currently Unavailable!",
description=(
"Sorry, the graphics service is currently unavailable!\n"
"Please try again in a few minutes.\n"
"If the error persists, please contact our [support team]({link})"
).format(link=conf.bot.support_guild),
colour=discord.Colour.dark_red()
)
return embed
def bugsplat(self, interaction, e):
error_embed = discord.Embed(title="Something went wrong!", colour=discord.Colour.red())
@@ -55,13 +77,14 @@ class LionTree(CommandTree):
details['interactiontype'] = f"`{interaction.type}`"
if interaction.command:
details['cmd'] = f"`{interaction.command.qualified_name}`"
details['locale'] = f"`{ctx_locale.get()}`"
if interaction.user:
details['user'] = f"`{interaction.user.id}` -- `{interaction.user}`"
if interaction.guild:
details['guild'] = f"`{interaction.guild.id}` -- `{interaction.guild.name}`"
details['my_guild_perms'] = f"`{interaction.guild.me.guild_permissions.value}`"
if interaction.user:
ownerstr = ' (owner)' if interaction.user == interaction.guild.owner else ''
ownerstr = ' (owner)' if interaction.user.id == interaction.guild.owner_id else ''
details['user_guild_perms'] = f"`{interaction.user.guild_permissions.value}{ownerstr}`"
if interaction.channel.type is discord.enums.ChannelType.private:
details['channel'] = "`Direct Message`"

View File

@@ -3,7 +3,7 @@ import configparser as cfgp
from .args import args
shard_number = args.shard or 0
shard_number = args.shard
class configEmoji(PartialEmoji):
__slots__ = ('fallback',)
@@ -87,7 +87,10 @@ class Conf:
"emoji": configEmoji.from_str,
}
)
self.config.read(configfile)
with open(configfile) as conff:
# Opening with read_file mainly to ensure the file exists
self.config.read_file(conff)
self.section_name = section_name if section_name in self.config else 'DEFAULT'

View File

@@ -10,6 +10,7 @@ from io import StringIO
from functools import wraps
from contextvars import ContextVar
import discord
from discord import Webhook, File
import aiohttp
@@ -188,6 +189,14 @@ class LessThanFilter(logging.Filter):
# non-zero return means we log this message
return 1 if record.levelno < self.max_level else 0
class ExactLevelFilter(logging.Filter):
def __init__(self, target_level, name=""):
super().__init__(name)
self.target_level = target_level
def filter(self, record):
return (record.levelno == self.target_level)
class ThreadFilter(logging.Filter):
def __init__(self, thread_name):
@@ -234,7 +243,6 @@ class ContextInjection(logging.Filter):
logging_handler_out = logging.StreamHandler(sys.stdout)
logging_handler_out.setLevel(logging.DEBUG)
logging_handler_out.setFormatter(log_fmt)
logging_handler_out.addFilter(LessThanFilter(logging.WARNING))
logging_handler_out.addFilter(ContextInjection())
logger.addHandler(logging_handler_out)
log_logger.addHandler(logging_handler_out)
@@ -297,6 +305,10 @@ class WebHookHandler(logging.StreamHandler):
self.webhook = Webhook.from_url(self.webhook_url, session=self.session)
async def post(self, record):
if record.context == 'Webhook Logger':
# Don't livelog livelog errors
# Otherwise we recurse and Cloudflare hates us
return
log_context.set("Webhook Logger")
log_action_stack.set(("Logging",))
log_app.set(record.app)
@@ -363,28 +375,35 @@ class WebHookHandler(logging.StreamHandler):
return
except BucketFull:
logger.warning(
f"Live logging webhook {self.webhook.id} going too fast! "
"Ignoring records until rate slows down."
"Can't keep up! "
f"Ignoring records on live-logger {self.webhook.id}."
)
self.ignored += 1
return
else:
if self.ignored > 0:
logger.warning(
"Can't keep up! "
f"{self.ignored} live logging records on webhook {self.webhook.id} skipped, continuing."
)
self.ignored = 0
if as_file or len(message) > 1900:
with StringIO(message) as fp:
fp.seek(0)
await self.webhook.send(
f"{self.prefix}\n`{message.splitlines()[0]}`",
file=File(fp, filename="logs.md"),
username=log_app.get()
)
else:
await self.webhook.send(self.prefix + '\n' + message, username=log_app.get())
try:
if as_file or len(message) > 1900:
with StringIO(message) as fp:
fp.seek(0)
await self.webhook.send(
f"{self.prefix}\n`{message.splitlines()[0]}`",
file=File(fp, filename="logs.md"),
username=log_app.get()
)
else:
await self.webhook.send(self.prefix + '\n' + message, username=log_app.get())
except discord.HTTPException:
logger.exception(
"Live logger errored. Slowing down live logger."
)
self.bucket.fill()
handlers = []
@@ -392,9 +411,15 @@ if webhook := conf.logging['general_log']:
handler = WebHookHandler(webhook, batch=True)
handlers.append(handler)
if webhook := conf.logging['warning_log']:
handler = WebHookHandler(webhook, prefix=conf.logging['warning_prefix'], batch=True)
handler.addFilter(ExactLevelFilter(logging.WARNING))
handler.setLevel(logging.WARNING)
handlers.append(handler)
if webhook := conf.logging['error_log']:
handler = WebHookHandler(webhook, prefix=conf.logging['error_prefix'], batch=True)
handler.setLevel(logging.WARNING)
handler.setLevel(logging.ERROR)
handlers.append(handler)
if webhook := conf.logging['critical_log']:

139
src/meta/monitor.py Normal file
View File

@@ -0,0 +1,139 @@
import logging
import asyncio
from enum import IntEnum
from collections import deque, ChainMap
import datetime as dt
logger = logging.getLogger(__name__)
class StatusLevel(IntEnum):
ERRORED = -2
UNSURE = -1
WAITING = 0
STARTING = 1
OKAY = 2
@property
def symbol(self):
return symbols[self]
symbols = {
StatusLevel.ERRORED: '🟥',
StatusLevel.UNSURE: '🟧',
StatusLevel.WAITING: '',
StatusLevel.STARTING: '🟫',
StatusLevel.OKAY: '🟩',
}
class ComponentStatus:
def __init__(self, level: StatusLevel, short_formatstr: str, long_formatstr: str, data: dict = {}):
self.level = level
self.short_formatstr = short_formatstr
self.long_formatstr = long_formatstr
self.data = data
self.created_at = dt.datetime.now(tz=dt.timezone.utc)
def format_args(self):
extra = {
'created_at': self.created_at,
'level': self.level,
'symbol': self.level.symbol,
}
return ChainMap(extra, self.data)
@property
def short(self):
return self.short_formatstr.format(**self.format_args())
@property
def long(self):
return self.long_formatstr.format(**self.format_args())
class ComponentMonitor:
_name = None
def __init__(self, name=None, callback=None):
self._callback = callback
self.name = name or self._name
if not self.name:
raise ValueError("ComponentMonitor must have a name")
async def _make_status(self, *args, **kwargs):
if self._callback is not None:
return await self._callback(*args, **kwargs)
else:
raise NotImplementedError
async def status(self) -> ComponentStatus:
try:
status = await self._make_status()
except Exception as e:
logger.exception(
f"Status callback for component '{self.name}' failed. This should not happen."
)
status = ComponentStatus(
level=StatusLevel.UNSURE,
short_formatstr="Status callback for '{name}' failed with error '{error}'",
long_formatstr="Status callback for '{name}' failed with error '{error}'",
data={
'name': self.name,
'error': repr(e)
}
)
return status
class SystemMonitor:
def __init__(self):
self.components = {}
self.recent = deque(maxlen=10)
def add_component(self, component: ComponentMonitor):
self.components[component.name] = component
return component
async def request(self):
"""
Request status from each component.
"""
tasks = {
name: asyncio.create_task(comp.status())
for name, comp in self.components.items()
}
await asyncio.gather(*tasks.values())
status = {
name: await fut for name, fut in tasks.items()
}
self.recent.append(status)
return status
async def _format_summary(self, status_dict: dict[str, ComponentStatus]):
"""
Format a one line summary from a status dict.
"""
freq = {level: 0 for level in StatusLevel}
for status in status_dict.values():
freq[status.level] += 1
summary = '\t'.join(f"{level.symbol} {count}" for level, count in freq.items() if count)
return summary
async def _format_overview(self, status_dict: dict[str, ComponentStatus]):
"""
Format an overview (one line per component) from a status dict.
"""
lines = []
for name, status in status_dict.items():
lines.append(f"{status.level.symbol} {name}: {status.short}")
summary = await self._format_summary(status_dict)
return '\n'.join((summary, *lines))
async def get_summary(self):
return await self._format_summary(await self.request())
async def get_overview(self):
return await self._format_overview(await self.request())

View File

@@ -190,7 +190,7 @@ class Economy(LionCog):
# First fetch the members which currently exist
query = self.bot.core.data.Member.table.select_where(guildid=ctx.guild.id)
query.select('userid').with_no_adapter()
if 2 * len(targets) < len(ctx.guild.members):
if 2 * len(targets) < ctx.guild.member_count:
# More efficient to fetch the targets explicitly
query.where(userid=list(targetids))
existent_rows = await query

View File

@@ -181,15 +181,17 @@ class MemberAdminCog(LionCog):
finally:
self._adding_roles.discard((member.guild.id, member.id))
@LionCog.listener('on_member_remove')
@LionCog.listener('on_raw_member_remove')
@log_wrap(action="Farewell")
async def admin_member_farewell(self, member: discord.Member):
async def admin_member_farewell(self, payload: discord.RawMemberRemoveEvent):
# Ignore members that just joined
if (member.guild.id, member.id) in self._adding_roles:
guildid = payload.guild_id
userid = payload.user.id
if (guildid, userid) in self._adding_roles:
return
# Set lion last_left, creating the lion_member if needed
lion = await self.bot.core.lions.fetch_member(member.guild.id, member.id)
lion = await self.bot.core.lions.fetch_member(guildid, userid)
await lion.data.update(last_left=utc_now())
# Save member roles
@@ -197,18 +199,21 @@ class MemberAdminCog(LionCog):
self.bot.db.conn = conn
async with conn.transaction():
await self.data.past_roles.delete_where(
guildid=member.guild.id,
userid=member.id
guildid=guildid,
userid=userid
)
# Insert current member roles
if member.roles:
print(type(payload.user))
if isinstance(payload.user, discord.Member) and payload.user.roles:
member = payload.user
await self.data.past_roles.insert_many(
('guildid', 'userid', 'roleid'),
*((member.guild.id, member.id, role.id) for role in member.roles)
*((guildid, userid, role.id) for role in member.roles)
)
logger.debug(
f"Stored persisting roles for member <uid:{member.id}> in <gid:{member.guild.id}>."
f"Stored persisting roles for member <uid:{userid}> in <gid:{guildid}>."
)
# TODO: Event log, and include info about unchunked members
@LionCog.listener('on_guild_join')
async def admin_init_guild(self, guild: discord.Guild):

View File

@@ -173,7 +173,7 @@ class MemberAdminSettings(SettingGroup):
'{guild_name}': guild.name,
'{guild_icon}': guild.icon.url if guild.icon else member.default_avatar.url,
'{studying_count}': str(active),
'{member_count}': len(guild.members),
'{member_count}': guild.member_count,
}
recurse_map(
@@ -297,7 +297,7 @@ class MemberAdminSettings(SettingGroup):
'{guild_name}': guild.name,
'{guild_icon}': guild.icon.url if guild.icon else member.default_avatar.url,
'{studying_count}': str(active),
'{member_count}': str(len(guild.members)),
'{member_count}': str(guild.member_count),
'{last_time}': str(last_seen or member.joined_at.timestamp()),
}

View File

@@ -210,7 +210,7 @@ class MemberAdminUI(ConfigUI):
t = self.bot.translator.t
title = t(_p(
'ui:memberadmin|embed|title',
"Member Admin Configuration Panel"
"Greetings and Initial Roles Panel"
))
embed = discord.Embed(
title=title,

View File

@@ -32,6 +32,6 @@ class MetaCog(LionCog):
ctx.bot,
ctx.author,
ctx.guild,
show_admin=await low_management(ctx.bot, ctx.author),
show_admin=await low_management(ctx.bot, ctx.author, ctx.guild),
)
await ui.run(ctx.interaction)

View File

@@ -10,6 +10,7 @@ from discord import app_commands as appcmds
from meta import LionCog, LionBot, LionContext
from meta.logger import log_wrap
from meta.sharding import THIS_SHARD
from meta.monitor import ComponentMonitor, ComponentStatus, StatusLevel
from utils.lib import utc_now
from wards import low_management_ward
@@ -42,12 +43,25 @@ class TimerCog(LionCog):
self.bot = bot
self.data = bot.db.load_registry(TimerData())
self.settings = TimerSettings()
self.monitor = ComponentMonitor('TimerCog', self._monitor)
self.timer_options = TimerOptions()
self.ready = False
self.timers = defaultdict(dict)
async def _monitor(self):
if not self.ready:
level = StatusLevel.STARTING
info = "(STARTING) Not ready. {timers} timers loaded."
else:
level = StatusLevel.OKAY
info = "(OK) {timers} timers loaded."
data = dict(timers=len(self.timers))
return ComponentStatus(level, info, info, data)
async def cog_load(self):
self.bot.system_monitor.add_component(self.monitor)
await self.data.init()
self.bot.core.guild_config.register_model_setting(self.settings.PomodoroChannel)
@@ -386,8 +400,9 @@ class TimerCog(LionCog):
)
else:
# Display the timer status ephemerally
await ctx.interaction.response.defer(thinking=True, ephemeral=True)
status = await timer.current_status(with_notify=False, with_warnings=False)
await ctx.reply(**status.send_args, ephemeral=True)
await ctx.interaction.edit_original_response(**status.edit_args)
if error is not None:
await ctx.reply(embed=error, ephemeral=True)

View File

@@ -12,6 +12,7 @@ from utils.lib import MessageArgs, utc_now, replace_multiple
from core.lion_guild import LionGuild
from core.data import CoreData
from babel.translator import ctx_locale
from gui.errors import RenderingException
from . import babel, logger
from .data import TimerData
@@ -83,6 +84,7 @@ class Timer:
self.destroyed = False
def __repr__(self):
# TODO: Add lock status and current state and stage
return (
"<Timer "
f"channelid={self.data.channelid} "
@@ -560,19 +562,20 @@ class Timer:
"Timer stopped! Press `Start` to restart the timer."
)).format(channel=f"<#{self.data.channelid}>")
card = await get_timer_card(self.bot, self, stage)
await card.render()
if (ui := self.status_view) is None:
ui = self.status_view = TimerStatusUI(self.bot, self, self.channel)
await ui.refresh()
return MessageArgs(
content=content,
file=card.as_file(f"pomodoro_{self.data.channelid}.png"),
view=ui
)
card = await get_timer_card(self.bot, self, stage)
try:
await card.render()
file = card.as_file(f"pomodoro_{self.data.channelid}.png")
args = MessageArgs(content=content, file=file, view=ui)
except RenderingException:
args = MessageArgs(content=content, view=ui)
return args
@log_wrap(action='Send Timer Status')
async def send_status(self, delete_last=True, **kwargs):
@@ -785,8 +788,8 @@ class Timer:
to_next_stage = (current.end - utc_now()).total_seconds()
# TODO: Consider request rate and load
if to_next_stage > 1 * 60 - drift:
time_to_sleep = 1 * 60
if to_next_stage > 5 * 60 - drift:
time_to_sleep = 5 * 60
else:
time_to_sleep = to_next_stage

View File

@@ -286,27 +286,78 @@ class RankCog(LionCog):
await task
async def _role_check(self, session_rank: SeasonRank):
guild = self.bot.get_guild(session_rank.guildid)
member = guild.get_member(session_rank.userid)
crank = session_rank.current_rank
roleid = crank.roleid if crank else None
last_roleid = session_rank.rankrow.last_roleid
if guild is not None and member is not None and roleid != last_roleid:
new_role = guild.get_role(roleid) if roleid else None
last_role = guild.get_role(last_roleid) if last_roleid else None
"""
Update the member's rank roles, if required.
"""
guildid = session_rank.guildid
guild = self.bot.get_guild(guildid)
userid = session_rank.userid
member = guild.get_member(userid)
if guild is not None and member is not None and guild.me.guild_permissions.manage_roles:
ranks = await self.get_guild_ranks(guildid)
crank = session_rank.current_rank
current_roleid = crank.roleid if crank else None
# First gather rank roleids, note that the last_roleid is an 'honourary' roleid
last_roleid = session_rank.rankrow.last_roleid
rank_roleids = {rank.roleid for rank in ranks}
rank_roleids.add(last_roleid)
# Gather member roleids
mem_roleids = {role.id: role for role in member.roles}
# Calculate diffs
to_add = guild.get_role(current_roleid) if (current_roleid not in mem_roleids) else None
to_rm = [
role for roleid, role in mem_roleids.items()
if roleid in rank_roleids and roleid != current_roleid
]
# Now update roles
new_last_roleid = last_roleid
if guild.me.guild_permissions.manage_roles:
# TODO: Event log here, including errors
to_rm = [role for role in to_rm if role.is_assignable()]
if to_rm:
try:
if last_role and last_role.is_assignable():
await member.remove_roles(last_role)
new_last_roleid = None
if new_role and new_role.is_assignable():
await member.add_roles(new_role)
new_last_roleid = roleid
except discord.HTTPClient:
pass
if new_last_roleid != last_roleid:
await session_rank.rankrow.update(last_roleid=new_last_roleid)
await member.remove_roles(
*to_rm,
reason="Removing Old Rank Roles",
atomic=True
)
roleids = ', '.join(str(role.id) for role in to_rm)
logger.info(
f"Removed old rank roles from <uid:{userid}> in <gid:{guildid}>: {roleids}"
)
new_last_roleid = None
except discord.HTTPException:
logger.warning(
f"Unexpected error removing old rank roles from <uid:{member.id}> in <gid:{guild.id}>: {to_rm}",
exc_info=True
)
if to_add and to_add.is_assignable():
try:
await member.add_roles(
to_add,
reason="Rewarding Activity Rank",
atomic=True
)
logger.info(
f"Rewarded rank role <rid:{to_add.id}> to <uid:{userid}> in <gid:{guildid}>."
)
new_last_roleid = to_add.id
except discord.HTTPException:
logger.warning(
f"Unexpected error giving <uid:{userid}> in <gid:{guildid}> their rank role <rid:{to_add.id}>",
exc_info=True
)
if new_last_roleid != last_roleid:
await session_rank.rankrow.update(last_roleid=new_last_roleid)
@log_wrap(action="Update Rank")
async def update_rank(self, session_rank):
@@ -336,23 +387,61 @@ class RankCog(LionCog):
if member is None:
return
new_role = guild.get_role(new_rank.roleid)
if last_roleid := session_rank.rankrow.last_roleid:
last_role = guild.get_role(last_roleid)
else:
last_role = None
last_roleid = session_rank.rankrow.last_roleid
# Update ranks
if guild.me.guild_permissions.manage_roles:
try:
if last_role and last_role.is_assignable():
await member.remove_roles(last_role)
# First gather rank roleids, note that the last_roleid is an 'honourary' roleid
rank_roleids = {rank.roleid for rank in ranks}
rank_roleids.add(last_roleid)
# Gather member roleids
mem_roleids = {role.id: role for role in member.roles}
# Calculate diffs
to_add = guild.get_role(new_rank.roleid) if (new_rank.roleid not in mem_roleids) else None
to_rm = [
role for roleid, role in mem_roleids.items()
if roleid in rank_roleids and roleid != new_rank.roleid
]
# Now update roles
# TODO: Event log here, including errors
to_rm = [role for role in to_rm if role.is_assignable()]
if to_rm:
try:
await member.remove_roles(
*to_rm,
reason="Removing Old Rank Roles",
atomic=True
)
roleids = ', '.join(str(role.id) for role in to_rm)
logger.info(
f"Removed old rank roles from <uid:{userid}> in <gid:{guildid}>: {roleids}"
)
last_roleid = None
if new_role and new_role.is_assignable():
await member.add_roles(new_role)
last_roleid = new_role.id
except discord.HTTPException:
# TODO: Event log either way
pass
except discord.HTTPException:
logger.warning(
f"Unexpected error removing old rank roles from <uid:{member.id}> in <gid:{guild.id}>: {to_rm}",
exc_info=True
)
if to_add and to_add.is_assignable():
try:
await member.add_roles(
to_add,
reason="Rewarding Activity Rank",
atomic=True
)
logger.info(
f"Rewarded rank role <rid:{to_add.id}> to <uid:{userid}> in <gid:{guildid}>."
)
last_roleid=to_add.id
except discord.HTTPException:
logger.warning(
f"Unexpected error giving <uid:{userid}> in <gid:{guildid}> their rank role <rid:{to_add.id}>",
exc_info=True
)
# Update MemberRank row
column = {
@@ -438,7 +527,7 @@ class RankCog(LionCog):
required = format_stat_range(rank_type, rank.required, short=False)
key_map = {
'{role_name}': role.name,
'{role_name}': role.name if role else 'Unknown',
'{guild_name}': guild.name,
'{user_name}': member.name,
'{role_id}': role.id,
@@ -495,6 +584,7 @@ class RankCog(LionCog):
await interaction.response.defer(thinking=False)
ui = RankRefreshUI(self.bot, guild, callerid=interaction.user.id, timeout=None)
await ui.send(interaction.channel)
ui.start()
# Retrieve fresh rank roles
ranks = await self.get_guild_ranks(guild.id, refresh=True)
@@ -503,7 +593,15 @@ class RankCog(LionCog):
# Ensure guild is chunked
if not guild.chunked:
members = await guild.chunk()
try:
members = await asyncio.wait_for(guild.chunk(), timeout=60)
except asyncio.TimeoutError:
error = t(_p(
'rank_refresh|error:cannot_chunk|desc',
"Could not retrieve member list from Discord. Please try again later."
))
await ui.set_error(error)
return
else:
members = guild.members
ui.stage_members = True
@@ -524,7 +622,7 @@ class RankCog(LionCog):
error = t(_p(
'rank_refresh|error:unassignable_roles|desc',
"I have insufficient permissions to assign the following role(s):\n{roles}"
)).format(roles='\n'.join(role.mention for role in failing)),
)).format(roles='\n'.join(role.mention for role in failing))
await ui.set_error(error)
return
@@ -609,8 +707,8 @@ class RankCog(LionCog):
'rank_refresh|remove_roles|small_error',
"*Could not remove ranks from {member}*"
)).format(member=to_remove[index][0].mention)
self.ui.errors.append(error)
if len(self.ui.errors) > 10:
ui.errors.append(error)
if len(ui.errors) > 10:
await ui.set_error(
t(_p(
'rank_refresh|remove_roles|error:too_many_issues',
@@ -644,8 +742,8 @@ class RankCog(LionCog):
'rank_refresh|add_roles|small_error',
"*Could not add {role} to {member}*"
)).format(member=to_add[index][0].mention, role=to_add[index][1].mention)
self.ui.errors.append(error)
if len(self.ui.errors) > 10:
ui.errors.append(error)
if len(ui.errors) > 10:
await ui.set_error(
t(_p(
'rank_refresh|add_roles|error:too_many_issues',

View File

@@ -12,6 +12,7 @@ from data import ORDER
from utils.ui import MessageUI, Confirm
from utils.lib import MessageArgs
from wards import equippable_role
from babel.translator import ctx_translator
from .. import babel, logger
@@ -185,25 +186,11 @@ class RankOverviewUI(MessageUI):
or edit an existing rank,
or throw an error if the role is @everyone or not manageable by the client.
"""
role: discord.Role = selected.values[0]
if role >= selection.user.top_role:
# Do not allow user to manage a role above their own top role
t = self.bot.translator.t
error = t(_p(
'ui:rank_overview|menu:roles|error:above_caller',
"You have insufficient permissions to assign {mention} as a rank role! "
"You may only manage roles below your top role."
)).format(mention=role.mention)
embed = discord.Embed(
title=t(_p(
'ui:rank_overview|menu:roles|error:above_caller|title',
"Insufficient permissions!"
)),
description=error,
colour=discord.Colour.brand_red()
)
await selection.response.send_message(embed=embed, ephemeral=True)
elif role.is_assignable():
if role.is_assignable():
# Create or edit the selected role
existing = next((rank for rank in self.ranks if rank.roleid == role.id), None)
if existing:
# Display and edit the given role
@@ -216,6 +203,8 @@ class RankOverviewUI(MessageUI):
)
else:
# Create new rank based on role
# Need to check the calling author has authority to manage this role
await equippable_role(self.bot, role, selection.user)
await RankEditor.create_rank(
selection,
self.rank_type,
@@ -380,34 +369,6 @@ class RankOverviewUI(MessageUI):
] or [[]]
lines = line_blocks[self.pagen]
desc = '\n'.join(reversed(lines))
# Add note about season start
note_name = t(_p(
'ui:rank_overview|embed|field:note|name',
"Note"
))
season_start = self.lguild.data.season_start
if season_start:
season_str = t(_p(
'ui:rank_overview|embed|field:note|value:with_season',
"Ranks are determined by activity since {timestamp}."
)).format(
timestamp=discord.utils.format_dt(season_start)
)
else:
season_str = t(_p(
'ui:rank_overview|embed|field:note|value:without_season',
"Ranks are determined by *all-time* statistics.\n"
"To reward ranks from a later time (e.g. to have monthly/quarterly/yearly ranks) "
"set the `season_start` with {stats_cmd}"
)).format(stats_cmd=self.bot.core.mention_cmd('configure statistics'))
if self.rank_type is RankType.VOICE:
addendum = t(_p(
'ui:rank_overview|embed|field:note|value|voice_addendum',
"Also note that ranks will only be updated when a member leaves a tracked voice channel! "
"Use the **Refresh Member Ranks** button below to update all members manually."
))
season_str = '\n'.join((season_str, addendum))
else:
# No ranks, give hints about adding ranks
desc = t(_p(
@@ -439,6 +400,33 @@ class RankOverviewUI(MessageUI):
description=desc
)
if show_note:
# Add note about season start
note_name = t(_p(
'ui:rank_overview|embed|field:note|name',
"Note"
))
season_start = self.lguild.data.season_start
if season_start:
season_str = t(_p(
'ui:rank_overview|embed|field:note|value:with_season',
"Ranks are determined by activity since {timestamp}."
)).format(
timestamp=discord.utils.format_dt(season_start)
)
else:
season_str = t(_p(
'ui:rank_overview|embed|field:note|value:without_season',
"Ranks are determined by *all-time* statistics.\n"
"To reward ranks from a later time (e.g. to have monthly/quarterly/yearly ranks) "
"set the `season_start` with {stats_cmd}"
)).format(stats_cmd=self.bot.core.mention_cmd('configure statistics'))
if self.rank_type is RankType.VOICE:
addendum = t(_p(
'ui:rank_overview|embed|field:note|value|voice_addendum',
"Also note that ranks will only be updated when a member leaves a tracked voice channel! "
"Use the **Refresh Member Ranks** button below to update all members manually."
))
season_str = '\n'.join((season_str, addendum))
embed.add_field(
name=note_name,
value=season_str,

View File

@@ -7,6 +7,7 @@ from discord.ui.button import button, Button, ButtonStyle
from meta import conf, LionBot
from core.data import RankType
from wards import equippable_role
from utils.ui import MessageUI, AButton, AsComponents
from utils.lib import MessageArgs, replace_multiple
@@ -112,6 +113,7 @@ class RankPreviewUI(MessageUI):
await submit.response.defer(thinking=False)
if self.parent is not None:
asyncio.create_task(self.parent.refresh())
self.bot.get_cog('RankCog').flush_guild_ranks(self.guild.id)
await self.refresh()
@button(label="DELETE_PLACEHOLDER", style=ButtonStyle.red)
@@ -130,6 +132,7 @@ class RankPreviewUI(MessageUI):
role = None
await self.rank.delete()
self.bot.get_cog('RankCog').flush_guild_ranks(self.guild.id)
mention = role.mention if role else str(self.rank.roleid)
@@ -212,25 +215,13 @@ class RankPreviewUI(MessageUI):
role: discord.Role = selected.values[0]
await selection.response.defer(thinking=True, ephemeral=True)
if role >= selection.user.top_role:
# Do not allow user to manage a role above their own top role
error = t(_p(
'ui:rank_preview|menu:roles|error:above_caller',
"You have insufficient permissions to assign {mention} as a rank role! "
"You may only manage roles below your top role."
))
embed = discord.Embed(
title=t(_p(
'ui:rank_preview|menu:roles|error:above_caller|title',
"Insufficient permissions!"
)),
description=error,
colour=discord.Colour.brand_red()
)
await selection.response.send_message(embed=embed, ephemeral=True)
elif role.is_assignable():
if role.is_assignable():
# Update the rank role
# Generic permission check for the new role
await equippable_role(self.bot, role, selection.user)
await self.rank.update(roleid=role.id)
self.bot.get_cog('RankCog').flush_guild_ranks(self.guild.id)
if self.parent is not None and not self.parent.is_finished():
asyncio.create_task(self.parent.refresh())
await self.refresh(thinking=selection)

View File

@@ -24,6 +24,9 @@ _p = babel._p
class RankRefreshUI(MessageUI):
# Cache of live rank UIs, mainly for introspection
_running = set()
def __init__(self, bot: LionBot, guild: discord.Guild, **kwargs):
super().__init__(**kwargs)
self.bot = bot
@@ -64,13 +67,18 @@ class RankRefreshUI(MessageUI):
def poke(self):
self._wakeup.set()
def start(self):
self._loop_task = asyncio.create_task(self._refresh_loop(), name='Rank RefreshUI Monitor')
self._running.add(self)
async def run(self, *args, **kwargs):
await super().run(*args, **kwargs)
self._loop_task = asyncio.create_task(self._refresh_loop(), name='refresh ui loop')
self.start()
async def cleanup(self):
if self._loop_task and not self._loop_task.done():
self._loop_task.cancel()
self._running.discard(self)
await super().cleanup()
def progress_bar(self, value, minimum, maximum, width=10) -> str:
@@ -104,12 +112,13 @@ class RankRefreshUI(MessageUI):
# Join all the sections together and return
return ''.join(bar)
@log_wrap('refresh ui loop')
@log_wrap(action='refresh ui loop')
async def _refresh_loop(self):
while True:
try:
await asyncio.sleep(1)
await asyncio.sleep(5)
await self._wakeup.wait()
self._wakeup.clear()
await self.refresh()
except asyncio.CancelledError:
break

View File

@@ -971,14 +971,21 @@ class RoleMenuCog(LionCog):
)
# TODO: Generate the custom message from the template if it doesn't exist
# TODO: Pathway for setting menu style
if rawmessage is not None:
msg_config = target.config.rawmessage
content = await msg_config.download_attachment(rawmessage)
data = await msg_config._parse_string(content)
data = await msg_config._parse_string(0, content)
update_args[msg_config._column] = data
if template is None:
update_args[self.data.RoleMenu.templateid.name] = None
ack_lines.append(msg_config.update_message)
ack_lines.append(
t(_p(
'cmd:rolemenu_edit|parse:custom_message|success',
"Custom menu message updated."
))
)
# Update the data, if applicable
if update_args:
@@ -1185,7 +1192,7 @@ class RoleMenuCog(LionCog):
label: Optional[appcmds.Range[str, 1, 100]] = None,
emoji: Optional[appcmds.Range[str, 0, 100]] = None,
description: Optional[appcmds.Range[str, 0, 100]] = None,
price: Optional[appcmds.Range[int, 0, MAX_COINS]] = None,
price: Optional[appcmds.Range[int, -MAX_COINS, MAX_COINS]] = None,
duration: Optional[Transform[int, DurationTransformer(60)]] = None,
):
# Type checking guards
@@ -1356,7 +1363,7 @@ class RoleMenuCog(LionCog):
await target.update_message()
if target_is_reaction:
try:
await self.menu.update_reactons()
await target.update_reactons()
except SafeCancellation as e:
embed.add_field(
name=t(_p(
@@ -1441,7 +1448,7 @@ class RoleMenuCog(LionCog):
label: Optional[appcmds.Range[str, 1, 100]] = None,
emoji: Optional[appcmds.Range[str, 0, 100]] = None,
description: Optional[appcmds.Range[str, 0, 100]] = None,
price: Optional[appcmds.Range[int, 0, MAX_COINS]] = None,
price: Optional[appcmds.Range[int, -MAX_COINS, MAX_COINS]] = None,
duration: Optional[Transform[int, DurationTransformer(60)]] = None,
):
# Type checking wards

View File

@@ -165,7 +165,7 @@ class RoleMenu:
await menu.attach()
return menu
async def fetch_message(self, refresh=False):
async def fetch_message(self, refresh=False) -> Optional[discord.Message]:
"""
Fetch the message the menu is attached to.
"""
@@ -529,11 +529,17 @@ class RoleMenu:
"Role removed"
))
)
if total_refund:
if total_refund > 0:
embed.description = t(_p(
'rolemenu|deselect|success:refund|desc',
"You have removed **{role}**, and been refunded {coin} **{amount}**."
)).format(role=role.name, coin=self.bot.config.emojis.coin, amount=total_refund)
if total_refund < 0:
# TODO: Consider disallowing them from removing roles if their balance would go negative
embed.description = t(_p(
'rolemenu|deselect|success:negrefund|desc',
"You have removed **{role}**, and have lost {coin} **{amount}**."
)).format(role=role.name, coin=self.bot.config.emojis.coin, amount=-total_refund)
else:
embed.description = t(_p(
'rolemenu|deselect|success:norefund|desc',
@@ -551,7 +557,7 @@ class RoleMenu:
raise UserInputError(
t(_p(
'rolemenu|select|error:required_role',
"You need to have the **{role}** role to use this!"
"You need to have the role **{role}** required to use this menu!"
)).format(role=name)
)
@@ -647,7 +653,7 @@ class RoleMenu:
"Role equipped"
))
)
if price > 0:
if price:
embed.description = t(_p(
'rolemenu|select|success:purchase|desc',
"You have purchased the role **{role}** for {coin}**{amount}**"
@@ -665,6 +671,7 @@ class RoleMenu:
)).format(
timestamp=discord.utils.format_dt(expiry)
)
# TODO Event logging
return embed
async def interactive_selection(self, interaction: discord.Interaction, menuroleid: int):

View File

@@ -1,14 +1,17 @@
from typing import Optional
import discord
from settings import ModelData
from settings.groups import SettingGroup, ModelConfig, SettingDotDict
from settings.setting_types import (
RoleSetting, BoolSetting, StringSetting, DurationSetting, EmojiSetting
RoleSetting, StringSetting, DurationSetting, EmojiSetting
)
from core.setting_types import CoinSetting
from utils.ui import AButton, AsComponents
from meta.errors import UserInputError
from meta import conf
from babel.translator import ctx_translator
from constants import MAX_COINS
from .data import RoleMenuData
from . import babel
@@ -74,6 +77,9 @@ class RoleMenuRoleOptions(SettingGroup):
"This menu item will now give the role {role}."
)).format(role=self.formatted)
return resp
else:
raise ValueError("Attempt to call update_message without a value.")
@RoleMenuRoleConfig.register_model_setting
class Label(ModelData, StringSetting):
@@ -138,7 +144,9 @@ class RoleMenuRoleOptions(SettingGroup):
return button
@classmethod
async def _parse_string(cls, parent_id, string: str, interaction: discord.Interaction = None, **kwargs):
async def _parse_string(cls, parent_id, string: str,
interaction: Optional[discord.Interaction] = None,
**kwargs):
emojistr = await super()._parse_string(parent_id, string, interaction=interaction, **kwargs)
if emojistr and interaction is not None:
# Use the interaction to test
@@ -151,7 +159,7 @@ class RoleMenuRoleOptions(SettingGroup):
view=view,
)
except discord.HTTPException:
t = interaction.client.translator.t
t = ctx_translator.get().t
raise UserInputError(t(_p(
'roleset:emoji|error:test_emoji',
"The selected emoji `{emoji}` is invalid or has been deleted."
@@ -218,34 +226,43 @@ class RoleMenuRoleOptions(SettingGroup):
_display_name = _p('roleset:price', "price")
_desc = _p(
'roleset:price|desc',
"Price of the role, in LionCoins."
"Price of the role, in LionCoins. May be negative."
)
_long_desc = _p(
'roleset:price|long_desc',
"How much the role costs when selected, in LionCoins."
"How many LionCoins should be deducted from a member's account "
"when they equip this role through this menu.\n"
"The price may be negative, in which case the member will instead be rewarded "
"coins when they equip the role."
)
_accepts = _p(
'roleset:price|accepts',
"Amount of coins that the role costs."
"Amount of coins that the role costs when equipped."
)
_default = 0
_min = - MAX_COINS
_model = RoleMenuData.RoleMenuRole
_column = RoleMenuData.RoleMenuRole.price.name
@property
def update_message(self) -> str:
t = ctx_translator.get().t
value = self.value
if value:
value = self.value or 0
if value > 0:
resp = t(_p(
'roleset:price|set_response:set',
"This role will now cost {price} to equip."
)).format(price=self.formatted)
'roleset:price|set_response:positive',
"Equipping this role will now cost {coin}**{price}**."
)).format(price=value, coin=conf.emojis.coin)
elif value == 0:
resp = t(_p(
'roleset:price|set_response:zero',
"Equipping this role is now free."
))
else:
resp = t(_p(
'roleset:price|set_response:unset',
"This role will now be free to equip from this role menu."
))
'roleset:price|set_response:negative',
"Equipping this role will now reward {coin}**{price}**."
)).format(price=-value, coin=conf.emojis.coin)
return resp
@RoleMenuRoleConfig.register_model_setting

View File

@@ -190,7 +190,10 @@ class MenuEditor(MessageUI):
if not userstr:
new_data = None
else:
new_data = await instance._parse_string(instance.parent_id, userstr)
new_data = await instance._parse_string(
instance.parent_id, userstr,
guildid=self.menu.data.guildid
)
instance.data = new_data
modified.append(instance)
if modified:
@@ -349,7 +352,9 @@ class MenuEditor(MessageUI):
if not userstr:
new_data = None
else:
new_data = await instance._parse_string(instance.parent_id, userstr, interaction=interaction)
new_data = await instance._parse_string(
instance.parent_id, userstr, interaction=interaction
)
instance.data = new_data
modified.append(instance)
if modified:
@@ -503,6 +508,11 @@ class MenuEditor(MessageUI):
await self.refresh(thinking=selection)
await self.update_preview()
await self.menu.update_message()
if self.menu.data.menutype is MenuType.REACTION:
try:
await self.menu.update_reactons()
except SafeCancellation:
pass
else:
await selection.response.defer(thinking=False)
@@ -591,6 +601,8 @@ class MenuEditor(MessageUI):
await self.refresh(thinking=selection)
await self.update_preview()
await self.menu.update_message()
if menutype is MenuType.REACTION:
await self.menu.update_reactons()
else:
await selection.response.defer()
@@ -644,7 +656,7 @@ class MenuEditor(MessageUI):
self.bot, json.loads(self.menu.data.rawmessage), callback=self._editor_callback, callerid=self._callerid
)
self._slaves.append(editor)
await editor.run(interaction)
await editor.run(interaction, ephemeral=True)
# Template/Custom Menu
@select(cls=Select, placeholder="TEMPLATE_MENU_PLACEHOLDER", min_values=1, max_values=1)
@@ -821,20 +833,15 @@ class MenuEditor(MessageUI):
"""
Display or update the preview message.
"""
args = await self.menu.make_args()
view = await self.menu.make_view()
if self._preview is not None:
try:
await self._preview.delete_original_response()
except discord.HTTPException:
pass
self._preview = None
await press.response.send_message(
**args.send_args,
view=view or discord.utils.MISSING,
ephemeral=True
)
await press.response.defer(thinking=True, ephemeral=True)
self._preview = press
await self.update_preview()
async def preview_button_refresh(self):
t = self.bot.translator.t
@@ -887,13 +894,14 @@ class MenuEditor(MessageUI):
description=desc
)
await selection.edit_original_response(embed=embed)
except discord.HTTPException:
except discord.HTTPException as e:
error = discord.Embed(
colour=discord.Colour.brand_red(),
description=t(_p(
'ui:menu_editor|button:repost|widget:repost|error:post_failed',
"An error ocurred while posting to {channel}. Do I have sufficient permissions?"
)).format(channel=channel.mention)
"An unknown error ocurred while posting to {channel}!\n"
"**Error:** `{exception}`"
)).format(channel=channel.mention, exception=e.text)
)
await selection.edit_original_response(embed=error)
else:
@@ -948,7 +956,7 @@ class MenuEditor(MessageUI):
title=title, description=desc
)
# Send as response with the repost widget attached
await press.response.send_message(embed=embed, view=AsComponents(repost_widget))
await press.response.send_message(embed=embed, view=AsComponents(repost_widget), ephemeral=True)
async def repost_button_refresh(self):
t = self.bot.translator.t
@@ -1039,7 +1047,7 @@ class MenuEditor(MessageUI):
role_index = int(splits[i-1])
mrole = self.menu.roles[role_index]
error = discord.Embed(
embed = discord.Embed(
colour=discord.Colour.brand_red(),
title=t(_p(
'ui:menu_editor|error:invald_emoji|title',
@@ -1051,7 +1059,7 @@ class MenuEditor(MessageUI):
)).format(emoji=mrole.config.emoji.data, label=mrole.config.label.data)
)
await mrole.data.update(emoji=None)
await self.channel.send(embed=error)
await self.channel.send(embed=embed)
async def _redraw(self, args):
try:

View File

@@ -13,6 +13,7 @@ from meta import LionCog, LionBot, LionContext
from meta.logger import log_wrap
from meta.errors import UserInputError, ResponseTimedOut
from meta.sharding import THIS_SHARD
from meta.monitor import ComponentMonitor, ComponentStatus, StatusLevel
from utils.lib import utc_now, error_embed
from utils.ui import Confirm
from utils.data import MULTIVALUE_IN, MEMBERS
@@ -38,6 +39,10 @@ class ScheduleCog(LionCog):
self.bot = bot
self.data: ScheduleData = bot.db.load_registry(ScheduleData())
self.settings = ScheduleSettings()
self.monitor = ComponentMonitor(
'ScheduleCog',
self._monitor
)
# Whether we are ready to take events
self.initialised = asyncio.Event()
@@ -57,12 +62,56 @@ class ScheduleCog(LionCog):
self.session_channels = self.settings.SessionChannels._cache
async def _monitor(self):
nowid = self.nowid
now = None
now_lock = self.slotlock(nowid)
if not self.initialised.is_set():
level = StatusLevel.STARTING
info = (
"(STARTING) "
"Not ready. "
"Spawn task is {spawn}. "
"Spawn lock is {spawn_lock}. "
"Active slots {active}."
)
elif nowid not in self.active_slots:
level = StatusLevel.UNSURE
info = (
"(UNSURE) "
"Setup, but current slotid {nowid} not active. "
"Spawn task is {spawn}. "
"Spawn lock is {spawn_lock}. "
"Now lock is {now_lock}. "
"Active slots {active}."
)
else:
now = self.active_slots[nowid]
level = StatusLevel.OKAY
info = (
"(OK) "
"Running current slot {now}. "
"Spawn lock is {spawn_lock}. "
"Now lock is {now_lock}. "
"Active slots {active}."
)
data = {
'spawn': self.spawn_task,
'spawn_lock': self.spawn_lock,
'active': self.active_slots,
'nowid': nowid,
'now_lock': now_lock,
'now': now,
}
return ComponentStatus(level, info, info, data)
@property
def nowid(self):
now = utc_now()
return time_to_slotid(now)
async def cog_load(self):
self.bot.system_monitor.add_component(self.monitor)
await self.data.init()
# Update the session channel cache

View File

@@ -253,6 +253,12 @@ class ScheduledSession:
overwrites = room.overwrites
for member in members:
mobj = guild.get_member(member.userid)
if not mobj and not guild.chunked:
self.bot.request_chunking_for(guild)
try:
mobj = await guild.fetch_member(member.userid)
except discord.HTTPException:
mobj = None
if mobj:
overwrites[mobj] = discord.PermissionOverwrite(connect=True, view_channel=True)
try:
@@ -297,6 +303,13 @@ class ScheduledSession:
}
for member in members:
mobj = guild.get_member(member.userid)
if not mobj and not guild.chunked:
self.bot.request_chunking_for(guild)
try:
mobj = await guild.fetch_member(member.userid)
except discord.HTTPException:
mobj = None
if mobj:
overwrites[mobj] = discord.PermissionOverwrite(connect=True, view_channel=True)
try:

View File

@@ -440,7 +440,7 @@ class TimeSlot:
)
def launch(self) -> asyncio.Task:
self.run_task = asyncio.create_task(self.run())
self.run_task = asyncio.create_task(self.run(), name=f"TimeSlot {self.slotid}")
return self.run_task
@log_wrap(action="TimeSlot Run")

View File

@@ -117,6 +117,7 @@ class Shopping(LionCog):
name=_p('cmd:shop', 'shop'),
description=_p('cmd:shop|desc', "Purchase coloures, roles, and other goodies with LionCoins.")
)
@appcmds.guild_only
async def shop_group(self, ctx: LionContext):
return
@@ -124,6 +125,7 @@ class Shopping(LionCog):
name=_p('cmd:shop_open', 'open'),
description=_p('cmd:shop_open|desc', "Open the server shop.")
)
@appcmds.guild_only
async def shop_open_cmd(self, ctx: LionContext):
"""
Opens the shop UI for the current guild.

View File

@@ -1,3 +1,4 @@
import asyncio
import logging
from typing import Optional
@@ -90,8 +91,12 @@ class StatsCog(LionCog):
)).format(loading=self.bot.config.emojis.loading),
timestamp=utc_now(),
)
await ctx.interaction.response(embed=waiting_embed)
await ctx.guild.chunk()
await ctx.interaction.response.send_message(embed=waiting_embed)
try:
await asyncio.wait_for(ctx.guild.chunk(), timeout=10)
pass
except asyncio.TimeoutError:
pass
else:
await ctx.interaction.response.defer(thinking=True)
ui = LeaderboardUI(self.bot, ctx.author, ctx.guild)

View File

@@ -44,7 +44,7 @@ async def get_profile_card(bot: LionBot, userid: int, guildid: int):
if crank:
roleid = crank.roleid
role = guild.get_role(roleid)
name = role.name if role else str(role.id)
name = role.name if role else 'Unknown Rank'
minimum = crank.required
maximum = nrank.required if nrank else None
rangestr = format_stat_range(rank_type, minimum, maximum)
@@ -63,7 +63,7 @@ async def get_profile_card(bot: LionBot, userid: int, guildid: int):
if nrank:
roleid = nrank.roleid
role = guild.get_role(roleid)
name = role.name if role else str(role.id)
name = role.name if role else 'Unknown Rank'
minimum = nrank.required
guild_ranks = await ranks.get_guild_ranks(guildid)

View File

@@ -75,6 +75,8 @@ class LeaderboardUI(StatsUI):
# (type, period) -> (pagen -> Optional[Future[Card]])
self.cache = {}
self.was_chunked: bool = guild.chunked
async def run(self, interaction: discord.Interaction):
self._original = interaction
@@ -136,6 +138,7 @@ class LeaderboardUI(StatsUI):
# Filter out members which are not in the server and unranked roles and bots
# Usually hits cache
self.was_chunked = self.guild.chunked
unranked_setting = await self.bot.get_cog('StatsCog').settings.UnrankedRoles.get(self.guild.id)
unranked_roleids = set(unranked_setting.data)
true_leaderboard = []
@@ -299,42 +302,42 @@ class LeaderboardUI(StatsUI):
@button(label="This Season", style=ButtonStyle.grey)
async def season_button(self, press: discord.Interaction, pressed: Button):
await press.response.defer(thinking=True)
await press.response.defer(thinking=True, ephemeral=True)
self.current_period = LBPeriod.SEASON
self.focused = True
await self.refresh(thinking=press)
@button(label="Today", style=ButtonStyle.grey)
async def day_button(self, press: discord.Interaction, pressed: Button):
await press.response.defer(thinking=True)
await press.response.defer(thinking=True, ephemeral=True)
self.current_period = LBPeriod.DAY
self.focused = True
await self.refresh(thinking=press)
@button(label="This Week", style=ButtonStyle.grey)
async def week_button(self, press: discord.Interaction, pressed: Button):
await press.response.defer(thinking=True)
await press.response.defer(thinking=True, ephemeral=True)
self.current_period = LBPeriod.WEEK
self.focused = True
await self.refresh(thinking=press)
@button(label="This Month", style=ButtonStyle.grey)
async def month_button(self, press: discord.Interaction, pressed: Button):
await press.response.defer(thinking=True)
await press.response.defer(thinking=True, ephemeral=True)
self.current_period = LBPeriod.MONTH
self.focused = True
await self.refresh(thinking=press)
@button(label="All Time", style=ButtonStyle.grey)
async def alltime_button(self, press: discord.Interaction, pressed: Button):
await press.response.defer(thinking=True)
await press.response.defer(thinking=True, ephemeral=True)
self.current_period = LBPeriod.ALLTIME
self.focused = True
await self.refresh(thinking=press)
@button(emoji=conf.emojis.backward, style=ButtonStyle.grey)
async def prev_button(self, press: discord.Interaction, pressed: Button):
await press.response.defer(thinking=True)
await press.response.defer(thinking=True, ephemeral=True)
self.pagen -= 1
self.focused = False
await self.refresh(thinking=press)
@@ -435,12 +438,19 @@ class LeaderboardUI(StatsUI):
Generate UI message arguments from stored data
"""
t = self.bot.translator.t
chunk_warning = t(_p(
'ui:leaderboard|chunk_warning',
"**Note:** Could not retrieve member list from Discord, so some members may be missing. "
"Try again in a minute!"
))
if self.card is not None:
period_start = self.period_starts[self.current_period]
header = t(_p(
'ui:leaderboard|since',
"Counting statistics since {timestamp}"
)).format(timestamp=discord.utils.format_dt(period_start))
if not self.was_chunked:
header = '\n'.join((header, chunk_warning))
args = MessageArgs(
embed=None,
content=header,
@@ -473,7 +483,11 @@ class LeaderboardUI(StatsUI):
)),
description=empty_description
)
args = MessageArgs(content=None, embed=embed, files=[])
args = MessageArgs(
content=chunk_warning if not self.was_chunked else None,
embed=embed,
files=[]
)
return args
async def refresh_components(self):

View File

@@ -186,6 +186,17 @@ def mk_print(fp: io.StringIO) -> Callable[..., None]:
return _print
def mk_status_printer(bot, printer):
async def _status(details=False):
if details:
status = await bot.system_monitor.get_overview()
else:
status = await bot.system_monitor.get_summary()
printer(status)
return status
return _status
@log_wrap(action="Code Exec")
async def _async(to_eval: str, style='exec'):
newline = '\n' * ('\n' in to_eval)
@@ -202,6 +213,7 @@ async def _async(to_eval: str, style='exec'):
scope['ctx'] = ctx = context.get()
scope['bot'] = ctx_bot.get()
scope['print'] = _print # type: ignore
scope['print_status'] = mk_status_printer(scope['bot'], _print)
try:
if ctx and ctx.message:
@@ -297,7 +309,7 @@ class Exec(LionCog):
file = discord.File(fp, filename=f"output-{target}.md")
await ctx.reply(file=file)
elif result:
await ctx.reply(f"```md{result}```")
await ctx.reply(f"```md\n{result}```")
else:
await ctx.reply("Command completed, and had no output.")
else:
@@ -351,7 +363,7 @@ class Exec(LionCog):
except asyncio.TimeoutError:
return
if ctx.interaction:
await ctx.interaction.response.defer(thinking=True, ephemeral=True)
await ctx.interaction.response.defer(thinking=True)
if target is not None:
if target not in shard_talk.peers:
embed = discord.Embed(description=f"Unknown peer {target}", colour=discord.Colour.red())
@@ -376,7 +388,7 @@ class Exec(LionCog):
await ctx.reply(file=file)
else:
# Send as message
await ctx.reply(f"```md\n{output}```", ephemeral=True)
await ctx.reply(f"```md\n{output}```")
asyncall_cmd.autocomplete('target')(_peer_acmpl)

View File

@@ -1,3 +1,4 @@
import asyncio
import datetime
import discord
@@ -22,8 +23,8 @@ class GuildLog(LionCog):
embed.set_author(name="Left guild!")
# Add more specific information about the guild
embed.add_field(name="Owner", value="{0.name} (ID: {0.id})".format(guild.owner), inline=False)
embed.add_field(name="Members (cached)", value="{}".format(len(guild.members)), inline=False)
embed.add_field(name="Owner", value="<@{}>".format(guild.owner_id), inline=False)
embed.add_field(name="Members", value="{}".format(guild.member_count), inline=False)
embed.add_field(name="Now studying in", value="{} guilds".format(len(self.bot.guilds)), inline=False)
# Retrieve the guild log channel and log the event
@@ -35,39 +36,51 @@ class GuildLog(LionCog):
@LionCog.listener('on_guild_join')
@log_wrap(action="Log Guild Join")
async def log_join_guild(self, guild: discord.Guild):
owner = guild.owner
try:
await asyncio.wait_for(guild.chunk(), timeout=60)
except asyncio.TimeoutError:
pass
bots = 0
known = 0
unknown = 0
other_members = set(mem.id for mem in self.bot.get_all_members() if mem.guild != guild)
# TODO: Add info about when we last joined this guild etc once we have it.
for member in guild.members:
if member.bot:
bots += 1
elif member.id in other_members:
known += 1
else:
unknown += 1
if guild.chunked:
bots = 0
known = 0
unknown = 0
other_members = set(mem.id for mem in self.bot.get_all_members() if mem.guild != guild)
for member in guild.members:
if member.bot:
bots += 1
elif member.id in other_members:
known += 1
else:
unknown += 1
mem1 = "people I know" if known != 1 else "person I know"
mem2 = "new friends" if unknown != 1 else "new friend"
mem3 = "bots" if bots != 1 else "bot"
mem4 = "total members"
known = "`{}`".format(known)
unknown = "`{}`".format(unknown)
bots = "`{}`".format(bots)
total = "`{}`".format(guild.member_count)
mem_str = "{0:<5}\t{4},\n{1:<5}\t{5},\n{2:<5}\t{6}, and\n{3:<5}\t{7}.".format(
known,
unknown,
bots,
total,
mem1,
mem2,
mem3,
mem4
)
else:
mem_str = (
"`{count}` total members.\n"
"(Could not chunk guild within `60` seconds.)"
).format(count=guild.member_count)
mem1 = "people I know" if known != 1 else "person I know"
mem2 = "new friends" if unknown != 1 else "new friend"
mem3 = "bots" if bots != 1 else "bot"
mem4 = "total members"
known = "`{}`".format(known)
unknown = "`{}`".format(unknown)
bots = "`{}`".format(bots)
total = "`{}`".format(guild.member_count)
mem_str = "{0:<5}\t{4},\n{1:<5}\t{5},\n{2:<5}\t{6}, and\n{3:<5}\t{7}.".format(
known,
unknown,
bots,
total,
mem1,
mem2,
mem3,
mem4
)
created = "<t:{}>".format(int(guild.created_at.timestamp()))
embed = discord.Embed(
@@ -77,7 +90,7 @@ class GuildLog(LionCog):
)
embed.set_author(name="Joined guild!")
embed.add_field(name="Owner", value="{0} (ID: {0.id})".format(owner), inline=False)
embed.add_field(name="Owner", value="<@{}>".format(guild.owner_id), inline=False)
embed.add_field(name="Created at", value=created, inline=False)
embed.add_field(name="Members", value=mem_str, inline=False)
embed.add_field(name="Now studying in", value="{} guilds".format(len(self.bot.guilds)), inline=False)

View File

@@ -396,7 +396,7 @@ class RoleSetting(InteractiveSetting[ParentID, int, Union[discord.Role, discord.
if data is not None:
role = None
guildid = cls._get_guildid(parent_id)
guildid = cls._get_guildid(parent_id, **kwargs)
bot = ctx_bot.get()
guild = bot.get_guild(guildid)
if guild is not None:
@@ -409,11 +409,14 @@ class RoleSetting(InteractiveSetting[ParentID, int, Union[discord.Role, discord.
async def _parse_string(cls, parent_id, string: str, **kwargs):
if not string or string.lower() == 'none':
return None
guildid = cls._get_guildid(parent_id, **kwargs)
t = ctx_translator.get().t
bot = ctx_bot.get()
role = None
guild = bot.get_guild(parent_id)
guild = bot.get_guild(guildid)
if guild is None:
raise ValueError("Attempting to parse role string with no guild.")
if string.isdigit():
maybe_id = int(string)

View File

@@ -80,6 +80,10 @@ class Bucket:
self._last_full = False
self._level += 1
def fill(self):
self._leak()
self._level = max(self._level, self.max_level + 1)
async def wait(self):
"""
Wait until the bucket has room.

View File

@@ -10,6 +10,8 @@ from discord.ui import Modal, View, Item
from meta.logger import log_action_stack, logging_context
from meta.errors import SafeCancellation
from gui.errors import RenderingException
from . import logger
from ..lib import MessageArgs, error_embed
@@ -228,6 +230,12 @@ class LeoUI(View):
f"Caught a safe cancellation from LeoUI: {e.details}",
extra={'action': 'Cancel'}
)
except RenderingException as e:
logger.info(
f"UI interaction failed due to rendering exception: {repr(e)}"
)
embed = interaction.client.tree.rendersplat(e)
await interaction.client.tree.error_reply(interaction, embed)
except Exception:
logger.exception(
f"Unhandled interaction exception occurred in item {item!r} of LeoUI {self!r} from interaction: "
@@ -235,15 +243,8 @@ class LeoUI(View):
extra={'with_ctx': True, 'action': 'UIError'}
)
# Explicitly handle the bugsplat ourselves
if not interaction.is_expired():
splat = interaction.client.tree.bugsplat(interaction, error)
try:
if interaction.response.is_done():
await interaction.followup.send(embed=splat, ephemeral=True)
else:
await interaction.response.send_message(embed=splat, ephemeral=True)
except discord.HTTPException:
pass
splat = interaction.client.tree.bugsplat(interaction, error)
await interaction.client.tree.error_reply(interaction, splat)
class MessageUI(LeoUI):
@@ -475,21 +476,20 @@ class LeoModal(Modal):
"""
try:
raise error
except RenderingException as e:
logger.info(
f"Modal submit failed due to rendering exception: {repr(e)}"
)
embed = interaction.client.tree.rendersplat(e)
await interaction.client.tree.error_reply(interaction, embed)
except Exception:
logger.exception(
f"Unhandled interaction exception occurred in {self!r}. Interaction: {interaction.data}",
extra={'with_ctx': True, 'action': 'ModalError'}
)
# Explicitly handle the bugsplat ourselves
if not interaction.is_expired():
splat = interaction.client.tree.bugsplat(interaction, error)
try:
if interaction.response.is_done():
await interaction.followup.send(embed=splat, ephemeral=True)
else:
await interaction.response.send_message(embed=splat, ephemeral=True)
except discord.HTTPException:
pass
splat = interaction.client.tree.bugsplat(interaction, error)
await interaction.client.tree.error_reply(interaction, splat)
def error_handler_for(exc):

View File

@@ -21,14 +21,18 @@ async def sys_admin(bot: LionBot, userid: int):
return userid in admins
async def high_management(bot: LionBot, member: discord.Member):
async def high_management(bot: LionBot, member: discord.Member, guild: discord.Guild):
if not guild:
return True
if await sys_admin(bot, member.id):
return True
return member.guild_permissions.administrator
async def low_management(bot: LionBot, member: discord.Member):
if await high_management(bot, member):
async def low_management(bot: LionBot, member: discord.Member, guild: discord.Guild):
if not guild:
return True
if await high_management(bot, member, guild):
return True
return member.guild_permissions.manage_guild
@@ -42,20 +46,20 @@ async def sys_admin_iward(interaction: discord.Interaction) -> bool:
async def high_management_iward(interaction: discord.Interaction) -> bool:
if not interaction.guild:
return False
return await high_management(interaction.client, interaction.user)
return await high_management(interaction.client, interaction.user, interaction.guild)
async def low_management_iward(interaction: discord.Interaction) -> bool:
if not interaction.guild:
return False
return await low_management(interaction.client, interaction.user)
return await low_management(interaction.client, interaction.user, interaction.guild)
# High level ctx wards
async def moderator_ctxward(ctx: LionContext) -> bool:
if not ctx.guild:
return False
passed = await low_management(ctx.bot, ctx.author)
passed = await low_management(ctx.bot, ctx.author, ctx.guild)
if passed:
return True
modrole = ctx.lguild.data.mod_role
@@ -85,7 +89,7 @@ async def sys_admin_ward(ctx: LionContext) -> bool:
async def high_management_ward(ctx: LionContext) -> bool:
if not ctx.guild:
return False
passed = await high_management(ctx.bot, ctx.author)
passed = await high_management(ctx.bot, ctx.author, ctx.guild)
if passed:
return True
else:
@@ -101,7 +105,7 @@ async def high_management_ward(ctx: LionContext) -> bool:
async def low_management_ward(ctx: LionContext) -> bool:
if not ctx.guild:
return False
passed = await low_management(ctx.bot, ctx.author)
passed = await low_management(ctx.bot, ctx.author, ctx.guild)
if passed:
return True
else:
@@ -192,7 +196,7 @@ async def equippable_role(bot: LionBot, target_role: discord.Role, actor: discor
"You need the `MANAGE_ROLES` permission before you can configure roles!"
)).format(role=target_role.mention)
)
elif actor.top_role <= target_role and not actor == guild.owner:
elif actor.top_role <= target_role and not actor.id == guild.owner_id:
raise UserInputError(
t(_p(
'ward:equippable_role|error:actor_top_role',