Merge branch 'release' into pillow
This commit is contained in:
4
.gitmodules
vendored
4
.gitmodules
vendored
@@ -1,6 +1,6 @@
|
||||
[submodule "bot/gui"]
|
||||
path = src/gui
|
||||
url = cgithub:StudyLions/StudyLion-Plugin-GUI.git
|
||||
url = https://github.com/StudyLions/StudyLion-Plugin-GUI.git
|
||||
[submodule "skins"]
|
||||
path = skins
|
||||
url = cgithub:StudyLions/StudyLion-Plugin-Skins.git
|
||||
url = https://github.com/StudyLions/StudyLion-Plugin-Skins.git
|
||||
|
||||
1011
data/schema.sql
1011
data/schema.sql
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-10-01 16:01+0300\n"
|
||||
"POT-Creation-Date: 2023-10-15 15:58+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"
|
||||
@@ -28,36 +28,36 @@ msgctxt "timer|stage:focus|name"
|
||||
msgid "FOCUS"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/pomodoro/timer.py:160
|
||||
#: src/modules/pomodoro/timer.py:164
|
||||
#, possible-python-brace-format
|
||||
msgctxt "timer|webhook|name"
|
||||
msgid "{bot_name} Pomodoro"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/pomodoro/timer.py:164
|
||||
#: src/modules/pomodoro/timer.py:168
|
||||
msgctxt "timer|webhook|audit_reason"
|
||||
msgid "Pomodoro Notifications"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/pomodoro/timer.py:175
|
||||
#: src/modules/pomodoro/timer.py:179
|
||||
msgctxt "timer|webhook|error:insufficient_permissions"
|
||||
msgid ""
|
||||
"I require the `MANAGE_WEBHOOKS` permission to send pomodoro notifications "
|
||||
"here!"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/pomodoro/timer.py:234
|
||||
#: src/modules/pomodoro/timer.py:238
|
||||
#, possible-python-brace-format
|
||||
msgctxt "timer|default_base_name"
|
||||
msgid "Timer {pattern}"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/pomodoro/timer.py:409
|
||||
#: src/modules/pomodoro/timer.py:413
|
||||
msgctxt "timer|disconnect|audit_reason"
|
||||
msgid "Disconnecting inactive member from timer."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/pomodoro/timer.py:421
|
||||
#: src/modules/pomodoro/timer.py:425
|
||||
#, possible-python-brace-format
|
||||
msgctxt "timer|kicked_message"
|
||||
msgid ""
|
||||
@@ -69,7 +69,7 @@ msgid_plural ""
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
#: src/modules/pomodoro/timer.py:434
|
||||
#: src/modules/pomodoro/timer.py:438
|
||||
#, possible-python-brace-format
|
||||
msgctxt "timer|kick_failed"
|
||||
msgid ""
|
||||
@@ -77,20 +77,20 @@ msgid ""
|
||||
"I lack the 'Move Members' permission to do this!"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/pomodoro/timer.py:529
|
||||
#: src/modules/pomodoro/timer.py:537
|
||||
#, 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:534
|
||||
#: src/modules/pomodoro/timer.py:542
|
||||
#, 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:566
|
||||
#: src/modules/pomodoro/timer.py:574
|
||||
#, possible-python-brace-format
|
||||
msgctxt "timer|status|warningline"
|
||||
msgid ""
|
||||
@@ -98,13 +98,13 @@ msgid ""
|
||||
"next stage."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/pomodoro/timer.py:585
|
||||
#: src/modules/pomodoro/timer.py:593
|
||||
#, possible-python-brace-format
|
||||
msgctxt "timer|status|stopped:auto"
|
||||
msgid "Timer stopped! Join {channel} to start the timer."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/pomodoro/timer.py:590
|
||||
#: src/modules/pomodoro/timer.py:598
|
||||
msgctxt "timer|status|stopped:manual"
|
||||
msgid "Timer stopped! Press `Start` to restart the timer."
|
||||
msgstr ""
|
||||
@@ -781,33 +781,33 @@ msgctxt "ui:timer_options|menu:voice_channel|placeholder"
|
||||
msgid "Set Voice Channel"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/pomodoro/ui/config.py:159
|
||||
#: src/modules/pomodoro/ui/config.py:157
|
||||
msgctxt "ui:timer_options|menu:notification_channel|placeholder"
|
||||
msgid "Set Notification Channel"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/pomodoro/ui/config.py:174
|
||||
#: src/modules/pomodoro/ui/config.py:172
|
||||
msgctxt "ui:timer_options|menu:manager_role|placeholder"
|
||||
msgid "Set Manager Role"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/pomodoro/ui/config.py:183
|
||||
#: src/modules/pomodoro/ui/config.py:181
|
||||
#, possible-python-brace-format
|
||||
msgctxt "ui:timer_options|embed|title"
|
||||
msgid "Timer Control Panel for {channel}"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/pomodoro/ui/config.py:190
|
||||
#: src/modules/pomodoro/ui/config.py:188
|
||||
msgctxt "ui:timer_options|embed|footer"
|
||||
msgid "Hover over the option names to view descriptions."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/pomodoro/ui/config.py:202
|
||||
#: src/modules/pomodoro/ui/config.py:200
|
||||
msgctxt "ui:timer_options|embed|field:pattern|name"
|
||||
msgid "Pattern"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/pomodoro/ui/config.py:205
|
||||
#: src/modules/pomodoro/ui/config.py:203
|
||||
#, possible-python-brace-format
|
||||
msgctxt "ui:timer_options|embed|field:pattern|value"
|
||||
msgid ""
|
||||
@@ -815,12 +815,12 @@ msgid ""
|
||||
"**`{break_len} minutes`** break"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/pomodoro/ui/config.py:216
|
||||
#: src/modules/pomodoro/ui/config.py:214
|
||||
msgctxt "ui:timer_options|embed|field:channel_name|name"
|
||||
msgid "Channel Name Preview"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/pomodoro/ui/config.py:220
|
||||
#: src/modules/pomodoro/ui/config.py:218
|
||||
#, possible-python-brace-format
|
||||
msgctxt "ui:timer_options|embed|field:channel_name|value"
|
||||
msgid ""
|
||||
@@ -828,35 +828,35 @@ msgid ""
|
||||
"(The actual channel name may not match due to ratelimits.)"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/pomodoro/ui/config.py:230
|
||||
#: src/modules/pomodoro/ui/config.py:228
|
||||
msgctxt "ui:timer_options|embed|field:issues|name"
|
||||
msgid "Issues"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/pomodoro/ui/config.py:248
|
||||
#: src/modules/pomodoro/ui/config.py:246
|
||||
msgctxt "ui:timer_options|issue:no_voice_channel"
|
||||
msgid "The configured voice channel does not exist! Please update it below."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/pomodoro/ui/config.py:259
|
||||
#: src/modules/pomodoro/ui/config.py:257
|
||||
#, possible-python-brace-format
|
||||
msgctxt "ui:timer_options|issue:cannot_speak"
|
||||
msgid "Voice alerts are on, but I don't have speaking permissions in {channel}"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/pomodoro/ui/config.py:266
|
||||
#: src/modules/pomodoro/ui/config.py:264
|
||||
#, possible-python-brace-format
|
||||
msgctxt "ui:timer_options|issue:cannot_change_name"
|
||||
msgid ""
|
||||
"I cannot update the name of {channel}! (Needs `MANAGE_CHANNELS` permission)"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/pomodoro/ui/config.py:277
|
||||
#: src/modules/pomodoro/ui/config.py:275
|
||||
msgctxt "ui:timer_options|issue:notif_channel_dne"
|
||||
msgid "Configured notification channel does not exist!"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/pomodoro/ui/config.py:286
|
||||
#: src/modules/pomodoro/ui/config.py:284
|
||||
#, possible-python-brace-format
|
||||
msgctxt "ui:timer_options|issue:notif_channel_write"
|
||||
msgid ""
|
||||
@@ -864,7 +864,7 @@ msgid ""
|
||||
"{channel}"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/pomodoro/ui/config.py:294
|
||||
#: src/modules/pomodoro/ui/config.py:292
|
||||
#, possible-python-brace-format
|
||||
msgctxt "ui:timer_options|issues:cannot_make_webhooks"
|
||||
msgid ""
|
||||
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-10-01 16:01+0300\n"
|
||||
"POT-Creation-Date: 2023-10-15 15:58+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"
|
||||
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-10-01 16:01+0300\n"
|
||||
"POT-Creation-Date: 2023-10-15 15:58+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,67 +17,43 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=CHARSET\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: src/modules/config/general.py:41
|
||||
msgctxt "guildset:timezone"
|
||||
msgid "timezone"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/config/general.py:44
|
||||
msgctxt "guildset:timezone|desc"
|
||||
msgid "Guild timezone for statistics display."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/config/general.py:48
|
||||
msgctxt "guildset:timezone|long_desc"
|
||||
msgid ""
|
||||
"Guild-wide timezone. Used to determine start of the day for the "
|
||||
"leaderboards, and as the default statistics timezone for members who have "
|
||||
"not set one."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/config/general.py:62
|
||||
#, possible-python-brace-format
|
||||
msgctxt "guildset:timezone|response"
|
||||
msgid "The guild timezone has been set to `{timezone}`."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/config/general.py:94
|
||||
#: src/modules/config/cog.py:56 src/modules/config/general.py:52
|
||||
msgctxt "cmd:configure_general"
|
||||
msgid "general"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/config/general.py:95
|
||||
#: src/modules/config/cog.py:57 src/modules/config/general.py:53
|
||||
msgctxt "cmd:configure_general|desc"
|
||||
msgid "General configuration panel"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/config/general.py:129
|
||||
#: src/modules/config/general.py:91
|
||||
msgctxt "cmd:configure_general|parse_failure:timezone"
|
||||
msgid "Could not set the timezone!"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/config/general.py:150
|
||||
#: src/modules/config/general.py:112
|
||||
msgctxt "cmd:configure_general|success"
|
||||
msgid "Settings Updated!"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/config/general.py:165
|
||||
#: src/modules/config/general.py:127
|
||||
msgctxt "cmd:configure_general|panel|title"
|
||||
msgid "General Configuration Panel"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/config/dashboard.py:98
|
||||
#: src/modules/config/dashboard.py:99
|
||||
#, possible-python-brace-format
|
||||
msgctxt "ui:dashboard|title"
|
||||
msgid "Guild Dashboard (Page {page}/{total})"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/config/dashboard.py:109
|
||||
#: src/modules/config/dashboard.py:110
|
||||
msgctxt "ui:dashboard|footer"
|
||||
msgid "Hover over setting names for a brief description"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/config/dashboard.py:172
|
||||
#: src/modules/config/dashboard.py:173
|
||||
msgctxt "ui:dashboard|menu:config|placeholder"
|
||||
msgid "Open Configuration Panel"
|
||||
msgstr ""
|
||||
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-10-01 16:01+0300\n"
|
||||
"POT-Creation-Date: 2023-10-15 15:58+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"
|
||||
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-10-01 16:01+0300\n"
|
||||
"POT-Creation-Date: 2023-10-15 15:58+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,88 +18,88 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
|
||||
|
||||
#: src/modules/economy/settingui.py:33
|
||||
#: src/modules/economy/settingui.py:34
|
||||
msgctxt "ui:economy_config|embed|title"
|
||||
msgid "Economy Configuration Panel"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/settingui.py:68
|
||||
#: src/modules/economy/settingui.py:67
|
||||
msgctxt "dash:economy|title"
|
||||
msgid "Economy Configuration ({commands[configure economy]})"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/settingui.py:72
|
||||
#: src/modules/economy/settingui.py:71
|
||||
msgctxt "dash:economy|dropdown|placeholder"
|
||||
msgid "Economy Panel"
|
||||
msgstr ""
|
||||
|
||||
#. ----- Economy group commands -----
|
||||
#: src/modules/economy/cog.py:86
|
||||
#: src/modules/economy/cog.py:87
|
||||
msgctxt "cmd:economy"
|
||||
msgid "economy"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/cog.py:92
|
||||
#: src/modules/economy/cog.py:93
|
||||
msgctxt "cmd:economy_balance"
|
||||
msgid "balance"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/cog.py:95
|
||||
#: src/modules/economy/cog.py:96
|
||||
msgctxt "cmd:economy_balance|desc"
|
||||
msgid "Display or modify LionCoin balance for members and roles."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/cog.py:99
|
||||
#: src/modules/economy/cog.py:100
|
||||
msgctxt "cmd:economy_balance|param:target"
|
||||
msgid "target"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/cog.py:100
|
||||
#: src/modules/economy/cog.py:101
|
||||
msgctxt "cmd:economy_balance|param:add"
|
||||
msgid "add"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/cog.py:101
|
||||
#: src/modules/economy/cog.py:102
|
||||
msgctxt "cmd:economy_balance|param:set"
|
||||
msgid "set"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/cog.py:106
|
||||
#: src/modules/economy/cog.py:107
|
||||
msgctxt "cmd:economy_balance|param:target|desc"
|
||||
msgid ""
|
||||
"Target user or role to view or update. Use @everyone to update the entire "
|
||||
"guild."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/cog.py:110
|
||||
#: src/modules/economy/cog.py:111
|
||||
msgctxt "cmd:economy_balance|param:add|desc"
|
||||
msgid ""
|
||||
"Number of LionCoins to add to the target member's balance. May be negative "
|
||||
"to remove."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/cog.py:114
|
||||
#: src/modules/economy/cog.py:115
|
||||
msgctxt "cmd:economy_balance|param:set|set"
|
||||
msgid "New balance to set the target's balance to."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/cog.py:152
|
||||
#: src/modules/economy/cog.py:153
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:economy_balance|error:no_target"
|
||||
msgid "There are no valid members in {role.mention}! It has a total of `0` LC."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/cog.py:163
|
||||
#: src/modules/economy/cog.py:164
|
||||
msgctxt "cmd:economy_balance|error:target_is_bot"
|
||||
msgid "Bots cannot have coin balances!"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/cog.py:174
|
||||
#: src/modules/economy/cog.py:175
|
||||
msgctxt "cmd:economy_balance|error:args"
|
||||
msgid "You cannot simultaneously `set` and `add` member balances!"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/cog.py:223
|
||||
#: src/modules/economy/cog.py:224
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:economy_balance|embed:success|affected"
|
||||
msgid "One user was affected."
|
||||
@@ -107,7 +107,7 @@ msgid_plural "**{count}** users were affected."
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
#: src/modules/economy/cog.py:229
|
||||
#: src/modules/economy/cog.py:230
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:economy_balance|confirm|affected"
|
||||
msgid "One user will be affected."
|
||||
@@ -115,17 +115,17 @@ msgid_plural "**{count}** users will be affected."
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
#: src/modules/economy/cog.py:236
|
||||
#: src/modules/economy/cog.py:237
|
||||
msgctxt "cmd:economy_balance|confirm|button:confirm"
|
||||
msgid "Yes, adjust balances"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/cog.py:240
|
||||
#: src/modules/economy/cog.py:241
|
||||
msgctxt "cmd:economy_balance|confirm|button:cancel"
|
||||
msgid "No, cancel"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/cog.py:247
|
||||
#: src/modules/economy/cog.py:248
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:economy_balance|embed:success_set|desc"
|
||||
msgid ""
|
||||
@@ -133,14 +133,14 @@ msgid ""
|
||||
"**{amount}**."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/cog.py:256
|
||||
#: src/modules/economy/cog.py:257
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:economy_balance|confirm_set|desc"
|
||||
msgid ""
|
||||
"Are you sure you want to set everyone's balance to {coin_emoji}**{amount}**?"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/cog.py:264
|
||||
#: src/modules/economy/cog.py:265
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:economy_balance|embed:success_set|desc"
|
||||
msgid ""
|
||||
@@ -148,7 +148,7 @@ msgid ""
|
||||
"**{amount}**."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/cog.py:273
|
||||
#: src/modules/economy/cog.py:274
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:economy_balance|confirm_set|desc"
|
||||
msgid ""
|
||||
@@ -156,32 +156,43 @@ msgid ""
|
||||
"{coin_emoji}**{amount}**?"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/cog.py:290
|
||||
#: src/modules/economy/cog.py:291
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:economy_balance|embed:success_set|desc"
|
||||
msgid "{user_mention} now has a balance of {coin_emoji}**{amount}**."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/cog.py:306
|
||||
#: src/modules/economy/cog.py:305
|
||||
msgctxt "eventlog|event:economy_set|title"
|
||||
msgid "Moderator Set Economy Balance"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/cog.py:309
|
||||
#, possible-python-brace-format
|
||||
msgctxt "eventlog|event:economy_set|desc"
|
||||
msgid "{moderator} set {target}'s balance to {amount}."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/cog.py:321
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:economy_balance|embed:success_add|desc"
|
||||
msgid ""
|
||||
"All members of **{guild_name}** have been given {coin_emoji}**{amount}**."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/cog.py:315
|
||||
#: src/modules/economy/cog.py:330
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:economy_balance|confirm_add|desc"
|
||||
msgid "Are you sure you want to add **{amount}** to everyone's balance?"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/cog.py:323
|
||||
#: src/modules/economy/cog.py:338
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:economy_balance|embed:success_add|desc"
|
||||
msgid "All members of {role_mention} have been given {coin_emoji}**{amount}**."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/cog.py:332
|
||||
#: src/modules/economy/cog.py:347
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:economy_balance|confirm_add|desc"
|
||||
msgid ""
|
||||
@@ -189,7 +200,7 @@ msgid ""
|
||||
"{role_mention}?"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/cog.py:354
|
||||
#: src/modules/economy/cog.py:369
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:economy_balance|embed:success_add|desc"
|
||||
msgid ""
|
||||
@@ -197,26 +208,37 @@ msgid ""
|
||||
"{coin_emoji}**{new_amount}**."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/cog.py:365
|
||||
#: src/modules/economy/cog.py:380
|
||||
msgctxt "eventlog|event:economy_add|title"
|
||||
msgid "Moderator Modified Economy Balance"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/cog.py:384
|
||||
#, possible-python-brace-format
|
||||
msgctxt "eventlog|event:economy_set|desc"
|
||||
msgid "{moderator} added {amount} to {target}'s balance."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/cog.py:394
|
||||
msgctxt "cmd:economy_balance|embed:success|title"
|
||||
msgid "Account successfully updated."
|
||||
msgid_plural "Accounts successfully updated."
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
#: src/modules/economy/cog.py:409
|
||||
#: src/modules/economy/cog.py:438
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:economy_balance|embed:role_lb|author"
|
||||
msgid "Balance sheet for {name}"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/cog.py:415
|
||||
#: src/modules/economy/cog.py:444
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:economy_balance|embed:role_lb|header"
|
||||
msgid "This server has a total balance of {coin_emoji}**{total}**."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/cog.py:423
|
||||
#: src/modules/economy/cog.py:452
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:economy_balance|embed:role_lb|header"
|
||||
msgid ""
|
||||
@@ -224,66 +246,66 @@ msgid ""
|
||||
"balance of {coin_emoji}**{total}**."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/cog.py:435
|
||||
#: src/modules/economy/cog.py:464
|
||||
msgctxt "cmd:economy_balance|embed:role_lb|row_format"
|
||||
msgid "`[{pos:>{numwidth}}]` | `{coins:>{coinwidth}} LC` | {mention}"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/cog.py:463
|
||||
#: src/modules/economy/cog.py:492
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:economy_balance|embed:role_lb|footer"
|
||||
msgid "Page {page}/{total}"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/cog.py:473
|
||||
#: src/modules/economy/cog.py:502
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:economy_balance|embed:role_lb|header"
|
||||
msgid "This server has a total balance of {coin_emoji}**0**."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/cog.py:480
|
||||
#: src/modules/economy/cog.py:509
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:economy_balance|embed:role_lb|header"
|
||||
msgid "The role {role_mention} has a total balance of {coin_emoji}**0**."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/cog.py:500
|
||||
#: src/modules/economy/cog.py:529
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:economy_balance|embed:single|desc"
|
||||
msgid "{mention} currently owns {coin_emoji} {coins}."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/cog.py:510
|
||||
#: src/modules/economy/cog.py:539
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:economy_balance|embed:single|author"
|
||||
msgid "Balance statement for {user}"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/cog.py:519
|
||||
#: src/modules/economy/cog.py:548
|
||||
msgctxt "cmd:economy_reset"
|
||||
msgid "reset"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/cog.py:522
|
||||
#: src/modules/economy/cog.py:551
|
||||
msgctxt "cmd:economy_reset|desc"
|
||||
msgid ""
|
||||
"Reset the coin balance for a target user or role. (See also \"economy "
|
||||
"balance\".)"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/cog.py:526
|
||||
#: src/modules/economy/cog.py:555
|
||||
msgctxt "cmd:economy_reset|param:target"
|
||||
msgid "target"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/cog.py:531
|
||||
#: src/modules/economy/cog.py:560
|
||||
msgctxt "cmd:economy_reset|param:target|desc"
|
||||
msgid ""
|
||||
"Target user or role to view or update. Use @everyone to reset the entire "
|
||||
"guild."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/cog.py:558
|
||||
#: src/modules/economy/cog.py:587
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:economy_reset|confirm:reset_guild|desc"
|
||||
msgid ""
|
||||
@@ -292,17 +314,17 @@ msgid ""
|
||||
"*This is not reversible!*"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/cog.py:566
|
||||
#: src/modules/economy/cog.py:595
|
||||
msgctxt "cmd:economy_reset|confirm:reset_guild|button:confirm"
|
||||
msgid "Yes, reset the economy"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/cog.py:570
|
||||
#: src/modules/economy/cog.py:599
|
||||
msgctxt "cmd:economy_reset|confirm:reset_guild|button:cancel"
|
||||
msgid "Cancel reset"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/cog.py:586
|
||||
#: src/modules/economy/cog.py:615
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:economy_reset|embed:success_guild|desc"
|
||||
msgid ""
|
||||
@@ -310,13 +332,13 @@ msgid ""
|
||||
"**{amount}**."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/cog.py:603
|
||||
#: src/modules/economy/cog.py:632
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:economy_reset|error:no_target|desc"
|
||||
msgid "The role {mention} has no members to reset!"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/cog.py:613
|
||||
#: src/modules/economy/cog.py:642
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:economy_reset|confirm:reset_role|desc"
|
||||
msgid ""
|
||||
@@ -324,17 +346,17 @@ msgid ""
|
||||
"**{count}** members will be affected."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/cog.py:622
|
||||
#: src/modules/economy/cog.py:651
|
||||
msgctxt "cmd:economy_reset|confirm:reset_role|button:confirm"
|
||||
msgid "Yes, complete economy reset"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/cog.py:626
|
||||
#: src/modules/economy/cog.py:655
|
||||
msgctxt "cmd:economy_reset|confirm:reset_role|button:cancel"
|
||||
msgid "Cancel"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/cog.py:643
|
||||
#: src/modules/economy/cog.py:672
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:economy_reset|embed:success_role|desc"
|
||||
msgid ""
|
||||
@@ -342,72 +364,72 @@ msgid ""
|
||||
"**{amount}**."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/cog.py:665
|
||||
#: src/modules/economy/cog.py:694
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:economy_reset|embed:success_user|desc"
|
||||
msgid "{mention}'s balance has been reset to {coin_emoji}**{amount}**."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/cog.py:675
|
||||
#: src/modules/economy/cog.py:704
|
||||
msgctxt "cmd:send"
|
||||
msgid "send"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/cog.py:678
|
||||
#: src/modules/economy/cog.py:707
|
||||
msgctxt "cmd:send|desc"
|
||||
msgid "Gift the target user a certain number of LionCoins."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/cog.py:682
|
||||
#: src/modules/economy/cog.py:711
|
||||
msgctxt "cmd:send|param:target"
|
||||
msgid "target"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/cog.py:683
|
||||
#: src/modules/economy/cog.py:712
|
||||
msgctxt "cmd:send|param:amount"
|
||||
msgid "amount"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/cog.py:684
|
||||
#: src/modules/economy/cog.py:713
|
||||
msgctxt "cmd:send|param:note"
|
||||
msgid "note"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/cog.py:687
|
||||
#: src/modules/economy/cog.py:716
|
||||
msgctxt "cmd:send|param:target|desc"
|
||||
msgid "User to send the gift to"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/cog.py:688
|
||||
#: src/modules/economy/cog.py:717
|
||||
msgctxt "cmd:send|param:amount|desc"
|
||||
msgid "Number of coins to send"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/cog.py:689
|
||||
#: src/modules/economy/cog.py:718
|
||||
msgctxt "cmd:send|param:note|desc"
|
||||
msgid "Optional note to add to the gift."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/cog.py:713
|
||||
#: src/modules/economy/cog.py:742
|
||||
msgctxt "cmd:send|error:not_allowed"
|
||||
msgid "Sorry, this server has disabled LionCoin transfers!"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/cog.py:722
|
||||
#: src/modules/economy/cog.py:751
|
||||
msgctxt "cmd:send|error:sending-to-self"
|
||||
msgid ""
|
||||
"What is this, tax evasion?\n"
|
||||
"(You can not send coins to yourself.)"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/cog.py:732
|
||||
#: src/modules/economy/cog.py:761
|
||||
msgctxt "cmd:send|error:sending-to-leo"
|
||||
msgid ""
|
||||
"I appreciate it, but you need it more than I do!\n"
|
||||
"(You cannot send coins to bots.)"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/cog.py:742
|
||||
#: src/modules/economy/cog.py:771
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:send|error:sending-to-bot"
|
||||
msgid ""
|
||||
@@ -416,7 +438,7 @@ msgid ""
|
||||
"(You cannot send coins to bots.)"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/cog.py:770
|
||||
#: src/modules/economy/cog.py:799
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:send|error:insufficient"
|
||||
msgid ""
|
||||
@@ -424,19 +446,30 @@ msgid ""
|
||||
"`Current Balance:` {coin_emoji}{balance}"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/cog.py:791
|
||||
#: src/modules/economy/cog.py:816
|
||||
msgctxt "eventlog|event:send|title"
|
||||
msgid "Coins Transferred"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/cog.py:820
|
||||
#, possible-python-brace-format
|
||||
msgctxt "eventlog|event:send|desc"
|
||||
msgid "{source} gifted {amount} to {target}"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/cog.py:833
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:send|embed:gift|title"
|
||||
msgid "{user} sent you a gift!"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/cog.py:795
|
||||
#: src/modules/economy/cog.py:837
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:send|embed:gift|desc"
|
||||
msgid "{mention} sent you {coin_emoji}**{amount}**."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/cog.py:820
|
||||
#: src/modules/economy/cog.py:862
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:send|embed:ack|desc"
|
||||
msgid ""
|
||||
@@ -444,85 +477,113 @@ msgid ""
|
||||
"{mention}!"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/cog.py:830
|
||||
#: src/modules/economy/cog.py:872
|
||||
msgctxt "cmd:send|embed:ack|desc|error:unreachable"
|
||||
msgid ""
|
||||
"Unfortunately, I was not able to message the recipient. Perhaps they have me "
|
||||
"blocked?"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/cog.py:842
|
||||
#: src/modules/economy/cog.py:884
|
||||
msgctxt "cmd:configure_economy"
|
||||
msgid "economy"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/cog.py:845
|
||||
#: src/modules/economy/cog.py:887
|
||||
msgctxt "cmd:configure_economy|desc"
|
||||
msgid "Configure LionCoin Economy"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/settings.py:35
|
||||
#: src/modules/economy/settings.py:36
|
||||
msgctxt "guildset:coins_per_xp"
|
||||
msgid "coins_per_100xp"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/settings.py:38
|
||||
#: src/modules/economy/settings.py:39
|
||||
msgctxt "guildset:coins_per_xp|desc"
|
||||
msgid "How many LionCoins to reward members per 100 XP they earn."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/settings.py:42
|
||||
#: src/modules/economy/settings.py:43
|
||||
msgctxt "guildset:coins_per_xp|long_desc"
|
||||
msgid ""
|
||||
"Members will be rewarded with this many LionCoins for every 100 XP they earn."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/settings.py:46
|
||||
#: src/modules/economy/settings.py:47
|
||||
msgctxt "guildset:coins_per_xp|long_desc"
|
||||
msgid "The number of coins to reward per 100 XP."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/settings.py:59
|
||||
#: src/modules/economy/settings.py:60
|
||||
#, possible-python-brace-format
|
||||
msgctxt "guildset:coins_per_xp|set_response"
|
||||
msgid ""
|
||||
"For every **100** XP they earn, members will now be given {coin}**{amount}**."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/settings.py:70
|
||||
#: src/modules/economy/settings.py:71
|
||||
msgctxt "guildset:allow_transfers"
|
||||
msgid "allow_transfers"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/settings.py:73
|
||||
#: src/modules/economy/settings.py:74
|
||||
msgctxt "guildset:allow_transfers|desc"
|
||||
msgid "Whether to allow members to transfer LionCoins to each other."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/settings.py:77
|
||||
#: src/modules/economy/settings.py:78
|
||||
msgctxt "guildset:allow_transfers|long_desc"
|
||||
msgid ""
|
||||
"If disabled, members will not be able to transfer LionCoins to each other."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/settings.py:85
|
||||
#: src/modules/economy/settings.py:86
|
||||
msgctxt "guildset:allow_transfers|outputs:true"
|
||||
msgid "Enabled (Coin transfers allowed.)"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/settings.py:86
|
||||
#: src/modules/economy/settings.py:87
|
||||
msgctxt "guildset:allow_transfers|outputs:false"
|
||||
msgid "Disabled (Coin transfers not allowed.)"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/settings.py:102
|
||||
#: src/modules/economy/settings.py:103
|
||||
#, possible-python-brace-format
|
||||
msgctxt "guildset:allow_transfers|set_response|set:true"
|
||||
msgid "Members will now be able to use {send_cmd} to transfer {coin}"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/settings.py:107
|
||||
#: src/modules/economy/settings.py:108
|
||||
#, possible-python-brace-format
|
||||
msgctxt "guildset:allow_transfers|set_response|set:false"
|
||||
msgid "Members will not be able to use {send_cmd} to transfer {coin}"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/settings.py:119
|
||||
msgctxt "guildset:starting_funds"
|
||||
msgid "starting_funds"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/settings.py:122
|
||||
msgctxt "guildset:starting_funds|desc"
|
||||
msgid "How many LionCoins should a member start with."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/settings.py:126
|
||||
msgctxt "guildset:starting_funds|long_desc"
|
||||
msgid ""
|
||||
"Members will be given this number of coins when they first interact with me, "
|
||||
"or first join the server."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/settings.py:130
|
||||
msgctxt "guildset:starting_funds|accepts"
|
||||
msgid "Number of coins to give to new members."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/economy/settings.py:142
|
||||
#, possible-python-brace-format
|
||||
msgctxt "guildset:starting_funds|set_response"
|
||||
msgid "New members will now start with {amount}"
|
||||
msgstr ""
|
||||
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-10-01 16:01+0300\n"
|
||||
"POT-Creation-Date: 2023-10-15 15:58+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"
|
||||
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-10-01 16:01+0300\n"
|
||||
"POT-Creation-Date: 2023-10-15 15:58+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"
|
||||
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-10-01 16:01+0300\n"
|
||||
"POT-Creation-Date: 2023-10-15 15:58+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"
|
||||
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-10-01 16:01+0300\n"
|
||||
"POT-Creation-Date: 2023-10-15 15:58+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,6 +17,79 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=CHARSET\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: src/core/lion_guild.py:31
|
||||
msgctxt "eventlog|field:start|name"
|
||||
msgid "Start"
|
||||
msgstr ""
|
||||
|
||||
#: src/core/lion_guild.py:36
|
||||
msgctxt "eventlog|field:expiry|name"
|
||||
msgid "Expires"
|
||||
msgstr ""
|
||||
|
||||
#: src/core/lion_guild.py:41
|
||||
msgctxt "eventlog|field:roles_given|name"
|
||||
msgid "Roles Given"
|
||||
msgstr ""
|
||||
|
||||
#: src/core/lion_guild.py:46
|
||||
msgctxt "eventlog|field:roles_given|name"
|
||||
msgid "Roles Taken"
|
||||
msgstr ""
|
||||
|
||||
#: src/core/lion_guild.py:51
|
||||
msgctxt "eventlog|field:coins_earned|name"
|
||||
msgid "Coins Earned"
|
||||
msgstr ""
|
||||
|
||||
#: src/core/lion_guild.py:56
|
||||
msgctxt "eventlog|field:price|name"
|
||||
msgid "Price"
|
||||
msgstr ""
|
||||
|
||||
#: src/core/lion_guild.py:61
|
||||
msgctxt "eventlog|field:balance|name"
|
||||
msgid "Balance"
|
||||
msgstr ""
|
||||
|
||||
#: src/core/lion_guild.py:66
|
||||
msgctxt "eventlog|field:refund|name"
|
||||
msgid "Coins Refunded"
|
||||
msgstr ""
|
||||
|
||||
#: src/core/lion_guild.py:71
|
||||
msgctxt "eventlog|field:memberid|name"
|
||||
msgid "Member"
|
||||
msgstr ""
|
||||
|
||||
#: src/core/lion_guild.py:76
|
||||
msgctxt "eventlog|field:channelid|name"
|
||||
msgid "Channel"
|
||||
msgstr ""
|
||||
|
||||
#: src/core/lion_guild.py:208
|
||||
msgctxt "eventlog|error:manage_webhooks"
|
||||
msgid ""
|
||||
"This channel is configured as an event log, but I am missing the 'Manage "
|
||||
"Webhooks' permission here."
|
||||
msgstr ""
|
||||
|
||||
#: src/core/lion_guild.py:220
|
||||
#, possible-python-brace-format
|
||||
msgctxt "eventlog|create|name"
|
||||
msgid "{bot_name} Event Log"
|
||||
msgstr ""
|
||||
|
||||
#: src/core/lion_guild.py:224
|
||||
msgctxt "eventlog|create|audit_reason"
|
||||
msgid "Creating event log webhook"
|
||||
msgstr ""
|
||||
|
||||
#: src/core/lion_guild.py:342
|
||||
msgctxt "eventlog|field:errors|name"
|
||||
msgid "Errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/core/setting_types.py:31
|
||||
msgctxt "settype:coin|accepts"
|
||||
msgid "A positive integral number of coins."
|
||||
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-10-01 16:01+0300\n"
|
||||
"POT-Creation-Date: 2023-10-15 15:58+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"
|
||||
@@ -67,39 +67,92 @@ msgctxt "dash:member_admin|section:initial_roles|name"
|
||||
msgid "Initial Roles ({commands[configure welcome]})"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/member_admin/cog.py:239
|
||||
#: src/modules/member_admin/cog.py:119
|
||||
msgctxt "eventlog|event:welcome|title"
|
||||
msgid "New Member Joined"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/member_admin/cog.py:123
|
||||
#, possible-python-brace-format
|
||||
msgctxt "eventlog|event:welcome|desc"
|
||||
msgid "{member} joined the server for the first time."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/member_admin/cog.py:207
|
||||
msgctxt "eventlog|event:returning|title"
|
||||
msgid "Member Rejoined"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/member_admin/cog.py:211
|
||||
#, possible-python-brace-format
|
||||
msgctxt "eventlog|event:returning|desc"
|
||||
msgid "{member} rejoined the server."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/member_admin/cog.py:220
|
||||
msgctxt "eventlog|event:returning|field:first_joined"
|
||||
msgid "First Joined"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/member_admin/cog.py:227
|
||||
msgctxt "eventlog|event:returning|field:last_seen"
|
||||
msgid "Last Seen"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/member_admin/cog.py:275
|
||||
msgctxt "eventlog|event:left|title"
|
||||
msgid "Member Left"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/member_admin/cog.py:279
|
||||
#, possible-python-brace-format
|
||||
msgctxt "eventlog|event:left|desc"
|
||||
msgid "{member} left the server."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/member_admin/cog.py:287
|
||||
msgctxt "eventlog|event:left|field:stored_roles"
|
||||
msgid "Stored Roles"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/member_admin/cog.py:294
|
||||
msgctxt "eventlog|event:left|field:first_joined"
|
||||
msgid "First Joined"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/member_admin/cog.py:324
|
||||
msgctxt "cmd:resetmember"
|
||||
msgid "resetmember"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/member_admin/cog.py:242
|
||||
#: src/modules/member_admin/cog.py:327
|
||||
msgctxt "cmd:resetmember|desc"
|
||||
msgid "Reset (server-associated) member data for the target member or user."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/member_admin/cog.py:246
|
||||
#: src/modules/member_admin/cog.py:331
|
||||
msgctxt "cmd:resetmember|param:target"
|
||||
msgid "member_to_reset"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/member_admin/cog.py:247
|
||||
#: src/modules/member_admin/cog.py:332
|
||||
msgctxt "cmd:resetmember|param:saved_roles"
|
||||
msgid "saved_roles"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/member_admin/cog.py:252
|
||||
#: src/modules/member_admin/cog.py:337
|
||||
msgctxt "cmd:resetmember|param:target|desc"
|
||||
msgid "Choose the member (current or past) you want to reset."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/member_admin/cog.py:256
|
||||
#: src/modules/member_admin/cog.py:341
|
||||
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:283
|
||||
#: src/modules/member_admin/cog.py:368
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:resetmember|reset:saved_roles|success"
|
||||
msgid ""
|
||||
@@ -107,17 +160,17 @@ msgid ""
|
||||
"roles if they rejoin."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/member_admin/cog.py:291
|
||||
#: src/modules/member_admin/cog.py:376
|
||||
msgctxt "cmd:resetmember|error:nothing_to_do"
|
||||
msgid "No reset operation selected, nothing to do."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/member_admin/cog.py:307
|
||||
#: src/modules/member_admin/cog.py:392
|
||||
msgctxt "cmd:configure_welcome"
|
||||
msgid "welcome"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/member_admin/cog.py:310
|
||||
#: src/modules/member_admin/cog.py:395
|
||||
msgctxt "cmd:configure_welcome|desc"
|
||||
msgid "Configure new member greetings and roles."
|
||||
msgstr ""
|
||||
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-10-01 16:01+0300\n"
|
||||
"POT-Creation-Date: 2023-10-15 15:58+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"
|
||||
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-10-01 16:01+0300\n"
|
||||
"POT-Creation-Date: 2023-10-15 15:58+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"
|
||||
@@ -57,34 +57,34 @@ msgctxt "cmd:configure_moderation|desc"
|
||||
msgid "Configure general moderation settings."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/moderation/ticket.py:147
|
||||
#: src/modules/moderation/ticket.py:148
|
||||
#, possible-python-brace-format
|
||||
msgctxt "ticket|title:auto"
|
||||
msgid "Ticket #{ticketid} | {state} | {type}[Auto] | {name}"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/moderation/ticket.py:152
|
||||
#: src/modules/moderation/ticket.py:153
|
||||
#, possible-python-brace-format
|
||||
msgctxt "ticket|title:manual"
|
||||
msgid "Ticket #{ticketid} | {state} | {type} | {name}"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/moderation/ticket.py:168
|
||||
#: src/modules/moderation/ticket.py:169
|
||||
msgctxt "ticket|field:target|name"
|
||||
msgid "Target"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/moderation/ticket.py:173
|
||||
#: src/modules/moderation/ticket.py:174
|
||||
msgctxt "ticket|field:moderator|name"
|
||||
msgid "Moderator"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/moderation/ticket.py:180
|
||||
#: src/modules/moderation/ticket.py:181
|
||||
msgctxt "ticket|field:expiry|mode:expiring|name"
|
||||
msgid "Expires At"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/moderation/ticket.py:183
|
||||
#: src/modules/moderation/ticket.py:184
|
||||
#, possible-python-brace-format
|
||||
msgctxt "ticket|field:expiry|mode:expiring|value"
|
||||
msgid ""
|
||||
@@ -92,44 +92,44 @@ msgid ""
|
||||
"Duration: `{duration}`"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/moderation/ticket.py:191
|
||||
#: src/modules/moderation/ticket.py:192
|
||||
msgctxt "ticket|field:expiry|mode:expired|name"
|
||||
msgid "Expired"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/moderation/ticket.py:194
|
||||
#: src/modules/moderation/ticket.py:195
|
||||
#, possible-python-brace-format
|
||||
msgctxt "ticket|field:expiry|mode:expired|value"
|
||||
msgid "{timestamp}"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/moderation/ticket.py:201
|
||||
#: src/modules/moderation/ticket.py:202
|
||||
msgctxt "ticket|field:expiry|mode:open|name"
|
||||
msgid "Expiry"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/moderation/ticket.py:204
|
||||
#: src/modules/moderation/ticket.py:205
|
||||
#, possible-python-brace-format
|
||||
msgctxt "ticket|field:expiry|mode:open|value"
|
||||
msgid "{timestamp}"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/moderation/ticket.py:212
|
||||
#: src/modules/moderation/ticket.py:213
|
||||
msgctxt "ticket|field:context|name"
|
||||
msgid "Context"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/moderation/ticket.py:219
|
||||
#: src/modules/moderation/ticket.py:220
|
||||
msgctxt "ticket|field:notes|name"
|
||||
msgid "Notes"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/moderation/ticket.py:226
|
||||
#: src/modules/moderation/ticket.py:227
|
||||
msgctxt "ticket|field:pardoned|name"
|
||||
msgid "Pardoned"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/moderation/ticket.py:229
|
||||
#: src/modules/moderation/ticket.py:230
|
||||
#, possible-python-brace-format
|
||||
msgctxt "ticket|field:pardoned|value"
|
||||
msgid ""
|
||||
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-10-01 16:01+0300\n"
|
||||
"POT-Creation-Date: 2023-10-15 15:58+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"
|
||||
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-10-01 16:01+0300\n"
|
||||
"POT-Creation-Date: 2023-10-15 15:58+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"
|
||||
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-10-01 16:01+0300\n"
|
||||
"POT-Creation-Date: 2023-10-15 15:58+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,22 +32,91 @@ msgctxt "cmd:configure_ranks|param:rank_type|choice:message"
|
||||
msgid "Message"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/ranks/cog.py:498
|
||||
#: src/modules/ranks/cog.py:351
|
||||
#, possible-python-brace-format
|
||||
msgctxt "eventlog|event:rank_check|error:remove_failed"
|
||||
msgid "Failed to remove old rank roles: `{error}`"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/ranks/cog.py:375
|
||||
#, possible-python-brace-format
|
||||
msgctxt "eventlog|event:rank_check|error:add_failed"
|
||||
msgid "Failed to add new rank role: `{error}`"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/ranks/cog.py:380
|
||||
msgctxt "eventlog|event:rank_check|error:add_impossible"
|
||||
msgid ""
|
||||
"Could not assign new activity rank role. Lacking permissions or invalid role."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/ranks/cog.py:386
|
||||
msgctxt "eventlog|event:rank_check|error:permissions"
|
||||
msgid ""
|
||||
"Could not update activity rank roles, I lack the 'Manage Roles' permission."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/ranks/cog.py:398
|
||||
msgctxt "eventlog|event:rank_check|name"
|
||||
msgid "Member Activity Rank Roles Updated"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/ranks/cog.py:478
|
||||
#, possible-python-brace-format
|
||||
msgctxt "eventlog|event:new_rank|error:remove_failed"
|
||||
msgid "Failed to remove old rank roles: `{error}`"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/ranks/cog.py:502
|
||||
#, possible-python-brace-format
|
||||
msgctxt "eventlog|event:new_rank|error:add_failed"
|
||||
msgid "Failed to add new rank role: `{error}`"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/ranks/cog.py:507
|
||||
msgctxt "eventlog|event:new_rank|error:add_impossible"
|
||||
msgid ""
|
||||
"Could not assign new activity rank role. Lacking permissions or invalid role."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/ranks/cog.py:513
|
||||
msgctxt "eventlog|event:new_rank|error:permissions"
|
||||
msgid ""
|
||||
"Could not update activity rank roles, I lack the 'Manage Roles' permission."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/ranks/cog.py:548
|
||||
msgctxt "eventlog|event:new_rank|error:notify_failed"
|
||||
msgid "Could not notify member."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/ranks/cog.py:555
|
||||
msgctxt "eventlog|event:new_rank|name"
|
||||
msgid "Member Achieved Activity rank"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/ranks/cog.py:559
|
||||
#, possible-python-brace-format
|
||||
msgctxt "eventlog|event:new_rank|desc"
|
||||
msgid "{member} earned the new activity rank {rank}"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/ranks/cog.py:587
|
||||
msgctxt "event:rank_update|embed:notify"
|
||||
msgid "New Activity Rank Attained!"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/ranks/cog.py:607
|
||||
#: src/modules/ranks/cog.py:692
|
||||
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:620
|
||||
#: src/modules/ranks/cog.py:705
|
||||
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:630
|
||||
#: src/modules/ranks/cog.py:715
|
||||
#, possible-python-brace-format
|
||||
msgctxt "rank_refresh|error:unassignable_roles|desc"
|
||||
msgid ""
|
||||
@@ -55,59 +124,73 @@ msgid ""
|
||||
"{roles}"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/ranks/cog.py:700
|
||||
#: src/modules/ranks/cog.py:785
|
||||
msgctxt "rank_refresh|remove_roles|audit"
|
||||
msgid "Removing invalid rank role."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/ranks/cog.py:714
|
||||
#: src/modules/ranks/cog.py:799
|
||||
#, possible-python-brace-format
|
||||
msgctxt "rank_refresh|remove_roles|small_error"
|
||||
msgid "*Could not remove ranks from {member}*"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/ranks/cog.py:721
|
||||
#: src/modules/ranks/cog.py:806
|
||||
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:735
|
||||
#: src/modules/ranks/cog.py:820
|
||||
msgctxt "rank_refresh|add_roles|audit"
|
||||
msgid "Adding rank role from refresh"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/ranks/cog.py:749
|
||||
#: src/modules/ranks/cog.py:834
|
||||
#, possible-python-brace-format
|
||||
msgctxt "rank_refresh|add_roles|small_error"
|
||||
msgid "*Could not add {role} to {member}*"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/ranks/cog.py:756
|
||||
#: src/modules/ranks/cog.py:841
|
||||
msgctxt "rank_refresh|add_roles|error:too_many_issues"
|
||||
msgid ""
|
||||
"Too many issues occurred while adding ranks! Please check my permissions and "
|
||||
"try again in a few minutes."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/ranks/cog.py:869
|
||||
msgctxt "eventlog|event:rank_refresh|name"
|
||||
msgid "Activity Ranks Refreshed"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/ranks/cog.py:873
|
||||
#, possible-python-brace-format
|
||||
msgctxt "eventlog|event:rank_refresh|desc"
|
||||
msgid ""
|
||||
"{actor} refresh member activity ranks.\n"
|
||||
"**`{removed}`** invalid rank roles removed.\n"
|
||||
"**`{added}`** new rank roles added."
|
||||
msgstr ""
|
||||
|
||||
#. ---------- Commands ----------
|
||||
#: src/modules/ranks/cog.py:781
|
||||
#: src/modules/ranks/cog.py:884
|
||||
msgctxt "cmd:ranks"
|
||||
msgid "ranks"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/ranks/cog.py:813
|
||||
#: src/modules/ranks/cog.py:916
|
||||
msgctxt "cmd:configure_ranks"
|
||||
msgid "ranks"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/ranks/cog.py:814
|
||||
#: src/modules/ranks/cog.py:917
|
||||
msgctxt "cmd:configure_ranks|desc"
|
||||
msgid "Configure Activity Ranks"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/ranks/cog.py:874
|
||||
#: src/modules/ranks/cog.py:977
|
||||
#, possible-python-brace-format
|
||||
msgctxt ""
|
||||
"cmd:configure_ranks|response:updated|setting:notification|withdm_withchannel"
|
||||
@@ -116,20 +199,20 @@ msgid ""
|
||||
"otherwise to {channel}"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/ranks/cog.py:880
|
||||
#: src/modules/ranks/cog.py:983
|
||||
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:886
|
||||
#: src/modules/ranks/cog.py:989
|
||||
#, 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:891
|
||||
#: src/modules/ranks/cog.py:994
|
||||
msgctxt ""
|
||||
"cmd:configure_ranks|response:updated|setting:notification|nodm_nochannel"
|
||||
msgid "Members will not be notified when their activity rank updates."
|
||||
@@ -618,49 +701,60 @@ msgctxt "ui:rank_overview|button:auto|label"
|
||||
msgid "Auto Create"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/ranks/ui/overview.py:110
|
||||
#: src/modules/ranks/ui/overview.py:111
|
||||
msgctxt "ui:rank_overview|button:refresh|label"
|
||||
msgid "Refresh Member Ranks"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/ranks/ui/overview.py:122
|
||||
#: src/modules/ranks/ui/overview.py:123
|
||||
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:127
|
||||
#: src/modules/ranks/ui/overview.py:128
|
||||
msgctxt "ui:rank_overview|button:clear|confirm|button:yes"
|
||||
msgid "Yes, clear ranks"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/ranks/ui/overview.py:133
|
||||
#: src/modules/ranks/ui/overview.py:134
|
||||
msgctxt "ui:rank_overview|button:clear|confirm|button:no"
|
||||
msgid "Cancel"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/ranks/ui/overview.py:149
|
||||
#: src/modules/ranks/ui/overview.py:150
|
||||
msgctxt "ui:rank_overview|button:clear|label"
|
||||
msgid "Clear Ranks"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/ranks/ui/overview.py:179
|
||||
#: src/modules/ranks/ui/overview.py:164
|
||||
msgctxt "ui:rank_overview|button:create|error:my_permissions"
|
||||
msgid "I lack the 'Manage Roles' permission required to create rank roles!"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/ranks/ui/overview.py:173
|
||||
#, possible-python-brace-format
|
||||
msgctxt "ui:rank_overview|button:create|success"
|
||||
msgid "Created a new rank {role}"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/ranks/ui/overview.py:190
|
||||
msgctxt "ui:rank_overview|button:create|label"
|
||||
msgid "Create Rank"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/ranks/ui/overview.py:222
|
||||
#: src/modules/ranks/ui/overview.py:233
|
||||
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:227
|
||||
#: src/modules/ranks/ui/overview.py:238
|
||||
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:232
|
||||
#: src/modules/ranks/ui/overview.py:243
|
||||
msgctxt ""
|
||||
"ui:rank_overview|menu:roles|error:not_assignable|suberror:no_permissions"
|
||||
msgid ""
|
||||
@@ -668,34 +762,34 @@ msgid ""
|
||||
"manage ranks!"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/ranks/ui/overview.py:237
|
||||
#: src/modules/ranks/ui/overview.py:248
|
||||
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:243
|
||||
#: src/modules/ranks/ui/overview.py:254
|
||||
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:249
|
||||
#: src/modules/ranks/ui/overview.py:260
|
||||
msgctxt "ui:rank_overview|menu:roles|error:not_assignable|title"
|
||||
msgid "Could not create rank!"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/ranks/ui/overview.py:273
|
||||
#: src/modules/ranks/ui/overview.py:284
|
||||
msgctxt "ui:rank_overview|menu:roles|placeholder"
|
||||
msgid "Create from role"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/ranks/ui/overview.py:290
|
||||
#: src/modules/ranks/ui/overview.py:301
|
||||
msgctxt "ui:rank_overview|menu:ranks|placeholder"
|
||||
msgid "View or edit rank"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/ranks/ui/overview.py:376
|
||||
#: src/modules/ranks/ui/overview.py:387
|
||||
msgctxt "ui:rank_overview|embed:noranks|desc"
|
||||
msgid ""
|
||||
"No activity ranks have been set up!\n"
|
||||
@@ -703,36 +797,36 @@ msgid ""
|
||||
"xp ranks, or select a role or press Create below!"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/ranks/ui/overview.py:384
|
||||
#: src/modules/ranks/ui/overview.py:395
|
||||
#, 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
|
||||
#: src/modules/ranks/ui/overview.py:400
|
||||
#, 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
|
||||
#: src/modules/ranks/ui/overview.py:405
|
||||
#, 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
|
||||
#: src/modules/ranks/ui/overview.py:417
|
||||
msgctxt "ui:rank_overview|embed|field:note|name"
|
||||
msgid "Note"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/ranks/ui/overview.py:412
|
||||
#: src/modules/ranks/ui/overview.py:423
|
||||
#, 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:419
|
||||
#: src/modules/ranks/ui/overview.py:430
|
||||
#, possible-python-brace-format
|
||||
msgctxt "ui:rank_overview|embed|field:note|value:without_season"
|
||||
msgid ""
|
||||
@@ -741,7 +835,7 @@ msgid ""
|
||||
"ranks) set the `season_start` with {stats_cmd}"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/ranks/ui/overview.py:426
|
||||
#: src/modules/ranks/ui/overview.py:437
|
||||
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 "
|
||||
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-10-01 16:01+0300\n"
|
||||
"POT-Creation-Date: 2023-10-15 15:58+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"
|
||||
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-10-01 16:01+0300\n"
|
||||
"POT-Creation-Date: 2023-10-15 15:58+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,45 +18,87 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
|
||||
|
||||
#: src/modules/rolemenus/cog.py:41
|
||||
#: src/modules/rolemenus/cog.py:43
|
||||
msgctxt "argtype:menu_style|opt:reaction"
|
||||
msgid "Reaction Roles"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:45
|
||||
#: src/modules/rolemenus/cog.py:47
|
||||
msgctxt "argtype:menu_style|opt:button"
|
||||
msgid "Button Menu"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:49
|
||||
#: src/modules/rolemenus/cog.py:51
|
||||
msgctxt "argtype:menu_style|opt:dropdown"
|
||||
msgid "Dropdown Menu"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:68
|
||||
#: src/modules/rolemenus/cog.py:70
|
||||
msgctxt "ctxcmd:rolemenu"
|
||||
msgid "Role Menu Editor"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:81
|
||||
#: src/modules/rolemenus/cog.py:83
|
||||
msgctxt "ctxcmd:rolemenu|error:author_perms"
|
||||
msgid ""
|
||||
"You need the `MANAGE_ROLES` permission in order to manage the server role "
|
||||
"menus."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:88
|
||||
#: src/modules/rolemenus/cog.py:90
|
||||
msgctxt "ctxcmd:rolemenus|error:my_perms"
|
||||
msgid ""
|
||||
"I lack the `MANAGE_ROLES` permission required to offer roles from role menus."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:301
|
||||
#: src/modules/rolemenus/cog.py:333
|
||||
msgctxt "eventlog|event:rolemenu_role_expire|error:remove_failed"
|
||||
msgid "Removed the role, but the member still has the role!!"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:344
|
||||
msgctxt "eventlog|event:rolemenu_role_expire|error:member_gone"
|
||||
msgid "Member could not be found.. role has been removed from saved roles."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:350
|
||||
#, possible-python-brace-format
|
||||
msgctxt "eventlog|event:rolemenu_role_expire|error:no_role"
|
||||
msgid "Role {role} no longer exists."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:356
|
||||
msgctxt "eventlog|event:rolemenu_role_expire|title"
|
||||
msgid "Equipped role has expired"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:360
|
||||
#, possible-python-brace-format
|
||||
msgctxt "eventlog|event:rolemenu_role_expire|desc"
|
||||
msgid "{member}'s role {role} has now expired."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:368
|
||||
msgctxt "eventlog|event:rolemenu_role_expire|field:menu"
|
||||
msgid "Obtained From"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:377
|
||||
msgctxt "eventlog|event:rolemenu_role_expire|field:menu"
|
||||
msgid "Obtained At"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:384
|
||||
msgctxt "eventlog|event:rolemenu_role_expire|field:expiry"
|
||||
msgid "Expiry"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:419
|
||||
msgctxt "parse:message_link|suberror:message_dne"
|
||||
msgid "Could not find the linked message, has it been deleted?"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:306
|
||||
#: src/modules/rolemenus/cog.py:424
|
||||
#, possible-python-brace-format
|
||||
msgctxt "parse:message_link|suberror:no_perms"
|
||||
msgid ""
|
||||
@@ -64,20 +106,20 @@ msgid ""
|
||||
"{channel}."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:311
|
||||
#: src/modules/rolemenus/cog.py:429
|
||||
#, possible-python-brace-format
|
||||
msgctxt "parse:message_link|suberror:channel_dne"
|
||||
msgid "The channel `{channelid}` could not be found in this server."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:316
|
||||
#: src/modules/rolemenus/cog.py:434
|
||||
msgctxt "parse:message_link|suberror:malformed_link"
|
||||
msgid ""
|
||||
"Malformed message link. Please copy the link by right clicking the target "
|
||||
"message."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:323
|
||||
#: src/modules/rolemenus/cog.py:441
|
||||
#, possible-python-brace-format
|
||||
msgctxt "parse:message_link|error"
|
||||
msgid ""
|
||||
@@ -85,136 +127,136 @@ msgid ""
|
||||
"**ERROR:** {error}"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:435
|
||||
#: src/modules/rolemenus/cog.py:553
|
||||
msgctxt "cmd:rolemenus"
|
||||
msgid "rolemenus"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:438
|
||||
#: src/modules/rolemenus/cog.py:556
|
||||
msgctxt "cmd:rolemenus|desc"
|
||||
msgid "View and configure the role menus in this server."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:455
|
||||
#: src/modules/rolemenus/cog.py:573
|
||||
msgctxt "cmd:rolemenus|error:author_perms"
|
||||
msgid ""
|
||||
"You need the `MANAGE_ROLES` permission in order to manage the server role "
|
||||
"menus."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:462
|
||||
#: src/modules/rolemenus/cog.py:580
|
||||
msgctxt "cmd:rolemenus|error:my_perms"
|
||||
msgid ""
|
||||
"I lack the `MANAGE_ROLES` permission required to offer roles from role menus."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:497
|
||||
#: src/modules/rolemenus/cog.py:615
|
||||
#, possible-python-brace-format
|
||||
msgctxt "acmpl:menus|choice:no_choices|name"
|
||||
msgid "No role menus matching '{partial}'"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:516
|
||||
#: src/modules/rolemenus/cog.py:634
|
||||
msgctxt "acmpl:menuroles|param:menu|keyname"
|
||||
msgid "menu"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:522
|
||||
#: src/modules/rolemenus/cog.py:640
|
||||
msgctxt "acmpl:menuroles|choice:no_menu|name"
|
||||
msgid "Please select a menu first"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:546
|
||||
#: src/modules/rolemenus/cog.py:664
|
||||
#, possible-python-brace-format
|
||||
msgctxt "acmpl:menuroles|choice:invalid_menu|name"
|
||||
msgid "Menu '{name}' does not exist!"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:575
|
||||
#: src/modules/rolemenus/cog.py:693
|
||||
#, possible-python-brace-format
|
||||
msgctxt "acmpl:menuroles|choice:no_matching|name"
|
||||
msgid "No roles in this menu matching '{partial}'"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:582
|
||||
#: src/modules/rolemenus/cog.py:700
|
||||
msgctxt "group:rolemenu"
|
||||
msgid "rolemenu"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:585
|
||||
#: src/modules/rolemenus/cog.py:703
|
||||
msgctxt "group:rolemenu|desc"
|
||||
msgid "Base command group for role menu configuration."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:594
|
||||
#: src/modules/rolemenus/cog.py:712
|
||||
msgctxt "cmd:rolemenu_create"
|
||||
msgid "newmenu"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:597
|
||||
#: src/modules/rolemenus/cog.py:715
|
||||
msgctxt "cmd:rolemenu_create|desc"
|
||||
msgid "Create a new role menu (optionally using an existing message)"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:611
|
||||
#: src/modules/rolemenus/cog.py:729
|
||||
msgctxt "cmd:rolemenu_create|param:message"
|
||||
msgid "message_link"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:612
|
||||
#: src/modules/rolemenus/cog.py:730
|
||||
msgctxt "cmd:rolemenu_create|param:menu_style"
|
||||
msgid "menu_style"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:613
|
||||
#: src/modules/rolemenus/cog.py:731
|
||||
msgctxt "cmd:rolemenu_create|param:remplate"
|
||||
msgid "template"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:614
|
||||
#: src/modules/rolemenus/cog.py:732
|
||||
msgctxt "cmd:rolemenu_create|param:rawmessage"
|
||||
msgid "custom_message"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:624
|
||||
#: src/modules/rolemenus/cog.py:742
|
||||
msgctxt "cmd:rolemenu_create|param:message|desc"
|
||||
msgid "Link to an existing message to turn it into a (reaction) role menu"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:628
|
||||
#: src/modules/rolemenus/cog.py:746
|
||||
msgctxt "cmd:rolemenu_create|param:menu_style"
|
||||
msgid "Selection style for this menu (using buttons, dropdowns, or reactions)"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:632
|
||||
#: src/modules/rolemenus/cog.py:750
|
||||
msgctxt "cmd:rolemenu_create|param:template"
|
||||
msgid "Template to use for the menu message body"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:636
|
||||
#: src/modules/rolemenus/cog.py:754
|
||||
msgctxt "cmd:rolemenu_create|param:rawmessage"
|
||||
msgid "Attach a custom menu message to use"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:665
|
||||
#: src/modules/rolemenus/cog.py:783
|
||||
msgctxt "cmd:rolemenu_create|error:author_perms"
|
||||
msgid ""
|
||||
"You need the `MANAGE_ROLES` permission in order to create new role menus."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:672
|
||||
#: src/modules/rolemenus/cog.py:790
|
||||
msgctxt "cmd:rolemenu_create|error:my_perms"
|
||||
msgid ""
|
||||
"I lack the `MANAGE_ROLES` permission needed to offer roles from role menus."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:691
|
||||
#: src/modules/rolemenus/cog.py:809
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:rolemenu_create|error:message_exists"
|
||||
msgid "The message {link} already has a role menu! Use {edit_cmd} to edit it."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:712
|
||||
#: src/modules/rolemenus/cog.py:830
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:rolemenu_create|error:style_notmine"
|
||||
msgid ""
|
||||
@@ -222,7 +264,7 @@ msgid ""
|
||||
"restriction)."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:719
|
||||
#: src/modules/rolemenus/cog.py:837
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:rolemenu_create|error:rawmessage_notmine"
|
||||
msgid ""
|
||||
@@ -230,7 +272,7 @@ msgid ""
|
||||
"message!"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:728
|
||||
#: src/modules/rolemenus/cog.py:846
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:rolemenu_create|error:template_notmine"
|
||||
msgid ""
|
||||
@@ -238,153 +280,153 @@ msgid ""
|
||||
"message!"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:741
|
||||
#: src/modules/rolemenus/cog.py:859
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:rolemenu_create|error:name_exists"
|
||||
msgid "A rolemenu called `{name}` already exists! Use {edit_cmd} to edit it."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:789
|
||||
#: src/modules/rolemenus/cog.py:907
|
||||
msgctxt "cmd:rolemenu_edit"
|
||||
msgid "editmenu"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:792
|
||||
#: src/modules/rolemenus/cog.py:910
|
||||
msgctxt "cmd:rolemenu_edit|desc"
|
||||
msgid "Edit an existing role menu."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:801
|
||||
#: src/modules/rolemenus/cog.py:919
|
||||
msgctxt "cmd:rolemenu_edit|param:name"
|
||||
msgid "name"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:802
|
||||
#: src/modules/rolemenus/cog.py:920
|
||||
msgctxt "cmd:rolemenu_edit|param:new_name"
|
||||
msgid "new_name"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:803
|
||||
#: src/modules/rolemenus/cog.py:921
|
||||
msgctxt "cmd:rolemenu_edit|param:channel"
|
||||
msgid "new_channel"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:808
|
||||
#: src/modules/rolemenus/cog.py:926
|
||||
msgctxt "cmd:rolemenu_edit|param:menu_style"
|
||||
msgid "menu_style"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:809
|
||||
#: src/modules/rolemenus/cog.py:927
|
||||
msgctxt "cmd:rolemenu_edit|param:remplate"
|
||||
msgid "template"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:810
|
||||
#: src/modules/rolemenus/cog.py:928
|
||||
msgctxt "cmd:rolemenu_edit|param:rawmessage"
|
||||
msgid "custom_message"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:815
|
||||
#: src/modules/rolemenus/cog.py:933
|
||||
msgctxt "cmd:rolemenu_edit|param:name|desc"
|
||||
msgid "Name of the menu to edit"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:819
|
||||
#: src/modules/rolemenus/cog.py:937
|
||||
msgctxt "cmd:rolemenu_edit|param:channel|desc"
|
||||
msgid "Server channel to move the menu to"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:828
|
||||
#: src/modules/rolemenus/cog.py:946
|
||||
msgctxt "cmd:rolemenu_edit|param:menu_style"
|
||||
msgid "Selection style for this menu (using buttons, dropdowns, or reactions)"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:832
|
||||
#: src/modules/rolemenus/cog.py:950
|
||||
msgctxt "cmd:rolemenu_edit|param:template"
|
||||
msgid "Template to use for the menu message body"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:836
|
||||
#: src/modules/rolemenus/cog.py:954
|
||||
msgctxt "cmd:rolemenu_edit|param:rawmessage"
|
||||
msgid "Attach a custom menu message to use"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:865
|
||||
#: src/modules/rolemenus/cog.py:983
|
||||
msgctxt "cmd:rolemenu_edit|error:author_perms"
|
||||
msgid "You need the `MANAGE_ROLES` permission in order to edit role menus."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:872
|
||||
#: src/modules/rolemenus/cog.py:990
|
||||
msgctxt "cmd:rolemenu_edit|error:my_perms"
|
||||
msgid ""
|
||||
"I lack the `MANAGE_ROLES` permission needed to offer roles from role menus."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:895
|
||||
#: src/modules/rolemenus/cog.py:1013
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:rolemenu_edit|error:menu_not_found"
|
||||
msgid "This server does not have a role menu called `{name}`!"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:916
|
||||
#: src/modules/rolemenus/cog.py:1034
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:rolemenu_edit|parse:new_name|error:name_exists"
|
||||
msgid "A role menu with the name **{new_name}** already exists!"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:953
|
||||
#: src/modules/rolemenus/cog.py:1071
|
||||
msgctxt "cmd:rolemenu_edit|parse:template|error:not_managed"
|
||||
msgid ""
|
||||
"Cannot set a template message for a role menu attached to a message I did "
|
||||
"not send."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:964
|
||||
#: src/modules/rolemenus/cog.py:1082
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:rolemenu_edit|parse:template|success:template"
|
||||
msgid "Now using the `{name}` menu message template."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:971
|
||||
#: src/modules/rolemenus/cog.py:1089
|
||||
msgctxt "cmd:rolemenu_edit|parse:template|success:custom"
|
||||
msgid "Now using a custom menu message."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:981
|
||||
#: src/modules/rolemenus/cog.py:1099
|
||||
msgctxt "cmd:rolemenu_edit|parse:style|error:not_managed"
|
||||
msgid ""
|
||||
"Cannot change the style of a role menu attached to a message I did not send."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:991
|
||||
#: src/modules/rolemenus/cog.py:1109
|
||||
msgctxt "cmd:rolemenu_edit|parse:style|error:too_many_reactions"
|
||||
msgid "Too many roles! Reaction role menus can have at most `20` roles."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:1001
|
||||
#: src/modules/rolemenus/cog.py:1119
|
||||
msgctxt "cmd:rolemenu_edit|parse:style|error:incomplete_emojis"
|
||||
msgid ""
|
||||
"Cannot switch to the reaction role style! Every role needs a distinct emoji "
|
||||
"first."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:1008
|
||||
#: src/modules/rolemenus/cog.py:1126
|
||||
msgctxt "cmd:rolemenu_edit|parse:style|success"
|
||||
msgid "Updated role menu style."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:1022
|
||||
#: src/modules/rolemenus/cog.py:1140
|
||||
msgctxt "cmd:rolemenu_edit|parse:custom_message|success"
|
||||
msgid "Custom menu message updated."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:1037
|
||||
#: src/modules/rolemenus/cog.py:1155
|
||||
#, 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:1048
|
||||
#: src/modules/rolemenus/cog.py:1166
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:rolemenu_edit|repost|error:forbidden"
|
||||
msgid ""
|
||||
@@ -392,7 +434,7 @@ msgid ""
|
||||
"permission in {channel}."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:1053
|
||||
#: src/modules/rolemenus/cog.py:1171
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:rolemenu_edit|repost|error:unknown"
|
||||
msgid ""
|
||||
@@ -400,40 +442,40 @@ msgid ""
|
||||
"**Error:** `{exception}`"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:1092
|
||||
#: src/modules/rolemenus/cog.py:1210
|
||||
msgctxt "cmd:rolemenu_delete"
|
||||
msgid "delmenu"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:1095
|
||||
#: src/modules/rolemenus/cog.py:1213
|
||||
msgctxt "cmd:rolemenu_delete|desc"
|
||||
msgid "Delete a role menu."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:1099
|
||||
#: src/modules/rolemenus/cog.py:1217
|
||||
msgctxt "cmd:rolemenu_delete|param:name"
|
||||
msgid "menu"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:1104
|
||||
#: src/modules/rolemenus/cog.py:1222
|
||||
msgctxt "cmd:rolemenu_delete|param:name|desc"
|
||||
msgid "Name of the rolemenu to delete."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:1119
|
||||
#: src/modules/rolemenus/cog.py:1237
|
||||
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:1142
|
||||
#: src/modules/rolemenus/cog.py:1260
|
||||
#, 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:1150
|
||||
#: src/modules/rolemenus/cog.py:1268
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:rolemenu_delete|confirm|title"
|
||||
msgid ""
|
||||
@@ -441,212 +483,212 @@ msgid ""
|
||||
"reversible!"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:1155
|
||||
#: src/modules/rolemenus/cog.py:1273
|
||||
msgctxt "cmd:rolemenu_delete|confirm|button:yes"
|
||||
msgid "Yes, Delete Now"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:1160
|
||||
#: src/modules/rolemenus/cog.py:1278
|
||||
msgctxt "cmd:rolemenu_delete|confirm|button:no"
|
||||
msgid "No, Cancel"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:1185
|
||||
#: src/modules/rolemenus/cog.py:1303
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:rolemenu_delete|success|desc"
|
||||
msgid "Successfully deleted the menu **{name}**"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:1193
|
||||
#: src/modules/rolemenus/cog.py:1311
|
||||
msgctxt "cmd:rolemenu_addrole"
|
||||
msgid "addrole"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:1196
|
||||
#: src/modules/rolemenus/cog.py:1314
|
||||
msgctxt "cmd:rolemenu_addrole|desc"
|
||||
msgid "Add a new role to an existing role menu."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:1201
|
||||
#: src/modules/rolemenus/cog.py:1319
|
||||
msgctxt "cmd:rolemenu_addrole|param:menu"
|
||||
msgid "menu"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:1204
|
||||
#: src/modules/rolemenus/cog.py:1322
|
||||
msgctxt "cmd:rolemenu_addrole|param:role"
|
||||
msgid "role"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:1215
|
||||
#: src/modules/rolemenus/cog.py:1333
|
||||
msgctxt "cmd:rolemenu_addrole|param:menu|desc"
|
||||
msgid "Name of the menu to add a role to"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:1219
|
||||
#: src/modules/rolemenus/cog.py:1337
|
||||
msgctxt "cmd:rolemenu_addrole|param:role|desc"
|
||||
msgid "Role to add to the menu"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:1227
|
||||
#: src/modules/rolemenus/cog.py:1345
|
||||
msgctxt "cmd:rolemenu_addrole|param:duration|desc"
|
||||
msgid "Lifetime of the role after selection in minutes."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:1275
|
||||
#: src/modules/rolemenus/cog.py:1393
|
||||
#, 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:1360
|
||||
#: src/modules/rolemenus/cog.py:1478
|
||||
msgctxt "cmd:rolemenu_addrole|success:create|title"
|
||||
msgid "Added Menu Role"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:1364
|
||||
#: src/modules/rolemenus/cog.py:1482
|
||||
#, 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:1382
|
||||
#: src/modules/rolemenus/cog.py:1500
|
||||
msgctxt "cmd:rolemenu_addrole|success:edit|title"
|
||||
msgid "Menu Role updated"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:1394
|
||||
#: src/modules/rolemenus/cog.py:1512
|
||||
#, 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:1412
|
||||
#: src/modules/rolemenus/cog.py:1530
|
||||
msgctxt "cmd:rolemenu_addrole|success|error:reaction|name"
|
||||
msgid "Note"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:1424
|
||||
#: src/modules/rolemenus/cog.py:1542
|
||||
msgctxt "cmd:rolemenu_addrole|success|button:editor|label"
|
||||
msgid "Edit Menu"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:1441
|
||||
#: src/modules/rolemenus/cog.py:1559
|
||||
msgctxt "cmd:rolemenu_editrole"
|
||||
msgid "editrole"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:1444
|
||||
#: src/modules/rolemenus/cog.py:1562
|
||||
msgctxt "cmd:rolemenu_editrole|desc"
|
||||
msgid "Edit role options in an existing role menu."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:1449
|
||||
#: src/modules/rolemenus/cog.py:1567
|
||||
msgctxt "cmd:rolemenu_editrole|param:menu"
|
||||
msgid "menu"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:1452
|
||||
#: src/modules/rolemenus/cog.py:1570
|
||||
msgctxt "cmd:rolemenu_editrole|param:menu_role"
|
||||
msgid "menu_role"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:1455
|
||||
#: src/modules/rolemenus/cog.py:1573
|
||||
msgctxt "cmd:rolemenu_editrole|param:role"
|
||||
msgid "new_role"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:1466
|
||||
#: src/modules/rolemenus/cog.py:1584
|
||||
msgctxt "cmd:rolemenu_editrole|param:menu|desc"
|
||||
msgid "Name of the menu to edit the role for"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:1470
|
||||
#: src/modules/rolemenus/cog.py:1588
|
||||
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:1474
|
||||
#: src/modules/rolemenus/cog.py:1592
|
||||
msgctxt "cmd:rolemenu_editrole|param:role|desc"
|
||||
msgid "New server role this menu role should give."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:1482
|
||||
#: src/modules/rolemenus/cog.py:1600
|
||||
msgctxt "cmd:rolemenu_editrole|param:duration|desc"
|
||||
msgid "Lifetime of the role after selection in minutes."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:1523
|
||||
#: src/modules/rolemenus/cog.py:1641
|
||||
#, 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:1551
|
||||
#: src/modules/rolemenus/cog.py:1669
|
||||
#, 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:1617
|
||||
#: src/modules/rolemenus/cog.py:1735
|
||||
msgctxt "cmd:rolemenu_editrole|success|title"
|
||||
msgid "Role menu role updated"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:1632
|
||||
#: src/modules/rolemenus/cog.py:1750
|
||||
msgctxt "cmd:rolemenu_editrole|success|error:reaction|name"
|
||||
msgid "Warning!"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:1657
|
||||
#: src/modules/rolemenus/cog.py:1775
|
||||
msgctxt "cmd:rolemenu_delrole"
|
||||
msgid "delrole"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:1660
|
||||
#: src/modules/rolemenus/cog.py:1778
|
||||
msgctxt "cmd:rolemenu_delrole|desc"
|
||||
msgid "Remove a role from a role menu."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:1664
|
||||
#: src/modules/rolemenus/cog.py:1782
|
||||
msgctxt "cmd:rolemenu_delrole|param:menu"
|
||||
msgid "menu"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:1665
|
||||
#: src/modules/rolemenus/cog.py:1783
|
||||
msgctxt "cmd:rolemenu_delrole|param:menu_role"
|
||||
msgid "menu_role"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:1670
|
||||
#: src/modules/rolemenus/cog.py:1788
|
||||
msgctxt "cmd:rolemenu_delrole|param:menu|desc"
|
||||
msgid "Name of the menu to delete the role from."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:1674
|
||||
#: src/modules/rolemenus/cog.py:1792
|
||||
msgctxt "cmd:rolemenu_delrole|param:menu_role|desc"
|
||||
msgid "Name, label, or mention of the role to delete."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/cog.py:1692
|
||||
#: src/modules/rolemenus/cog.py:1810
|
||||
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:1716
|
||||
#: src/modules/rolemenus/cog.py:1834
|
||||
#, 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:1744
|
||||
#: src/modules/rolemenus/cog.py:1862
|
||||
#, 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:1761
|
||||
#: src/modules/rolemenus/cog.py:1879
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:rolemenu_delrole|success"
|
||||
msgid "The role **{name}** was successfully removed from the menu **{menu}**."
|
||||
@@ -951,58 +993,80 @@ msgctxt "rolemenu|select|expires_at"
|
||||
msgid "The role will expire at {timestamp}."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/rolemenu.py:627
|
||||
#: src/modules/rolemenus/rolemenu.py:616
|
||||
msgctxt "rolemenu|eventlog|event:role_equipped|title"
|
||||
msgid "Member equipped role from role menu"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/rolemenu.py:620
|
||||
#, possible-python-brace-format
|
||||
msgctxt "rolemenu|eventlog|event:role_equipped|desc"
|
||||
msgid "{member} equipped {role} from {menu}"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/rolemenu.py:644
|
||||
#, possible-python-brace-format
|
||||
msgctxt "rolemenu|deselect|error:sticky"
|
||||
msgid "**{role}** is a sticky role, you cannot remove it with this menu!"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/rolemenu.py:638
|
||||
#: src/modules/rolemenus/rolemenu.py:655
|
||||
msgctxt "rolemenu|deselect|error:perms"
|
||||
msgid "I don't have enough permissions to remove this role from you!"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/rolemenu.py:645
|
||||
#: src/modules/rolemenus/rolemenu.py:662
|
||||
msgctxt "rolemenu|deselect|error:discord"
|
||||
msgid "An unknown error occurred removing your role! Please try again later."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/rolemenu.py:674
|
||||
#: src/modules/rolemenus/rolemenu.py:691
|
||||
msgctxt "rolemenu|deslect|success|title"
|
||||
msgid "Role removed"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/rolemenu.py:680
|
||||
#: src/modules/rolemenus/rolemenu.py:697
|
||||
#, possible-python-brace-format
|
||||
msgctxt "rolemenu|deselect|success:refund|desc"
|
||||
msgid "You have removed **{role}**, and been refunded {coin} **{amount}**."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/rolemenu.py:686
|
||||
#: src/modules/rolemenus/rolemenu.py:703
|
||||
#, 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:691
|
||||
#: src/modules/rolemenus/rolemenu.py:708
|
||||
#, possible-python-brace-format
|
||||
msgctxt "rolemenu|deselect|success:norefund|desc"
|
||||
msgid "You have unequipped **{role}**."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/rolemenu.py:713
|
||||
#: src/modules/rolemenus/rolemenu.py:715
|
||||
msgctxt "rolemenu|eventlog|event:role_unequipped|title"
|
||||
msgid "Member unequipped role from role menu"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/rolemenu.py:719
|
||||
#, possible-python-brace-format
|
||||
msgctxt "rolemenu|eventlog|event:role_unequipped|desc"
|
||||
msgid "{member} unequipped {role} from {menu}"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/rolemenu.py:747
|
||||
#, possible-python-brace-format
|
||||
msgctxt "rolemenu|error:role_gone"
|
||||
msgid "The role **{name}** no longer exists!"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/rolemenu.py:726
|
||||
#: src/modules/rolemenus/rolemenu.py:760
|
||||
#, possible-python-brace-format
|
||||
msgctxt "rolemenu|select|error:required_role"
|
||||
msgid "You need to have the role **{role}** required to use this menu!"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rolemenus/rolemenu.py:784
|
||||
#: src/modules/rolemenus/rolemenu.py:818
|
||||
#, possible-python-brace-format
|
||||
msgctxt "rolemenu|content:reactions"
|
||||
msgid "[Click here]({jump_link}) to jump back."
|
||||
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-10-01 16:01+0300\n"
|
||||
"POT-Creation-Date: 2023-10-15 15:58+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"
|
||||
@@ -178,83 +178,124 @@ msgctxt "ui:room_status|embed|field:members|name"
|
||||
msgid "Members ({count}/{cap})"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/cog.py:321
|
||||
#: src/modules/rooms/cog.py:175 src/modules/rooms/room.py:364
|
||||
msgctxt "room|eventlog|event:room_deleted|title"
|
||||
msgid "Private Room Deleted"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/cog.py:179 src/modules/rooms/room.py:368
|
||||
#, possible-python-brace-format
|
||||
msgctxt "room|eventlog|event:room_deleted|desc"
|
||||
msgid "{owner}'s private room was deleted."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/cog.py:270
|
||||
#, possible-python-brace-format
|
||||
msgctxt "create_room|create_channel|audit_reason"
|
||||
msgid "Creating Private Room for {ownerid}"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/cog.py:279
|
||||
msgctxt "eventlog|event:private_room_create_error|name"
|
||||
msgid "Private Room Creation Failed"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/cog.py:283
|
||||
#, possible-python-brace-format
|
||||
msgctxt "eventlog|event:private_room_create_error|desc"
|
||||
msgid ""
|
||||
"{owner} attempted to rent a new private room, but I could not create it!\n"
|
||||
"They were not charged."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/cog.py:329
|
||||
msgctxt "eventlog|event:private_room_create|name"
|
||||
msgid "Private Room Rented"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/cog.py:333
|
||||
#, possible-python-brace-format
|
||||
msgctxt "eventlog|event:private_room_create|desc"
|
||||
msgid "{owner} has rented a new private room {channel}!"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/cog.py:368
|
||||
msgctxt "cmd:room"
|
||||
msgid "room"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/cog.py:322
|
||||
#: src/modules/rooms/cog.py:369
|
||||
msgctxt "cmd:room|desc"
|
||||
msgid "Base command group for private room configuration."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/cog.py:329
|
||||
#: src/modules/rooms/cog.py:376
|
||||
msgctxt "cmd:room_rent"
|
||||
msgid "rent"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/cog.py:332
|
||||
#: src/modules/rooms/cog.py:379
|
||||
msgctxt "cmd:room_rent|desc"
|
||||
msgid "Rent a private voice channel with LionCoins."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/cog.py:336
|
||||
#: src/modules/rooms/cog.py:383
|
||||
msgctxt "cmd:room_rent|param:days"
|
||||
msgid "days"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/cog.py:337
|
||||
#: src/modules/rooms/cog.py:384
|
||||
msgctxt "cmd:room_rent|param:members"
|
||||
msgid "members"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/cog.py:338
|
||||
#: src/modules/rooms/cog.py:385
|
||||
msgctxt "cmd:room_rent|param:name"
|
||||
msgid "name"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/cog.py:343
|
||||
#: src/modules/rooms/cog.py:390
|
||||
msgctxt "cmd:room_rent|param:days|desc"
|
||||
msgid "Number of days to pre-purchase. (Default: 1)"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/cog.py:347
|
||||
#: src/modules/rooms/cog.py:394
|
||||
msgctxt "cmd:room_rent|param:members|desc"
|
||||
msgid "Mention the members you want to add to your private room."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/cog.py:351
|
||||
#: src/modules/rooms/cog.py:398
|
||||
msgctxt "cmd:room_rent|param:name|desc"
|
||||
msgid "Name of your private voice channel."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/cog.py:369
|
||||
#: src/modules/rooms/cog.py:416
|
||||
msgctxt "cmd:room_rent|error:not_setup"
|
||||
msgid ""
|
||||
"The private room system has not been set up! A private room category needs "
|
||||
"to be set first with `/configure rooms`."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/cog.py:380
|
||||
#: src/modules/rooms/cog.py:427
|
||||
msgctxt "cmd:room_rent|error:insufficient_perms"
|
||||
msgid ""
|
||||
"I do not have enough permissions to create a new channel under the "
|
||||
"configured private room category!"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/cog.py:394
|
||||
#: src/modules/rooms/cog.py:441
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:room_rent|error:room_exists"
|
||||
msgid "You already own a private room! Click to visit: {channel}"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/cog.py:414
|
||||
#: src/modules/rooms/cog.py:461
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:room_rent|error:member_not_found"
|
||||
msgid "Could not find the requested member {mention} in this server!"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/cog.py:428
|
||||
#: src/modules/rooms/cog.py:475
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:room_rent|error:too_many_members"
|
||||
msgid ""
|
||||
@@ -262,7 +303,7 @@ msgid ""
|
||||
"but the maximum private room size is `{cap}`!"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/cog.py:442
|
||||
#: src/modules/rooms/cog.py:489
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:room_rent|confirm:purchase"
|
||||
msgid ""
|
||||
@@ -274,18 +315,18 @@ msgid_plural ""
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
#: src/modules/rooms/cog.py:467
|
||||
#: src/modules/rooms/cog.py:514
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:room_rent|success"
|
||||
msgid "Successfully created your private room {channel}!"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/cog.py:472
|
||||
#: src/modules/rooms/cog.py:519
|
||||
msgctxt "cmd:room_rent|success|title"
|
||||
msgid "Private Room Created!"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/cog.py:506
|
||||
#: src/modules/rooms/cog.py:553
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:room_rent|error:insufficient_funds"
|
||||
msgid ""
|
||||
@@ -297,14 +338,14 @@ msgid_plural ""
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
#: src/modules/rooms/cog.py:540
|
||||
#: src/modules/rooms/cog.py:587
|
||||
msgctxt "cmd:room_rent|error:my_permissions"
|
||||
msgid ""
|
||||
"Could not create your private room! You were not charged.\n"
|
||||
"I have insufficient permissions to create a private room channel."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/cog.py:552
|
||||
#: src/modules/rooms/cog.py:598
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:room_rent|error:unknown"
|
||||
msgid ""
|
||||
@@ -313,62 +354,62 @@ msgid ""
|
||||
"`{error}`"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/cog.py:562
|
||||
#: src/modules/rooms/cog.py:607
|
||||
msgctxt "cmd:room_status"
|
||||
msgid "status"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/cog.py:565
|
||||
#: src/modules/rooms/cog.py:610
|
||||
msgctxt "cmd:room_status|desc"
|
||||
msgid "Display the status of your current room."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/cog.py:582
|
||||
#: src/modules/rooms/cog.py:627
|
||||
msgctxt "cmd:room_status|error:no_target"
|
||||
msgid ""
|
||||
"Could not identify target private room! Please re-run the command in the "
|
||||
"private room you wish to view the status of."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/cog.py:597
|
||||
#: src/modules/rooms/cog.py:642
|
||||
msgctxt "cmd:room_invite"
|
||||
msgid "invite"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/cog.py:600
|
||||
#: src/modules/rooms/cog.py:645
|
||||
msgctxt "cmd:room_invite|desc"
|
||||
msgid "Add members to your private room."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/cog.py:604
|
||||
#: src/modules/rooms/cog.py:649
|
||||
msgctxt "cmd:room_invite|param:members"
|
||||
msgid "members"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/cog.py:609
|
||||
#: src/modules/rooms/cog.py:654
|
||||
msgctxt "cmd:room_invite|param:members|desc"
|
||||
msgid "Mention the members you want to add."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/cog.py:623
|
||||
#: src/modules/rooms/cog.py:668
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:room_invite|error:no_room"
|
||||
msgid ""
|
||||
"You do not own a private room! Use `/room rent` to rent one with {coin}!"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/cog.py:644
|
||||
#: src/modules/rooms/cog.py:689
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:room_invite|error:member_not_found"
|
||||
msgid "Could not find the invited member {mention} in this server!"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/cog.py:655
|
||||
#: src/modules/rooms/cog.py:700
|
||||
msgctxt "cmd:room_invite|error:no_new_members"
|
||||
msgid "All members mentioned are already in the room!"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/cog.py:669
|
||||
#: src/modules/rooms/cog.py:714
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:room_invite|error:too_many_members"
|
||||
msgid ""
|
||||
@@ -376,89 +417,89 @@ msgid ""
|
||||
"you already have `{current}`, and the member cap is `{cap}`!"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/cog.py:693
|
||||
#: src/modules/rooms/cog.py:738
|
||||
msgctxt "cmd:room_invite|success|ack"
|
||||
msgid "Members Invited successfully."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/cog.py:701
|
||||
#: src/modules/rooms/cog.py:746
|
||||
msgctxt "cmd:room_kick"
|
||||
msgid "kick"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/cog.py:704
|
||||
#: src/modules/rooms/cog.py:749
|
||||
msgctxt "cmd:room_kick|desc"
|
||||
msgid "Remove a members from your private room."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/cog.py:708
|
||||
#: src/modules/rooms/cog.py:753
|
||||
msgctxt "cmd:room_kick|param:members"
|
||||
msgid "members"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/cog.py:713
|
||||
#: src/modules/rooms/cog.py:758
|
||||
msgctxt "cmd:room_kick|param:members|desc"
|
||||
msgid ""
|
||||
"Mention the members you want to remove. Also accepts space-separated user "
|
||||
"ids."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/cog.py:727
|
||||
#: src/modules/rooms/cog.py:772
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:room_kick|error:no_room"
|
||||
msgid ""
|
||||
"You do not own a private room! Use `/room rent` to rent one with {coin}!"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/cog.py:744
|
||||
#: src/modules/rooms/cog.py:789
|
||||
msgctxt "cmd:room_kick|error:no_matching_members"
|
||||
msgid "None of the mentioned members are in this room!"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/cog.py:761
|
||||
#: src/modules/rooms/cog.py:806
|
||||
msgctxt "cmd:room_kick|success|ack"
|
||||
msgid "Members removed."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/cog.py:767
|
||||
#: src/modules/rooms/cog.py:812
|
||||
msgctxt "cmd:room_transfer"
|
||||
msgid "transfer"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/cog.py:770
|
||||
#: src/modules/rooms/cog.py:815
|
||||
msgctxt "cmd:room_transfer|desc"
|
||||
msgid "Transfer your private room to another room member. Not reversible!"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/cog.py:774
|
||||
#: src/modules/rooms/cog.py:819
|
||||
msgctxt "cmd:room_transfer|param:new_owner"
|
||||
msgid "new_owner"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/cog.py:779
|
||||
#: src/modules/rooms/cog.py:824
|
||||
msgctxt "cmd:room_transfer|param:new_owner"
|
||||
msgid "The room member you would like to transfer your room to."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/cog.py:793
|
||||
#: src/modules/rooms/cog.py:838
|
||||
msgctxt "cmd:room_transfer|error:no_room"
|
||||
msgid "You do not own a private room to transfer!"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/cog.py:805
|
||||
#: src/modules/rooms/cog.py:850
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:room_transfer|error:target_not_member"
|
||||
msgid ""
|
||||
"{mention} is not a member of your private room! You must invite them first."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/cog.py:817
|
||||
#: src/modules/rooms/cog.py:862
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:room_transfer|error:target_has_room"
|
||||
msgid "{mention} already owns a room! Members can only own one room at a time."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/cog.py:826
|
||||
#: src/modules/rooms/cog.py:871
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:room_transfer|confirm|question"
|
||||
msgid ""
|
||||
@@ -466,101 +507,164 @@ msgid ""
|
||||
"{new_owner}? This action is not reversible!"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/cog.py:846
|
||||
#: src/modules/rooms/cog.py:891
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:room_transfer|success|description"
|
||||
msgid ""
|
||||
"You have successfully transferred ownership of {channel} to {new_owner}."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/cog.py:852
|
||||
#: src/modules/rooms/cog.py:897
|
||||
msgctxt "cmd:room_deposit"
|
||||
msgid "deposit"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/cog.py:855
|
||||
#: src/modules/rooms/cog.py:900
|
||||
msgctxt "cmd:room_deposit|desc"
|
||||
msgid ""
|
||||
"Deposit LionCoins in your private room bank to add more days. (Members may "
|
||||
"also deposit!)"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/cog.py:859
|
||||
#: src/modules/rooms/cog.py:904
|
||||
msgctxt "cmd:room_deposit|param:coins"
|
||||
msgid "coins"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/cog.py:864
|
||||
#: src/modules/rooms/cog.py:909
|
||||
msgctxt "cmd:room_deposit|param:coins|desc"
|
||||
msgid "Number of coins to deposit."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/cog.py:884
|
||||
#: src/modules/rooms/cog.py:929
|
||||
msgctxt "cmd:room_deposit|error:no_target"
|
||||
msgid ""
|
||||
"Could not identify target private room! Please re-run the command in the "
|
||||
"private room you wish to contribute to."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/cog.py:900
|
||||
#: src/modules/rooms/cog.py:945
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:room_deposit|error:insufficient_funds"
|
||||
msgid ""
|
||||
"You cannot deposit {coin}**{amount}**! You only have {coin}**{balance}**."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/cog.py:921
|
||||
#: src/modules/rooms/cog.py:966
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:room_depost|success"
|
||||
msgid ""
|
||||
"Success! You have contributed {coin}**{amount}** to the private room bank."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/cog.py:936
|
||||
#: src/modules/rooms/cog.py:981
|
||||
msgctxt "cmd:configure_rooms"
|
||||
msgid "rooms"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/cog.py:937
|
||||
#: src/modules/rooms/cog.py:982
|
||||
msgctxt "cmd:configure_rooms|desc"
|
||||
msgid "Configure Rented Private Rooms"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/room.py:81
|
||||
#: src/modules/rooms/room.py:78
|
||||
msgctxt "room|eventlog|field:owner"
|
||||
msgid "Owner"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/room.py:84
|
||||
msgctxt "room|eventlog|field:channel"
|
||||
msgid "Channel"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/room.py:90
|
||||
msgctxt "room|eventlog|field:balance"
|
||||
msgid "Room Balance"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/room.py:96
|
||||
msgctxt "room|eventlog|field:created"
|
||||
msgid "Created At"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/room.py:102
|
||||
msgctxt "room|eventlog|field:tick"
|
||||
msgid "Next Rent Due"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/room.py:108
|
||||
msgctxt "room|eventlog|field:members"
|
||||
msgid "Private Room Members"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/room.py:123
|
||||
#, possible-python-brace-format
|
||||
msgctxt "room|notify:deposit|description"
|
||||
msgid "{member} has deposited {coin}**{amount}** into the room bank!"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/room.py:104
|
||||
#: src/modules/rooms/room.py:146
|
||||
msgctxt "room|notify:new_members|title"
|
||||
msgid "New Members!"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/room.py:108
|
||||
#: src/modules/rooms/room.py:150
|
||||
#, possible-python-brace-format
|
||||
msgctxt "room|notify:new_members|desc"
|
||||
msgid "Welcome {members}"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/room.py:183
|
||||
#: src/modules/rooms/room.py:156
|
||||
msgctxt "room|eventlog|event:new_members|title"
|
||||
msgid "Members invited to private room"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/room.py:160
|
||||
#, possible-python-brace-format
|
||||
msgctxt "room|eventlog|event:new_members|desc"
|
||||
msgid "{owner} added members to their private room: {members}"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/room.py:191
|
||||
msgctxt "room|eventlog|event:rm_members|title"
|
||||
msgid "Members removed from private room"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/room.py:195
|
||||
#, possible-python-brace-format
|
||||
msgctxt "room|eventlog|event:rm_members|desc"
|
||||
msgid "{owner} removed members from their private room: {members}"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/room.py:254
|
||||
#, possible-python-brace-format
|
||||
msgctxt "room|notify:transfer|description"
|
||||
msgid "{old_owner} has transferred private room ownership to {new_owner}"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/room.py:247
|
||||
#: src/modules/rooms/room.py:318
|
||||
msgctxt "room|embed:expiry|title"
|
||||
msgid "Private Room Expired!"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/room.py:251
|
||||
#: src/modules/rooms/room.py:322
|
||||
#, possible-python-brace-format
|
||||
msgctxt "room|embed:expiry|description"
|
||||
msgid "Your private room in **{guild}** has expired!"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/room.py:265
|
||||
#: src/modules/rooms/room.py:332
|
||||
msgctxt "room|eventlog|event:expired|title"
|
||||
msgid "Private Room Expired"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/room.py:336
|
||||
#, possible-python-brace-format
|
||||
msgctxt "room|eventlog|event:expired|desc"
|
||||
msgid "{owner}'s private room has expired."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/rooms/room.py:349
|
||||
#, possible-python-brace-format
|
||||
msgctxt "room|tick|rent_deducted"
|
||||
msgid "Daily rent deducted from room balance. New balance: {coin}**{amount}**"
|
||||
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-10-01 16:01+0300\n"
|
||||
"POT-Creation-Date: 2023-10-15 15:58+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"
|
||||
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-10-01 16:01+0300\n"
|
||||
"POT-Creation-Date: 2023-10-15 15:58+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"
|
||||
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-10-01 16:01+0300\n"
|
||||
"POT-Creation-Date: 2023-10-15 15:58+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"
|
||||
@@ -133,52 +133,63 @@ msgctxt "shop:colour|purchase|error:failed_unknown"
|
||||
msgid "An unknown error occurred while giving you this colour role!"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:348
|
||||
#: src/modules/shop/shops/colours.py:306
|
||||
msgctxt "eventlog|event:purchase_colour|title"
|
||||
msgid "Member Purchased Colour Role"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:310
|
||||
#, possible-python-brace-format
|
||||
msgctxt "eventlog|event:purchase_colour|desc"
|
||||
msgid "{member} purchased {role} from the colour shop."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:365
|
||||
msgctxt "grp:editshop_colours"
|
||||
msgid "colours"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:353
|
||||
#: src/modules/shop/shops/colours.py:370
|
||||
msgctxt "cmd:editshop_colours_create"
|
||||
msgid "create"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:356
|
||||
#: src/modules/shop/shops/colours.py:373
|
||||
msgctxt "cmd:editshop_colours_create|desc"
|
||||
msgid "Create a new colour role with the given colour."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:360
|
||||
#: src/modules/shop/shops/colours.py:377
|
||||
msgctxt "cmd:editshop_colours_create|param:colour"
|
||||
msgid "colour"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:361
|
||||
#: src/modules/shop/shops/colours.py:378
|
||||
msgctxt "cmd:editshop_colours_create|param:name"
|
||||
msgid "name"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:362
|
||||
#: src/modules/shop/shops/colours.py:379
|
||||
msgctxt "cmd:editshop_colours_create|param:price"
|
||||
msgid "price"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:367
|
||||
#: src/modules/shop/shops/colours.py:384
|
||||
msgctxt "cmd:editshop_colours_create|param:colour|desc"
|
||||
msgid "What colour should the role be? (As a hex code, e.g. #AB22AB)"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:371
|
||||
#: src/modules/shop/shops/colours.py:388
|
||||
msgctxt "cmd:editshop_colours_create|param:name|desc"
|
||||
msgid "What should the colour role be called?"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:375
|
||||
#: src/modules/shop/shops/colours.py:392
|
||||
msgctxt "cmd:editshop_colours_create|param:price|desc"
|
||||
msgid "How much should the colour role cost?"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:397
|
||||
#: src/modules/shop/shops/colours.py:414
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:editshop_colours_create|error:parse_colour"
|
||||
msgid ""
|
||||
@@ -186,7 +197,7 @@ msgid ""
|
||||
"Please enter the colour as a hex string, e.g. `#FA0BC1`"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:407
|
||||
#: src/modules/shop/shops/colours.py:424
|
||||
msgctxt "cmd:editshop_colours_create|error:perms"
|
||||
msgid ""
|
||||
"I do not have permission to create server roles!\n"
|
||||
@@ -194,26 +205,26 @@ msgid ""
|
||||
"editshop colours add` instead."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:424
|
||||
#: src/modules/shop/shops/colours.py:441
|
||||
msgctxt "cmd:editshop_colours_create|error:max_colours"
|
||||
msgid ""
|
||||
"This server already has the maximum of `25` colour roles!\n"
|
||||
"Please remove some before adding or creating more."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:443
|
||||
#: src/modules/shop/shops/colours.py:460
|
||||
msgctxt "cmd:editshop_colours_create|error:failed_unknown"
|
||||
msgid ""
|
||||
"An unknown Discord error occurred while creating your colour role!\n"
|
||||
"Please try again in a few minutes."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:493
|
||||
#: src/modules/shop/shops/colours.py:515
|
||||
msgctxt "cmd:editshop_colours_create|resp:done|title"
|
||||
msgid "Colour Role Created"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:497
|
||||
#: src/modules/shop/shops/colours.py:519
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:editshop_colours_create|resp:done|desc"
|
||||
msgid ""
|
||||
@@ -221,80 +232,80 @@ msgid ""
|
||||
"{coin}**{price}**!"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:504
|
||||
#: src/modules/shop/shops/colours.py:526
|
||||
msgctxt "cmd:editshop_colours_create|resp:done|field:position_note|value"
|
||||
msgid ""
|
||||
"The new colour role was added below all other roles. Remember a member's "
|
||||
"active colour is determined by their highest coloured role!"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:508
|
||||
#: src/modules/shop/shops/colours.py:530
|
||||
msgctxt "cmd:editshop_colours_create|resp:done|field:position_note|name"
|
||||
msgid "Note"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:517
|
||||
#: src/modules/shop/shops/colours.py:539
|
||||
msgctxt "cmd:editshop_colours_edit"
|
||||
msgid "edit"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:520
|
||||
#: src/modules/shop/shops/colours.py:542
|
||||
msgctxt "cmd:editshop_colours_edit|desc"
|
||||
msgid "Edit the name, colour, or price of a colour role."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:524
|
||||
#: src/modules/shop/shops/colours.py:546
|
||||
msgctxt "cmd:editshop_colours_edit|param:role"
|
||||
msgid "role"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:525
|
||||
#: src/modules/shop/shops/colours.py:547
|
||||
msgctxt "cmd:editshop_colours_edit|param:name"
|
||||
msgid "name"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:526
|
||||
#: src/modules/shop/shops/colours.py:548
|
||||
msgctxt "cmd:editshop_colours_edit|param:colour"
|
||||
msgid "colour"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:527
|
||||
#: src/modules/shop/shops/colours.py:549
|
||||
msgctxt "cmd:editshop_colours_edit|param:price"
|
||||
msgid "price"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:532
|
||||
#: src/modules/shop/shops/colours.py:554
|
||||
msgctxt "cmd:editshop_colours_edit|param:role|desc"
|
||||
msgid "Select a colour role to edit."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:536
|
||||
#: src/modules/shop/shops/colours.py:558
|
||||
msgctxt "cmd:editshop_colours_edit|param:name|desc"
|
||||
msgid "New name to give the colour role."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:540
|
||||
#: src/modules/shop/shops/colours.py:562
|
||||
msgctxt "cmd:editshop_colours_edit|param:colour|desc"
|
||||
msgid "New colour for the colour role (as hex, e.g. #AB12AB)."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:544
|
||||
#: src/modules/shop/shops/colours.py:566
|
||||
msgctxt "cmd:editshop_colours_edit|param:price|desc"
|
||||
msgid "New price for the colour role."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:573
|
||||
#: src/modules/shop/shops/colours.py:595
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:editshop_colours_edit|error:invalid_role"
|
||||
msgid "{mention} is not in the colour role shop!"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:587
|
||||
#: src/modules/shop/shops/colours.py:609
|
||||
msgctxt "cmd:editshop_colours_edit|error:no_args"
|
||||
msgid "You must give me one of `name`, `colour`, or `price` to update!"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:603
|
||||
#: src/modules/shop/shops/colours.py:625
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:editshop_colours_edit|error:parse_colour"
|
||||
msgid ""
|
||||
@@ -302,82 +313,82 @@ msgid ""
|
||||
"Please enter the colour as a hex string, e.g. `#FA0BC1`"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:618
|
||||
#: src/modules/shop/shops/colours.py:640
|
||||
msgctxt "cmd:editshop_colours_edit|error:perms"
|
||||
msgid "I do not have sufficient server permissions to edit this role!"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:634
|
||||
#: src/modules/shop/shops/colours.py:656
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:editshop_colours_edit|resp:done|line:price"
|
||||
msgid "{tick} Set price to {coin}**{price}**"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:651
|
||||
#: src/modules/shop/shops/colours.py:673
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:editshop_colours_edit|resp:done|line:role"
|
||||
msgid "{tick} Updated role to {mention}"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:661
|
||||
#: src/modules/shop/shops/colours.py:683
|
||||
msgctxt "cmd:editshop_colours_edit|resp:done|embed:title"
|
||||
msgid "Colour Role Updated"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:667
|
||||
#: src/modules/shop/shops/colours.py:689
|
||||
msgctxt "cmd:editshop_colours_auto"
|
||||
msgid "auto"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:668
|
||||
#: src/modules/shop/shops/colours.py:690
|
||||
msgctxt "cmd:editshop_colours_auto|desc"
|
||||
msgid "Automatically create a set of colour roles."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:677
|
||||
#: src/modules/shop/shops/colours.py:699
|
||||
msgctxt "cmd:editshop_colours_add"
|
||||
msgid "add"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:680
|
||||
#: src/modules/shop/shops/colours.py:702
|
||||
msgctxt "cmd:editshop_colours_add|desc"
|
||||
msgid "Add an existing role to the colour shop."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:684
|
||||
#: src/modules/shop/shops/colours.py:706
|
||||
msgctxt "cmd:editshop_colours_add|param:role"
|
||||
msgid "role"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:685
|
||||
#: src/modules/shop/shops/colours.py:707
|
||||
msgctxt "cmd:editshop_colours_add|param:price"
|
||||
msgid "price"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:690
|
||||
#: src/modules/shop/shops/colours.py:712
|
||||
msgctxt "cmd:editshop_colours_add|param:role|desc"
|
||||
msgid "Select a role to add to the colour shop."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:694
|
||||
#: src/modules/shop/shops/colours.py:716
|
||||
msgctxt "cmd:editshop_colours_add|param:price|desc"
|
||||
msgid "How much should this role cost?"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:719
|
||||
#: src/modules/shop/shops/colours.py:741
|
||||
msgctxt "cmd:editshop_colours_add|error:max_colours"
|
||||
msgid ""
|
||||
"This server already has the maximum of `25` colour roles!\n"
|
||||
"Please remove some before adding or creating more."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:728
|
||||
#: src/modules/shop/shops/colours.py:750
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:editshop_colours_add|error:role_exists"
|
||||
msgid "The role {mention} is already registered as a colour role!"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:737
|
||||
#: src/modules/shop/shops/colours.py:759
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:editshop_colours_add|error:role_perms"
|
||||
msgid ""
|
||||
@@ -386,7 +397,7 @@ msgid ""
|
||||
"role."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:748
|
||||
#: src/modules/shop/shops/colours.py:770
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:editshop_colours_add|error:caller_perms"
|
||||
msgid ""
|
||||
@@ -395,138 +406,138 @@ msgid ""
|
||||
"role."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:760
|
||||
#: src/modules/shop/shops/colours.py:782
|
||||
msgctxt "cmd:editshop_colours_add|error:role_has_admin"
|
||||
msgid ""
|
||||
"I refuse to add an administrator role to the LionCoin shop. That is a really "
|
||||
"bad idea."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:781
|
||||
#: src/modules/shop/shops/colours.py:803
|
||||
msgctxt "cmd:editshop_colours_add|resp:done|embed:title"
|
||||
msgid "Colour Role Created"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:784
|
||||
#: src/modules/shop/shops/colours.py:806
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:editshop_colours_add|resp:done|embed:desc"
|
||||
msgid "You have added {mention} to the colour shop for {coin}**{price}**!"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:792
|
||||
#: src/modules/shop/shops/colours.py:814
|
||||
msgctxt "cmd:editshop_colours_clear"
|
||||
msgid "clear"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:795
|
||||
#: src/modules/shop/shops/colours.py:817
|
||||
msgctxt "cmd:editshop_colours_clear|desc"
|
||||
msgid ""
|
||||
"Remove all the colour roles from the shop, and optionally delete the roles."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:799
|
||||
#: src/modules/shop/shops/colours.py:821
|
||||
msgctxt "cmd:editshop_colours_clear|param:delete"
|
||||
msgid "delete_roles"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:804
|
||||
#: src/modules/shop/shops/colours.py:826
|
||||
msgctxt "cmd:editshop_colours_clear|param:delete|desc"
|
||||
msgid "Also delete the associated roles."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:846
|
||||
#: src/modules/shop/shops/colours.py:868
|
||||
msgctxt "cmd:editshop_colours_clear|error:no_colours"
|
||||
msgid "There are no coloured roles to remove!"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:890
|
||||
#: src/modules/shop/shops/colours.py:912
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:editshop_colours_clear|resp:done|line:clear"
|
||||
msgid "{tick} Colour shop cleared."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:901
|
||||
#: src/modules/shop/shops/colours.py:923
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:editshop_colours_clear|resp:done|line:refunding"
|
||||
msgid "{loading} Refunded **{count}/{total}** members."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:907
|
||||
#: src/modules/shop/shops/colours.py:929
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:editshop_colours_clear|resp:done|line:refunded"
|
||||
msgid "{tick} Refunded **{total}/{total}** members."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:920
|
||||
#: src/modules/shop/shops/colours.py:942
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:editshop_colours_clear|resp:done|line:deleted_failed"
|
||||
msgid ""
|
||||
"{emoji} Deleted **{count}/{total}** colour roles. (**{failed}** failed!)"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:925
|
||||
#: src/modules/shop/shops/colours.py:947
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:editshop_colours_clear|resp:done|line:deleted"
|
||||
msgid "{emoji} Deleted **{count}/{total}** colour roles."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:974
|
||||
#: src/modules/shop/shops/colours.py:996
|
||||
msgctxt "cmd:editshop_colours_remove"
|
||||
msgid "remove"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:977
|
||||
#: src/modules/shop/shops/colours.py:999
|
||||
msgctxt "cmd:editshop_colours_remove|desc"
|
||||
msgid "Remove a specific colour role from the shop."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:981
|
||||
#: src/modules/shop/shops/colours.py:1003
|
||||
msgctxt "cmd:editshop_colours_remove|param:role"
|
||||
msgid "role"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:982
|
||||
#: src/modules/shop/shops/colours.py:1004
|
||||
msgctxt "cmd:editshop_colours_remove"
|
||||
msgid "delete_role"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:987
|
||||
#: src/modules/shop/shops/colours.py:1009
|
||||
msgctxt "cmd:editshop_colours_remove|param:role|desc"
|
||||
msgid "Select the colour role to remove."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:991
|
||||
#: src/modules/shop/shops/colours.py:1013
|
||||
msgctxt "cmd:editshop_colours_remove|param:delete_role|desc"
|
||||
msgid "Whether to delete the associated role."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:1016
|
||||
#: src/modules/shop/shops/colours.py:1038
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:editshop_colours_remove|error:not_colour"
|
||||
msgid "{mention} is not in the colour role shop!"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:1034
|
||||
#: src/modules/shop/shops/colours.py:1056
|
||||
msgctxt "cmd:editshop_colours_remove|resp:done|line:delete"
|
||||
msgid "Successfully deleted the role."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:1039
|
||||
#: src/modules/shop/shops/colours.py:1061
|
||||
msgctxt "cmd:editshop_colours_remove|resp:done|line:delete"
|
||||
msgid "I do not have sufficient permissions to delete the role."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:1044
|
||||
#: src/modules/shop/shops/colours.py:1066
|
||||
msgctxt "cmd:editshop_colours_remove|resp:done|line:delete"
|
||||
msgid "Failed to delete the role for an unknown reason."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:1049
|
||||
#: src/modules/shop/shops/colours.py:1071
|
||||
msgctxt "cmd:editshop_colours_remove|resp:done|line:delete"
|
||||
msgid "Could not find the role in order to delete it."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:1060
|
||||
#: src/modules/shop/shops/colours.py:1082
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:editshop_colours_remove|resp:done|embed:desc"
|
||||
msgid ""
|
||||
@@ -534,61 +545,61 @@ msgid ""
|
||||
"{delete_line}"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:1138
|
||||
#: src/modules/shop/shops/colours.py:1160
|
||||
msgctxt "ui:colourstore|menu:buycolours|embed:error|title"
|
||||
msgid "Purchase Failed!"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:1149
|
||||
#: src/modules/shop/shops/colours.py:1171
|
||||
#, possible-python-brace-format
|
||||
msgctxt "ui:colourstore|menu:buycolours|resp:done|desc"
|
||||
msgid "{tick} You have purchased {mention}"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:1184
|
||||
#: src/modules/shop/shops/colours.py:1206
|
||||
msgctxt "ui:colourstore|menu:buycolours|placeholder"
|
||||
msgid "There are no colour roles available to purchase!"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:1190
|
||||
#: src/modules/shop/shops/colours.py:1212
|
||||
msgctxt "ui:colourstore|menu:buycolours|placeholder"
|
||||
msgid "Select a colour role to purchase!"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:1236
|
||||
#: src/modules/shop/shops/colours.py:1258
|
||||
#, possible-python-brace-format
|
||||
msgctxt "ui:colourstore|embed|line:owned_item"
|
||||
msgid "`[{j:02}]` | `{price} LC` | {mention} (You own this!)"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:1241
|
||||
#: src/modules/shop/shops/colours.py:1263
|
||||
#, possible-python-brace-format
|
||||
msgctxt "ui:colourstore|embed|line:item"
|
||||
msgid "`[{j:02}]` | `{price} LC` | {mention}"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:1248
|
||||
#: src/modules/shop/shops/colours.py:1270
|
||||
msgctxt "ui:colourstore|embed|desc"
|
||||
msgid "No colour roles available for purchase!"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:1251
|
||||
#: src/modules/shop/shops/colours.py:1273
|
||||
msgctxt "ui:colourstore|embed|title"
|
||||
msgid "Colour Role Shop"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:1257
|
||||
#: src/modules/shop/shops/colours.py:1279
|
||||
#, possible-python-brace-format
|
||||
msgctxt "ui:colourstore|embed|footer:paged"
|
||||
msgid "Page {current}/{total}"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:1264
|
||||
#: src/modules/shop/shops/colours.py:1286
|
||||
msgctxt "ui:colourstore|embed|field:warning|name"
|
||||
msgid "Note!"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/shop/shops/colours.py:1268
|
||||
#: src/modules/shop/shops/colours.py:1290
|
||||
#, possible-python-brace-format
|
||||
msgctxt "ui:colourstore|embed|field:warning|value"
|
||||
msgid ""
|
||||
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-10-01 16:01+0300\n"
|
||||
"POT-Creation-Date: 2023-10-15 15:58+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,93 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=CHARSET\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: src/modules/statistics/cog.py:43
|
||||
#: src/modules/statistics/cog.py:47
|
||||
msgctxt "cmd:me"
|
||||
msgid "me"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/statistics/cog.py:46
|
||||
#: src/modules/statistics/cog.py:50
|
||||
msgctxt "cmd:me|desc"
|
||||
msgid "Display your personal profile and summary statistics."
|
||||
msgid "Edit your personal profile and see your statistics."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/statistics/cog.py:57
|
||||
#: src/modules/statistics/cog.py:61
|
||||
msgctxt "cmd:profile"
|
||||
msgid "profile"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/statistics/cog.py:64
|
||||
msgctxt "cmd:profile|desc"
|
||||
msgid "Display the target's profile and statistics summary."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/statistics/cog.py:68
|
||||
msgctxt "cmd:profile|param:member"
|
||||
msgid "member"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/statistics/cog.py:72
|
||||
msgctxt "cmd:profile|param:member|desc"
|
||||
msgid "Member to display profile for."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/statistics/cog.py:105
|
||||
msgctxt "cmd:stats"
|
||||
msgid "stats"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/statistics/cog.py:60
|
||||
#: src/modules/statistics/cog.py:108
|
||||
msgctxt "cmd:stats|desc"
|
||||
msgid "Weekly and monthly statistics for your recent activity."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/statistics/cog.py:74
|
||||
#: src/modules/statistics/cog.py:122
|
||||
msgctxt "cmd:leaderboard"
|
||||
msgid "leaderboard"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/statistics/cog.py:77
|
||||
#: src/modules/statistics/cog.py:125
|
||||
msgctxt "cmd:leaderboard|desc"
|
||||
msgid "Server leaderboard."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/statistics/cog.py:92
|
||||
#: src/modules/statistics/cog.py:140
|
||||
#, 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:115
|
||||
#: src/modules/statistics/cog.py:157
|
||||
msgctxt "cmd:achievements"
|
||||
msgid "achievements"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/statistics/cog.py:160
|
||||
msgctxt "cmd:achievements|desc"
|
||||
msgid "View your progress towards the activity achievement awards!"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/statistics/cog.py:177
|
||||
msgctxt "cmd:achievements|embed:title"
|
||||
msgid "Achievements"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/statistics/cog.py:195
|
||||
msgctxt "cmd:configure_statistics"
|
||||
msgid "statistics"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/statistics/cog.py:116
|
||||
#: src/modules/statistics/cog.py:196
|
||||
msgctxt "cmd:configure_statistics|desc"
|
||||
msgid "Statistics configuration panel"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/statistics/cog.py:119
|
||||
#: src/modules/statistics/cog.py:199
|
||||
msgctxt "cmd:configure_statistics|param:season_start"
|
||||
msgid "season_start"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/statistics/cog.py:124
|
||||
#: src/modules/statistics/cog.py:204
|
||||
msgctxt "cmd:configure_statistics|param:season_start|desc"
|
||||
msgid ""
|
||||
"Time from which to start counting activity for rank badges and season "
|
||||
@@ -330,6 +365,12 @@ msgctxt "dash:stats|dropdown|placeholder"
|
||||
msgid "Activity Statistics Panel"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/statistics/graphics/stats.py:63
|
||||
#, possible-python-brace-format
|
||||
msgctxt "gui:stats|mode:voice|month"
|
||||
msgid "{hours} hours"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/statistics/ui/goals.py:92
|
||||
msgctxt "ui:MonthlyUI|name"
|
||||
msgid "Monthly"
|
||||
@@ -613,108 +654,108 @@ msgid ""
|
||||
"again to revert."
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/statistics/ui/leaderboard.py:253
|
||||
#: src/modules/statistics/ui/leaderboard.py:257
|
||||
msgctxt "ui:leaderboard|menu:stats|placeholder"
|
||||
msgid "Select Activity Type"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/statistics/ui/leaderboard.py:262
|
||||
#: src/modules/statistics/ui/leaderboard.py:266
|
||||
msgctxt "ui:leaderboard|menu:stats|item:voice"
|
||||
msgid "Voice Activity"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/statistics/ui/leaderboard.py:273
|
||||
#: src/modules/statistics/ui/leaderboard.py:277
|
||||
msgctxt "ui:leaderboard|menu:stats|item:study"
|
||||
msgid "Study Statistics"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/statistics/ui/leaderboard.py:284
|
||||
#: src/modules/statistics/ui/leaderboard.py:288
|
||||
msgctxt "ui:leaderboard|menu:stats|item:message"
|
||||
msgid "Message Activity"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/statistics/ui/leaderboard.py:295
|
||||
#: src/modules/statistics/ui/leaderboard.py:299
|
||||
msgctxt "ui:leaderboard|menu;stats|item:anki"
|
||||
msgid "Anki Cards Reviewed"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/statistics/ui/leaderboard.py:349
|
||||
#: src/modules/statistics/ui/leaderboard.py:353
|
||||
msgctxt "ui:leaderboard|button:season|label"
|
||||
msgid "This Season"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/statistics/ui/leaderboard.py:353
|
||||
#: src/modules/statistics/ui/leaderboard.py:357
|
||||
msgctxt "ui:leaderboard|button:day|label"
|
||||
msgid "Today"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/statistics/ui/leaderboard.py:357
|
||||
#: src/modules/statistics/ui/leaderboard.py:361
|
||||
msgctxt "ui:leaderboard|button:week|label"
|
||||
msgid "This Week"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/statistics/ui/leaderboard.py:361
|
||||
#: src/modules/statistics/ui/leaderboard.py:365
|
||||
msgctxt "ui:leaderboard|button:month|label"
|
||||
msgid "This Month"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/statistics/ui/leaderboard.py:365
|
||||
#: src/modules/statistics/ui/leaderboard.py:369
|
||||
msgctxt "ui:leaderboard|button:alltime|label"
|
||||
msgid "All Time"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/statistics/ui/leaderboard.py:369
|
||||
#: src/modules/statistics/ui/leaderboard.py:373
|
||||
msgctxt "ui:leaderboard|button:jump|label"
|
||||
msgid "Jump"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/statistics/ui/leaderboard.py:384
|
||||
#: src/modules/statistics/ui/leaderboard.py:388
|
||||
msgctxt "ui:leaderboard|button:jump|input:title"
|
||||
msgid "Jump to page"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/statistics/ui/leaderboard.py:388
|
||||
#: src/modules/statistics/ui/leaderboard.py:392
|
||||
msgctxt "ui:leaderboard|button:jump|input:question"
|
||||
msgid "Page number to jump to"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/statistics/ui/leaderboard.py:399
|
||||
#: src/modules/statistics/ui/leaderboard.py:403
|
||||
msgctxt "ui:leaderboard|button:jump|error:invalid_page"
|
||||
msgid "Invalid page number, please try again!"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/statistics/ui/leaderboard.py:443
|
||||
#: src/modules/statistics/ui/leaderboard.py:447
|
||||
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
|
||||
#: src/modules/statistics/ui/leaderboard.py:454
|
||||
#, possible-python-brace-format
|
||||
msgctxt "ui:leaderboard|since"
|
||||
msgid "Counting statistics since {timestamp}"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/statistics/ui/leaderboard.py:463
|
||||
#: src/modules/statistics/ui/leaderboard.py:467
|
||||
#, 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:468
|
||||
#: src/modules/statistics/ui/leaderboard.py:472
|
||||
#, 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:473
|
||||
#: src/modules/statistics/ui/leaderboard.py:477
|
||||
#, 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:482
|
||||
#: src/modules/statistics/ui/leaderboard.py:486
|
||||
msgctxt "ui:leaderboard|message:empty|title"
|
||||
msgid "Leaderboard Empty!"
|
||||
msgstr ""
|
||||
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-10-01 16:01+0300\n"
|
||||
"POT-Creation-Date: 2023-10-15 15:58+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,121 +18,121 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#. First column
|
||||
#: src/gui/cards/stats.py:72
|
||||
#: src/gui/cards/stats.py:61
|
||||
msgctxt "skin:stats|header:col1"
|
||||
msgid "STATISTICS"
|
||||
msgstr ""
|
||||
|
||||
#: src/gui/cards/stats.py:75
|
||||
#: src/gui/cards/stats.py:64
|
||||
msgctxt "skin:stats|subheader:leaderboard"
|
||||
msgid "LEADERBOARD POSITION"
|
||||
msgstr ""
|
||||
|
||||
#: src/gui/cards/stats.py:79
|
||||
#: src/gui/cards/stats.py:68
|
||||
msgctxt "skin:stats|mode:study|subheader:study"
|
||||
msgid "STUDY TIME"
|
||||
msgstr ""
|
||||
|
||||
#: src/gui/cards/stats.py:83
|
||||
#: src/gui/cards/stats.py:72
|
||||
msgctxt "skin:stats|mode:voice|subheader:study"
|
||||
msgid "VOICE TIME"
|
||||
msgstr ""
|
||||
|
||||
#: src/gui/cards/stats.py:87
|
||||
#: src/gui/cards/stats.py:76
|
||||
msgctxt "skin:stats|mode:text|subheader:study"
|
||||
msgid "MESSAGES"
|
||||
msgstr ""
|
||||
|
||||
#: src/gui/cards/stats.py:91
|
||||
#: src/gui/cards/stats.py:80
|
||||
msgctxt "skin:stats|mode:anki|subheader:study"
|
||||
msgid "CARDS REVIEWED"
|
||||
msgstr ""
|
||||
|
||||
#: src/gui/cards/stats.py:102
|
||||
#: src/gui/cards/stats.py:91
|
||||
msgctxt "skin:stats|subheader:workouts"
|
||||
msgid "WORKOUTS"
|
||||
msgstr ""
|
||||
|
||||
#: src/gui/cards/stats.py:118
|
||||
#: src/gui/cards/stats.py:107
|
||||
msgctxt "skin:stats|field:daily"
|
||||
msgid "DAILY"
|
||||
msgstr ""
|
||||
|
||||
#: src/gui/cards/stats.py:122
|
||||
#: src/gui/cards/stats.py:111
|
||||
msgctxt "skin:stats|field:weekly"
|
||||
msgid "WEEKLY"
|
||||
msgstr ""
|
||||
|
||||
#: src/gui/cards/stats.py:126
|
||||
#: src/gui/cards/stats.py:115
|
||||
msgctxt "skin:stats|field:monthly"
|
||||
msgid "MONTHLY"
|
||||
msgstr ""
|
||||
|
||||
#: src/gui/cards/stats.py:130
|
||||
#: src/gui/cards/stats.py:119
|
||||
msgctxt "skin:stats|field:alltime"
|
||||
msgid "ALL TIME"
|
||||
msgstr ""
|
||||
|
||||
#: src/gui/cards/stats.py:134
|
||||
#: src/gui/cards/stats.py:123
|
||||
msgctxt "skin:stats|field:time"
|
||||
msgid "TIME"
|
||||
msgstr ""
|
||||
|
||||
#: src/gui/cards/stats.py:138
|
||||
#: src/gui/cards/stats.py:127
|
||||
msgctxt "skin:stats|field:anki"
|
||||
msgid "ANKI: COMING SOON"
|
||||
msgstr ""
|
||||
|
||||
#: src/gui/cards/stats.py:155
|
||||
#: src/gui/cards/stats.py:144
|
||||
msgctxt "skin:stats|mode:study|header:col2"
|
||||
msgid "STUDY STREAK"
|
||||
msgstr ""
|
||||
|
||||
#: src/gui/cards/stats.py:159
|
||||
#: src/gui/cards/stats.py:148
|
||||
msgctxt "skin:stats|mode:voice|header:col2"
|
||||
msgid "VOICE STREAK"
|
||||
msgstr ""
|
||||
|
||||
#: src/gui/cards/stats.py:163
|
||||
#: src/gui/cards/stats.py:152
|
||||
msgctxt "skin:stats|mode:text|header:col2"
|
||||
msgid "ACTIVITY STREAK"
|
||||
msgstr ""
|
||||
|
||||
#: src/gui/cards/stats.py:167
|
||||
#: src/gui/cards/stats.py:156
|
||||
msgctxt "skin:stats|mode:anki|header:col2"
|
||||
msgid "ANKI REVIEW STREAK"
|
||||
msgstr ""
|
||||
|
||||
#: src/gui/cards/stats.py:180
|
||||
#: src/gui/cards/stats.py:169
|
||||
#, possible-python-brace-format
|
||||
msgctxt "skin:stats|mode:study|field:col2_summary"
|
||||
msgid "{amount} HRS"
|
||||
msgstr ""
|
||||
|
||||
#: src/gui/cards/stats.py:184
|
||||
#: src/gui/cards/stats.py:173
|
||||
#, possible-python-brace-format
|
||||
msgctxt "skin:stats|mode:voice|field:col2_summary"
|
||||
msgid "{amount} HRS"
|
||||
msgstr ""
|
||||
|
||||
#: src/gui/cards/stats.py:188
|
||||
#: src/gui/cards/stats.py:177
|
||||
#, possible-python-brace-format
|
||||
msgctxt "skin:stats|mode:text|field:col2_summary"
|
||||
msgid "{amount} XP"
|
||||
msgstr ""
|
||||
|
||||
#: src/gui/cards/stats.py:192
|
||||
#: src/gui/cards/stats.py:181
|
||||
#, possible-python-brace-format
|
||||
msgctxt "skin:stats|mode:anki|field:col2_summary"
|
||||
msgid "{amount} CARDS"
|
||||
msgstr ""
|
||||
|
||||
#: src/gui/cards/stats.py:223
|
||||
#: src/gui/cards/stats.py:212
|
||||
msgctxt "skin:stats|cal|weekdays"
|
||||
msgid "S,M,T,W,T,F,S"
|
||||
msgstr ""
|
||||
|
||||
#: src/gui/cards/stats.py:227
|
||||
#: src/gui/cards/stats.py:216
|
||||
msgctxt "skin:stats|cal|months"
|
||||
msgid ""
|
||||
"JANUARY,FEBRUARY,MARCH,APRIL,MAY,JUNE,JULY,AUGUST,SEPTEMBER,OCTOBER,NOVEMBER,"
|
||||
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-10-01 16:01+0300\n"
|
||||
"POT-Creation-Date: 2023-10-15 15:58+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"
|
||||
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-10-01 16:01+0300\n"
|
||||
"POT-Creation-Date: 2023-10-15 15:58+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"
|
||||
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-10-01 16:01+0300\n"
|
||||
"POT-Creation-Date: 2023-10-15 15:58+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,66 +17,66 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=CHARSET\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: src/modules/test/test.py:59 src/modules/test/test.py:66
|
||||
#: src/modules/test/test.py:62 src/modules/test/test.py:145
|
||||
msgid "test"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/test/test.py:67
|
||||
#: src/modules/test/test.py:146
|
||||
msgid "Test"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/test/test.py:74
|
||||
#: src/modules/test/test.py:153
|
||||
msgid "editor"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/test/test.py:75
|
||||
#: src/modules/test/test.py:154
|
||||
msgid "Test message editor"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/test/test.py:101
|
||||
#: src/modules/test/test.py:180
|
||||
msgid "test_ephemeral"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/test/test.py:102
|
||||
#: src/modules/test/test.py:181
|
||||
msgid "Test ephemeral delete and edit"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/test/test.py:114
|
||||
#: src/modules/test/test.py:193
|
||||
msgid "colours"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/test/test.py:115
|
||||
#: src/modules/test/test.py:194
|
||||
msgid "Test Ansi colours"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/test/test.py:135
|
||||
#: src/modules/test/test.py:214
|
||||
msgid "fail"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/test/test.py:143
|
||||
#: src/modules/test/test.py:222
|
||||
msgid "failui"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/test/test.py:150
|
||||
#: src/modules/test/test.py:229
|
||||
msgid "pager"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/test/test.py:178
|
||||
#: src/modules/test/test.py:257
|
||||
msgid "pager2"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/test/test.py:209
|
||||
#: src/modules/test/test.py:288
|
||||
msgid "prettyusers"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/test/test.py:259
|
||||
#: src/modules/test/test.py:338
|
||||
msgid "dmview"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/test/test.py:270
|
||||
#: src/modules/test/test.py:349
|
||||
msgid "multiview"
|
||||
msgstr ""
|
||||
|
||||
#: src/modules/test/test.py:287
|
||||
#: src/modules/test/test.py:366
|
||||
msgid "stats-card"
|
||||
msgstr ""
|
||||
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-10-01 16:01+0300\n"
|
||||
"POT-Creation-Date: 2023-10-15 15:58+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,22 +17,22 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=CHARSET\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: src/tracking/text/cog.py:261
|
||||
#: src/tracking/text/cog.py:307
|
||||
msgctxt "cmd:configure_message_exp"
|
||||
msgid "message_exp"
|
||||
msgstr ""
|
||||
|
||||
#: src/tracking/text/cog.py:264
|
||||
#: src/tracking/text/cog.py:310
|
||||
msgctxt "cmd:configure_message_exp|desc"
|
||||
msgid "Configure Message Tracking & Experience"
|
||||
msgstr ""
|
||||
|
||||
#: src/tracking/text/cog.py:327
|
||||
#: src/tracking/text/cog.py:373
|
||||
msgctxt "cmd:leo_configure_exp_rates"
|
||||
msgid "experience_rates"
|
||||
msgstr ""
|
||||
|
||||
#: src/tracking/text/cog.py:330
|
||||
#: src/tracking/text/cog.py:376
|
||||
msgctxt "cmd:leo_configure_exp_rates|desc"
|
||||
msgid "Global experience rate configuration"
|
||||
msgstr ""
|
||||
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-10-01 16:01+0300\n"
|
||||
"POT-Creation-Date: 2023-10-15 15:58+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"
|
||||
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-10-01 16:01+0300\n"
|
||||
"POT-Creation-Date: 2023-10-15 15:58+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"
|
||||
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-10-01 16:01+0300\n"
|
||||
"POT-Creation-Date: 2023-10-15 15:58+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"
|
||||
@@ -28,77 +28,77 @@ msgid ""
|
||||
"Jump to a given page of the ouput of a previous command in this channel."
|
||||
msgstr ""
|
||||
|
||||
#: src/utils/cog.py:34
|
||||
#: src/utils/cog.py:35
|
||||
msgctxt "cmd:page|error:no_pager"
|
||||
msgid "No pager listening in this channel!"
|
||||
msgstr ""
|
||||
|
||||
#: src/utils/cog.py:45
|
||||
#: src/utils/cog.py:46
|
||||
msgctxt "cmd:page_next"
|
||||
msgid "next"
|
||||
msgstr ""
|
||||
|
||||
#: src/utils/cog.py:46
|
||||
#: src/utils/cog.py:47
|
||||
msgctxt "cmd:page_next|desc"
|
||||
msgid "Jump to the next page of output."
|
||||
msgstr ""
|
||||
|
||||
#: src/utils/cog.py:52
|
||||
#: src/utils/cog.py:53
|
||||
msgctxt "cmd:page_prev"
|
||||
msgid "prev"
|
||||
msgstr ""
|
||||
|
||||
#: src/utils/cog.py:53
|
||||
#: src/utils/cog.py:54
|
||||
msgctxt "cmd:page_prev|desc"
|
||||
msgid "Jump to the previous page of output."
|
||||
msgstr ""
|
||||
|
||||
#: src/utils/cog.py:59
|
||||
#: src/utils/cog.py:60
|
||||
msgctxt "cmd:page_first"
|
||||
msgid "first"
|
||||
msgstr ""
|
||||
|
||||
#: src/utils/cog.py:60
|
||||
#: src/utils/cog.py:61
|
||||
msgctxt "cmd:page_first|desc"
|
||||
msgid "Jump to the first page of output."
|
||||
msgstr ""
|
||||
|
||||
#: src/utils/cog.py:66
|
||||
#: src/utils/cog.py:67
|
||||
msgctxt "cmd:page_last"
|
||||
msgid "last"
|
||||
msgstr ""
|
||||
|
||||
#: src/utils/cog.py:67
|
||||
#: src/utils/cog.py:68
|
||||
msgctxt "cmd:page_last|desc"
|
||||
msgid "Jump to the last page of output."
|
||||
msgstr ""
|
||||
|
||||
#: src/utils/cog.py:73
|
||||
#: src/utils/cog.py:74
|
||||
msgctxt "cmd:page_select"
|
||||
msgid "select"
|
||||
msgstr ""
|
||||
|
||||
#: src/utils/cog.py:74
|
||||
#: src/utils/cog.py:75
|
||||
msgctxt "cmd:page_select|desc"
|
||||
msgid "Select a page of the output to jump to."
|
||||
msgstr ""
|
||||
|
||||
#: src/utils/cog.py:77
|
||||
#: src/utils/cog.py:78
|
||||
msgctxt "cmd:page_select|param:page"
|
||||
msgid "page"
|
||||
msgstr ""
|
||||
|
||||
#: src/utils/cog.py:80
|
||||
#: src/utils/cog.py:81
|
||||
msgctxt "cmd:page_select|param:page|desc"
|
||||
msgid "The page name or number to jump to."
|
||||
msgstr ""
|
||||
|
||||
#: src/utils/cog.py:86
|
||||
#: src/utils/cog.py:87
|
||||
msgctxt "cmd:page_select|error:no_pager"
|
||||
msgid "No pager listening in this channel!"
|
||||
msgstr ""
|
||||
|
||||
#: src/utils/cog.py:97
|
||||
#: src/utils/cog.py:98
|
||||
msgctxt "cmd:page_select|acmpl|error:no_pager"
|
||||
msgid "No active pagers in this channel!"
|
||||
msgstr ""
|
||||
@@ -145,17 +145,17 @@ msgctxt "ui:configui|check|not_permitted"
|
||||
msgid "You have insufficient server permissions to use this UI!"
|
||||
msgstr ""
|
||||
|
||||
#: src/utils/ui/config.py:153
|
||||
#: src/utils/ui/config.py:156
|
||||
msgctxt "ui:configui|button:edit|label"
|
||||
msgid "Edit"
|
||||
msgstr ""
|
||||
|
||||
#: src/utils/ui/config.py:192
|
||||
#: src/utils/ui/config.py:195
|
||||
msgctxt "ui:guild_config_base|button:reset|label"
|
||||
msgid "Reset"
|
||||
msgstr ""
|
||||
|
||||
#: src/utils/ui/config.py:327
|
||||
#: src/utils/ui/config.py:330
|
||||
msgctxt "ui:dashboard|error:section_too_long"
|
||||
msgid ""
|
||||
"Oops, the settings in this configuration section are too large, and I can "
|
||||
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-10-01 16:01+0300\n"
|
||||
"POT-Creation-Date: 2023-10-15 15:58+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"
|
||||
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-10-01 16:01+0300\n"
|
||||
"POT-Creation-Date: 2023-10-15 15:58+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,129 +17,175 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=CHARSET\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: src/tracking/voice/cog.py:661
|
||||
#: src/tracking/voice/session.py:293
|
||||
msgctxt "eventlog|event:voice_session_expired|title"
|
||||
msgid "Member Voice Session Expired"
|
||||
msgstr ""
|
||||
|
||||
#: src/tracking/voice/session.py:297
|
||||
#, possible-python-brace-format
|
||||
msgctxt "eventlog|event:voice_session_expired|desc"
|
||||
msgid ""
|
||||
"{member}'s voice session in {channel} expired because they reached the daily "
|
||||
"voice cap."
|
||||
msgstr ""
|
||||
|
||||
#: src/tracking/voice/session.py:336
|
||||
msgctxt "eventlog|event:voice_session_closed|title"
|
||||
msgid "Member Voice Session Ended"
|
||||
msgstr ""
|
||||
|
||||
#: src/tracking/voice/session.py:340
|
||||
#, possible-python-brace-format
|
||||
msgctxt "eventlog|event:voice_session_closed|desc"
|
||||
msgid "{member} completed their voice session in {channel}."
|
||||
msgstr ""
|
||||
|
||||
#: src/tracking/voice/session.py:352
|
||||
msgctxt "eventlog|event:voice_session_cancelled|title"
|
||||
msgid "Member Voice Session Cancelled"
|
||||
msgstr ""
|
||||
|
||||
#: src/tracking/voice/session.py:356
|
||||
#, possible-python-brace-format
|
||||
msgctxt "eventlog|event:voice_session_cancelled|desc"
|
||||
msgid "{member} left {channel} before their voice session started."
|
||||
msgstr ""
|
||||
|
||||
#: src/tracking/voice/cog.py:518
|
||||
msgctxt "eventlog|event:voice_session_start|title"
|
||||
msgid "Member Joined Tracked Voice Channel"
|
||||
msgstr ""
|
||||
|
||||
#: src/tracking/voice/cog.py:522
|
||||
#, possible-python-brace-format
|
||||
msgctxt "eventlog|event:voice_session_start|desc"
|
||||
msgid "{member} joined {channel}."
|
||||
msgstr ""
|
||||
|
||||
#: src/tracking/voice/cog.py:657
|
||||
msgctxt "cmd:now"
|
||||
msgid "now"
|
||||
msgstr ""
|
||||
|
||||
#: src/tracking/voice/cog.py:664
|
||||
#: src/tracking/voice/cog.py:660
|
||||
msgctxt "cmd:now|desc"
|
||||
msgid ""
|
||||
"Describe what you are working on, or see what your friends are working on!"
|
||||
msgstr ""
|
||||
|
||||
#: src/tracking/voice/cog.py:668
|
||||
#: src/tracking/voice/cog.py:664
|
||||
msgctxt "cmd:now|param:tag"
|
||||
msgid "tag"
|
||||
msgstr ""
|
||||
|
||||
#: src/tracking/voice/cog.py:669
|
||||
#: src/tracking/voice/cog.py:665
|
||||
msgctxt "cmd:now|param:user"
|
||||
msgid "user"
|
||||
msgstr ""
|
||||
|
||||
#: src/tracking/voice/cog.py:670
|
||||
#: src/tracking/voice/cog.py:666
|
||||
msgctxt "cmd:now|param:clear"
|
||||
msgid "clear"
|
||||
msgstr ""
|
||||
|
||||
#: src/tracking/voice/cog.py:675
|
||||
#: src/tracking/voice/cog.py:671
|
||||
msgctxt "cmd:now|param:tag|desc"
|
||||
msgid "Describe what you are working on in 10 characters or less!"
|
||||
msgstr ""
|
||||
|
||||
#: src/tracking/voice/cog.py:679
|
||||
#: src/tracking/voice/cog.py:675
|
||||
msgctxt "cmd:now|param:user|desc"
|
||||
msgid "Check what a friend is working on."
|
||||
msgstr ""
|
||||
|
||||
#: src/tracking/voice/cog.py:683
|
||||
#: src/tracking/voice/cog.py:679
|
||||
msgctxt "cmd:now|param:clear|desc"
|
||||
msgid "Unset your activity tag (or the target user's tag, for moderators)."
|
||||
msgstr ""
|
||||
|
||||
#: src/tracking/voice/cog.py:710
|
||||
#: src/tracking/voice/cog.py:706
|
||||
msgctxt "cmd:now|target:self|error:target_inactive"
|
||||
msgid ""
|
||||
"You have no running session! Join a tracked voice channel to start a session."
|
||||
msgstr ""
|
||||
|
||||
#: src/tracking/voice/cog.py:719
|
||||
#: src/tracking/voice/cog.py:715
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:now|target:other|error:target_inactive"
|
||||
msgid "{mention} has no running session!"
|
||||
msgstr ""
|
||||
|
||||
#: src/tracking/voice/cog.py:734
|
||||
#: src/tracking/voice/cog.py:730
|
||||
msgctxt "cmd:now|target:self|mode:clear|success|title"
|
||||
msgid "Session Tag Cleared"
|
||||
msgstr ""
|
||||
|
||||
#: src/tracking/voice/cog.py:738
|
||||
#: src/tracking/voice/cog.py:734
|
||||
msgctxt "cmd:now|target:self|mode:clear|success|desc"
|
||||
msgid "Successfully unset your session tag."
|
||||
msgstr ""
|
||||
|
||||
#: src/tracking/voice/cog.py:747
|
||||
#: src/tracking/voice/cog.py:743
|
||||
msgctxt "cmd:now|target:other|mode:clear|error:perms|title"
|
||||
msgid "You can't do that!"
|
||||
msgstr ""
|
||||
|
||||
#: src/tracking/voice/cog.py:751
|
||||
#: src/tracking/voice/cog.py:747
|
||||
msgctxt "cmd:now|target:other|mode:clear|error:perms|desc"
|
||||
msgid "You need to be a moderator to set or clear someone else's session tag."
|
||||
msgstr ""
|
||||
|
||||
#: src/tracking/voice/cog.py:761
|
||||
#: src/tracking/voice/cog.py:757
|
||||
msgctxt "cmd:now|target:other|mode:clear|success|title"
|
||||
msgid "Session Tag Cleared!"
|
||||
msgstr ""
|
||||
|
||||
#: src/tracking/voice/cog.py:765
|
||||
#: src/tracking/voice/cog.py:761
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:now|target:other|mode:clear|success|desc"
|
||||
msgid "Cleared {target}'s session tag."
|
||||
msgstr ""
|
||||
|
||||
#: src/tracking/voice/cog.py:777
|
||||
#: src/tracking/voice/cog.py:773
|
||||
msgctxt "cmd:now|target:self|mode:set|success|title"
|
||||
msgid "Session Tag Set!"
|
||||
msgstr ""
|
||||
|
||||
#: src/tracking/voice/cog.py:781
|
||||
#: src/tracking/voice/cog.py:777
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:now|target:self|mode:set|success|desc"
|
||||
msgid "You are now working on `{new_tag}`. Good luck!"
|
||||
msgstr ""
|
||||
|
||||
#: src/tracking/voice/cog.py:790
|
||||
#: src/tracking/voice/cog.py:786
|
||||
msgctxt "cmd:now|target:other|mode:set|error:perms|title"
|
||||
msgid "You can't do that!"
|
||||
msgstr ""
|
||||
|
||||
#: src/tracking/voice/cog.py:794
|
||||
#: src/tracking/voice/cog.py:790
|
||||
msgctxt "cmd:now|target:other|mode:set|error:perms|desc"
|
||||
msgid "You need to be a moderator to set or clear someone else's session tag!"
|
||||
msgstr ""
|
||||
|
||||
#: src/tracking/voice/cog.py:804
|
||||
#: src/tracking/voice/cog.py:800
|
||||
msgctxt "cmd:now|target:other|mode:set|success|title"
|
||||
msgid "Session Tag Set!"
|
||||
msgstr ""
|
||||
|
||||
#: src/tracking/voice/cog.py:808
|
||||
#: src/tracking/voice/cog.py:804
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:now|target:other|mode:set|success|desc"
|
||||
msgid "Set {target}'s session tag to `{new_tag}`."
|
||||
msgstr ""
|
||||
|
||||
#: src/tracking/voice/cog.py:817
|
||||
#: src/tracking/voice/cog.py:813
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:now|target:self|mode:show_with_tag|desc"
|
||||
msgid "You have been working on **`{tag}`** in {channel} since {time}!"
|
||||
msgstr ""
|
||||
|
||||
#: src/tracking/voice/cog.py:822
|
||||
#: src/tracking/voice/cog.py:818
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:now|target:self|mode:show_without_tag|desc"
|
||||
msgid ""
|
||||
@@ -148,7 +194,7 @@ msgid ""
|
||||
"Use `/now <tag>` to set what you are working on."
|
||||
msgstr ""
|
||||
|
||||
#: src/tracking/voice/cog.py:829
|
||||
#: src/tracking/voice/cog.py:825
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:now|target:other|mode:show_with_tag|desc"
|
||||
msgid ""
|
||||
@@ -156,23 +202,23 @@ msgid ""
|
||||
"They have been working on **{tag}** since {time}."
|
||||
msgstr ""
|
||||
|
||||
#: src/tracking/voice/cog.py:835
|
||||
#: src/tracking/voice/cog.py:831
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:now|target:other|mode:show_without_tag|desc"
|
||||
msgid "{target} has been working in {channel} since {time}!"
|
||||
msgstr ""
|
||||
|
||||
#: src/tracking/voice/cog.py:858
|
||||
#: src/tracking/voice/cog.py:854
|
||||
msgctxt "cmd:configure_voice_rates"
|
||||
msgid "voice_rewards"
|
||||
msgstr ""
|
||||
|
||||
#: src/tracking/voice/cog.py:861
|
||||
#: src/tracking/voice/cog.py:857
|
||||
msgctxt "cmd:configure_voice_rates|desc"
|
||||
msgid "Configure Voice tracking rewards and experience"
|
||||
msgstr ""
|
||||
|
||||
#: src/tracking/voice/cog.py:918
|
||||
#: src/tracking/voice/cog.py:914
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:configure_voice_tracking|mode:voice|resp:success|desc"
|
||||
msgid ""
|
||||
@@ -181,7 +227,7 @@ msgid ""
|
||||
"day."
|
||||
msgstr ""
|
||||
|
||||
#: src/tracking/voice/cog.py:929
|
||||
#: src/tracking/voice/cog.py:925
|
||||
#, possible-python-brace-format
|
||||
msgctxt "cmd:configure_voice_tracking|mode:study|resp:success|desc"
|
||||
msgid ""
|
||||
@@ -364,27 +410,27 @@ msgctxt "ui:voice_tracker_config|menu:untracked_channels|placeholder"
|
||||
msgid "Set Untracked Channels"
|
||||
msgstr ""
|
||||
|
||||
#: src/tracking/voice/settings.py:428 src/tracking/voice/settings.py:482
|
||||
#: src/tracking/voice/settings.py:428 src/tracking/voice/settings.py:485
|
||||
msgctxt "ui:voice_tracker_config|mode:voice|embed|title"
|
||||
msgid "Voice Tracker Configuration Panel"
|
||||
msgstr ""
|
||||
|
||||
#: src/tracking/voice/settings.py:433 src/tracking/voice/settings.py:487
|
||||
#: src/tracking/voice/settings.py:433 src/tracking/voice/settings.py:490
|
||||
msgctxt "ui:voice_tracker_config|mode:study|embed|title"
|
||||
msgid "Study Tracker Configuration Panel"
|
||||
msgstr ""
|
||||
|
||||
#: src/tracking/voice/settings.py:472
|
||||
#: src/tracking/voice/settings.py:475
|
||||
msgctxt "ui:voice_tracker_config|menu:untracked_channels|placeholder"
|
||||
msgid "Select Untracked Channels"
|
||||
msgstr ""
|
||||
|
||||
#: src/tracking/voice/settings.py:528
|
||||
#: src/tracking/voice/settings.py:531
|
||||
msgctxt "dash:voice_tracker|title"
|
||||
msgid "Voice Tracker Configuration ({commands[configure voice_rewards]})"
|
||||
msgstr ""
|
||||
|
||||
#: src/tracking/voice/settings.py:532
|
||||
#: src/tracking/voice/settings.py:535
|
||||
msgctxt "dash:voice_tracking|dropdown|placeholder"
|
||||
msgid "Voice Activity Panel"
|
||||
msgstr ""
|
||||
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-10-01 16:01+0300\n"
|
||||
"POT-Creation-Date: 2023-10-15 15:58+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"
|
||||
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-10-01 16:01+0300\n"
|
||||
"POT-Creation-Date: 2023-10-15 15:58+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"
|
||||
|
||||
@@ -24,9 +24,9 @@ for name in conf.config.options('LOGGING_LEVELS', no_defaults=True):
|
||||
class AnalyticsServer:
|
||||
# TODO: Move these to the config
|
||||
# How often to request snapshots
|
||||
snap_period = 120
|
||||
snap_period = 900
|
||||
# How soon after a snapshot failure (e.g. not all shards online) to retry
|
||||
snap_retry_period = 10
|
||||
snap_retry_period = 60
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.db = Database(conf.data['args'])
|
||||
|
||||
@@ -241,7 +241,7 @@ class BabelCog(LionCog):
|
||||
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)
|
||||
appcmds.Choice(name=localestr[:100], value=locale)
|
||||
for locale, localestr in matching
|
||||
]
|
||||
else:
|
||||
@@ -250,7 +250,7 @@ class BabelCog(LionCog):
|
||||
name=t(_p(
|
||||
'acmpl:language|no_match',
|
||||
"No supported languages matching {partial}"
|
||||
)).format(partial=partial),
|
||||
)).format(partial=partial)[:100],
|
||||
value=partial
|
||||
)
|
||||
]
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import gettext
|
||||
from typing import Optional
|
||||
import logging
|
||||
from contextvars import ContextVar
|
||||
from collections import defaultdict
|
||||
from enum import Enum
|
||||
|
||||
import gettext
|
||||
|
||||
from discord.app_commands import Translator, locale_str
|
||||
from discord.enums import Locale
|
||||
|
||||
@@ -70,7 +72,8 @@ class LeoBabel(Translator):
|
||||
async def unload(self):
|
||||
self.translators.clear()
|
||||
|
||||
def get_translator(self, locale, domain):
|
||||
def get_translator(self, locale: Optional[str], domain):
|
||||
locale = locale or SOURCE_LOCALE
|
||||
locale = locale.replace('-', '_') if locale else None
|
||||
if locale == SOURCE_LOCALE:
|
||||
translator = null
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
from .cog import CoreCog
|
||||
from .config import ConfigCog
|
||||
|
||||
from babel.translator import LocalBabel
|
||||
|
||||
|
||||
@@ -8,5 +5,8 @@ babel = LocalBabel('lion-core')
|
||||
|
||||
|
||||
async def setup(bot):
|
||||
from .cog import CoreCog
|
||||
from .config import ConfigCog
|
||||
|
||||
await bot.add_cog(CoreCog(bot))
|
||||
await bot.add_cog(ConfigCog(bot))
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from typing import Optional
|
||||
from collections import defaultdict
|
||||
from weakref import WeakValueDictionary
|
||||
|
||||
import discord
|
||||
import discord.app_commands as appcmd
|
||||
@@ -16,6 +17,7 @@ from .lion import Lions
|
||||
from .lion_guild import GuildConfig
|
||||
from .lion_member import MemberConfig
|
||||
from .lion_user import UserConfig
|
||||
from .hooks import HookedChannel
|
||||
|
||||
|
||||
class keydefaultdict(defaultdict):
|
||||
@@ -54,6 +56,7 @@ class CoreCog(LionCog):
|
||||
self.app_cmd_cache: list[discord.app_commands.AppCommand] = []
|
||||
self.cmd_name_cache: dict[str, discord.app_commands.AppCommand] = {}
|
||||
self.mention_cache: dict[str, str] = keydefaultdict(self.mention_cmd)
|
||||
self.hook_cache: WeakValueDictionary[int, HookedChannel] = WeakValueDictionary()
|
||||
|
||||
async def cog_load(self):
|
||||
# Fetch (and possibly create) core data rows.
|
||||
@@ -91,7 +94,7 @@ class CoreCog(LionCog):
|
||||
cache |= subcache
|
||||
return cache
|
||||
|
||||
def mention_cmd(self, name):
|
||||
def mention_cmd(self, name: str):
|
||||
"""
|
||||
Create an application command mention for the given names.
|
||||
|
||||
@@ -103,6 +106,12 @@ class CoreCog(LionCog):
|
||||
mention = f"</{name}:1110834049204891730>"
|
||||
return mention
|
||||
|
||||
def hooked_channel(self, channelid: int):
|
||||
if (hooked := self.hook_cache.get(channelid, None)) is None:
|
||||
hooked = HookedChannel(self.bot, channelid)
|
||||
self.hook_cache[channelid] = hooked
|
||||
return hooked
|
||||
|
||||
async def cog_unload(self):
|
||||
await self.bot.remove_cog(self.lions.qualified_name)
|
||||
self.bot.remove_listener(self.shard_update_guilds, name='on_guild_join')
|
||||
|
||||
@@ -373,3 +373,6 @@ class CoreData(Registry, name="core"):
|
||||
webhook = discord.Webhook.partial(self.webhookid, self.token, **kwargs)
|
||||
webhook.proxy = conf.bot.get('proxy', None)
|
||||
return webhook
|
||||
|
||||
workouts = Table('workout_sessions')
|
||||
topgg = Table('topgg')
|
||||
|
||||
106
src/core/hooks.py
Normal file
106
src/core/hooks.py
Normal file
@@ -0,0 +1,106 @@
|
||||
from typing import Optional
|
||||
import logging
|
||||
import asyncio
|
||||
|
||||
import discord
|
||||
|
||||
from meta import LionBot
|
||||
|
||||
from .data import CoreData
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
MISSING = discord.utils.MISSING
|
||||
|
||||
|
||||
class HookedChannel:
|
||||
def __init__(self, bot: LionBot, channelid: int):
|
||||
self.bot = bot
|
||||
self.channelid = channelid
|
||||
|
||||
self.webhook: Optional[discord.Webhook] | MISSING = None
|
||||
self.data: Optional[CoreData.LionHook] = None
|
||||
|
||||
self.lock = asyncio.Lock()
|
||||
|
||||
@property
|
||||
def channel(self) -> Optional[discord.TextChannel | discord.VoiceChannel | discord.StageChannel]:
|
||||
if not self.bot.is_ready():
|
||||
raise ValueError("Cannot get hooked channel before ready.")
|
||||
channel = self.bot.get_channel(self.channelid)
|
||||
if channel and not isinstance(channel, (discord.TextChannel, discord.VoiceChannel, discord.StageChannel)):
|
||||
raise ValueError(f"Hooked channel expects GuildChannel not '{channel.__class__.__name__}'")
|
||||
return channel
|
||||
|
||||
async def get_webhook(self) -> Optional[discord.Webhook]:
|
||||
"""
|
||||
Fetch the saved discord.Webhook for this channel.
|
||||
|
||||
Uses cached webhook if possible, but instantiates if required.
|
||||
Does not create a new webhook, use `create_webhook` for that.
|
||||
"""
|
||||
async with self.lock:
|
||||
if self.webhook is MISSING:
|
||||
hook = None
|
||||
elif self.webhook is None:
|
||||
# Fetch webhook data
|
||||
data = await CoreData.LionHook.fetch(self.channelid)
|
||||
if data is not None:
|
||||
# Instantiate Webhook
|
||||
hook = self.webhook = data.as_webhook(client=self.bot)
|
||||
else:
|
||||
self.webhook = MISSING
|
||||
hook = None
|
||||
else:
|
||||
hook = self.webhook
|
||||
|
||||
return hook
|
||||
|
||||
async def create_webhook(self, **creation_kwargs) -> Optional[discord.Webhook]:
|
||||
"""
|
||||
Create and save a new webhook in this channel.
|
||||
|
||||
Returns None if we could not create a new webhook.
|
||||
"""
|
||||
async with self.lock:
|
||||
if self.webhook is not MISSING:
|
||||
# Delete any existing webhook
|
||||
if self.webhook is not None:
|
||||
try:
|
||||
await self.webhook.delete()
|
||||
except discord.HTTPException as e:
|
||||
logger.info(
|
||||
f"Ignoring exception while refreshing webhook for {self.channelid}: {repr(e)}"
|
||||
)
|
||||
await self.bot.core.data.LionHook.table.delete_where(channelid=self.channelid)
|
||||
self.webhook = MISSING
|
||||
self.data = None
|
||||
|
||||
channel = self.channel
|
||||
if channel is not None and channel.permissions_for(channel.guild.me).manage_webhooks:
|
||||
if 'avatar' not in creation_kwargs:
|
||||
avatar = self.bot.user.avatar if self.bot.user else None
|
||||
creation_kwargs['avatar'] = (await avatar.to_file()).fp.read() if avatar else None
|
||||
webhook = await channel.create_webhook(**creation_kwargs)
|
||||
self.data = await self.bot.core.data.LionHook.create(
|
||||
channelid=self.channelid,
|
||||
token=webhook.token,
|
||||
webhookid=webhook.id,
|
||||
)
|
||||
self.webhook = webhook
|
||||
return webhook
|
||||
|
||||
async def invalidate(self, webhook: discord.Webhook):
|
||||
"""
|
||||
Invalidate the given webhook.
|
||||
|
||||
To be used when the webhook has been deleted on the Discord side.
|
||||
"""
|
||||
async with self.lock:
|
||||
if self.webhook is not None and self.webhook is not MISSING and self.webhook.id == webhook.id:
|
||||
# Webhook provided matches current webhook
|
||||
# Delete current webhook
|
||||
self.webhook = MISSING
|
||||
self.data = None
|
||||
await self.bot.core.data.LionHook.table.delete_where(webhookid=webhook.id)
|
||||
@@ -150,7 +150,10 @@ class Lions(LionCog):
|
||||
if (lmember := self.lion_members.get(key, None)) is None:
|
||||
lguild = await self.fetch_guild(guildid, member.guild if member is not None else None)
|
||||
luser = await self.fetch_user(userid, member)
|
||||
data = await self.data.Member.fetch_or_create(guildid, userid)
|
||||
data = await self.data.Member.fetch_or_create(
|
||||
guildid, userid,
|
||||
coins=lguild.config.get('starting_funds').value
|
||||
)
|
||||
lmember = LionMember(self.bot, data, lguild, luser, member)
|
||||
self.lion_members[key] = lmember
|
||||
return lmember
|
||||
@@ -182,8 +185,8 @@ class Lions(LionCog):
|
||||
# Create any member rows that are still missing
|
||||
if missing:
|
||||
new_rows = await self.data.Member.table.insert_many(
|
||||
('guildid', 'userid'),
|
||||
*missing
|
||||
('guildid', 'userid', 'coins'),
|
||||
*((gid, uid, lguilds[gid].config.get('starting_funds').value) for gid, uid in missing)
|
||||
).with_adapter(self.data.Member._make_rows)
|
||||
rows = itertools.chain(rows, new_rows)
|
||||
|
||||
|
||||
@@ -1,20 +1,85 @@
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
from enum import Enum
|
||||
import asyncio
|
||||
import datetime as dt
|
||||
import pytz
|
||||
import discord
|
||||
import logging
|
||||
|
||||
from meta import LionBot
|
||||
from utils.lib import Timezoned
|
||||
from meta import LionBot, conf
|
||||
from meta.logger import log_wrap
|
||||
from utils.lib import Timezoned, utc_now
|
||||
from settings.groups import ModelConfig, SettingDotDict
|
||||
from babel.translator import ctx_locale
|
||||
|
||||
from .hooks import HookedChannel
|
||||
from .data import CoreData
|
||||
from . import babel
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# TODO: Import Settings for Config type hinting
|
||||
pass
|
||||
|
||||
|
||||
_p = babel._p
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
event_fields = {
|
||||
'start': (
|
||||
_p('eventlog|field:start|name', "Start"),
|
||||
"{value}",
|
||||
True,
|
||||
),
|
||||
'expiry': (
|
||||
_p('eventlog|field:expiry|name', "Expires"),
|
||||
"{value}",
|
||||
True,
|
||||
),
|
||||
'roles_given' : (
|
||||
_p('eventlog|field:roles_given|name', "Roles Given"),
|
||||
"{value}",
|
||||
True,
|
||||
),
|
||||
'roles_taken' : (
|
||||
_p('eventlog|field:roles_given|name', "Roles Taken"),
|
||||
"{value}",
|
||||
True,
|
||||
),
|
||||
'coins_earned' : (
|
||||
_p('eventlog|field:coins_earned|name', "Coins Earned"),
|
||||
"{coin} {{value}}".format(coin=conf.emojis.coin),
|
||||
True,
|
||||
),
|
||||
'price' : (
|
||||
_p('eventlog|field:price|name', "Price"),
|
||||
"{coin} {{value}}".format(coin=conf.emojis.coin),
|
||||
True,
|
||||
),
|
||||
'balance' : (
|
||||
_p('eventlog|field:balance|name', "Balance"),
|
||||
"{coin} {{value}}".format(coin=conf.emojis.coin),
|
||||
True,
|
||||
),
|
||||
'refund' : (
|
||||
_p('eventlog|field:refund|name', "Coins Refunded"),
|
||||
"{coin} {{value}}".format(coin=conf.emojis.coin),
|
||||
True,
|
||||
),
|
||||
'memberid': (
|
||||
_p('eventlog|field:memberid|name', "Member"),
|
||||
"<@{value}>",
|
||||
True,
|
||||
),
|
||||
'channelid': (
|
||||
_p('eventlog|field:channelid|name', "Channel"),
|
||||
"<#{value}>",
|
||||
True
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
class VoiceMode(Enum):
|
||||
STUDY = 0
|
||||
VOICE = 1
|
||||
@@ -49,7 +114,16 @@ class LionGuild(Timezoned):
|
||||
No guarantee is made that the client is in the corresponding Guild,
|
||||
or that the corresponding Guild even exists.
|
||||
"""
|
||||
__slots__ = ('bot', 'data', 'guildid', 'config', '_guild', 'voice_lock', '__weakref__')
|
||||
__slots__ = (
|
||||
'bot', 'data',
|
||||
'guildid',
|
||||
'config',
|
||||
'_guild',
|
||||
'voice_lock',
|
||||
'_eventlogger',
|
||||
'_tasks',
|
||||
'__weakref__'
|
||||
)
|
||||
|
||||
Config = GuildConfig
|
||||
settings = Config.settings
|
||||
@@ -68,6 +142,24 @@ class LionGuild(Timezoned):
|
||||
# Avoids voice race-states
|
||||
self.voice_lock = asyncio.Lock()
|
||||
|
||||
# HookedChannel managing the webhook used to send guild event logs
|
||||
# May be None if no event log is set or if the channel does not exist
|
||||
self._eventlogger: Optional[HookedChannel] = None
|
||||
|
||||
# Set of background tasks associated with this guild (e.g. event logs)
|
||||
# In theory we should ensure these are finished before the lguild is gcd
|
||||
# But this is *probably* not an actual problem in practice
|
||||
self._tasks = set()
|
||||
|
||||
@property
|
||||
def eventlogger(self) -> Optional[HookedChannel]:
|
||||
channelid = self.data.event_log_channel
|
||||
if channelid is None:
|
||||
self._eventlogger = None
|
||||
elif self._eventlogger is None or self._eventlogger.channelid != channelid:
|
||||
self._eventlogger = self.bot.core.hooked_channel(channelid)
|
||||
return self._eventlogger
|
||||
|
||||
@property
|
||||
def guild(self):
|
||||
if self._guild is None:
|
||||
@@ -80,7 +172,7 @@ class LionGuild(Timezoned):
|
||||
return GuildMode.StudyGuild
|
||||
|
||||
@property
|
||||
def timezone(self) -> pytz.timezone:
|
||||
def timezone(self) -> str:
|
||||
return self.config.timezone.value
|
||||
|
||||
@property
|
||||
@@ -93,3 +185,168 @@ class LionGuild(Timezoned):
|
||||
"""
|
||||
if self.data.name != guild.name:
|
||||
await self.data.update(name=guild.name)
|
||||
|
||||
@log_wrap(action='get event hook')
|
||||
async def get_event_hook(self) -> Optional[discord.Webhook]:
|
||||
hooked = self.eventlogger
|
||||
ctx_locale.set(self.locale)
|
||||
|
||||
if hooked:
|
||||
hook = await hooked.get_webhook()
|
||||
if hook is not None:
|
||||
pass
|
||||
elif (channel := hooked.channel) is None:
|
||||
# Event log channel doesn't exist
|
||||
pass
|
||||
elif not channel.permissions_for(channel.guild.me).manage_webhooks:
|
||||
# Cannot create a webhook here
|
||||
if channel.permissions_for(channel.guild.me).send_messages:
|
||||
t = self.bot.translator.t
|
||||
try:
|
||||
await channel.send(t(_p(
|
||||
'eventlog|error:manage_webhooks',
|
||||
"This channel is configured as an event log, "
|
||||
"but I am missing the 'Manage Webhooks' permission here."
|
||||
)))
|
||||
except discord.HTTPException:
|
||||
pass
|
||||
else:
|
||||
# We should be able to create the hook
|
||||
t = self.bot.translator.t
|
||||
try:
|
||||
hook = await hooked.create_webhook(
|
||||
name=t(_p(
|
||||
'eventlog|create|name',
|
||||
"{bot_name} Event Log"
|
||||
)).format(bot_name=channel.guild.me.name),
|
||||
reason=t(_p(
|
||||
'eventlog|create|audit_reason',
|
||||
"Creating event log webhook"
|
||||
)),
|
||||
)
|
||||
except discord.HTTPException:
|
||||
logger.warning(
|
||||
f"Unexpected exception while creating event log webhook for <gid: {self.guildid}>",
|
||||
exc_info=True
|
||||
)
|
||||
return hook
|
||||
|
||||
@log_wrap(action="Log Event")
|
||||
async def _log_event(self, embed: discord.Embed, retry=True):
|
||||
logger.debug(f"Logging event log event: {embed.to_dict()}")
|
||||
|
||||
hook = await self.get_event_hook()
|
||||
if hook is not None:
|
||||
try:
|
||||
await hook.send(embed=embed)
|
||||
except discord.NotFound:
|
||||
logger.info(
|
||||
f"Event log in <gid: {self.guildid}> invalidated. Recreating: {retry}"
|
||||
)
|
||||
hooked = self.eventlogger
|
||||
if hooked is not None:
|
||||
await hooked.invalidate(hook)
|
||||
if retry:
|
||||
await self._log_event(embed, retry=False)
|
||||
except discord.HTTPException:
|
||||
logger.warning(
|
||||
f"Discord exception occurred sending event log event: {embed.to_dict()}.",
|
||||
exc_info=True
|
||||
)
|
||||
except Exception:
|
||||
logger.exception(
|
||||
f"Unknown exception occurred sending event log event: {embed.to_dict()}."
|
||||
)
|
||||
|
||||
def log_event(self,
|
||||
title: Optional[str]=None, description: Optional[str]=None,
|
||||
timestamp: Optional[dt.datetime]=None,
|
||||
*,
|
||||
embed: Optional[discord.Embed] = None,
|
||||
fields: dict[str, tuple[str, bool]]={},
|
||||
errors: list[str]=[],
|
||||
**kwargs: str | int):
|
||||
"""
|
||||
Synchronously log an event to the guild event log.
|
||||
|
||||
Does nothing if the event log has not been set up.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
title: str
|
||||
Embed title
|
||||
description: str
|
||||
Embed description
|
||||
timestamp: dt.datetime
|
||||
Embed timestamp. Defaults to `now` if not given.
|
||||
embed: discord.Embed
|
||||
Optional base embed to use.
|
||||
May be used to completely customise log message.
|
||||
fields: dict[str, tuple[str, bool]]
|
||||
Optional embed fields to add.
|
||||
errors: list[str]
|
||||
Optional list of errors to add.
|
||||
Errors will always be added last.
|
||||
kwargs: str | int
|
||||
Optional embed fields to add to the embed.
|
||||
These differ from `fields` in that the kwargs keys will be automatically matched and localised
|
||||
if possible.
|
||||
These will be added before the `fields` given.
|
||||
"""
|
||||
t = self.bot.translator.t
|
||||
|
||||
# Build embed
|
||||
if embed is not None:
|
||||
base = embed
|
||||
else:
|
||||
base = discord.Embed(
|
||||
colour=(discord.Colour.brand_red() if errors else discord.Colour.dark_orange())
|
||||
)
|
||||
if description is not None:
|
||||
base.description = description
|
||||
if title is not None:
|
||||
base.title = title
|
||||
if timestamp is not None:
|
||||
base.timestamp = timestamp
|
||||
else:
|
||||
base.timestamp = utc_now()
|
||||
|
||||
# Add embed fields
|
||||
for key, value in kwargs.items():
|
||||
if value is None:
|
||||
continue
|
||||
if key in event_fields:
|
||||
_field_name, _field_value, inline = event_fields[key]
|
||||
field_name = t(_field_name, locale=self.locale)
|
||||
field_value = _field_value.format(value=value)
|
||||
else:
|
||||
field_name = key
|
||||
field_value = value
|
||||
inline = False
|
||||
base.add_field(
|
||||
name=field_name,
|
||||
value=field_value,
|
||||
inline=inline
|
||||
)
|
||||
|
||||
for key, (value, inline) in fields.items():
|
||||
base.add_field(
|
||||
name=key,
|
||||
value=value,
|
||||
inline=inline,
|
||||
)
|
||||
|
||||
if errors:
|
||||
error_name = t(_p(
|
||||
'eventlog|field:errors|name',
|
||||
"Errors"
|
||||
))
|
||||
error_value = '\n'.join(f"- {line}" for line in errors)
|
||||
base.add_field(
|
||||
name=error_name, value=error_value, inline=False
|
||||
)
|
||||
|
||||
# Send embed
|
||||
task = asyncio.create_task(self._log_event(embed=base), name='event-log')
|
||||
self._tasks.add(task)
|
||||
task.add_done_callback(self._tasks.discard)
|
||||
|
||||
@@ -2,6 +2,7 @@ from typing import Optional
|
||||
import datetime as dt
|
||||
import pytz
|
||||
import discord
|
||||
import logging
|
||||
|
||||
from meta import LionBot
|
||||
from utils.lib import Timezoned
|
||||
@@ -13,6 +14,9 @@ from .lion_user import LionUser
|
||||
from .lion_guild import LionGuild
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MemberConfig(ModelConfig):
|
||||
settings = SettingDotDict()
|
||||
_model_settings = set()
|
||||
@@ -103,14 +107,30 @@ class LionMember(Timezoned):
|
||||
|
||||
async def remove_role(self, role: discord.Role):
|
||||
member = await self.fetch_member()
|
||||
if member is not None and role in member.roles:
|
||||
if member is not None:
|
||||
try:
|
||||
await member.remove_roles(role)
|
||||
except discord.HTTPException:
|
||||
except discord.HTTPException as e:
|
||||
# TODO: Logging, audit logging
|
||||
pass
|
||||
logger.warning(
|
||||
"Lion role removal failed for "
|
||||
f"<uid: {member.id}>, <gid: {member.guild.id}>, <rid: {role.id}>. "
|
||||
f"Error: {repr(e)}",
|
||||
)
|
||||
else:
|
||||
if role not in member.roles:
|
||||
logger.info(
|
||||
f"Removed role <rid: {role.id}> from member <uid: {self.userid}> in <gid: {self.guildid}>"
|
||||
)
|
||||
else:
|
||||
logger.error(
|
||||
f"Tried to remove role <rid: {role.id}> "
|
||||
f"from member <uid: {self.userid}> in <gid: {self.guildid}>. "
|
||||
"Role remove succeeded, but member still has the role."
|
||||
)
|
||||
else:
|
||||
# Remove the role from persistent role storage
|
||||
cog = self.bot.get_cog('MemberAdminCog')
|
||||
if cog:
|
||||
await cog.absent_remove_role(self.guildid, self.userid, role.id)
|
||||
logger.info(f"Removed role <rid: {role.id}> from absent lion <uid: {self.userid}> in <gid: {self.guildid}>")
|
||||
|
||||
2
src/gui
2
src/gui
Submodule src/gui updated: 24e94d10e2...f2760218ef
@@ -46,7 +46,7 @@ class LionBot(Bot):
|
||||
# self.appdata = appdata
|
||||
self.config = config
|
||||
self.app_ipc = app_ipc
|
||||
self.core: Optional['CoreCog'] = None
|
||||
self.core: 'CoreCog' = None
|
||||
self.translator = translator
|
||||
|
||||
self.system_monitor = SystemMonitor()
|
||||
|
||||
@@ -38,6 +38,7 @@ class LionTree(CommandTree):
|
||||
await self.error_reply(interaction, embed)
|
||||
except Exception:
|
||||
logger.exception(f"Unhandled exception in interaction: {interaction}", extra={'action': 'TreeError'})
|
||||
if interaction.type is not InteractionType.autocomplete:
|
||||
embed = self.bugsplat(interaction, error)
|
||||
await self.error_reply(interaction, embed)
|
||||
|
||||
@@ -144,7 +145,10 @@ class LionTree(CommandTree):
|
||||
raise AppCommandError(
|
||||
'This should not happen, but there is no focused element. This is a Discord bug.'
|
||||
)
|
||||
try:
|
||||
await command._invoke_autocomplete(interaction, focused, namespace)
|
||||
except Exception as e:
|
||||
await self.on_error(interaction, e)
|
||||
return
|
||||
|
||||
set_logging_context(action=f"Run {command.qualified_name}")
|
||||
|
||||
@@ -6,8 +6,6 @@ babel = LocalBabel('config')
|
||||
|
||||
|
||||
async def setup(bot):
|
||||
from .general import GeneralSettingsCog
|
||||
from .cog import DashCog
|
||||
from .cog import GuildConfigCog
|
||||
|
||||
await bot.add_cog(GeneralSettingsCog(bot))
|
||||
await bot.add_cog(DashCog(bot))
|
||||
await bot.add_cog(GuildConfigCog(bot))
|
||||
|
||||
@@ -1,24 +1,35 @@
|
||||
from typing import Optional
|
||||
|
||||
import discord
|
||||
from discord import app_commands as appcmds
|
||||
from discord.ext import commands as cmds
|
||||
|
||||
from meta import LionBot, LionContext, LionCog
|
||||
from wards import low_management_ward
|
||||
|
||||
from . import babel
|
||||
from .dashboard import GuildDashboard
|
||||
from .settings import GeneralSettings
|
||||
from .settingui import GeneralSettingUI
|
||||
|
||||
_p = babel._p
|
||||
|
||||
|
||||
class DashCog(LionCog):
|
||||
class GuildConfigCog(LionCog):
|
||||
depends = {'CoreCog'}
|
||||
|
||||
def __init__(self, bot: LionBot):
|
||||
self.bot = bot
|
||||
self.settings = GeneralSettings()
|
||||
|
||||
async def cog_load(self):
|
||||
...
|
||||
self.bot.core.guild_config.register_model_setting(GeneralSettings.Timezone)
|
||||
self.bot.core.guild_config.register_model_setting(GeneralSettings.EventLog)
|
||||
|
||||
async def cog_unload(self):
|
||||
...
|
||||
configcog = self.bot.get_cog('ConfigCog')
|
||||
if configcog is None:
|
||||
raise ValueError("Cannot load GuildConfigCog without ConfigCog")
|
||||
self.crossload_group(self.configure_group, configcog.configure_group)
|
||||
|
||||
@cmds.hybrid_command(
|
||||
name="dashboard",
|
||||
@@ -27,6 +38,75 @@ class DashCog(LionCog):
|
||||
@appcmds.guild_only
|
||||
@appcmds.default_permissions(manage_guild=True)
|
||||
async def dashboard_cmd(self, ctx: LionContext):
|
||||
if not ctx.guild or not ctx.interaction:
|
||||
return
|
||||
|
||||
ui = GuildDashboard(self.bot, ctx.guild, ctx.author.id, ctx.channel.id)
|
||||
await ui.run(ctx.interaction)
|
||||
await ui.wait()
|
||||
|
||||
# ----- Configuration -----
|
||||
@LionCog.placeholder_group
|
||||
@cmds.hybrid_group("configure", with_app_command=False)
|
||||
async def configure_group(self, ctx: LionContext):
|
||||
# Placeholder configure group command.
|
||||
...
|
||||
|
||||
@configure_group.command(
|
||||
name=_p('cmd:configure_general', "general"),
|
||||
description=_p('cmd:configure_general|desc', "General configuration panel")
|
||||
)
|
||||
@appcmds.rename(
|
||||
timezone=GeneralSettings.Timezone._display_name,
|
||||
event_log=GeneralSettings.EventLog._display_name,
|
||||
)
|
||||
@appcmds.describe(
|
||||
timezone=GeneralSettings.Timezone._desc,
|
||||
event_log=GeneralSettings.EventLog._desc,
|
||||
)
|
||||
@appcmds.guild_only()
|
||||
@appcmds.default_permissions(manage_guild=True)
|
||||
@low_management_ward
|
||||
async def cmd_configure_general(self, ctx: LionContext,
|
||||
timezone: Optional[str] = None,
|
||||
event_log: Optional[discord.TextChannel] = None,
|
||||
):
|
||||
t = self.bot.translator.t
|
||||
|
||||
# Typechecker guards because they don't understand the check ward
|
||||
if not ctx.guild:
|
||||
return
|
||||
if not ctx.interaction:
|
||||
return
|
||||
await ctx.interaction.response.defer(thinking=True)
|
||||
|
||||
modified = []
|
||||
|
||||
if timezone is not None:
|
||||
setting = self.settings.Timezone
|
||||
instance = await setting.from_string(ctx.guild.id, timezone)
|
||||
modified.append(instance)
|
||||
|
||||
if event_log is not None:
|
||||
setting = self.settings.EventLog
|
||||
instance = await setting.from_value(ctx.guild.id, event_log)
|
||||
modified.append(instance)
|
||||
|
||||
if modified:
|
||||
ack_lines = []
|
||||
for instance in modified:
|
||||
await instance.write()
|
||||
ack_lines.append(instance.update_message)
|
||||
|
||||
tick = self.bot.config.emojis.tick
|
||||
embed = discord.Embed(
|
||||
colour=discord.Colour.brand_green(),
|
||||
description='\n'.join(f"{tick} {line}" for line in ack_lines)
|
||||
)
|
||||
await ctx.reply(embed=embed)
|
||||
|
||||
if ctx.channel.id not in GeneralSettingUI._listening or not modified:
|
||||
ui = GeneralSettingUI(self.bot, ctx.guild.id, ctx.channel.id)
|
||||
await ui.run(ctx.interaction)
|
||||
await ui.wait()
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ from modules.statistics.settings import StatisticsDashboard
|
||||
from modules.member_admin.settingui import MemberAdminDashboard
|
||||
from modules.moderation.settingui import ModerationDashboard
|
||||
from modules.video_channels.settingui import VideoDashboard
|
||||
from modules.config.settingui import GeneralDashboard
|
||||
|
||||
|
||||
from . import babel, logger
|
||||
@@ -35,7 +36,7 @@ class GuildDashboard(BasePager):
|
||||
Paged UI providing an overview of the guild configuration.
|
||||
"""
|
||||
pages = [
|
||||
(MemberAdminDashboard, LocaleDashboard, EconomyDashboard,),
|
||||
(MemberAdminDashboard, LocaleDashboard, EconomyDashboard, GeneralDashboard,),
|
||||
(ModerationDashboard, VideoDashboard,),
|
||||
(VoiceTrackerDashboard, TextTrackerDashboard, RankDashboard, StatisticsDashboard,),
|
||||
(TasklistDashboard, RoomDashboard, TimerDashboard,),
|
||||
@@ -185,23 +186,28 @@ class GuildDashboard(BasePager):
|
||||
# ----- UI Control -----
|
||||
async def reload(self, *args):
|
||||
self._cached_pages.clear()
|
||||
if not self._original.is_expired():
|
||||
if self._original and not self._original.is_expired():
|
||||
await self.redraw()
|
||||
else:
|
||||
await self.close()
|
||||
|
||||
async def refresh(self):
|
||||
await super().refresh()
|
||||
await self.config_menu_refresh()
|
||||
self._layout = [
|
||||
self.set_layout(
|
||||
(self.config_menu,),
|
||||
(self.prev_page_button, self.next_page_button)
|
||||
]
|
||||
)
|
||||
|
||||
async def redraw(self, *args):
|
||||
await self.refresh()
|
||||
if self._original and not self._original.is_expired():
|
||||
await self._original.edit_original_response(
|
||||
**self.current_page.edit_args,
|
||||
view=self
|
||||
)
|
||||
else:
|
||||
await self.close()
|
||||
|
||||
async def run(self, interaction: discord.Interaction):
|
||||
await self.refresh()
|
||||
|
||||
@@ -26,48 +26,6 @@ from . import babel
|
||||
_p = babel._p
|
||||
|
||||
|
||||
class GeneralSettings(SettingGroup):
|
||||
class Timezone(ModelData, TimezoneSetting):
|
||||
"""
|
||||
Guild timezone configuration.
|
||||
|
||||
Exposed via `/configure general timezone:`, and the standard interface.
|
||||
The `timezone` setting acts as the default timezone for all members,
|
||||
and the timezone used to display guild-wide statistics.
|
||||
"""
|
||||
setting_id = 'timezone'
|
||||
_event = 'guild_setting_update_timezone'
|
||||
|
||||
_display_name = _p('guildset:timezone', "timezone")
|
||||
_desc = _p(
|
||||
'guildset:timezone|desc',
|
||||
"Guild timezone for statistics display."
|
||||
)
|
||||
_long_desc = _p(
|
||||
'guildset:timezone|long_desc',
|
||||
"Guild-wide timezone. "
|
||||
"Used to determine start of the day for the leaderboards, "
|
||||
"and as the default statistics timezone for members who have not set one."
|
||||
)
|
||||
_default = 'UTC'
|
||||
|
||||
_model = CoreData.Guild
|
||||
_column = CoreData.Guild.timezone.name
|
||||
|
||||
@property
|
||||
def update_message(self):
|
||||
t = ctx_translator.get().t
|
||||
return t(_p(
|
||||
'guildset:timezone|response',
|
||||
"The guild timezone has been set to `{timezone}`."
|
||||
)).format(timezone=self.data)
|
||||
|
||||
@property
|
||||
def set_str(self):
|
||||
bot = ctx_bot.get()
|
||||
return bot.core.mention_cmd('configure general') if bot else None
|
||||
|
||||
|
||||
class GeneralSettingsCog(LionCog):
|
||||
depends = {'CoreCog'}
|
||||
|
||||
@@ -95,16 +53,20 @@ class GeneralSettingsCog(LionCog):
|
||||
description=_p('cmd:configure_general|desc', "General configuration panel")
|
||||
)
|
||||
@appcmds.rename(
|
||||
timezone=GeneralSettings.Timezone._display_name
|
||||
timezone=GeneralSettings.Timezone._display_name,
|
||||
event_log=GeneralSettings.EventLog._display_name,
|
||||
)
|
||||
@appcmds.describe(
|
||||
timezone=GeneralSettings.Timezone._desc
|
||||
timezone=GeneralSettings.Timezone._desc,
|
||||
event_log=GeneralSettings.EventLog._display_name,
|
||||
)
|
||||
@appcmds.guild_only()
|
||||
@appcmds.default_permissions(manage_guild=True)
|
||||
@low_management_ward
|
||||
async def cmd_configure_general(self, ctx: LionContext,
|
||||
timezone: Optional[str] = None):
|
||||
timezone: Optional[str] = None,
|
||||
event_log: Optional[discord.TextChannel] = None,
|
||||
):
|
||||
t = self.bot.translator.t
|
||||
|
||||
# Typechecker guards because they don't understand the check ward
|
||||
|
||||
110
src/modules/config/settings.py
Normal file
110
src/modules/config/settings.py
Normal file
@@ -0,0 +1,110 @@
|
||||
from typing import Optional
|
||||
import discord
|
||||
|
||||
from settings import ModelData
|
||||
from settings.setting_types import TimezoneSetting, ChannelSetting
|
||||
from settings.groups import SettingGroup
|
||||
|
||||
from meta.context import ctx_bot
|
||||
from meta.errors import UserInputError
|
||||
from core.data import CoreData
|
||||
from babel.translator import ctx_translator
|
||||
|
||||
from . import babel
|
||||
|
||||
_p = babel._p
|
||||
|
||||
|
||||
class GeneralSettings(SettingGroup):
|
||||
class Timezone(ModelData, TimezoneSetting):
|
||||
"""
|
||||
Guild timezone configuration.
|
||||
|
||||
Exposed via `/configure general timezone:`, and the standard interface.
|
||||
The `timezone` setting acts as the default timezone for all members,
|
||||
and the timezone used to display guild-wide statistics.
|
||||
"""
|
||||
setting_id = 'timezone'
|
||||
_event = 'guildset_timezone'
|
||||
_set_cmd = 'configure general'
|
||||
|
||||
_display_name = _p('guildset:timezone', "timezone")
|
||||
_desc = _p(
|
||||
'guildset:timezone|desc',
|
||||
"Guild timezone for statistics display."
|
||||
)
|
||||
_long_desc = _p(
|
||||
'guildset:timezone|long_desc',
|
||||
"Guild-wide timezone. "
|
||||
"Used to determine start of the day for the leaderboards, "
|
||||
"and as the default statistics timezone for members who have not set one."
|
||||
)
|
||||
_default = 'UTC'
|
||||
|
||||
_model = CoreData.Guild
|
||||
_column = CoreData.Guild.timezone.name
|
||||
|
||||
@property
|
||||
def update_message(self):
|
||||
t = ctx_translator.get().t
|
||||
return t(_p(
|
||||
'guildset:timezone|response',
|
||||
"The guild timezone has been set to `{timezone}`."
|
||||
)).format(timezone=self.data)
|
||||
|
||||
class EventLog(ModelData, ChannelSetting):
|
||||
"""
|
||||
Guild event log channel.
|
||||
"""
|
||||
setting_id = 'eventlog'
|
||||
_event = 'guildset_eventlog'
|
||||
_set_cmd = 'configure general'
|
||||
|
||||
_display_name = _p('guildset:eventlog', "event_log")
|
||||
_desc = _p(
|
||||
'guildset:eventlog|desc',
|
||||
"My audit log channel where I send server actions and events (e.g. rankgs and expiring roles)."
|
||||
)
|
||||
_long_desc = _p(
|
||||
'guildset:eventlog|long_desc',
|
||||
"If configured, I will log most significant actions taken "
|
||||
"or events which occur through my interface, into this channel. "
|
||||
"Logged events include, for example:\n"
|
||||
"- Member voice activity\n"
|
||||
"- Roles equipped and expiring from rolemenus\n"
|
||||
"- Privated rooms rented and expiring\n"
|
||||
"- Activity ranks earned\n"
|
||||
"I must have the 'Manage Webhooks' permission in this channel."
|
||||
)
|
||||
|
||||
_model = CoreData.Guild
|
||||
_column = CoreData.Guild.event_log_channel.name
|
||||
|
||||
|
||||
@classmethod
|
||||
async def _check_value(cls, parent_id: int, value: Optional[discord.abc.GuildChannel], **kwargs):
|
||||
if value is not None:
|
||||
t = ctx_translator.get().t
|
||||
if not value.permissions_for(value.guild.me).manage_webhooks:
|
||||
raise UserInputError(
|
||||
t(_p(
|
||||
'guildset:eventlog|check_value|error:perms|perm:manage_webhooks',
|
||||
"Cannot set {channel} as an event log! I lack the 'Manage Webhooks' permission there."
|
||||
)).format(channel=value)
|
||||
)
|
||||
|
||||
@property
|
||||
def update_message(self):
|
||||
t = ctx_translator.get().t
|
||||
channel = self.value
|
||||
if channel is not None:
|
||||
response = t(_p(
|
||||
'guildset:eventlog|response|set',
|
||||
"Events will now be logged to {channel}"
|
||||
)).format(channel=channel.mention)
|
||||
else:
|
||||
response = t(_p(
|
||||
'guildset:eventlog|response|unset',
|
||||
"Guild events will no longer be logged."
|
||||
))
|
||||
return response
|
||||
105
src/modules/config/settingui.py
Normal file
105
src/modules/config/settingui.py
Normal file
@@ -0,0 +1,105 @@
|
||||
import asyncio
|
||||
|
||||
import discord
|
||||
from discord.ui.select import select, ChannelSelect
|
||||
|
||||
from meta import LionBot
|
||||
from meta.errors import UserInputError
|
||||
|
||||
from utils.ui import ConfigUI, DashboardSection
|
||||
from utils.lib import MessageArgs
|
||||
|
||||
from . import babel
|
||||
from .settings import GeneralSettings
|
||||
|
||||
|
||||
_p = babel._p
|
||||
|
||||
|
||||
class GeneralSettingUI(ConfigUI):
|
||||
setting_classes = (
|
||||
GeneralSettings.Timezone,
|
||||
GeneralSettings.EventLog,
|
||||
)
|
||||
|
||||
def __init__(self, bot: LionBot, guildid: int, channelid: int, **kwargs):
|
||||
self.settings = bot.get_cog('GuildConfigCog').settings
|
||||
super().__init__(bot, guildid, channelid, **kwargs)
|
||||
|
||||
# ----- UI Components -----
|
||||
# Event log
|
||||
@select(
|
||||
cls=ChannelSelect,
|
||||
channel_types=[discord.ChannelType.text, discord.ChannelType.voice],
|
||||
placeholder='EVENT_LOG_PLACEHOLDER',
|
||||
min_values=0, max_values=1,
|
||||
)
|
||||
async def eventlog_menu(self, selection: discord.Interaction, selected: ChannelSelect):
|
||||
"""
|
||||
Single channel selector for the event log.
|
||||
"""
|
||||
await selection.response.defer(thinking=True, ephemeral=True)
|
||||
|
||||
setting = self.get_instance(GeneralSettings.EventLog)
|
||||
|
||||
value = selected.values[0].resolve() if selected.values else None
|
||||
setting = await setting.from_value(self.guildid, value)
|
||||
await setting.write()
|
||||
await selection.delete_original_response()
|
||||
|
||||
async def eventlog_menu_refresh(self):
|
||||
menu = self.eventlog_menu
|
||||
t = self.bot.translator.t
|
||||
menu.placeholder = t(_p(
|
||||
'ui:general_config|menu:event_log|placeholder',
|
||||
"Select Event Log"
|
||||
))
|
||||
|
||||
# ----- UI Flow -----
|
||||
async def make_message(self) -> MessageArgs:
|
||||
t = self.bot.translator.t
|
||||
title = t(_p(
|
||||
'ui:general_config|embed:title',
|
||||
"General Configuration"
|
||||
))
|
||||
embed = discord.Embed(
|
||||
title=title,
|
||||
colour=discord.Colour.orange()
|
||||
)
|
||||
for setting in self.instances:
|
||||
embed.add_field(**setting.embed_field, inline=False)
|
||||
|
||||
return MessageArgs(embed=embed)
|
||||
|
||||
async def reload(self):
|
||||
self.instances = [
|
||||
await setting.get(self.guildid)
|
||||
for setting in self.setting_classes
|
||||
]
|
||||
|
||||
async def refresh_components(self):
|
||||
to_refresh = (
|
||||
self.edit_button_refresh(),
|
||||
self.close_button_refresh(),
|
||||
self.reset_button_refresh(),
|
||||
self.eventlog_menu_refresh(),
|
||||
)
|
||||
await asyncio.gather(*to_refresh)
|
||||
|
||||
self.set_layout(
|
||||
(self.eventlog_menu,),
|
||||
(self.edit_button, self.reset_button, self.close_button,),
|
||||
)
|
||||
|
||||
|
||||
class GeneralDashboard(DashboardSection):
|
||||
section_name = _p(
|
||||
"dash:general|title",
|
||||
"General Configuration ({commands[configure general]})"
|
||||
)
|
||||
_option_name = _p(
|
||||
"dash:general|option|name",
|
||||
"General Configuration Panel"
|
||||
)
|
||||
configui = GeneralSettingUI
|
||||
setting_classes = configui.setting_classes
|
||||
@@ -56,6 +56,7 @@ class Economy(LionCog):
|
||||
|
||||
self.bot.core.guild_config.register_model_setting(self.settings.AllowTransfers)
|
||||
self.bot.core.guild_config.register_model_setting(self.settings.CoinsPerXP)
|
||||
self.bot.core.guild_config.register_model_setting(self.settings.StartingFunds)
|
||||
|
||||
configcog = self.bot.get_cog('ConfigCog')
|
||||
if configcog is None:
|
||||
@@ -298,6 +299,20 @@ class Economy(LionCog):
|
||||
).set(
|
||||
coins=set_to
|
||||
)
|
||||
ctx.lguild.log_event(
|
||||
title=t(_p(
|
||||
'eventlog|event:economy_set|title',
|
||||
"Moderator Set Economy Balance"
|
||||
)),
|
||||
description=t(_p(
|
||||
'eventlog|event:economy_set|desc',
|
||||
"{moderator} set {target}'s balance to {amount}."
|
||||
)).format(
|
||||
moderator=ctx.author.mention,
|
||||
target=target.mention,
|
||||
amount=f"{cemoji}**{set_to}**",
|
||||
)
|
||||
)
|
||||
else:
|
||||
if role:
|
||||
if role.is_default():
|
||||
@@ -359,6 +374,20 @@ class Economy(LionCog):
|
||||
amount=add,
|
||||
new_amount=results[0]['coins']
|
||||
)
|
||||
ctx.lguild.log_event(
|
||||
title=t(_p(
|
||||
'eventlog|event:economy_add|title',
|
||||
"Moderator Modified Economy Balance"
|
||||
)),
|
||||
description=t(_p(
|
||||
'eventlog|event:economy_set|desc',
|
||||
"{moderator} added {amount} to {target}'s balance."
|
||||
)).format(
|
||||
moderator=ctx.author.mention,
|
||||
target=target.mention,
|
||||
amount=f"{cemoji}**{add}**",
|
||||
)
|
||||
)
|
||||
|
||||
title = t(_np(
|
||||
'cmd:economy_balance|embed:success|title',
|
||||
@@ -781,7 +810,20 @@ class Economy(LionCog):
|
||||
await ctx.alion.data.update(coins=(Member.coins - amount))
|
||||
await target_lion.data.update(coins=(Member.coins + amount))
|
||||
|
||||
# TODO: Audit trail
|
||||
ctx.lguild.log_event(
|
||||
title=t(_p(
|
||||
"eventlog|event:send|title",
|
||||
"Coins Transferred"
|
||||
)),
|
||||
description=t(_p(
|
||||
'eventlog|event:send|desc',
|
||||
"{source} gifted {amount} to {target}"
|
||||
)).format(
|
||||
source=ctx.author.mention,
|
||||
target=target.mention,
|
||||
amount=f"{self.bot.config.emojis.coin}**{amount}**"
|
||||
),
|
||||
)
|
||||
await asyncio.create_task(wrapped(), name="wrapped-send")
|
||||
|
||||
# Message target
|
||||
@@ -847,11 +889,13 @@ class Economy(LionCog):
|
||||
)
|
||||
@appcmds.rename(
|
||||
allow_transfers=EconomySettings.AllowTransfers._display_name,
|
||||
coins_per_xp=EconomySettings.CoinsPerXP._display_name
|
||||
coins_per_xp=EconomySettings.CoinsPerXP._display_name,
|
||||
starting_funds=EconomySettings.StartingFunds._display_name,
|
||||
)
|
||||
@appcmds.describe(
|
||||
allow_transfers=EconomySettings.AllowTransfers._desc,
|
||||
coins_per_xp=EconomySettings.CoinsPerXP._desc
|
||||
coins_per_xp=EconomySettings.CoinsPerXP._desc,
|
||||
starting_funds=EconomySettings.StartingFunds._desc,
|
||||
)
|
||||
@appcmds.choices(
|
||||
allow_transfers=[
|
||||
@@ -863,7 +907,9 @@ class Economy(LionCog):
|
||||
@moderator_ward
|
||||
async def configure_economy(self, ctx: LionContext,
|
||||
allow_transfers: Optional[appcmds.Choice[int]] = None,
|
||||
coins_per_xp: Optional[appcmds.Range[int, 0, 2**15]] = None):
|
||||
coins_per_xp: Optional[appcmds.Range[int, 0, MAX_COINS]] = None,
|
||||
starting_funds: Optional[appcmds.Range[int, 0, MAX_COINS]] = None,
|
||||
):
|
||||
t = self.bot.translator.t
|
||||
if not ctx.interaction:
|
||||
return
|
||||
@@ -872,6 +918,7 @@ class Economy(LionCog):
|
||||
|
||||
setting_allow_transfers = ctx.lguild.config.get('allow_transfers')
|
||||
setting_coins_per_xp = ctx.lguild.config.get('coins_per_xp')
|
||||
setting_starting_funds = ctx.lguild.config.get('starting_funds')
|
||||
|
||||
modified = []
|
||||
if allow_transfers is not None:
|
||||
@@ -882,6 +929,10 @@ class Economy(LionCog):
|
||||
setting_coins_per_xp.data = coins_per_xp
|
||||
await setting_coins_per_xp.write()
|
||||
modified.append(setting_coins_per_xp)
|
||||
if starting_funds is not None:
|
||||
setting_starting_funds.data = starting_funds
|
||||
await setting_starting_funds.write()
|
||||
modified.append(setting_starting_funds)
|
||||
|
||||
if modified:
|
||||
desc = '\n'.join(f"{conf.emojis.tick} {setting.update_message}" for setting in modified)
|
||||
|
||||
@@ -15,6 +15,7 @@ from meta.config import conf
|
||||
from meta.sharding import THIS_SHARD
|
||||
from meta.logger import log_wrap
|
||||
from core.data import CoreData
|
||||
from core.setting_types import CoinSetting
|
||||
from babel.translator import ctx_translator
|
||||
|
||||
from . import babel, logger
|
||||
@@ -29,7 +30,7 @@ class EconomySettings(SettingGroup):
|
||||
coins_per_100xp
|
||||
allow_transfers
|
||||
"""
|
||||
class CoinsPerXP(ModelData, IntegerSetting):
|
||||
class CoinsPerXP(ModelData, CoinSetting):
|
||||
setting_id = 'coins_per_xp'
|
||||
|
||||
_display_name = _p('guildset:coins_per_xp', "coins_per_100xp")
|
||||
@@ -111,3 +112,32 @@ class EconomySettings(SettingGroup):
|
||||
coin=conf.emojis.coin
|
||||
)
|
||||
return formatted
|
||||
|
||||
class StartingFunds(ModelData, CoinSetting):
|
||||
setting_id = 'starting_funds'
|
||||
|
||||
_display_name = _p('guildset:starting_funds', "starting_funds")
|
||||
_desc = _p(
|
||||
'guildset:starting_funds|desc',
|
||||
"How many LionCoins should a member start with."
|
||||
)
|
||||
_long_desc = _p(
|
||||
'guildset:starting_funds|long_desc',
|
||||
"Members will be given this number of coins when they first interact with me, or first join the server."
|
||||
)
|
||||
_accepts = _p(
|
||||
'guildset:starting_funds|accepts',
|
||||
"Number of coins to give to new members."
|
||||
)
|
||||
_default = 0
|
||||
|
||||
_model = CoreData.Guild
|
||||
_column = CoreData.Guild.starting_funds.name
|
||||
|
||||
@property
|
||||
def update_message(self):
|
||||
t = ctx_translator.get().t
|
||||
return t(_p(
|
||||
'guildset:starting_funds|set_response',
|
||||
"New members will now start with {amount}"
|
||||
)).format(amount=self.formatted)
|
||||
|
||||
@@ -17,8 +17,9 @@ _p = babel._p
|
||||
|
||||
class EconomyConfigUI(ConfigUI):
|
||||
setting_classes = (
|
||||
EconomySettings.StartingFunds,
|
||||
EconomySettings.CoinsPerXP,
|
||||
EconomySettings.AllowTransfers
|
||||
EconomySettings.AllowTransfers,
|
||||
)
|
||||
|
||||
def __init__(self, bot: LionBot,
|
||||
@@ -44,11 +45,9 @@ class EconomyConfigUI(ConfigUI):
|
||||
|
||||
async def reload(self):
|
||||
lguild = await self.bot.core.lions.fetch_guild(self.guildid)
|
||||
coins_per_xp = lguild.config.get(self.settings.CoinsPerXP.setting_id)
|
||||
allow_transfers = lguild.config.get(self.settings.AllowTransfers.setting_id)
|
||||
self.instances = (
|
||||
coins_per_xp,
|
||||
allow_transfers
|
||||
self.instances = tuple(
|
||||
lguild.config.get(cls.setting_id)
|
||||
for cls in self.setting_classes
|
||||
)
|
||||
|
||||
async def refresh_components(self):
|
||||
@@ -57,9 +56,9 @@ class EconomyConfigUI(ConfigUI):
|
||||
self.close_button_refresh(),
|
||||
self.reset_button_refresh(),
|
||||
)
|
||||
self._layout = [
|
||||
self.set_layout(
|
||||
(self.edit_button, self.reset_button, self.close_button),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class EconomyDashboard(DashboardSection):
|
||||
|
||||
@@ -8,6 +8,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 babel.translator import ctx_locale
|
||||
from utils.lib import utc_now
|
||||
|
||||
from wards import low_management_ward, equippable_role, high_management_ward
|
||||
@@ -109,6 +110,23 @@ class MemberAdminCog(LionCog):
|
||||
)
|
||||
finally:
|
||||
self._adding_roles.discard((member.guild.id, member.id))
|
||||
|
||||
t = self.bot.translator.t
|
||||
ctx_locale.set(lion.lguild.locale)
|
||||
lion.lguild.log_event(
|
||||
title=t(_p(
|
||||
'eventlog|event:welcome|title',
|
||||
"New Member Joined"
|
||||
)),
|
||||
name=t(_p(
|
||||
'eventlog|event:welcome|desc',
|
||||
"{member} joined the server for the first time.",
|
||||
)).format(
|
||||
member=member.mention
|
||||
),
|
||||
roles_given='\n'.join(role.mention for role in roles) if roles else None,
|
||||
balance=lion.data.coins,
|
||||
)
|
||||
else:
|
||||
# Returning member
|
||||
|
||||
@@ -181,6 +199,39 @@ class MemberAdminCog(LionCog):
|
||||
finally:
|
||||
self._adding_roles.discard((member.guild.id, member.id))
|
||||
|
||||
t = self.bot.translator.t
|
||||
ctx_locale.set(lion.lguild.locale)
|
||||
lion.lguild.log_event(
|
||||
title=t(_p(
|
||||
'eventlog|event:returning|title',
|
||||
"Member Rejoined"
|
||||
)),
|
||||
name=t(_p(
|
||||
'eventlog|event:returning|desc',
|
||||
"{member} rejoined the server.",
|
||||
)).format(
|
||||
member=member.mention
|
||||
),
|
||||
balance=lion.data.coins,
|
||||
roles_given='\n'.join(role.mention for role in roles) if roles else None,
|
||||
fields={
|
||||
t(_p(
|
||||
'eventlog|event:returning|field:first_joined',
|
||||
"First Joined"
|
||||
)): (
|
||||
discord.utils.format_dt(lion.data.first_joined) if lion.data.first_joined else 'Unknown',
|
||||
True
|
||||
),
|
||||
t(_p(
|
||||
'eventlog|event:returning|field:last_seen',
|
||||
"Last Seen"
|
||||
)): (
|
||||
discord.utils.format_dt(lion.data.last_left) if lion.data.last_left else 'Unknown',
|
||||
True
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
@LionCog.listener('on_raw_member_remove')
|
||||
@log_wrap(action="Farewell")
|
||||
async def admin_member_farewell(self, payload: discord.RawMemberRemoveEvent):
|
||||
@@ -195,6 +246,7 @@ class MemberAdminCog(LionCog):
|
||||
await lion.data.update(last_left=utc_now())
|
||||
|
||||
# Save member roles
|
||||
roles = None
|
||||
async with self.bot.db.connection() as conn:
|
||||
self.bot.db.conn = conn
|
||||
async with conn.transaction():
|
||||
@@ -206,6 +258,7 @@ class MemberAdminCog(LionCog):
|
||||
print(type(payload.user))
|
||||
if isinstance(payload.user, discord.Member) and payload.user.roles:
|
||||
member = payload.user
|
||||
roles = member.roles
|
||||
await self.data.past_roles.insert_many(
|
||||
('guildid', 'userid', 'roleid'),
|
||||
*((guildid, userid, role.id) for role in member.roles)
|
||||
@@ -213,7 +266,38 @@ class MemberAdminCog(LionCog):
|
||||
logger.debug(
|
||||
f"Stored persisting roles for member <uid:{userid}> in <gid:{guildid}>."
|
||||
)
|
||||
# TODO: Event log, and include info about unchunked members
|
||||
|
||||
t = self.bot.translator.t
|
||||
ctx_locale.set(lion.lguild.locale)
|
||||
lion.lguild.log_event(
|
||||
title=t(_p(
|
||||
'eventlog|event:left|title',
|
||||
"Member Left"
|
||||
)),
|
||||
name=t(_p(
|
||||
'eventlog|event:left|desc',
|
||||
"{member} left the server.",
|
||||
)).format(
|
||||
member=f"<@{userid}>"
|
||||
),
|
||||
balance=lion.data.coins,
|
||||
fields={
|
||||
t(_p(
|
||||
'eventlog|event:left|field:stored_roles',
|
||||
"Stored Roles"
|
||||
)): (
|
||||
'\n'.join(role.mention for role in roles) if roles is not None else 'None',
|
||||
True
|
||||
),
|
||||
t(_p(
|
||||
'eventlog|event:left|field:first_joined',
|
||||
"First Joined"
|
||||
)): (
|
||||
discord.utils.format_dt(lion.data.first_joined) if lion.data.first_joined else 'Unknown',
|
||||
True
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
@LionCog.listener('on_guild_join')
|
||||
async def admin_init_guild(self, guild: discord.Guild):
|
||||
@@ -227,7 +311,8 @@ class MemberAdminCog(LionCog):
|
||||
logger.info(f"Cleared persisting roles for guild <gid:{guild.id}> because we left the guild.")
|
||||
|
||||
@LionCog.listener('on_guildset_role_persistence')
|
||||
async def clear_stored_roles(self, guildid, data):
|
||||
async def clear_stored_roles(self, guildid, setting: MemberAdminSettings.RolePersistence):
|
||||
data = setting.data
|
||||
if data is False:
|
||||
await self.data.past_roles.delete_where(guildid=guildid)
|
||||
logger.info(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import asyncio
|
||||
import pytz
|
||||
import datetime as dt
|
||||
from typing import Optional
|
||||
|
||||
@@ -161,7 +162,7 @@ class Ticket:
|
||||
embed = discord.Embed(
|
||||
title=title,
|
||||
description=data.content,
|
||||
timestamp=data.created_at,
|
||||
timestamp=data.created_at.replace(tzinfo=pytz.utc),
|
||||
colour=discord.Colour.orange()
|
||||
)
|
||||
embed.add_field(
|
||||
|
||||
@@ -73,7 +73,7 @@ class TimerCog(LionCog):
|
||||
launched=sum(1 for timer in timers if timer._run_task and not timer._run_task.done()),
|
||||
looping=sum(1 for timer in timers if timer._loop_task and not timer._loop_task.done()),
|
||||
locked=sum(1 for timer in timers if timer._lock.locked()),
|
||||
voice_locked=sum(1 for timer in timers if timer._voice_update_lock.locked()),
|
||||
voice_locked=sum(1 for timer in timers if timer.voice_lock.locked()),
|
||||
)
|
||||
if not self.ready:
|
||||
level = StatusLevel.STARTING
|
||||
@@ -345,7 +345,7 @@ class TimerCog(LionCog):
|
||||
|
||||
@LionCog.listener('on_guildset_pomodoro_channel')
|
||||
@log_wrap(action='Update Pomodoro Channels')
|
||||
async def _update_pomodoro_channels(self, guildid: int, data: Optional[int]):
|
||||
async def _update_pomodoro_channels(self, guildid: int, setting: TimerSettings.PomodoroChannel):
|
||||
"""
|
||||
Request a send_status for all guild timers which need to move channel.
|
||||
"""
|
||||
|
||||
@@ -57,7 +57,7 @@ class TimerOptions(SettingGroup):
|
||||
_allow_object = False
|
||||
|
||||
@classmethod
|
||||
async def _check_value(cls, parent_id: int, value: Optional[discord.abc.GuildChannel], **kwargs):
|
||||
async def _check_value(cls, parent_id: int, value, **kwargs):
|
||||
if value is not None:
|
||||
# TODO: Check we either have or can create a webhook
|
||||
# TODO: Check we can send messages, embeds, and files
|
||||
|
||||
@@ -136,6 +136,10 @@ class Timer:
|
||||
channel = self.channel
|
||||
return channel
|
||||
|
||||
@property
|
||||
def voice_lock(self):
|
||||
return self.lguild.voice_lock
|
||||
|
||||
async def get_notification_webhook(self) -> Optional[discord.Webhook]:
|
||||
channel = self.notification_channel
|
||||
if channel:
|
||||
@@ -474,14 +478,13 @@ class Timer:
|
||||
async with self.lguild.voice_lock:
|
||||
try:
|
||||
if self.guild.voice_client:
|
||||
print("Disconnecting")
|
||||
await self.guild.voice_client.disconnect(force=True)
|
||||
print("Disconnected")
|
||||
alert_file = focus_alert_path if stage.focused else break_alert_path
|
||||
try:
|
||||
print("Connecting")
|
||||
voice_client = await self.channel.connect(timeout=60, reconnect=False)
|
||||
print("Connected")
|
||||
voice_client = await asyncio.wait_for(
|
||||
self.channel.connect(timeout=30, reconnect=False),
|
||||
timeout=60
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
logger.warning(f"Timed out while connecting to voice channel in timer {self!r}")
|
||||
return
|
||||
@@ -508,13 +511,18 @@ class Timer:
|
||||
_, pending = await asyncio.wait([sleep_task, wait_task], return_when=asyncio.FIRST_COMPLETED)
|
||||
for task in pending:
|
||||
task.cancel()
|
||||
|
||||
if self.guild and self.guild.voice_client:
|
||||
await self.guild.voice_client.disconnect(force=True)
|
||||
except asyncio.TimeoutError:
|
||||
logger.warning(
|
||||
f"Timed out while sending voice alert for timer {self!r}",
|
||||
exc_info=True
|
||||
)
|
||||
except Exception:
|
||||
logger.exception(
|
||||
f"Exception occurred while playing voice alert for timer {self!r}"
|
||||
)
|
||||
finally:
|
||||
if self.guild and self.guild.voice_client:
|
||||
await self.guild.voice_client.disconnect(force=True)
|
||||
|
||||
def stageline(self, stage: Stage):
|
||||
t = self.bot.translator.t
|
||||
@@ -777,7 +785,7 @@ class Timer:
|
||||
logger.info(f"Timer {self!r} has stopped. Auto restart is {'on' if auto_restart else 'off'}")
|
||||
|
||||
@log_wrap(action="Destroy Timer")
|
||||
async def destroy(self, reason: str = None):
|
||||
async def destroy(self, reason: Optional[str] = None):
|
||||
"""
|
||||
Deconstructs the timer, stopping all tasks.
|
||||
"""
|
||||
|
||||
@@ -145,9 +145,7 @@ class TimerOptionsUI(MessageUI):
|
||||
value = selected.values[0] if selected.values else None
|
||||
setting = self.timer.config.get('notification_channel')
|
||||
|
||||
if issue := await setting._check_value(self.timer.data.channelid, value):
|
||||
await selection.edit_original_response(embed=error_embed(issue))
|
||||
else:
|
||||
await setting._check_value(self.timer.data.channelid, value)
|
||||
setting.value = value
|
||||
await setting.write()
|
||||
await self.timer.send_status()
|
||||
|
||||
@@ -319,10 +319,15 @@ class RankCog(LionCog):
|
||||
if roleid in rank_roleids and roleid != current_roleid
|
||||
]
|
||||
|
||||
t = self.bot.translator.t
|
||||
log_errors: list[str] = []
|
||||
log_added = None
|
||||
log_removed = None
|
||||
|
||||
# Now update roles
|
||||
new_last_roleid = last_roleid
|
||||
|
||||
# TODO: Event log here, including errors
|
||||
# TODO: Factor out role updates
|
||||
to_rm = [role for role in to_rm if role.is_assignable()]
|
||||
if to_rm:
|
||||
try:
|
||||
@@ -336,13 +341,19 @@ class RankCog(LionCog):
|
||||
f"Removed old rank roles from <uid:{userid}> in <gid:{guildid}>: {roleids}"
|
||||
)
|
||||
new_last_roleid = None
|
||||
except discord.HTTPException:
|
||||
except discord.HTTPException as e:
|
||||
logger.warning(
|
||||
f"Unexpected error removing old rank roles from <uid:{member.id}> in <gid:{guild.id}>: {to_rm}",
|
||||
exc_info=True
|
||||
)
|
||||
log_errors.append(t(_p(
|
||||
'eventlog|event:rank_check|error:remove_failed',
|
||||
"Failed to remove old rank roles: `{error}`"
|
||||
)).format(error=str(e)))
|
||||
log_removed = '\n'.join(role.mention for role in to_rm)
|
||||
|
||||
if to_add and to_add.is_assignable():
|
||||
if to_add:
|
||||
if to_add.is_assignable():
|
||||
try:
|
||||
await member.add_roles(
|
||||
to_add,
|
||||
@@ -352,16 +363,46 @@ class RankCog(LionCog):
|
||||
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:
|
||||
last_roleid=to_add.id
|
||||
except discord.HTTPException as e:
|
||||
logger.warning(
|
||||
f"Unexpected error giving <uid:{userid}> in <gid:{guildid}> their rank role <rid:{to_add.id}>",
|
||||
f"Unexpected error giving <uid:{userid}> in <gid:{guildid}> "
|
||||
f"their rank role <rid:{to_add.id}>",
|
||||
exc_info=True
|
||||
)
|
||||
log_errors.append(t(_p(
|
||||
'eventlog|event:rank_check|error:add_failed',
|
||||
"Failed to add new rank role: `{error}`"
|
||||
)).format(error=str(e)))
|
||||
else:
|
||||
log_errors.append(t(_p(
|
||||
'eventlog|event:rank_check|error:add_impossible',
|
||||
"Could not assign new activity rank role. Lacking permissions or invalid role."
|
||||
)))
|
||||
log_added = to_add.mention
|
||||
else:
|
||||
log_errors.append(t(_p(
|
||||
'eventlog|event:rank_check|error:permissions',
|
||||
"Could not update activity rank roles, I lack the 'Manage Roles' permission."
|
||||
)))
|
||||
|
||||
if new_last_roleid != last_roleid:
|
||||
await session_rank.rankrow.update(last_roleid=new_last_roleid)
|
||||
|
||||
if to_add or to_rm:
|
||||
# Log rank role update
|
||||
lguild = await self.bot.core.lions.fetch_guild(guildid)
|
||||
lguild.log_event(
|
||||
t(_p(
|
||||
'eventlog|event:rank_check|name',
|
||||
"Member Activity Rank Roles Updated"
|
||||
)),
|
||||
memberid=member.id,
|
||||
roles_given=log_added,
|
||||
roles_taken=log_removed,
|
||||
errors=log_errors,
|
||||
)
|
||||
|
||||
@log_wrap(action="Update Rank")
|
||||
async def update_rank(self, session_rank):
|
||||
# Identify target rank
|
||||
@@ -390,6 +431,11 @@ class RankCog(LionCog):
|
||||
if member is None:
|
||||
return
|
||||
|
||||
t = self.bot.translator.t
|
||||
log_errors: list[str] = []
|
||||
log_added = None
|
||||
log_removed = None
|
||||
|
||||
last_roleid = session_rank.rankrow.last_roleid
|
||||
|
||||
# Update ranks
|
||||
@@ -409,7 +455,6 @@ class RankCog(LionCog):
|
||||
]
|
||||
|
||||
# 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:
|
||||
@@ -423,13 +468,19 @@ class RankCog(LionCog):
|
||||
f"Removed old rank roles from <uid:{userid}> in <gid:{guildid}>: {roleids}"
|
||||
)
|
||||
last_roleid = None
|
||||
except discord.HTTPException:
|
||||
except discord.HTTPException as e:
|
||||
logger.warning(
|
||||
f"Unexpected error removing old rank roles from <uid:{member.id}> in <gid:{guild.id}>: {to_rm}",
|
||||
exc_info=True
|
||||
)
|
||||
log_errors.append(t(_p(
|
||||
'eventlog|event:new_rank|error:remove_failed',
|
||||
"Failed to remove old rank roles: `{error}`"
|
||||
)).format(error=str(e)))
|
||||
log_removed = '\n'.join(role.mention for role in to_rm)
|
||||
|
||||
if to_add and to_add.is_assignable():
|
||||
if to_add:
|
||||
if to_add.is_assignable():
|
||||
try:
|
||||
await member.add_roles(
|
||||
to_add,
|
||||
@@ -440,11 +491,27 @@ class RankCog(LionCog):
|
||||
f"Rewarded rank role <rid:{to_add.id}> to <uid:{userid}> in <gid:{guildid}>."
|
||||
)
|
||||
last_roleid=to_add.id
|
||||
except discord.HTTPException:
|
||||
except discord.HTTPException as e:
|
||||
logger.warning(
|
||||
f"Unexpected error giving <uid:{userid}> in <gid:{guildid}> their rank role <rid:{to_add.id}>",
|
||||
f"Unexpected error giving <uid:{userid}> in <gid:{guildid}> "
|
||||
f"their rank role <rid:{to_add.id}>",
|
||||
exc_info=True
|
||||
)
|
||||
log_errors.append(t(_p(
|
||||
'eventlog|event:new_rank|error:add_failed',
|
||||
"Failed to add new rank role: `{error}`"
|
||||
)).format(error=str(e)))
|
||||
else:
|
||||
log_errors.append(t(_p(
|
||||
'eventlog|event:new_rank|error:add_impossible',
|
||||
"Could not assign new activity rank role. Lacking permissions or invalid role."
|
||||
)))
|
||||
log_added = to_add.mention
|
||||
else:
|
||||
log_errors.append(t(_p(
|
||||
'eventlog|event:new_rank|error:permissions',
|
||||
"Could not update activity rank roles, I lack the 'Manage Roles' permission."
|
||||
)))
|
||||
|
||||
# Update MemberRank row
|
||||
column = {
|
||||
@@ -473,7 +540,29 @@ class RankCog(LionCog):
|
||||
)
|
||||
|
||||
# Send notification
|
||||
try:
|
||||
await self._notify_rank_update(guildid, userid, new_rank)
|
||||
except discord.HTTPException:
|
||||
log_errors.append(t(_p(
|
||||
'eventlog|event:new_rank|error:notify_failed',
|
||||
"Could not notify member."
|
||||
)))
|
||||
|
||||
# Log rank achieved
|
||||
lguild.log_event(
|
||||
t(_p(
|
||||
'eventlog|event:new_rank|name',
|
||||
"Member Achieved Activity rank"
|
||||
)),
|
||||
t(_p(
|
||||
'eventlog|event:new_rank|desc',
|
||||
"{member} earned the new activity rank {rank}"
|
||||
)).format(member=member.mention, rank=f"<@&{new_rank.roleid}>"),
|
||||
roles_given=log_added,
|
||||
roles_taken=log_removed,
|
||||
coins_earned=new_rank.reward,
|
||||
errors=log_errors,
|
||||
)
|
||||
|
||||
async def _notify_rank_update(self, guildid, userid, new_rank):
|
||||
"""
|
||||
@@ -516,11 +605,7 @@ class RankCog(LionCog):
|
||||
text = member.mention
|
||||
|
||||
# Post!
|
||||
try:
|
||||
await destination.send(embed=embed, content=text)
|
||||
except discord.HTTPException:
|
||||
# TODO: Logging, guild logging, invalidate channel if permissions are wrong
|
||||
pass
|
||||
|
||||
def get_message_map(self,
|
||||
rank_type: RankType,
|
||||
@@ -777,6 +862,24 @@ class RankCog(LionCog):
|
||||
self.flush_guild_ranks(guild.id)
|
||||
await ui.set_done()
|
||||
|
||||
# Event log
|
||||
lguild.log_event(
|
||||
t(_p(
|
||||
'eventlog|event:rank_refresh|name',
|
||||
"Activity Ranks Refreshed"
|
||||
)),
|
||||
t(_p(
|
||||
'eventlog|event:rank_refresh|desc',
|
||||
"{actor} refresh member activity ranks.\n"
|
||||
"**`{removed}`** invalid rank roles removed.\n"
|
||||
"**`{added}`** new rank roles added."
|
||||
)).format(
|
||||
actor=interaction.user.mention,
|
||||
removed=ui.removed,
|
||||
added=ui.added,
|
||||
)
|
||||
)
|
||||
|
||||
# ---------- Commands ----------
|
||||
@cmds.hybrid_command(name=_p('cmd:ranks', "ranks"))
|
||||
async def ranks_cmd(self, ctx: LionContext):
|
||||
|
||||
@@ -6,7 +6,7 @@ from discord.ui.select import select, Select, SelectOption, RoleSelect
|
||||
from discord.ui.button import button, Button, ButtonStyle
|
||||
|
||||
from meta import conf, LionBot
|
||||
from meta.errors import ResponseTimedOut
|
||||
from meta.errors import ResponseTimedOut, SafeCancellation
|
||||
from core.data import RankType
|
||||
from data import ORDER
|
||||
|
||||
@@ -16,7 +16,7 @@ from wards import equippable_role
|
||||
from babel.translator import ctx_translator
|
||||
|
||||
from .. import babel, logger
|
||||
from ..data import AnyRankData
|
||||
from ..data import AnyRankData, RankData
|
||||
from ..utils import rank_model_from_type, format_stat_range, stat_data_to_value
|
||||
from .editor import RankEditor
|
||||
from .preview import RankPreviewUI
|
||||
@@ -101,6 +101,7 @@ class RankOverviewUI(MessageUI):
|
||||
Refresh the current ranks,
|
||||
ensuring that all members have the correct rank.
|
||||
"""
|
||||
await press.response.defer(thinking=True)
|
||||
async with self.cog.ranklock(self.guild.id):
|
||||
await self.cog.interactive_rank_refresh(press, self.guild)
|
||||
|
||||
@@ -156,11 +157,21 @@ class RankOverviewUI(MessageUI):
|
||||
|
||||
Errors if the client does not have permission to create roles.
|
||||
"""
|
||||
t = self.bot.translator.t
|
||||
if not self.guild.me.guild_permissions.manage_roles:
|
||||
raise SafeCancellation(t(_p(
|
||||
'ui:rank_overview|button:create|error:my_permissions',
|
||||
"I lack the 'Manage Roles' permission required to create rank roles!"
|
||||
)))
|
||||
|
||||
async def _create_callback(rank, submit: discord.Interaction):
|
||||
await submit.response.send_message(
|
||||
embed=discord.Embed(
|
||||
colour=discord.Colour.brand_green(),
|
||||
description="Rank Created!"
|
||||
description=t(_p(
|
||||
'ui:rank_overview|button:create|success',
|
||||
"Created a new rank {role}"
|
||||
)).format(role=f"<@&{rank.roleid}>")
|
||||
),
|
||||
ephemeral=True
|
||||
)
|
||||
|
||||
@@ -447,7 +447,7 @@ class Reminders(LionCog):
|
||||
))
|
||||
value = 'None'
|
||||
choices = [
|
||||
appcmds.Choice(name=name, value=value)
|
||||
appcmds.Choice(name=name[:100], value=value)
|
||||
]
|
||||
else:
|
||||
# Build list of reminder strings
|
||||
@@ -463,7 +463,7 @@ class Reminders(LionCog):
|
||||
# Build list of valid choices
|
||||
choices = [
|
||||
appcmds.Choice(
|
||||
name=string[0],
|
||||
name=string[0][:100],
|
||||
value=f"rid:{string[1].reminderid}"
|
||||
)
|
||||
for string in matches
|
||||
@@ -474,7 +474,7 @@ class Reminders(LionCog):
|
||||
name=t(_p(
|
||||
'cmd:reminders_cancel|acmpl:reminder|error:no_matches',
|
||||
"You do not have any reminders matching \"{partial}\""
|
||||
)).format(partial=partial),
|
||||
)).format(partial=partial)[:100],
|
||||
value=partial
|
||||
)
|
||||
]
|
||||
@@ -562,7 +562,7 @@ class Reminders(LionCog):
|
||||
name=t(_p(
|
||||
'cmd:remindme_at|acmpl:time|error:parse',
|
||||
"Cannot parse \"{partial}\" as a time. Try the format HH:MM or YYYY-MM-DD HH:MM"
|
||||
)).format(partial=partial),
|
||||
)).format(partial=partial)[:100],
|
||||
value=partial
|
||||
)
|
||||
return [choice]
|
||||
|
||||
@@ -14,10 +14,12 @@ from meta import LionCog, LionBot, LionContext
|
||||
from meta.logger import log_wrap
|
||||
from meta.errors import ResponseTimedOut, UserInputError, UserCancelled, SafeCancellation
|
||||
from meta.sharding import THIS_SHARD
|
||||
from utils.lib import utc_now, error_embed
|
||||
from meta.monitor import ComponentMonitor, ComponentStatus, StatusLevel
|
||||
from utils.lib import utc_now, error_embed, jumpto
|
||||
from utils.ui import Confirm, ChoicedEnum, Transformed, AButton, AsComponents
|
||||
from utils.transformers import DurationTransformer
|
||||
from utils.monitor import TaskMonitor
|
||||
from babel.translator import ctx_locale
|
||||
from constants import MAX_COINS
|
||||
from data import NULL
|
||||
|
||||
@@ -142,6 +144,9 @@ class RoleMenuCog(LionCog):
|
||||
def __init__(self, bot: LionBot):
|
||||
self.bot = bot
|
||||
self.data = bot.db.load_registry(RoleMenuData())
|
||||
self.monitor = ComponentMonitor('RoleMenus', self._monitor)
|
||||
|
||||
self.ready = asyncio.Event()
|
||||
|
||||
# Menu caches
|
||||
self.live_menus = RoleMenu.attached_menus # guildid -> messageid -> menuid
|
||||
@@ -149,11 +154,42 @@ class RoleMenuCog(LionCog):
|
||||
# Expiry manage
|
||||
self.expiry_monitor = ExpiryMonitor(executor=self._expire)
|
||||
|
||||
async def _monitor(self):
|
||||
state = (
|
||||
"<"
|
||||
"RoleMenus"
|
||||
" ready={ready}"
|
||||
" cached={cached}"
|
||||
" views={views}"
|
||||
" live={live}"
|
||||
" expiry={expiry}"
|
||||
">"
|
||||
)
|
||||
data = dict(
|
||||
ready=self.ready.is_set(),
|
||||
live=sum(len(gmenus) for gmenus in self.live_menus.values()),
|
||||
expiry=repr(self.expiry_monitor),
|
||||
cached=len(RoleMenu._menus),
|
||||
views=len(RoleMenu.menu_views),
|
||||
)
|
||||
if not self.ready.is_set():
|
||||
level = StatusLevel.STARTING
|
||||
info = f"(STARTING) Not initialised. {state}"
|
||||
elif not self.expiry_monitor._monitor_task:
|
||||
level = StatusLevel.ERRORED
|
||||
info = f"(ERRORED) Expiry monitor not running. {state}"
|
||||
else:
|
||||
level = StatusLevel.OKAY
|
||||
info = f"(OK) RoleMenu loaded and listening. {state}"
|
||||
|
||||
return ComponentStatus(level, info, info, data)
|
||||
|
||||
# ----- Initialisation -----
|
||||
async def cog_load(self):
|
||||
self.bot.system_monitor.add_component(self.monitor)
|
||||
await self.data.init()
|
||||
|
||||
self.bot.tree.add_command(rolemenu_ctxcmd)
|
||||
self.bot.tree.add_command(rolemenu_ctxcmd, override=True)
|
||||
|
||||
if self.bot.is_ready():
|
||||
await self.initialise()
|
||||
@@ -164,17 +200,28 @@ class RoleMenuCog(LionCog):
|
||||
self.live_menus.clear()
|
||||
if self.expiry_monitor._monitor_task:
|
||||
self.expiry_monitor._monitor_task.cancel()
|
||||
self.bot.tree.remove_command(rolemenu_ctxcmd)
|
||||
|
||||
@LionCog.listener('on_ready')
|
||||
@log_wrap(action="Initialise Role Menus")
|
||||
async def initialise(self):
|
||||
self.ready.clear()
|
||||
|
||||
# Clean up live menu tasks
|
||||
for menu in list(RoleMenu._menus.values()):
|
||||
menu.detach()
|
||||
self.live_menus.clear()
|
||||
if self.expiry_monitor._monitor_task:
|
||||
self.expiry_monitor._monitor_task.cancel()
|
||||
|
||||
# Start monitor
|
||||
self.expiry_monitor = ExpiryMonitor(executor=self._expire)
|
||||
self.expiry_monitor.start()
|
||||
|
||||
# Load guilds
|
||||
guildids = [guild.id for guild in self.bot.guilds]
|
||||
if guildids:
|
||||
await self._initialise_guilds(*guildids)
|
||||
self.ready.set()
|
||||
|
||||
async def _initialise_guilds(self, *guildids):
|
||||
"""
|
||||
@@ -269,14 +316,85 @@ class RoleMenuCog(LionCog):
|
||||
menu = await self.data.RoleMenu.fetch(equip_row.menuid)
|
||||
guild = self.bot.get_guild(menu.guildid)
|
||||
if guild is not None:
|
||||
log_errors = []
|
||||
lguild = await self.bot.core.lions.fetch_guild(menu.guildid)
|
||||
t = self.bot.translator.t
|
||||
ctx_locale.set(lguild.locale)
|
||||
|
||||
role = guild.get_role(equip_row.roleid)
|
||||
if role is not None:
|
||||
lion = await self.bot.core.lions.fetch_member(guild.id, equip_row.userid)
|
||||
await lion.remove_role(role)
|
||||
if (member := lion.member):
|
||||
if role in member.roles:
|
||||
logger.error(f"Expired {equipid}, but the member still has the role!")
|
||||
log_errors.append(t(_p(
|
||||
'eventlog|event:rolemenu_role_expire|error:remove_failed',
|
||||
"Removed the role, but the member still has the role!!"
|
||||
)))
|
||||
else:
|
||||
logger.info(f"Expired {equipid}, and successfully removed the role from the member!")
|
||||
else:
|
||||
logger.info(
|
||||
f"Expired {equipid} for non-existent member {equip_row.userid}. "
|
||||
"Removed from persistent roles."
|
||||
)
|
||||
log_errors.append(t(_p(
|
||||
'eventlog|event:rolemenu_role_expire|error:member_gone',
|
||||
"Member could not be found.. role has been removed from saved roles."
|
||||
)))
|
||||
else:
|
||||
logger.info(f"Could not expire {equipid} because the role was not found.")
|
||||
log_errors.append(t(_p(
|
||||
'eventlog|event:rolemenu_role_expire|error:no_role',
|
||||
"Role {role} no longer exists."
|
||||
)).format(role=f"`{equip_row.roleid}`"))
|
||||
now = utc_now()
|
||||
lguild.log_event(
|
||||
title=t(_p(
|
||||
'eventlog|event:rolemenu_role_expire|title',
|
||||
"Equipped role has expired"
|
||||
)),
|
||||
description=t(_p(
|
||||
'eventlog|event:rolemenu_role_expire|desc',
|
||||
"{member}'s role {role} has now expired."
|
||||
)).format(
|
||||
member=f"<@{equip_row.userid}>",
|
||||
role=f"<@&{equip_row.roleid}>",
|
||||
),
|
||||
fields={
|
||||
t(_p(
|
||||
'eventlog|event:rolemenu_role_expire|field:menu',
|
||||
"Obtained From"
|
||||
)): (
|
||||
jumpto(
|
||||
menu.guildid, menu.channelid, menu.messageid
|
||||
) if menu and menu.messageid else f"**{menu.name}**",
|
||||
True
|
||||
),
|
||||
t(_p(
|
||||
'eventlog|event:rolemenu_role_expire|field:menu',
|
||||
"Obtained At"
|
||||
)): (
|
||||
discord.utils.format_dt(equip_row.obtained_at),
|
||||
True
|
||||
),
|
||||
t(_p(
|
||||
'eventlog|event:rolemenu_role_expire|field:expiry',
|
||||
"Expiry"
|
||||
)): (
|
||||
discord.utils.format_dt(equip_row.expires_at),
|
||||
True
|
||||
),
|
||||
},
|
||||
errors=log_errors
|
||||
)
|
||||
await equip_row.update(removed_at=now)
|
||||
else:
|
||||
logger.info(f"Could not expire {equipid} because the guild was not found.")
|
||||
else:
|
||||
# equipid is no longer valid or is not expiring
|
||||
logger.info(f"RoleMenu equipped role {equipid} is no longer valid or is not expiring.")
|
||||
pass
|
||||
|
||||
# ----- Private Utils -----
|
||||
@@ -304,7 +422,7 @@ class RoleMenuCog(LionCog):
|
||||
error = t(_p(
|
||||
'parse:message_link|suberror:no_perms',
|
||||
"Insufficient permissions! I need the `MESSAGE_HISTORY` permission in {channel}."
|
||||
)).format(channel=channel.menion)
|
||||
)).format(channel=channel.mention)
|
||||
else:
|
||||
error = t(_p(
|
||||
'parse:message_link|suberror:channel_dne',
|
||||
@@ -487,7 +605,7 @@ class RoleMenuCog(LionCog):
|
||||
choice_name = menu.data.name
|
||||
choice_value = f"menuid:{menu.data.menuid}"
|
||||
choices.append(
|
||||
appcmds.Choice(name=choice_name, value=choice_value)
|
||||
appcmds.Choice(name=choice_name[:100], value=choice_value)
|
||||
)
|
||||
|
||||
if not choices:
|
||||
@@ -498,7 +616,7 @@ class RoleMenuCog(LionCog):
|
||||
)).format(partial=partial)
|
||||
choice_value = partial
|
||||
choice = appcmds.Choice(
|
||||
name=choice_name, value=choice_value
|
||||
name=choice_name[:100], value=choice_value
|
||||
)
|
||||
choices.append(choice)
|
||||
|
||||
@@ -522,7 +640,7 @@ class RoleMenuCog(LionCog):
|
||||
"Please select a menu first"
|
||||
))
|
||||
choice_value = partial
|
||||
choices = [appcmds.Choice(name=choice_name, value=choice_value)]
|
||||
choices = [appcmds.Choice(name=choice_name[:100], value=choice_value)]
|
||||
else:
|
||||
# Resolve the menu name
|
||||
menu: RoleMenu
|
||||
@@ -544,7 +662,7 @@ class RoleMenuCog(LionCog):
|
||||
name=t(_p(
|
||||
'acmpl:menuroles|choice:invalid_menu|name',
|
||||
"Menu '{name}' does not exist!"
|
||||
)).format(name=menu_name),
|
||||
)).format(name=menu_name)[:100],
|
||||
value=partial
|
||||
)
|
||||
choices = [choice]
|
||||
@@ -564,7 +682,7 @@ class RoleMenuCog(LionCog):
|
||||
else:
|
||||
name = mrole.data.label
|
||||
choice = appcmds.Choice(
|
||||
name=name,
|
||||
name=name[:100],
|
||||
value=f"<@&{mrole.data.roleid}>"
|
||||
)
|
||||
choices.append(choice)
|
||||
@@ -573,7 +691,7 @@ class RoleMenuCog(LionCog):
|
||||
name=t(_p(
|
||||
'acmpl:menuroles|choice:no_matching|name',
|
||||
"No roles in this menu matching '{partial}'"
|
||||
)).format(partial=partial),
|
||||
)).format(partial=partial)[:100],
|
||||
value=partial
|
||||
)
|
||||
return choices[:25]
|
||||
|
||||
@@ -609,7 +609,24 @@ class RoleMenu:
|
||||
if remove_line:
|
||||
embed.description = '\n'.join((remove_line, embed.description))
|
||||
|
||||
# TODO Event logging
|
||||
lguild = await self.bot.core.lions.fetch_guild(self.data.guildid)
|
||||
lguild.log_event(
|
||||
title=t(_p(
|
||||
'rolemenu|eventlog|event:role_equipped|title',
|
||||
"Member equipped role from role menu"
|
||||
)),
|
||||
description=t(_p(
|
||||
'rolemenu|eventlog|event:role_equipped|desc',
|
||||
"{member} equipped {role} from {menu}"
|
||||
)).format(
|
||||
member=member.mention,
|
||||
role=role.mention,
|
||||
menu=self.jump_link
|
||||
),
|
||||
roles_given=role.mention,
|
||||
price=price,
|
||||
expiry=discord.utils.format_dt(expiry) if expiry is not None else None,
|
||||
)
|
||||
return embed
|
||||
|
||||
async def _handle_negative(self, lion, member: discord.Member, mrole: RoleMenuRole) -> discord.Embed:
|
||||
@@ -690,12 +707,29 @@ class RoleMenu:
|
||||
'rolemenu|deselect|success:norefund|desc',
|
||||
"You have unequipped **{role}**."
|
||||
)).format(role=role.name)
|
||||
|
||||
lguild = await self.bot.core.lions.fetch_guild(self.data.guildid)
|
||||
lguild.log_event(
|
||||
title=t(_p(
|
||||
'rolemenu|eventlog|event:role_unequipped|title',
|
||||
"Member unequipped role from role menu"
|
||||
)),
|
||||
description=t(_p(
|
||||
'rolemenu|eventlog|event:role_unequipped|desc',
|
||||
"{member} unequipped {role} from {menu}"
|
||||
)).format(
|
||||
member=member.mention,
|
||||
role=role.mention,
|
||||
menu=self.jump_link,
|
||||
),
|
||||
roles_given=role.mention,
|
||||
refund=total_refund,
|
||||
)
|
||||
return embed
|
||||
|
||||
async def _handle_selection(self, lion, member: discord.Member, menuroleid: int):
|
||||
lock_key = ('rmenu', member.id, member.guild.id)
|
||||
async with self.bot.idlock(lock_key):
|
||||
# TODO: Selection locking
|
||||
mrole = self.rolemap.get(menuroleid, None)
|
||||
if mrole is None:
|
||||
raise ValueError(
|
||||
|
||||
@@ -168,19 +168,34 @@ class RoomCog(LionCog):
|
||||
async def _destroy_channel_room(self, channel: discord.abc.GuildChannel):
|
||||
room = self._room_cache[channel.guild.id].get(channel.id, None)
|
||||
if room is not None:
|
||||
t = self.bot.translator.t
|
||||
room.lguild.log_event(
|
||||
title=t(_p(
|
||||
'room|eventlog|event:room_deleted|title',
|
||||
"Private Room Deleted"
|
||||
)),
|
||||
description=t(_p(
|
||||
'room|eventlog|event:room_deleted|desc',
|
||||
"{owner}'s private room was deleted."
|
||||
)).format(
|
||||
owner="<@{mid}>".format(mid=room.data.ownerid),
|
||||
),
|
||||
fields=room.eventlog_fields()
|
||||
)
|
||||
await room.destroy(reason="Underlying Channel Deleted")
|
||||
|
||||
# Setting event handlers
|
||||
@LionCog.listener('on_guildset_rooms_category')
|
||||
@log_wrap(action='Update Rooms Category')
|
||||
async def _update_rooms_category(self, guildid: int, data: Optional[int]):
|
||||
async def _update_rooms_category(self, guildid: int, setting: RoomSettings.Category):
|
||||
"""
|
||||
Move all active private channels to the new category.
|
||||
|
||||
This shouldn't affect the channel function at all.
|
||||
"""
|
||||
data = setting.data
|
||||
guild = self.bot.get_guild(guildid)
|
||||
new_category = guild.get_channel(data) if guild else None
|
||||
new_category = guild.get_channel(data) if guild and data else None
|
||||
if new_category:
|
||||
tasks = []
|
||||
for room in list(self._room_cache[guildid].values()):
|
||||
@@ -196,10 +211,11 @@ class RoomCog(LionCog):
|
||||
|
||||
@LionCog.listener('on_guildset_rooms_visible')
|
||||
@log_wrap(action='Update Rooms Visibility')
|
||||
async def _update_rooms_visibility(self, guildid: int, data: bool):
|
||||
async def _update_rooms_visibility(self, guildid: int, setting: RoomSettings.Visible):
|
||||
"""
|
||||
Update the everyone override on each room to reflect the new setting.
|
||||
"""
|
||||
data = setting.data
|
||||
tasks = []
|
||||
for room in list(self._room_cache[guildid].values()):
|
||||
if room.channel:
|
||||
@@ -226,6 +242,7 @@ class RoomCog(LionCog):
|
||||
"""
|
||||
Create a new private room.
|
||||
"""
|
||||
t = self.bot.translator.t
|
||||
lguild = await self.bot.core.lions.fetch_guild(guild.id)
|
||||
|
||||
# TODO: Consider extending invites to members rather than giving them immediate access
|
||||
@@ -245,12 +262,31 @@ class RoomCog(LionCog):
|
||||
overwrites[member] = member_overwrite
|
||||
|
||||
# Create channel
|
||||
try:
|
||||
channel = await guild.create_voice_channel(
|
||||
name=name,
|
||||
reason=f"Creating Private Room for {owner.id}",
|
||||
reason=t(_p(
|
||||
'create_room|create_channel|audit_reason',
|
||||
"Creating Private Room for {ownerid}"
|
||||
)).format(ownerid=owner.id),
|
||||
category=lguild.config.get(RoomSettings.Category.setting_id).value,
|
||||
overwrites=overwrites
|
||||
)
|
||||
except discord.HTTPException as e:
|
||||
lguild.log_event(
|
||||
t(_p(
|
||||
'eventlog|event:private_room_create_error|name',
|
||||
"Private Room Creation Failed"
|
||||
)),
|
||||
t(_p(
|
||||
'eventlog|event:private_room_create_error|desc',
|
||||
"{owner} attempted to rent a new private room, but I could not create it!\n"
|
||||
"They were not charged."
|
||||
)).format(owner=owner.mention),
|
||||
errors=[f"`{repr(e)}`"]
|
||||
)
|
||||
raise
|
||||
|
||||
try:
|
||||
# Create Room
|
||||
now = utc_now()
|
||||
@@ -287,6 +323,17 @@ class RoomCog(LionCog):
|
||||
logger.info(
|
||||
f"New private room created: {room.data!r}"
|
||||
)
|
||||
lguild.log_event(
|
||||
t(_p(
|
||||
'eventlog|event:private_room_create|name',
|
||||
"Private Room Rented"
|
||||
)),
|
||||
t(_p(
|
||||
'eventlog|event:private_room_create|desc',
|
||||
"{owner} has rented a new private room {channel}!"
|
||||
)).format(owner=owner.mention, channel=channel.mention),
|
||||
fields=room.eventlog_fields(),
|
||||
)
|
||||
|
||||
return room
|
||||
|
||||
@@ -488,7 +535,7 @@ class RoomCog(LionCog):
|
||||
await ui.send(room.channel)
|
||||
|
||||
@log_wrap(action='create_room')
|
||||
async def _do_create_room(self, ctx, required, days, rent, name, provided) -> Room:
|
||||
async def _do_create_room(self, ctx, required, days, rent, name, provided) -> Optional[Room]:
|
||||
t = self.bot.translator.t
|
||||
# TODO: Rollback the channel create if this fails
|
||||
async with self.bot.db.connection() as conn:
|
||||
@@ -543,7 +590,6 @@ class RoomCog(LionCog):
|
||||
)
|
||||
)
|
||||
await ctx.alion.data.update(coins=CoreData.Member.coins + required)
|
||||
return
|
||||
except discord.HTTPException as e:
|
||||
await ctx.reply(
|
||||
embed=error_embed(
|
||||
@@ -556,7 +602,6 @@ class RoomCog(LionCog):
|
||||
)
|
||||
)
|
||||
await ctx.alion.data.update(coins=CoreData.Member.coins + required)
|
||||
return
|
||||
|
||||
@room_group.command(
|
||||
name=_p('cmd:room_status', "status"),
|
||||
|
||||
@@ -71,6 +71,48 @@ class Room:
|
||||
def deleted(self):
|
||||
return bool(self.data.deleted_at)
|
||||
|
||||
def eventlog_fields(self) -> dict[str, tuple[str, bool]]:
|
||||
t = self.bot.translator.t
|
||||
fields = {
|
||||
t(_p(
|
||||
'room|eventlog|field:owner', "Owner"
|
||||
)): (
|
||||
f"<@{self.data.ownerid}>",
|
||||
True
|
||||
),
|
||||
t(_p(
|
||||
'room|eventlog|field:channel', "Channel"
|
||||
)): (
|
||||
f"<#{self.data.channelid}>",
|
||||
True
|
||||
),
|
||||
t(_p(
|
||||
'room|eventlog|field:balance', "Room Balance"
|
||||
)): (
|
||||
f"{self.bot.config.emojis.coin} **{self.data.coin_balance}**",
|
||||
True
|
||||
),
|
||||
t(_p(
|
||||
'room|eventlog|field:created', "Created At"
|
||||
)): (
|
||||
discord.utils.format_dt(self.data.created_at, 'F'),
|
||||
True
|
||||
),
|
||||
t(_p(
|
||||
'room|eventlog|field:tick', "Next Rent Due"
|
||||
)): (
|
||||
discord.utils.format_dt(self.next_tick, 'R'),
|
||||
True
|
||||
),
|
||||
t(_p(
|
||||
'room|eventlog|field:members', "Private Room Members"
|
||||
)): (
|
||||
','.join(f"<@{member}>" for member in self.members),
|
||||
False
|
||||
),
|
||||
}
|
||||
return fields
|
||||
|
||||
async def notify_deposit(self, member: discord.Member, amount: int):
|
||||
# Assumes locale is set correctly
|
||||
t = self.bot.translator.t
|
||||
@@ -108,6 +150,20 @@ class Room:
|
||||
"Welcome {members}"
|
||||
)).format(members=', '.join(f"<@{mid}>" for mid in memberids))
|
||||
)
|
||||
self.lguild.log_event(
|
||||
title=t(_p(
|
||||
'room|eventlog|event:new_members|title',
|
||||
"Members invited to private room"
|
||||
)),
|
||||
description=t(_p(
|
||||
'room|eventlog|event:new_members|desc',
|
||||
"{owner} added members to their private room: {members}"
|
||||
)).format(
|
||||
members=', '.join(f"<@{mid}>" for mid in memberids),
|
||||
owner="<@{mid}>".format(mid=self.data.ownerid),
|
||||
),
|
||||
fields=self.eventlog_fields()
|
||||
)
|
||||
if self.channel:
|
||||
try:
|
||||
await self.channel.send(embed=notification)
|
||||
@@ -128,6 +184,21 @@ class Room:
|
||||
await member_data.table.delete_where(channelid=self.data.channelid, userid=list(memberids))
|
||||
self.members = list(set(self.members).difference(memberids))
|
||||
# No need to notify for removal
|
||||
t = self.bot.translator.t
|
||||
self.lguild.log_event(
|
||||
title=t(_p(
|
||||
'room|eventlog|event:rm_members|title',
|
||||
"Members removed from private room"
|
||||
)),
|
||||
description=t(_p(
|
||||
'room|eventlog|event:rm_members|desc',
|
||||
"{owner} removed members from their private room: {members}"
|
||||
)).format(
|
||||
members=', '.join(f"<@{mid}>" for mid in memberids),
|
||||
owner="<@{mid}>".format(mid=self.data.ownerid),
|
||||
),
|
||||
fields=self.eventlog_fields()
|
||||
)
|
||||
if self.channel:
|
||||
guild = self.channel.guild
|
||||
members = [guild.get_member(memberid) for memberid in memberids]
|
||||
@@ -255,6 +326,19 @@ class Room:
|
||||
await owner.send(embed=embed)
|
||||
except discord.HTTPException:
|
||||
pass
|
||||
self.lguild.log_event(
|
||||
title=t(_p(
|
||||
'room|eventlog|event:expired|title',
|
||||
"Private Room Expired"
|
||||
)),
|
||||
description=t(_p(
|
||||
'room|eventlog|event:expired|desc',
|
||||
"{owner}'s private room has expired."
|
||||
)).format(
|
||||
owner="<@{mid}>".format(mid=self.data.ownerid),
|
||||
),
|
||||
fields=self.eventlog_fields()
|
||||
)
|
||||
await self.destroy(reason='Room Expired')
|
||||
elif self.channel:
|
||||
# Notify channel
|
||||
@@ -274,6 +358,19 @@ class Room:
|
||||
else:
|
||||
# No channel means room was deleted
|
||||
# Just cleanup quietly
|
||||
self.lguild.log_event(
|
||||
title=t(_p(
|
||||
'room|eventlog|event:room_deleted|title',
|
||||
"Private Room Deleted"
|
||||
)),
|
||||
description=t(_p(
|
||||
'room|eventlog|event:room_deleted|desc',
|
||||
"{owner}'s private room was deleted."
|
||||
)).format(
|
||||
owner="<@{mid}>".format(mid=self.data.ownerid),
|
||||
),
|
||||
fields=self.eventlog_fields()
|
||||
)
|
||||
await self.destroy(reason='Channel Missing')
|
||||
|
||||
@log_wrap(action="Destroy Room")
|
||||
|
||||
@@ -904,10 +904,10 @@ class ScheduleCog(LionCog):
|
||||
|
||||
if not interaction.guild or not isinstance(interaction.user, discord.Member):
|
||||
choice = appcmds.Choice(
|
||||
name=_p(
|
||||
name=t(_p(
|
||||
'cmd:schedule|acmpl:book|error:not_in_guild',
|
||||
"You need to be in a server to book sessions!"
|
||||
),
|
||||
))[:100],
|
||||
value='None'
|
||||
)
|
||||
choices = [choice]
|
||||
@@ -917,10 +917,10 @@ class ScheduleCog(LionCog):
|
||||
blacklist_role = (await self.settings.BlacklistRole.get(interaction.guild.id)).value
|
||||
if blacklist_role and blacklist_role in member.roles:
|
||||
choice = appcmds.Choice(
|
||||
name=_p(
|
||||
name=t(_p(
|
||||
'cmd:schedule|acmpl:book|error:blacklisted',
|
||||
"Cannot Book -- Blacklisted"
|
||||
),
|
||||
))[:100],
|
||||
value='None'
|
||||
)
|
||||
choices = [choice]
|
||||
@@ -947,7 +947,7 @@ class ScheduleCog(LionCog):
|
||||
)
|
||||
choices.append(
|
||||
appcmds.Choice(
|
||||
name=tzstring, value='None',
|
||||
name=tzstring[:100], value='None',
|
||||
)
|
||||
)
|
||||
|
||||
@@ -968,7 +968,7 @@ class ScheduleCog(LionCog):
|
||||
if partial.lower() in name.lower():
|
||||
choices.append(
|
||||
appcmds.Choice(
|
||||
name=name,
|
||||
name=name[:100],
|
||||
value=str(slotid)
|
||||
)
|
||||
)
|
||||
@@ -978,11 +978,11 @@ class ScheduleCog(LionCog):
|
||||
name=t(_p(
|
||||
"cmd:schedule|acmpl:book|no_matching",
|
||||
"No bookable sessions matching '{partial}'"
|
||||
)).format(partial=partial[:25]),
|
||||
)).format(partial=partial[:25])[:100],
|
||||
value=partial
|
||||
)
|
||||
)
|
||||
return choices
|
||||
return choices[:25]
|
||||
|
||||
@schedule_cmd.autocomplete('cancel')
|
||||
async def schedule_cmd_cancel_acmpl(self, interaction: discord.Interaction, partial: str):
|
||||
@@ -998,10 +998,10 @@ class ScheduleCog(LionCog):
|
||||
can_cancel = list(slotid for slotid in schedule if slotid > minid)
|
||||
if not can_cancel:
|
||||
choice = appcmds.Choice(
|
||||
name=_p(
|
||||
name=t(_p(
|
||||
'cmd:schedule|acmpl:cancel|error:empty_schedule',
|
||||
"You do not have any upcoming sessions to cancel!"
|
||||
),
|
||||
))[:100],
|
||||
value='None'
|
||||
)
|
||||
choices.append(choice)
|
||||
@@ -1025,7 +1025,7 @@ class ScheduleCog(LionCog):
|
||||
if partial.lower() in name.lower():
|
||||
choices.append(
|
||||
appcmds.Choice(
|
||||
name=name,
|
||||
name=name[:100],
|
||||
value=str(slotid)
|
||||
)
|
||||
)
|
||||
@@ -1034,7 +1034,7 @@ class ScheduleCog(LionCog):
|
||||
name=t(_p(
|
||||
'cmd:schedule|acmpl:cancel|error:no_matching',
|
||||
"No cancellable sessions matching '{partial}'"
|
||||
)).format(partial=partial[:25]),
|
||||
)).format(partial=partial[:25])[:100],
|
||||
value='None'
|
||||
)
|
||||
choices.append(choice)
|
||||
|
||||
@@ -442,7 +442,7 @@ class ScheduledSession:
|
||||
'session|notify|dm|join_line:channels',
|
||||
"Please attend your session by joining one of the following:"
|
||||
))
|
||||
join_line = '\n'.join(join_line, *(channel.mention for channel in valid[:20]))
|
||||
join_line = '\n'.join((join_line, *(channel.mention for channel in valid[:20])))
|
||||
if len(valid) > 20:
|
||||
join_line += '\n...'
|
||||
|
||||
|
||||
@@ -296,6 +296,23 @@ class ColourShop(Shop):
|
||||
# TODO: Event log
|
||||
pass
|
||||
await self.data.MemberInventory.table.delete_where(inventoryid=owned.data.inventoryid)
|
||||
else:
|
||||
owned_role = None
|
||||
|
||||
lguild = await self.bot.core.lions.fetch_guild(guild.id)
|
||||
lguild.log_event(
|
||||
title=t(_p(
|
||||
'eventlog|event:purchase_colour|title',
|
||||
"Member Purchased Colour Role"
|
||||
)),
|
||||
description=t(_p(
|
||||
'eventlog|event:purchase_colour|desc',
|
||||
"{member} purchased {role} from the colour shop."
|
||||
)).format(member=member.mention, role=role.mention),
|
||||
price=item['price'],
|
||||
roles_given=role.mention,
|
||||
roles_taken=owned_role.mention if owned_role else None,
|
||||
)
|
||||
|
||||
# Purchase complete, update the shop and customer
|
||||
await self.refresh()
|
||||
@@ -446,7 +463,7 @@ class ColourShopping(ShopCog):
|
||||
),
|
||||
ephemeral=True
|
||||
)
|
||||
await logger.warning(
|
||||
logger.warning(
|
||||
"Unexpected Discord exception occurred while creating a colour role.",
|
||||
exc_info=True
|
||||
)
|
||||
@@ -469,8 +486,13 @@ class ColourShopping(ShopCog):
|
||||
# Due to the imprecise nature of Discord role ordering, this may fail.
|
||||
try:
|
||||
role = await role.edit(position=position)
|
||||
except discord.Forbidden:
|
||||
except discord.HTTPException as e:
|
||||
if e.code == 50013 or e.status == 403:
|
||||
# Forbidden case
|
||||
# But Discord sends its 'Missing Permissions' with a 400 code for position issues
|
||||
position = 0
|
||||
else:
|
||||
raise
|
||||
|
||||
# Now that the role is set up, add it to data
|
||||
item = await self.data.ShopItem.create(
|
||||
@@ -1090,7 +1112,7 @@ class ColourShopping(ShopCog):
|
||||
for i, item in enumerate(items, start=1)
|
||||
]
|
||||
options = [option for option in options if partial.lower() in option[1].lower()]
|
||||
return [appcmds.Choice(name=option[1], value=option[0]) for option in options]
|
||||
return [appcmds.Choice(name=option[1][:100], value=option[0]) for option in options]
|
||||
|
||||
|
||||
class ColourStore(Store):
|
||||
|
||||
460
src/modules/statistics/achievements.py
Normal file
460
src/modules/statistics/achievements.py
Normal file
@@ -0,0 +1,460 @@
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
import asyncio
|
||||
import datetime as dt
|
||||
|
||||
import pytz
|
||||
import discord
|
||||
|
||||
from data import ORDER, NULL
|
||||
from meta import conf, LionBot
|
||||
from meta.logger import log_wrap
|
||||
from babel.translator import LazyStr
|
||||
|
||||
from . import babel, logger
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .cog import StatsCog
|
||||
|
||||
_p = babel._p
|
||||
|
||||
|
||||
emojis = [
|
||||
(conf.emojis.active_achievement_1, conf.emojis.inactive_achievement_1),
|
||||
(conf.emojis.active_achievement_2, conf.emojis.inactive_achievement_2),
|
||||
(conf.emojis.active_achievement_3, conf.emojis.inactive_achievement_3),
|
||||
(conf.emojis.active_achievement_4, conf.emojis.inactive_achievement_4),
|
||||
(conf.emojis.active_achievement_5, conf.emojis.inactive_achievement_5),
|
||||
(conf.emojis.active_achievement_6, conf.emojis.inactive_achievement_6),
|
||||
(conf.emojis.active_achievement_7, conf.emojis.inactive_achievement_7),
|
||||
(conf.emojis.active_achievement_8, conf.emojis.inactive_achievement_8),
|
||||
]
|
||||
|
||||
def progress_bar(value, minimum, maximum, width=10) -> str:
|
||||
"""
|
||||
Build a text progress bar representing `value` between `minimum` and `maximum`.
|
||||
"""
|
||||
emojis = conf.emojis
|
||||
|
||||
proportion = (value - minimum) / (maximum - minimum)
|
||||
sections = min(max(int(proportion * width), 0), width)
|
||||
|
||||
bar = []
|
||||
# Starting segment
|
||||
bar.append(str(emojis.progress_left_empty) if sections == 0 else str(emojis.progress_left_full))
|
||||
|
||||
# Full segments up to transition or end
|
||||
if sections >= 2:
|
||||
bar.append(str(emojis.progress_middle_full) * (sections - 2))
|
||||
|
||||
# Transition, if required
|
||||
if 1 < sections < width:
|
||||
bar.append(str(emojis.progress_middle_transition))
|
||||
|
||||
# Empty sections up to end
|
||||
if sections < width:
|
||||
bar.append(str(emojis.progress_middle_empty) * (width - max(sections, 1) - 1))
|
||||
|
||||
# End section
|
||||
bar.append(str(emojis.progress_right_empty) if sections < width else str(emojis.progress_right_full))
|
||||
|
||||
# Join all the sections together and return
|
||||
return ''.join(bar)
|
||||
|
||||
|
||||
class Achievement:
|
||||
"""
|
||||
ABC for a member achievement.
|
||||
"""
|
||||
# Achievement title
|
||||
_name: LazyStr
|
||||
|
||||
# Text describing achievement
|
||||
_subtext: LazyStr
|
||||
|
||||
# Congratulations text
|
||||
_congrats: LazyStr = _p(
|
||||
'achievement|congrats',
|
||||
"Congratulations! You have completed this challenge."
|
||||
)
|
||||
|
||||
# Index used for visual display of achievement
|
||||
emoji_index: int
|
||||
|
||||
# Achievement threshold
|
||||
threshold: int
|
||||
|
||||
def __init__(self, bot: LionBot, guildid: int, userid: int):
|
||||
self.bot = bot
|
||||
self.guildid = guildid
|
||||
self.userid = userid
|
||||
|
||||
self.value: Optional[int] = None
|
||||
|
||||
@property
|
||||
def achieved(self) -> bool:
|
||||
if self.value is None:
|
||||
raise ValueError("Cannot get achievement status with no value.")
|
||||
return self.value >= self.threshold
|
||||
|
||||
@property
|
||||
def progress_text(self) -> str:
|
||||
if self.value is None:
|
||||
raise ValueError("Cannot get progress text with no value.")
|
||||
return f"{int(self.value)}/{int(self.threshold)}"
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self.bot.translator.t(self._name)
|
||||
|
||||
@property
|
||||
def subtext(self) -> str:
|
||||
return self.bot.translator.t(self._subtext)
|
||||
|
||||
@property
|
||||
def congrats(self) -> str:
|
||||
return self.bot.translator.t(self._congrats)
|
||||
|
||||
@property
|
||||
def emoji(self):
|
||||
return emojis[self.emoji_index][int(not self.achieved)]
|
||||
|
||||
@classmethod
|
||||
async def fetch(cls, bot: LionBot, guildid: int, userid: int):
|
||||
self = cls(bot, guildid, userid)
|
||||
await self.update()
|
||||
return self
|
||||
|
||||
def make_field(self):
|
||||
name = f"{self.emoji} {self.name} ({self.progress_text})"
|
||||
value = "**0** {bar} **{threshold}**\n*{subtext}*".format(
|
||||
subtext=self.congrats if self.achieved else self.subtext,
|
||||
bar=progress_bar(self.value, 0, self.threshold),
|
||||
threshold=self.threshold
|
||||
)
|
||||
return (name, value)
|
||||
|
||||
async def update(self):
|
||||
self.value = await self._calculate()
|
||||
|
||||
async def _calculate(self) -> int:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class Workout(Achievement):
|
||||
_name = _p(
|
||||
'achievement:workout|name',
|
||||
"It's about Power"
|
||||
)
|
||||
_subtext = _p(
|
||||
'achievement:workout|subtext',
|
||||
"Workout 50 times"
|
||||
)
|
||||
|
||||
threshold = 50
|
||||
emoji_index = 3
|
||||
|
||||
@log_wrap(action='Calc Workout')
|
||||
async def _calculate(self):
|
||||
"""
|
||||
Count the number of completed workout sessions this user has.
|
||||
"""
|
||||
record = await self.bot.core.data.workouts.select_one_where(
|
||||
guildid=self.guildid, userid=self.userid
|
||||
).select(total='COUNT(*)')
|
||||
return int(record['total'] or 0)
|
||||
|
||||
|
||||
class VoiceHours(Achievement):
|
||||
_name = _p(
|
||||
'achievement:voicehours|name',
|
||||
"Dream Big"
|
||||
)
|
||||
_subtext = _p(
|
||||
'achievement:voicehours|subtext',
|
||||
"Study a total of 1000 hours"
|
||||
)
|
||||
|
||||
threshold = 1000
|
||||
emoji_index = 0
|
||||
|
||||
@log_wrap(action='Calc VoiceHours')
|
||||
async def _calculate(self):
|
||||
"""
|
||||
Returns the total number of hours this member has spent in voice.
|
||||
"""
|
||||
stats: 'StatsCog' = self.bot.get_cog('StatsCog')
|
||||
records = await stats.data.VoiceSessionStats.table.select_where(
|
||||
guildid=self.guildid, userid=self.userid
|
||||
).select(total='SUM(duration) / 3600').with_no_adapter()
|
||||
hours = records[0]['total'] if records else 0
|
||||
return int(hours or 0)
|
||||
|
||||
|
||||
class VoiceStreak(Achievement):
|
||||
_name = _p(
|
||||
'achievement:voicestreak|name',
|
||||
"Consistency is Key"
|
||||
)
|
||||
_subtext = _p(
|
||||
'achievement:voicestreak|subtext',
|
||||
"Reach a 100-day voice streak"
|
||||
)
|
||||
|
||||
threshold = 100
|
||||
emoji_index = 1
|
||||
|
||||
@log_wrap(action='Calc VoiceStreak')
|
||||
async def _calculate(self):
|
||||
stats: 'StatsCog' = self.bot.get_cog('StatsCog')
|
||||
|
||||
# TODO: make this more efficient by calc in database..
|
||||
history = await stats.data.VoiceSessionStats.table.select_where(
|
||||
guildid=self.guildid, userid=self.userid
|
||||
).select(
|
||||
'start_time', 'end_time'
|
||||
).order_by('start_time', ORDER.DESC).with_no_adapter()
|
||||
|
||||
lion = await self.bot.core.lions.fetch_member(self.guildid, self.userid)
|
||||
|
||||
# Streak statistics
|
||||
streak = 0
|
||||
max_streak = 0
|
||||
current_streak = None
|
||||
|
||||
day_attended = None
|
||||
date = lion.today
|
||||
daydiff = dt.timedelta(days=1)
|
||||
|
||||
periods = [(row['start_time'], row['end_time']) for row in history]
|
||||
|
||||
i = 0
|
||||
while i < len(periods):
|
||||
row = periods[i]
|
||||
i += 1
|
||||
if row[1] > date:
|
||||
# They attended this day
|
||||
day_attended = True
|
||||
continue
|
||||
elif day_attended is None:
|
||||
# Didn't attend today, but don't break streak
|
||||
day_attended = False
|
||||
date -= daydiff
|
||||
i -= 1
|
||||
continue
|
||||
elif not day_attended:
|
||||
# Didn't attend the day, streak broken
|
||||
date -= daydiff
|
||||
i -= 1
|
||||
pass
|
||||
else:
|
||||
# Attended the day
|
||||
streak += 1
|
||||
|
||||
# Move window to the previous day and try the row again
|
||||
day_attended = False
|
||||
prev_date = date
|
||||
date -= daydiff
|
||||
i -= 1
|
||||
|
||||
# Special case, when the last session started in the previous day
|
||||
# Then the day is already attended
|
||||
if i > 1 and date < periods[i-2][0] <= prev_date:
|
||||
day_attended = True
|
||||
|
||||
continue
|
||||
|
||||
if current_streak is None:
|
||||
current_streak = streak
|
||||
max_streak = max(max_streak, streak)
|
||||
streak = 0
|
||||
|
||||
# Handle loop exit state, i.e. the last streak
|
||||
if day_attended:
|
||||
streak += 1
|
||||
max_streak = max(max_streak, streak)
|
||||
if current_streak is None:
|
||||
current_streak = streak
|
||||
|
||||
return max_streak if max_streak >= self.threshold else current_streak
|
||||
|
||||
class Voting(Achievement):
|
||||
_name = _p(
|
||||
'achievement:voting|name',
|
||||
"We're a Team"
|
||||
)
|
||||
_subtext = _p(
|
||||
'achievement:voting|subtext',
|
||||
"Vote 100 times on top.gg"
|
||||
)
|
||||
|
||||
threshold = 100
|
||||
emoji_index = 6
|
||||
|
||||
@log_wrap(action='Calc Voting')
|
||||
async def _calculate(self):
|
||||
record = await self.bot.core.data.topgg.select_one_where(
|
||||
userid=self.userid
|
||||
).select(total='COUNT(*)')
|
||||
return int(record['total'] or 0)
|
||||
|
||||
|
||||
class VoiceDays(Achievement):
|
||||
_name = _p(
|
||||
'achievement:days|name',
|
||||
"Aim For The Moon"
|
||||
)
|
||||
_subtext = _p(
|
||||
'achievement:days|subtext',
|
||||
"Join Voice on 90 different days"
|
||||
)
|
||||
|
||||
threshold = 90
|
||||
emoji_index = 2
|
||||
|
||||
@log_wrap(action='Calc VoiceDays')
|
||||
async def _calculate(self):
|
||||
stats: 'StatsCog' = self.bot.get_cog('StatsCog')
|
||||
|
||||
lion = await self.bot.core.lions.fetch_member(self.guildid, self.userid)
|
||||
offset = int(lion.today.utcoffset().total_seconds())
|
||||
|
||||
records = await stats.data.VoiceSessionStats.table.select_where(
|
||||
guildid=self.guildid, userid=self.userid
|
||||
).select(
|
||||
total="COUNT(DISTINCT(date_trunc('day', (start_time AT TIME ZONE 'utc') + interval '{} seconds')))".format(offset)
|
||||
).with_no_adapter()
|
||||
days = records[0]['total'] if records else 0
|
||||
return int(days or 0)
|
||||
|
||||
|
||||
class TasksComplete(Achievement):
|
||||
_name = _p(
|
||||
'achievement:tasks|name',
|
||||
"One Step at a Time"
|
||||
)
|
||||
_subtext = _p(
|
||||
'achievement:tasks|subtext',
|
||||
"Complete 1000 tasks"
|
||||
)
|
||||
|
||||
threshold = 1000
|
||||
emoji_index = 7
|
||||
|
||||
@log_wrap(action='Calc TasksComplete')
|
||||
async def _calculate(self):
|
||||
cog = self.bot.get_cog('TasklistCog')
|
||||
if cog is None:
|
||||
raise ValueError("Cannot calc TasksComplete without Tasklist Cog")
|
||||
|
||||
records = await cog.data.Task.table.select_where(
|
||||
cog.data.Task.completed_at != NULL,
|
||||
userid=self.userid,
|
||||
).select(
|
||||
total="COUNT(*)"
|
||||
).with_no_adapter()
|
||||
|
||||
completed = records[0]['total'] if records else 0
|
||||
return int(completed or 0)
|
||||
|
||||
|
||||
class ScheduledSessions(Achievement):
|
||||
_name = _p(
|
||||
'achievement:schedule|name',
|
||||
"Be Accountable"
|
||||
)
|
||||
_subtext = _p(
|
||||
'achievement:schedule|subtext',
|
||||
"Attend 500 Scheduled Sessions"
|
||||
)
|
||||
|
||||
threshold = 500
|
||||
emoji_index = 4
|
||||
|
||||
@log_wrap(action='Calc ScheduledSessions')
|
||||
async def _calculate(self):
|
||||
cog = self.bot.get_cog('ScheduleCog')
|
||||
if not cog:
|
||||
raise ValueError("Cannot calc scheduled sessions without ScheduleCog.")
|
||||
|
||||
model = cog.data.ScheduleSessionMember
|
||||
records = await model.table.select_where(
|
||||
userid=self.userid, guildid=self.guildid, attended=True
|
||||
).select(
|
||||
total='COUNT(*)'
|
||||
).with_no_adapter()
|
||||
|
||||
return int((records[0]['total'] or 0) if records else 0)
|
||||
|
||||
|
||||
class MonthlyHours(Achievement):
|
||||
_name = _p(
|
||||
'achievement:monthlyhours|name',
|
||||
"The 30 Days Challenge"
|
||||
)
|
||||
_subtext = _p(
|
||||
'achievement:monthlyhours|subtext',
|
||||
"Be active for 100 hours in a month"
|
||||
)
|
||||
|
||||
threshold = 100
|
||||
emoji_index = 5
|
||||
|
||||
@log_wrap(action='Calc MonthlyHours')
|
||||
async def _calculate(self):
|
||||
stats: 'StatsCog' = self.bot.get_cog('StatsCog')
|
||||
|
||||
lion = await self.bot.core.lions.fetch_member(self.guildid, self.userid)
|
||||
|
||||
records = await stats.data.VoiceSessionStats.table.select_where(
|
||||
userid=self.userid,
|
||||
guildid=self.guildid,
|
||||
).select(
|
||||
_first='MIN(start_time)'
|
||||
).with_no_adapter()
|
||||
first_session = records[0]['_first'] if records else None
|
||||
if not first_session:
|
||||
return 0
|
||||
|
||||
# Build the list of month start timestamps
|
||||
month_start = lion.month_start
|
||||
months = [month_start.astimezone(pytz.utc)]
|
||||
|
||||
while month_start >= first_session:
|
||||
month_start -= dt.timedelta(days=1)
|
||||
month_start = month_start.replace(day=1)
|
||||
months.append(month_start.astimezone(pytz.utc))
|
||||
|
||||
# Query the study times
|
||||
times = await stats.data.VoiceSessionStats.study_times_between(
|
||||
self.guildid, self.userid, *reversed(months), lion.now
|
||||
)
|
||||
max_time = max(times) // 3600
|
||||
return max_time if max_time >= self.threshold else times[-1] // 3600
|
||||
|
||||
|
||||
achievements = [
|
||||
Workout,
|
||||
VoiceHours,
|
||||
VoiceStreak,
|
||||
Voting,
|
||||
VoiceDays,
|
||||
TasksComplete,
|
||||
ScheduledSessions,
|
||||
MonthlyHours,
|
||||
]
|
||||
achievements.sort(key=lambda cls: cls.emoji_index)
|
||||
|
||||
|
||||
@log_wrap(action='Get Achievements')
|
||||
async def get_achievements_for(bot: LionBot, guildid: int, userid: int):
|
||||
"""
|
||||
Asynchronously fetch achievements for the given member.
|
||||
"""
|
||||
member_achieved = [
|
||||
ach(bot, guildid, userid) for ach in achievements
|
||||
]
|
||||
update_tasks = [
|
||||
asyncio.create_task(ach.update()) for ach in member_achieved
|
||||
]
|
||||
await asyncio.gather(*update_tasks)
|
||||
return member_achieved
|
||||
@@ -8,14 +8,18 @@ from discord import app_commands as appcmds
|
||||
from discord.ui.button import ButtonStyle
|
||||
|
||||
from meta import LionBot, LionCog, LionContext
|
||||
from core.lion_guild import VoiceMode
|
||||
from utils.lib import error_embed
|
||||
from utils.ui import LeoUI, AButton, utc_now
|
||||
from gui.base import CardMode
|
||||
from wards import low_management_ward
|
||||
|
||||
from . import babel
|
||||
from .data import StatsData
|
||||
from .ui import ProfileUI, WeeklyMonthlyUI, LeaderboardUI
|
||||
from .settings import StatisticsSettings, StatisticsConfigUI
|
||||
from .graphics.profilestats import get_full_profile
|
||||
from .achievements import get_achievements_for
|
||||
|
||||
_p = babel._p
|
||||
|
||||
@@ -43,7 +47,7 @@ class StatsCog(LionCog):
|
||||
name=_p('cmd:me', "me"),
|
||||
description=_p(
|
||||
'cmd:me|desc',
|
||||
"Display your personal profile and summary statistics."
|
||||
"Edit your personal profile and see your statistics."
|
||||
)
|
||||
)
|
||||
@appcmds.guild_only
|
||||
@@ -53,6 +57,50 @@ class StatsCog(LionCog):
|
||||
await ui.run(ctx.interaction)
|
||||
await ui.wait()
|
||||
|
||||
@cmds.hybrid_command(
|
||||
name=_p('cmd:profile', 'profile'),
|
||||
description=_p(
|
||||
'cmd:profile|desc',
|
||||
"Display the target's profile and statistics summary."
|
||||
)
|
||||
)
|
||||
@appcmds.rename(
|
||||
member=_p('cmd:profile|param:member', "member")
|
||||
)
|
||||
@appcmds.describe(
|
||||
member=_p(
|
||||
'cmd:profile|param:member|desc', "Member to display profile for."
|
||||
)
|
||||
)
|
||||
@appcmds.guild_only
|
||||
async def profile_cmd(self, ctx: LionContext, member: Optional[discord.Member] = None):
|
||||
if not ctx.guild:
|
||||
return
|
||||
if not ctx.interaction:
|
||||
return
|
||||
|
||||
member = member if member is not None else ctx.author
|
||||
if member.bot:
|
||||
# TODO: Localise
|
||||
await ctx.reply(
|
||||
"Bots cannot have profiles!",
|
||||
ephemeral=True
|
||||
)
|
||||
return
|
||||
await ctx.interaction.response.defer(thinking=True)
|
||||
# Ensure the lion exists
|
||||
await self.bot.core.lions.fetch_member(member.guild.id, member.id, member=member)
|
||||
|
||||
if ctx.lguild.guild_mode.voice:
|
||||
mode = CardMode.VOICE
|
||||
else:
|
||||
mode = CardMode.TEXT
|
||||
|
||||
profile_data = await get_full_profile(self.bot, member.id, member.guild.id, mode)
|
||||
with profile_data:
|
||||
file = discord.File(profile_data, 'profile.png')
|
||||
await ctx.reply(file=file)
|
||||
|
||||
@cmds.hybrid_command(
|
||||
name=_p('cmd:stats', "stats"),
|
||||
description=_p(
|
||||
@@ -105,6 +153,38 @@ class StatsCog(LionCog):
|
||||
await ui.run(ctx.interaction)
|
||||
await ui.wait()
|
||||
|
||||
@cmds.hybrid_command(
|
||||
name=_p('cmd:achievements', 'achievements'),
|
||||
description=_p(
|
||||
'cmd:achievements|desc',
|
||||
"View your progress towards the activity achievement awards!"
|
||||
)
|
||||
)
|
||||
@appcmds.guild_only
|
||||
async def achievements_cmd(self, ctx: LionContext):
|
||||
if not ctx.guild:
|
||||
return
|
||||
if not ctx.interaction:
|
||||
return
|
||||
t = self.bot.translator.t
|
||||
|
||||
await ctx.interaction.response.defer(thinking=True)
|
||||
|
||||
achievements = await get_achievements_for(self.bot, ctx.guild.id, ctx.author.id)
|
||||
embed = discord.Embed(
|
||||
title=t(_p(
|
||||
'cmd:achievements|embed:title',
|
||||
"Achievements"
|
||||
)),
|
||||
colour=discord.Colour.orange()
|
||||
)
|
||||
for achievement in achievements:
|
||||
name, value = achievement.make_field()
|
||||
embed.add_field(
|
||||
name=name, value=value, inline=False
|
||||
)
|
||||
await ctx.reply(embed=embed)
|
||||
|
||||
# Setting commands
|
||||
@LionCog.placeholder_group
|
||||
@cmds.hybrid_group('configure', with_app_command=False)
|
||||
|
||||
@@ -122,7 +122,7 @@ class StatsData(Registry):
|
||||
"SELECT study_time_between(%s, %s, %s, %s)",
|
||||
(guildid, userid, _start, _end)
|
||||
)
|
||||
return (await cursor.fetchone()[0]) or 0
|
||||
return (await cursor.fetchone())[0] or 0
|
||||
|
||||
@classmethod
|
||||
@log_wrap(action='study_times_between')
|
||||
@@ -162,11 +162,11 @@ class StatsData(Registry):
|
||||
"SELECT study_time_since(%s, %s, %s)",
|
||||
(guildid, userid, _start)
|
||||
)
|
||||
return (await cursor.fetchone()[0]) or 0
|
||||
return (await cursor.fetchone())[0] or 0
|
||||
|
||||
@classmethod
|
||||
@log_wrap(action='study_times_since')
|
||||
async def study_times_since(cls, guildid: Optional[int], userid: int, *starts) -> int:
|
||||
async def study_times_since(cls, guildid: Optional[int], userid: int, *starts) -> list[int]:
|
||||
if len(starts) < 1:
|
||||
raise ValueError('No starting points given!')
|
||||
|
||||
@@ -251,7 +251,7 @@ class StatsData(Registry):
|
||||
return leaderboard
|
||||
|
||||
@classmethod
|
||||
@log_wrap('leaderboard_all')
|
||||
@log_wrap(action='leaderboard_all')
|
||||
async def leaderboard_all(cls, guildid: int):
|
||||
"""
|
||||
Return the all-time voice totals for the given guild.
|
||||
|
||||
@@ -8,6 +8,7 @@ from gui.cards import ProfileCard
|
||||
|
||||
from modules.ranks.cog import RankCog
|
||||
from modules.ranks.utils import format_stat_range
|
||||
from ..achievements import get_achievements_for
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..cog import StatsCog
|
||||
@@ -17,11 +18,11 @@ async def get_profile_card(bot: LionBot, userid: int, guildid: int):
|
||||
ranks: Optional[RankCog] = bot.get_cog('RankCog')
|
||||
stats: Optional[StatsCog] = bot.get_cog('StatsCog')
|
||||
if ranks is None or stats is None:
|
||||
return
|
||||
raise ValueError("Cannot get profile card without ranks and stats cog loaded.")
|
||||
|
||||
guild = bot.get_guild(guildid)
|
||||
if guild is None:
|
||||
return
|
||||
raise ValueError(f"Cannot get profile card without guild {guildid}")
|
||||
|
||||
lion = await bot.core.lions.fetch_member(guildid, userid)
|
||||
luser = lion.luser
|
||||
@@ -76,14 +77,15 @@ async def get_profile_card(bot: LionBot, userid: int, guildid: int):
|
||||
else:
|
||||
next_rank = None
|
||||
|
||||
achievements = (0, 1, 2, 3)
|
||||
achievements = await get_achievements_for(bot, guildid, userid)
|
||||
achieved = tuple(ach.emoji_index for ach in achievements if ach.achieved)
|
||||
|
||||
card = ProfileCard(
|
||||
user=username,
|
||||
avatar=(userid, avatar),
|
||||
coins=lion.data.coins, gems=luser.data.gems, gifts=0,
|
||||
profile_badges=profile_badges,
|
||||
achievements=achievements,
|
||||
achievements=achieved,
|
||||
current_rank=current_rank,
|
||||
rank_progress=rank_progress,
|
||||
next_rank=next_rank
|
||||
|
||||
62
src/modules/statistics/graphics/profilestats.py
Normal file
62
src/modules/statistics/graphics/profilestats.py
Normal file
@@ -0,0 +1,62 @@
|
||||
import asyncio
|
||||
from io import BytesIO
|
||||
|
||||
from PIL import Image
|
||||
|
||||
from meta import LionBot
|
||||
from gui.base import CardMode
|
||||
|
||||
from .stats import get_stats_card
|
||||
from .profile import get_profile_card
|
||||
|
||||
|
||||
card_gap = 10
|
||||
|
||||
|
||||
async def get_full_profile(bot: LionBot, userid: int, guildid: int, mode: CardMode) -> BytesIO:
|
||||
"""
|
||||
Render both profile and stats for the target member in the given mode.
|
||||
|
||||
Combines the resulting cards into a single image and returns the image data.
|
||||
"""
|
||||
# Prepare cards for rendering
|
||||
get_tasks = (
|
||||
asyncio.create_task(get_stats_card(bot, userid, guildid, mode), name='get-stats-for-combined'),
|
||||
asyncio.create_task(get_profile_card(bot, userid, guildid), name='get-profile-for-combined'),
|
||||
)
|
||||
stats_card, profile_card = await asyncio.gather(*get_tasks)
|
||||
|
||||
# Render cards
|
||||
render_tasks = (
|
||||
asyncio.create_task(stats_card.render(), name='render-stats-for-combined'),
|
||||
asyncio.create_task(profile_card.render(), name='render=profile-for-combined'),
|
||||
)
|
||||
|
||||
# Load the card data into images
|
||||
stats_data, profile_data = await asyncio.gather(*render_tasks)
|
||||
with BytesIO(stats_data) as stats_stream, BytesIO(profile_data) as profile_stream:
|
||||
with Image.open(stats_stream) as stats_image, Image.open(profile_stream) as profile_image:
|
||||
# Create a new blank image of the correct dimenstions
|
||||
stats_bbox = stats_image.getbbox(alpha_only=False)
|
||||
profile_bbox = profile_image.getbbox(alpha_only=False)
|
||||
|
||||
if stats_bbox is None or profile_bbox is None:
|
||||
# Should be impossible, image is already checked by GUI client
|
||||
raise ValueError("Could not combine, empty stats or profile image.")
|
||||
|
||||
combined = Image.new(
|
||||
'RGBA',
|
||||
(
|
||||
max(stats_bbox[2], profile_bbox[2]),
|
||||
stats_bbox[3] + card_gap + profile_bbox[3]
|
||||
),
|
||||
color=None
|
||||
)
|
||||
with combined:
|
||||
combined.alpha_composite(profile_image)
|
||||
combined.alpha_composite(stats_image, (0, profile_bbox[3] + card_gap))
|
||||
|
||||
results = BytesIO()
|
||||
combined.save(results, format='PNG', compress_type=3, compress_level=1)
|
||||
results.seek(0)
|
||||
return results
|
||||
@@ -6,11 +6,28 @@ import discord
|
||||
from meta import LionBot
|
||||
from gui.cards import StatsCard
|
||||
from gui.base import CardMode
|
||||
from tracking.text.data import TextTrackerData
|
||||
|
||||
from .. import babel
|
||||
from ..data import StatsData
|
||||
|
||||
|
||||
_p = babel._p
|
||||
|
||||
|
||||
def format_time(seconds):
|
||||
return "{:02}:{:02}".format(
|
||||
int(seconds // 3600),
|
||||
int(seconds % 3600 // 60)
|
||||
)
|
||||
|
||||
|
||||
def format_xp(messages, xp):
|
||||
return f"{messages} ({xp} XP)"
|
||||
|
||||
|
||||
async def get_stats_card(bot: LionBot, userid: int, guildid: int, mode: CardMode):
|
||||
t = bot.translator.t
|
||||
data: StatsData = bot.get_cog('StatsCog').data
|
||||
|
||||
# TODO: Workouts
|
||||
@@ -32,28 +49,41 @@ async def get_stats_card(bot: LionBot, userid: int, guildid: int, mode: CardMode
|
||||
)
|
||||
|
||||
# Extract the study times for each period
|
||||
if mode in (CardMode.STUDY, CardMode.VOICE):
|
||||
if mode in (CardMode.STUDY, CardMode.VOICE, CardMode.ANKI):
|
||||
model = data.VoiceSessionStats
|
||||
refkey = (guildid or None, userid)
|
||||
ref_since = model.study_times_since
|
||||
ref_between = model.study_times_between
|
||||
|
||||
period_activity = await ref_since(*refkey, *period_timestamps)
|
||||
period_strings = [format_time(activity) for activity in reversed(period_activity)]
|
||||
month_activity = period_activity[1]
|
||||
month_string = t(_p(
|
||||
'gui:stats|mode:voice|month',
|
||||
"{hours} hours"
|
||||
)).format(hours=int(month_activity // 3600))
|
||||
elif mode is CardMode.TEXT:
|
||||
msgmodel = TextTrackerData.TextSessions
|
||||
if guildid:
|
||||
model = data.MemberExp
|
||||
msg_since = msgmodel.member_messages_since
|
||||
refkey = (guildid, userid)
|
||||
else:
|
||||
model = data.UserExp
|
||||
msg_since = msgmodel.member_messages_between
|
||||
refkey = (userid,)
|
||||
ref_since = model.xp_since
|
||||
ref_between = model.xp_between
|
||||
else:
|
||||
# TODO ANKI
|
||||
model = data.VoiceSessionStats
|
||||
refkey = (guildid, userid)
|
||||
ref_since = model.study_times_since
|
||||
ref_between = model.study_times_between
|
||||
|
||||
study_times = await ref_since(*refkey, *period_timestamps)
|
||||
xp_period_activity = await ref_since(*refkey, *period_timestamps)
|
||||
msg_period_activity = await msg_since(*refkey, *period_timestamps)
|
||||
period_strings = [
|
||||
format_xp(msgs, xp)
|
||||
for msgs, xp in zip(reversed(msg_period_activity), reversed(xp_period_activity))
|
||||
]
|
||||
month_string = f"{xp_period_activity[1]} XP"
|
||||
else:
|
||||
raise ValueError(f"Mode {mode} not supported")
|
||||
|
||||
# Get leaderboard position
|
||||
# TODO: Efficiency
|
||||
@@ -89,7 +119,8 @@ async def get_stats_card(bot: LionBot, userid: int, guildid: int, mode: CardMode
|
||||
|
||||
card = StatsCard(
|
||||
(position, 0),
|
||||
list(reversed(study_times)),
|
||||
period_strings,
|
||||
month_string,
|
||||
100,
|
||||
streaks,
|
||||
skin={'mode': mode}
|
||||
|
||||
@@ -41,7 +41,7 @@ class StatsUI(LeoUI):
|
||||
"""
|
||||
ID of guild to render stats for, or None if global.
|
||||
"""
|
||||
return self.guild.id if not self._showing_global else None
|
||||
return self.guild.id if self.guild and not self._showing_global else None
|
||||
|
||||
@property
|
||||
def userid(self) -> int:
|
||||
@@ -67,6 +67,7 @@ class StatsUI(LeoUI):
|
||||
Delete the output message and close the UI.
|
||||
"""
|
||||
await press.response.defer()
|
||||
if self._original and not self._original.is_expired():
|
||||
await self._original.delete_original_response()
|
||||
self._original = None
|
||||
await self.close()
|
||||
@@ -93,7 +94,10 @@ class StatsUI(LeoUI):
|
||||
args = await self.make_message()
|
||||
if thinking is not None and not thinking.is_expired() and thinking.response.is_done():
|
||||
asyncio.create_task(thinking.delete_original_response())
|
||||
if self._original and not self._original.is_expired():
|
||||
await self._original.edit_original_response(**args.edit_args, view=self)
|
||||
else:
|
||||
await self.close()
|
||||
|
||||
async def refresh(self, thinking: Optional[discord.Interaction] = None):
|
||||
"""
|
||||
|
||||
@@ -41,6 +41,7 @@ class StatType(IntEnum):
|
||||
|
||||
class LeaderboardUI(StatsUI):
|
||||
page_size = 10
|
||||
guildid: int
|
||||
|
||||
def __init__(self, bot, user, guild, **kwargs):
|
||||
super().__init__(bot, user, guild, **kwargs)
|
||||
@@ -199,6 +200,9 @@ class LeaderboardUI(StatsUI):
|
||||
mode = CardMode.TEXT
|
||||
elif self.stat_type is StatType.ANKI:
|
||||
mode = CardMode.ANKI
|
||||
else:
|
||||
raise ValueError
|
||||
|
||||
card = await get_leaderboard_card(
|
||||
self.bot, self.userid, self.guildid,
|
||||
mode,
|
||||
|
||||
@@ -166,7 +166,7 @@ class ProfileUI(StatsUI):
|
||||
t = self.bot.translator.t
|
||||
data: StatsData = self.bot.get_cog('StatsCog').data
|
||||
|
||||
tags = await data.ProfileTag.fetch_tags(self.guildid, self.userid)
|
||||
tags = await data.ProfileTag.fetch_tags(self.guild.id, self.userid)
|
||||
|
||||
modal = ProfileEditor()
|
||||
modal.editor.default = '\n'.join(tags)
|
||||
@@ -177,7 +177,7 @@ class ProfileUI(StatsUI):
|
||||
await interaction.response.defer(thinking=True, ephemeral=True)
|
||||
|
||||
# Set the new tags and refresh
|
||||
await data.ProfileTag.set_tags(self.guildid, self.userid, new_tags)
|
||||
await data.ProfileTag.set_tags(self.guild.id, self.userid, new_tags)
|
||||
if self._original is not None:
|
||||
self._profile_card = None
|
||||
await self.refresh(thinking=interaction)
|
||||
@@ -310,7 +310,7 @@ class ProfileUI(StatsUI):
|
||||
"""
|
||||
Create and render the XP and stats cards.
|
||||
"""
|
||||
card = await get_profile_card(self.bot, self.userid, self.guildid)
|
||||
card = await get_profile_card(self.bot, self.userid, self.guild.id)
|
||||
if card:
|
||||
await card.render()
|
||||
self._profile_card = card
|
||||
|
||||
@@ -329,7 +329,7 @@ class Exec(LionCog):
|
||||
results = [
|
||||
appcmd.Choice(name=f"No peers found matching {partial}", value=partial)
|
||||
]
|
||||
return results
|
||||
return results[:25]
|
||||
|
||||
async_cmd.autocomplete('target')(_peer_acmpl)
|
||||
|
||||
|
||||
@@ -242,6 +242,7 @@ class PresenceCtrl(LionCog):
|
||||
await self.data.init()
|
||||
if (leo_setting_cog := self.bot.get_cog('LeoSettings')) is not None:
|
||||
leo_setting_cog.bot_setting_groups.append(self.settings)
|
||||
self.crossload_group(self.leo_group, leo_setting_cog.leo_group)
|
||||
|
||||
await self.reload_presence()
|
||||
self.update_listeners()
|
||||
@@ -372,7 +373,12 @@ class PresenceCtrl(LionCog):
|
||||
"Unhandled exception occurred running client presence update loop. Closing loop."
|
||||
)
|
||||
|
||||
@cmds.hybrid_command(
|
||||
@LionCog.placeholder_group
|
||||
@cmds.hybrid_group('configure', with_app_command=False)
|
||||
async def leo_group(self, ctx: LionContext):
|
||||
...
|
||||
|
||||
@leo_group.command(
|
||||
name="presence",
|
||||
description="Globally set the bot status and activity."
|
||||
)
|
||||
|
||||
@@ -291,7 +291,7 @@ class TasklistCog(LionCog):
|
||||
name=t(_p(
|
||||
'argtype:taskid|error:no_tasks',
|
||||
"Tasklist empty! No matching tasks."
|
||||
)),
|
||||
))[:100],
|
||||
value=partial
|
||||
)
|
||||
]
|
||||
@@ -319,7 +319,7 @@ class TasklistCog(LionCog):
|
||||
if matching:
|
||||
# If matches were found, assume user wants one of the matches
|
||||
options = [
|
||||
appcmds.Choice(name=task_string, value=label)
|
||||
appcmds.Choice(name=task_string[:100], value=label)
|
||||
for label, task_string in matching
|
||||
]
|
||||
elif multi and partial.lower().strip() in ('-', 'all'):
|
||||
@@ -328,7 +328,7 @@ class TasklistCog(LionCog):
|
||||
name=t(_p(
|
||||
'argtype:taskid|match:all',
|
||||
"All tasks"
|
||||
)),
|
||||
))[:100],
|
||||
value='-'
|
||||
)
|
||||
]
|
||||
@@ -353,7 +353,7 @@ class TasklistCog(LionCog):
|
||||
multi_name = f"{partial[:remaining-1]} {error}"
|
||||
|
||||
multi_option = appcmds.Choice(
|
||||
name=multi_name,
|
||||
name=multi_name[:100],
|
||||
value=partial
|
||||
)
|
||||
options = [multi_option]
|
||||
@@ -371,7 +371,7 @@ class TasklistCog(LionCog):
|
||||
if not matching:
|
||||
matching = [(label, task) for label, task in labels if last_split.lower() in task.lower()]
|
||||
options.extend(
|
||||
appcmds.Choice(name=task_string, value=label)
|
||||
appcmds.Choice(name=task_string[:100], value=label)
|
||||
for label, task_string in matching
|
||||
)
|
||||
else:
|
||||
@@ -380,7 +380,7 @@ class TasklistCog(LionCog):
|
||||
name=t(_p(
|
||||
'argtype:taskid|error:no_matching',
|
||||
"No tasks matching '{partial}'!",
|
||||
)).format(partial=partial[:100]),
|
||||
)).format(partial=partial[:100])[:100],
|
||||
value=partial
|
||||
)
|
||||
]
|
||||
|
||||
@@ -728,7 +728,7 @@ class TasklistUI(BasePager):
|
||||
)
|
||||
try:
|
||||
await press.user.send(contents, file=file, silent=True)
|
||||
except discord.HTTPClient:
|
||||
except discord.HTTPException:
|
||||
fp.seek(0)
|
||||
file = discord.File(fp, filename='tasklist.md')
|
||||
await press.followup.send(
|
||||
@@ -736,7 +736,7 @@ class TasklistUI(BasePager):
|
||||
'ui:tasklist|button:save|error:dms',
|
||||
"Could not DM you! Do you have me blocked? Tasklist attached below."
|
||||
)),
|
||||
file=file
|
||||
file=file,
|
||||
)
|
||||
else:
|
||||
fp.seek(0)
|
||||
|
||||
@@ -393,7 +393,7 @@ class VideoCog(LionCog):
|
||||
only_warn = True
|
||||
|
||||
# Ack based on ticket created
|
||||
alert_ref = message.to_reference(fail_if_not_exists=False)
|
||||
alert_ref = message.to_reference(fail_if_not_exists=False) if message else None
|
||||
if only_warn:
|
||||
# TODO: Warn ticket
|
||||
warning = discord.Embed(
|
||||
|
||||
@@ -35,6 +35,8 @@ class VideoTicket(Ticket):
|
||||
**kwargs
|
||||
)
|
||||
|
||||
await ticket_data.update(created_at=utc_now().replace(tzinfo=None))
|
||||
|
||||
lguild = await bot.core.lions.fetch_guild(member.guild.id, guild=member.guild)
|
||||
new_ticket = cls(lguild, ticket_data)
|
||||
|
||||
|
||||
@@ -237,7 +237,7 @@ class ChannelSetting(Generic[ParentID, CT], InteractiveSetting[ParentID, int, CT
|
||||
|
||||
_selector_placeholder = "Select a Channel"
|
||||
channel_types: list[discord.ChannelType] = []
|
||||
_allow_object = True
|
||||
_allow_object = False
|
||||
|
||||
@classmethod
|
||||
def _data_from_value(cls, parent_id, value, **kwargs):
|
||||
@@ -368,7 +368,7 @@ class RoleSetting(InteractiveSetting[ParentID, int, Union[discord.Role, discord.
|
||||
_accepts = _p('settype:role|accepts', "A role name or id")
|
||||
|
||||
_selector_placeholder = "Select a Role"
|
||||
_allow_object = True
|
||||
_allow_object = False
|
||||
|
||||
@classmethod
|
||||
def _get_guildid(cls, parent_id: int, **kwargs) -> int:
|
||||
@@ -915,7 +915,7 @@ class TimezoneSetting(InteractiveSetting[ParentID, str, TZT]):
|
||||
name=t(_p(
|
||||
'set_type:timezone|acmpl|no_matching',
|
||||
"No timezones matching '{input}'!"
|
||||
)).format(input=partial),
|
||||
)).format(input=partial)[:100],
|
||||
value=partial
|
||||
)
|
||||
]
|
||||
@@ -930,7 +930,7 @@ class TimezoneSetting(InteractiveSetting[ParentID, str, TZT]):
|
||||
"{tz} (Currently {now})"
|
||||
)).format(tz=tz, now=nowstr)
|
||||
choice = appcmds.Choice(
|
||||
name=name,
|
||||
name=name[:100],
|
||||
value=tz
|
||||
)
|
||||
choices.append(choice)
|
||||
|
||||
@@ -236,7 +236,7 @@ class InteractiveSetting(BaseSetting[ParentID, SettingData, SettingValue]):
|
||||
Callable[[ParentID, SettingData], Coroutine[Any, Any, None]]
|
||||
"""
|
||||
if self._event is not None and (bot := ctx_bot.get()) is not None:
|
||||
bot.dispatch(self._event, self.parent_id, self.data)
|
||||
bot.dispatch(self._event, self.parent_id, self)
|
||||
|
||||
def get_listener(self, key):
|
||||
return self._listeners_.get(key, None)
|
||||
@@ -453,6 +453,12 @@ class InteractiveSetting(BaseSetting[ParentID, SettingData, SettingValue]):
|
||||
data = await cls._parse_string(parent_id, userstr, **kwargs)
|
||||
return cls(parent_id, data, **kwargs)
|
||||
|
||||
@classmethod
|
||||
async def from_value(cls, parent_id, value, **kwargs):
|
||||
await cls._check_value(parent_id, value, **kwargs)
|
||||
data = cls._data_from_value(parent_id, value, **kwargs)
|
||||
return cls(parent_id, data, **kwargs)
|
||||
|
||||
@classmethod
|
||||
async def _parse_string(cls, parent_id, string: str, **kwargs) -> Optional[SettingData]:
|
||||
"""
|
||||
@@ -471,15 +477,14 @@ class InteractiveSetting(BaseSetting[ParentID, SettingData, SettingValue]):
|
||||
raise NotImplementedError
|
||||
|
||||
@classmethod
|
||||
async def _check_value(cls, parent_id, value, **kwargs) -> Optional[str]:
|
||||
async def _check_value(cls, parent_id, value, **kwargs):
|
||||
"""
|
||||
Check the provided value is valid.
|
||||
|
||||
Many setting update methods now provide Discord objects instead of raw data or user strings.
|
||||
This method may be used for value-checking such a value.
|
||||
|
||||
Returns `None` if there are no issues, otherwise an error message.
|
||||
Subclasses should override this to implement a value checker.
|
||||
Raises UserInputError if the value fails validation.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ from meta.errors import UserInputError
|
||||
from meta.logger import log_wrap, logging_context
|
||||
from meta.sharding import THIS_SHARD
|
||||
from meta.app import appname
|
||||
from meta.monitor import ComponentMonitor, ComponentStatus, StatusLevel
|
||||
from utils.lib import utc_now, error_embed
|
||||
|
||||
from wards import low_management_ward, sys_admin_ward
|
||||
@@ -42,10 +43,14 @@ class TextTrackerCog(LionCog):
|
||||
self.data = bot.db.load_registry(TextTrackerData())
|
||||
self.settings = TextTrackerSettings()
|
||||
self.global_settings = TextTrackerGlobalSettings()
|
||||
self.monitor = ComponentMonitor('TextTracker', self._monitor)
|
||||
self.babel = babel
|
||||
|
||||
self.sessionq = asyncio.Queue(maxsize=0)
|
||||
|
||||
self.ready = asyncio.Event()
|
||||
self.errors = 0
|
||||
|
||||
# Map of ongoing text sessions
|
||||
# guildid -> (userid -> TextSession)
|
||||
self.ongoing = defaultdict(dict)
|
||||
@@ -54,7 +59,41 @@ class TextTrackerCog(LionCog):
|
||||
|
||||
self.untracked_channels = self.settings.UntrackedTextChannels._cache
|
||||
|
||||
async def _monitor(self):
|
||||
state = (
|
||||
"<"
|
||||
"TextTracker"
|
||||
" ready={ready}"
|
||||
" queued={queued}"
|
||||
" errors={errors}"
|
||||
" running={running}"
|
||||
" consumer={consumer}"
|
||||
">"
|
||||
)
|
||||
data = dict(
|
||||
ready=self.ready.is_set(),
|
||||
queued=self.sessionq.qsize(),
|
||||
errors=self.errors,
|
||||
running=sum(len(usessions) for usessions in self.ongoing.values()),
|
||||
consumer="'Running'" if (self._consumer_task and not self._consumer_task.done()) else "'Not Running'",
|
||||
)
|
||||
if not self.ready.is_set():
|
||||
level = StatusLevel.STARTING
|
||||
info = f"(STARTING) Not initialised. {state}"
|
||||
elif not self._consumer_task:
|
||||
level = StatusLevel.ERRORED
|
||||
info = f"(ERROR) Consumer task not running. {state}"
|
||||
elif self.errors > 1:
|
||||
level = StatusLevel.UNSURE
|
||||
info = f"(UNSURE) Errors occurred while consuming. {state}"
|
||||
else:
|
||||
level = StatusLevel.OKAY
|
||||
info = f"(OK) Message tracking operational. {state}"
|
||||
|
||||
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.XPPerPeriod)
|
||||
@@ -83,6 +122,7 @@ class TextTrackerCog(LionCog):
|
||||
await self.initialise()
|
||||
|
||||
async def cog_unload(self):
|
||||
self.ready.clear()
|
||||
if self._consumer_task is not None:
|
||||
self._consumer_task.cancel()
|
||||
|
||||
@@ -104,7 +144,7 @@ class TextTrackerCog(LionCog):
|
||||
await self.bot.core.lions.fetch_member(session.guildid, session.userid)
|
||||
self.sessionq.put_nowait(session)
|
||||
|
||||
@log_wrap(stack=['Text Sessions', 'Message Event'])
|
||||
@log_wrap(stack=['Text Sessions', 'Consumer'])
|
||||
async def _session_consumer(self):
|
||||
"""
|
||||
Process completed sessions in batches of length `batchsize`.
|
||||
@@ -132,6 +172,7 @@ class TextTrackerCog(LionCog):
|
||||
logger.exception(
|
||||
"Unknown exception processing batch of text sessions! Discarding and continuing."
|
||||
)
|
||||
self.errors += 1
|
||||
batch = []
|
||||
counter = 0
|
||||
last_time = time.monotonic()
|
||||
@@ -157,6 +198,9 @@ class TextTrackerCog(LionCog):
|
||||
|
||||
# Batch-fetch lguilds
|
||||
lguilds = await self.bot.core.lions.fetch_guilds(*{session.guildid for session in batch})
|
||||
await self.bot.core.lions.fetch_members(
|
||||
*((session.guildid, session.userid) for session in batch)
|
||||
)
|
||||
|
||||
# Build data
|
||||
rows = []
|
||||
@@ -202,9 +246,11 @@ class TextTrackerCog(LionCog):
|
||||
"""
|
||||
Launch the session consumer.
|
||||
"""
|
||||
self.ready.clear()
|
||||
if self._consumer_task and not self._consumer_task.cancelled():
|
||||
self._consumer_task.cancel()
|
||||
self._consumer_task = asyncio.create_task(self._session_consumer())
|
||||
self._consumer_task = asyncio.create_task(self._session_consumer(), name='text-session-consumer')
|
||||
self.ready.set()
|
||||
logger.info("Launched text session consumer.")
|
||||
|
||||
@LionCog.listener('on_message')
|
||||
|
||||
@@ -289,6 +289,42 @@ class TextTrackerData(Registry):
|
||||
)
|
||||
return [r['messages'] or 0 for r in await cursor.fetchall()]
|
||||
|
||||
@classmethod
|
||||
@log_wrap(action='user_messages_since')
|
||||
async def user_messages_since(cls, userid: int, *points):
|
||||
"""
|
||||
Compute messages written between the given points.
|
||||
"""
|
||||
query = sql.SQL(
|
||||
"""
|
||||
SELECT
|
||||
(
|
||||
SELECT
|
||||
SUM(messages)
|
||||
FROM text_sessions s
|
||||
WHERE
|
||||
s.userid = %s
|
||||
AND s.start_time >= t._start
|
||||
) AS messages
|
||||
FROM
|
||||
(VALUES {})
|
||||
AS
|
||||
t (_start)
|
||||
ORDER BY t._start
|
||||
"""
|
||||
).format(
|
||||
sql.SQL(', ').join(
|
||||
sql.SQL("({})").format(sql.Placeholder()) for _ in points
|
||||
)
|
||||
)
|
||||
async with cls._connector.connection() as conn:
|
||||
async with conn.cursor() as cursor:
|
||||
await cursor.execute(
|
||||
query,
|
||||
tuple(chain((userid,), points))
|
||||
)
|
||||
return [r['messages'] or 0 for r in await cursor.fetchall()]
|
||||
|
||||
@classmethod
|
||||
@log_wrap(action='msgs_leaderboard_all')
|
||||
async def leaderboard_since(cls, guildid: int, since):
|
||||
@@ -301,7 +337,7 @@ class TextTrackerData(Registry):
|
||||
FROM text_sessions
|
||||
WHERE guildid = %s AND start_time >= %s
|
||||
GROUP BY userid
|
||||
ORDER BY
|
||||
ORDER BY user_total DESC
|
||||
"""
|
||||
)
|
||||
async with cls._connector.connection() as conn:
|
||||
@@ -325,7 +361,7 @@ class TextTrackerData(Registry):
|
||||
FROM text_sessions
|
||||
WHERE guildid = %s
|
||||
GROUP BY userid
|
||||
ORDER BY
|
||||
ORDER BY user_total DESC
|
||||
"""
|
||||
)
|
||||
async with cls._connector.connection() as conn:
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
from typing import Optional
|
||||
import asyncio
|
||||
import itertools
|
||||
import datetime as dt
|
||||
from collections import defaultdict
|
||||
|
||||
import discord
|
||||
from discord.ext import commands as cmds
|
||||
from discord import app_commands as appcmds
|
||||
|
||||
from data import Condition
|
||||
from meta import LionBot, LionCog, LionContext
|
||||
from meta.errors import UserInputError
|
||||
from meta.logger import log_wrap, logging_context
|
||||
from meta.logger import log_wrap
|
||||
from meta.sharding import THIS_SHARD
|
||||
from utils.lib import utc_now, error_embed
|
||||
from meta.monitor import ComponentMonitor, ComponentStatus, StatusLevel
|
||||
from utils.lib import utc_now
|
||||
from core.lion_guild import VoiceMode
|
||||
|
||||
from wards import low_management_ward, moderator_ctxward
|
||||
@@ -35,6 +36,7 @@ class VoiceTrackerCog(LionCog):
|
||||
self.data = bot.db.load_registry(VoiceTrackerData())
|
||||
self.settings = VoiceTrackerSettings()
|
||||
self.babel = babel
|
||||
self.monitor = ComponentMonitor('VoiceTracker', self._monitor)
|
||||
|
||||
# State
|
||||
# Flag indicating whether local voice sessions have been initialised
|
||||
@@ -44,7 +46,77 @@ class VoiceTrackerCog(LionCog):
|
||||
|
||||
self.untracked_channels = self.settings.UntrackedChannels._cache
|
||||
|
||||
self.active_sessions = VoiceSession._active_sessions_
|
||||
|
||||
async def _monitor(self):
|
||||
state = (
|
||||
"<"
|
||||
"VoiceTracker"
|
||||
" initialised={initialised}"
|
||||
" active={active}"
|
||||
" pending={pending}"
|
||||
" ongoing={ongoing}"
|
||||
" locked={locked}"
|
||||
" actual={actual}"
|
||||
" channels={channels}"
|
||||
" cached={cached}"
|
||||
" initial_event={initial_event}"
|
||||
" lock={lock}"
|
||||
">"
|
||||
)
|
||||
data = dict(
|
||||
initialised=self.initialised.is_set(),
|
||||
active=0,
|
||||
pending=0,
|
||||
ongoing=0,
|
||||
locked=0,
|
||||
actual=0,
|
||||
channels=0,
|
||||
cached=sum(len(gsessions) for gsessions in VoiceSession._sessions_.values()),
|
||||
initial_event=self.initialised,
|
||||
lock=self.tracking_lock
|
||||
)
|
||||
channels = set()
|
||||
for tguild in self.active_sessions.values():
|
||||
for session in tguild.values():
|
||||
data['active'] += 1
|
||||
if session.activity is SessionState.ONGOING:
|
||||
data['ongoing'] += 1
|
||||
elif session.activity is SessionState.PENDING:
|
||||
data['pending'] += 1
|
||||
|
||||
if session.lock.locked():
|
||||
data['locked'] += 1
|
||||
|
||||
if session.state:
|
||||
channels.add(session.state.channelid)
|
||||
data['channels'] = len(channels)
|
||||
|
||||
for guild in self.bot.guilds:
|
||||
for channel in itertools.chain(guild.voice_channels, guild.stage_channels):
|
||||
if not self.is_untracked(channel):
|
||||
for member in channel.members:
|
||||
if member.voice and not member.bot:
|
||||
data['actual'] += 1
|
||||
|
||||
if not self.initialised.is_set():
|
||||
level = StatusLevel.STARTING
|
||||
info = f"(STARTING) Not initialised. {state}"
|
||||
elif self.tracking_lock.locked():
|
||||
level = StatusLevel.WAITING
|
||||
info = f"(WAITING) Waiting for tracking lock. {state}"
|
||||
elif data['actual'] != data['active']:
|
||||
level = StatusLevel.UNSURE
|
||||
info = f"(UNSURE) Actual sessions do not match active. {state}"
|
||||
else:
|
||||
level = StatusLevel.OKAY
|
||||
info = f"(OK) Voice tracking operational. {state}"
|
||||
|
||||
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.HourlyReward)
|
||||
@@ -71,7 +143,8 @@ class VoiceTrackerCog(LionCog):
|
||||
# Simultaneously!
|
||||
...
|
||||
|
||||
def get_session(self, guildid, userid, **kwargs) -> VoiceSession:
|
||||
# ----- Cog API -----
|
||||
def get_session(self, guildid, userid, **kwargs):
|
||||
"""
|
||||
Get the VoiceSession for the given member.
|
||||
|
||||
@@ -91,6 +164,199 @@ class VoiceTrackerCog(LionCog):
|
||||
untracked = False
|
||||
return untracked
|
||||
|
||||
@log_wrap(action='load sessions')
|
||||
async def _load_sessions(self,
|
||||
states: dict[tuple[int, int], TrackedVoiceState],
|
||||
ongoing: list[VoiceTrackerData.VoiceSessionsOngoing]):
|
||||
"""
|
||||
Load voice sessions from provided states and ongoing data.
|
||||
|
||||
Provided data may cross multiple guilds.
|
||||
Assumes all states which do not have data should be started.
|
||||
Assumes all ongoing data which does not have states should be ended.
|
||||
Assumes untracked channel data is up to date.
|
||||
"""
|
||||
OngoingData = VoiceTrackerData.VoiceSessionsOngoing
|
||||
|
||||
# Compute time to end complete sessions
|
||||
now = utc_now()
|
||||
last_update = max((row.last_update for row in ongoing), default=now)
|
||||
end_at = min(last_update + dt.timedelta(seconds=3600), now)
|
||||
|
||||
# Bulk fetches for voice-active members and guilds
|
||||
active_memberids = list(states.keys())
|
||||
active_guildids = set(gid for gid, _ in states)
|
||||
|
||||
if states:
|
||||
lguilds = await self.bot.core.lions.fetch_guilds(*active_guildids)
|
||||
await self.bot.core.lions.fetch_members(*active_memberids)
|
||||
tracked_today_data = await self.data.VoiceSessions.multiple_voice_tracked_since(
|
||||
*((guildid, userid, lguilds[guildid].today) for guildid, userid in active_memberids)
|
||||
)
|
||||
tracked_today = {(row['guildid'], row['userid']): row['tracked'] for row in tracked_today_data}
|
||||
else:
|
||||
lguilds = {}
|
||||
tracked_today = {}
|
||||
|
||||
# Zip session information together by memberid keys
|
||||
sessions: dict[tuple[int, int], tuple[Optional[TrackedVoiceState], Optional[OngoingData]]] = {}
|
||||
for row in ongoing:
|
||||
key = (row.guildid, row.userid)
|
||||
sessions[key] = (states.pop(key, None), row)
|
||||
for key, state in states.items():
|
||||
sessions[key] = (state, None)
|
||||
|
||||
# Now split up session information to fill action maps
|
||||
close_ongoing = []
|
||||
update_ongoing = []
|
||||
create_ongoing = []
|
||||
expiries = {}
|
||||
load_sessions = []
|
||||
schedule_sessions = {}
|
||||
|
||||
for (gid, uid), (state, data) in sessions.items():
|
||||
if state is not None:
|
||||
# Member is active
|
||||
if data is not None and data.channelid != state.channelid:
|
||||
# Ongoing session does not match active state
|
||||
# Close the session, but still create/schedule the state
|
||||
close_ongoing.append((gid, uid, end_at))
|
||||
data = None
|
||||
|
||||
# Now create/update/schedule active session
|
||||
# Also create/update data if required
|
||||
lguild = lguilds[gid]
|
||||
tomorrow = lguild.today + dt.timedelta(days=1)
|
||||
cap = lguild.config.get('daily_voice_cap').value
|
||||
tracked = tracked_today[gid, uid]
|
||||
hourly_rate = await self._calculate_rate(gid, uid, state)
|
||||
|
||||
if tracked >= cap:
|
||||
# Active session is already over cap
|
||||
# Stop ongoing if it exists, and schedule next session start
|
||||
delay = (tomorrow - now).total_seconds()
|
||||
start_time = tomorrow
|
||||
expiry = tomorrow + dt.timedelta(seconds=cap)
|
||||
schedule_sessions[(gid, uid)] = (delay, start_time, expiry, state, hourly_rate)
|
||||
if data is not None:
|
||||
close_ongoing.append((
|
||||
gid, uid,
|
||||
max(now - dt.timedelta(seconds=tracked - cap), data.last_update)
|
||||
))
|
||||
else:
|
||||
# Active session, update/create data
|
||||
expiry = now + dt.timedelta(seconds=(cap - tracked))
|
||||
if expiry > tomorrow:
|
||||
expiry = tomorrow + dt.timedelta(seconds=cap)
|
||||
expiries[(gid, uid)] = expiry
|
||||
if data is not None:
|
||||
update_ongoing.append((gid, uid, now, state.stream, state.video, hourly_rate))
|
||||
else:
|
||||
create_ongoing.append((
|
||||
gid, uid, state.channelid, now, now, state.stream, state.video, hourly_rate
|
||||
))
|
||||
elif data is not None:
|
||||
# Ongoing data has no state, close the session
|
||||
close_ongoing.append((gid, uid, end_at))
|
||||
|
||||
# Close data that needs closing
|
||||
if close_ongoing:
|
||||
logger.info(
|
||||
f"Ending {len(close_ongoing)} ongoing voice sessions with no matching voice state."
|
||||
)
|
||||
await self.data.VoiceSessionsOngoing.close_voice_sessions_at(*close_ongoing)
|
||||
|
||||
# Update data that needs updating
|
||||
if update_ongoing:
|
||||
logger.info(
|
||||
f"Continuing {len(update_ongoing)} ongoing voice sessions with matching voice state."
|
||||
)
|
||||
rows = await self.data.VoiceSessionsOngoing.update_voice_sessions_at(*update_ongoing)
|
||||
load_sessions.extend(rows)
|
||||
|
||||
# Create data that needs creating
|
||||
if create_ongoing:
|
||||
logger.info(
|
||||
f"Creating {len(create_ongoing)} voice sessions from new voice states."
|
||||
)
|
||||
# First ensure the tracked channels exist
|
||||
cids = set((item[2], item[0]) for item in create_ongoing)
|
||||
await self.data.TrackedChannel.fetch_multiple(*cids)
|
||||
|
||||
# Then create the sessions
|
||||
rows = await self.data.VoiceSessionsOngoing.table.insert_many(
|
||||
('guildid', 'userid', 'channelid', 'start_time', 'last_update', 'live_stream',
|
||||
'live_video', 'hourly_coins'),
|
||||
*create_ongoing
|
||||
).with_adapter(self.data.VoiceSessionsOngoing._make_rows)
|
||||
load_sessions.extend(rows)
|
||||
|
||||
# Create sessions from ongoing, with expiry
|
||||
for row in load_sessions:
|
||||
VoiceSession.from_ongoing(self.bot, row, expiries[(row.guildid, row.userid)])
|
||||
|
||||
# Schedule starting sessions
|
||||
for (gid, uid), args in schedule_sessions.items():
|
||||
session = VoiceSession.get(self.bot, gid, uid)
|
||||
await session.schedule_start(*args)
|
||||
|
||||
logger.info(
|
||||
f"Successfully loaded {len(load_sessions)} and scheduled {len(schedule_sessions)} voice sessions."
|
||||
)
|
||||
|
||||
@log_wrap(action='refresh guild sessions')
|
||||
async def refresh_guild_sessions(self, guild: discord.Guild):
|
||||
"""
|
||||
Idempotently refresh all guild voice sessions in the given guild.
|
||||
|
||||
Essentially a lighter version of `initialise`.
|
||||
"""
|
||||
# TODO: There is a very small potential window for a race condition here
|
||||
# Since we do not have a version of 'handle_events' for the guild
|
||||
# We may actually handle events before starting refresh
|
||||
# Causing sessions to have invalid state.
|
||||
# If this becomes an actual problem, implement an `ignore_guilds` set flag of some form...
|
||||
logger.debug(f"Beginning voice state refresh for <gid: {guild.id}>")
|
||||
|
||||
async with self.tracking_lock:
|
||||
# TODO: Add a 'lock holder' attribute which is readable by the monitor
|
||||
logger.debug(f"Voice state refresh for <gid: {guild.id}> is past lock")
|
||||
|
||||
# Deactivate any ongoing session tasks in this guild
|
||||
active = self.active_sessions.pop(guild.id, {}).values()
|
||||
for session in active:
|
||||
session.cancel()
|
||||
# Clear registry
|
||||
VoiceSession._sessions_.pop(guild.id, None)
|
||||
|
||||
# Update untracked channel information for this guild
|
||||
self.untracked_channels.pop(guild.id, None)
|
||||
await self.settings.UntrackedChannels.get(guild.id)
|
||||
|
||||
# Read tracked voice states
|
||||
states = {}
|
||||
for channel in itertools.chain(guild.voice_channels, guild.stage_channels):
|
||||
if not self.is_untracked(channel):
|
||||
for member in channel.members:
|
||||
if member.voice and not member.bot:
|
||||
state = TrackedVoiceState.from_voice_state(member.voice)
|
||||
states[(guild.id, member.id)] = state
|
||||
logger.debug(f"Loaded {len(states)} tracked voice states for <gid: {guild.id}>.")
|
||||
|
||||
# Read ongoing session data
|
||||
ongoing = await self.data.VoiceSessionsOngoing.fetch_where(guildid=guild.id)
|
||||
logger.debug(
|
||||
f"Loaded {len(ongoing)} ongoing voice sessions from data for <gid: {guild.id}>. Beginning reload."
|
||||
)
|
||||
|
||||
await self._load_sessions(states, ongoing)
|
||||
logger.info(
|
||||
f"Completed guild voice session reload for <gid: {guild.id}> "
|
||||
f"with '{len(self.active_sessions[guild.id])}' active sessions."
|
||||
)
|
||||
|
||||
|
||||
# ----- Event Handlers -----
|
||||
@LionCog.listener('on_ready')
|
||||
@log_wrap(action='Init Voice Sessions')
|
||||
async def initialise(self):
|
||||
@@ -99,192 +365,54 @@ class VoiceTrackerCog(LionCog):
|
||||
|
||||
Ends ongoing sessions for members who are not in the given voice channel.
|
||||
"""
|
||||
# First take the tracking lock
|
||||
# Ensures current event handling completes before re-initialisation
|
||||
async with self.tracking_lock:
|
||||
logger.info("Reloading ongoing voice sessions")
|
||||
|
||||
logger.debug("Disabling voice state event handling.")
|
||||
logger.info("Beginning voice session state initialisation. Disabling voice event handling.")
|
||||
# If `on_ready` is called, that means we are initialising
|
||||
# or we missed events and need to re-initialise.
|
||||
# Start ignoring events because they may be working on stale or partial state
|
||||
self.handle_events = False
|
||||
|
||||
# Services which read our cache should wait for initialisation before taking the lock
|
||||
self.initialised.clear()
|
||||
|
||||
# Wait for running events to complete
|
||||
# And make sure future events will be processed after initialisation
|
||||
# Note only events occurring after our voice state snapshot will be processed
|
||||
async with self.tracking_lock:
|
||||
# Deactivate all ongoing sessions
|
||||
active = [session for gsessions in self.active_sessions.values() for session in gsessions.values()]
|
||||
for session in active:
|
||||
session.cancel()
|
||||
self.active_sessions.clear()
|
||||
|
||||
# Also clear the session registry cache
|
||||
VoiceSession._sessions_.clear()
|
||||
|
||||
# Refresh untracked information for all guilds we are in
|
||||
await self.settings.UntrackedChannels.setup(self.bot)
|
||||
|
||||
# Read and save the tracked voice states of all visible voice channels
|
||||
voice_members = {} # (guildid, userid) -> TrackedVoiceState
|
||||
voice_guilds = set()
|
||||
states = {}
|
||||
for guild in self.bot.guilds:
|
||||
untracked = self.untracked_channels.get(guild.id, ())
|
||||
for channel in guild.voice_channels:
|
||||
if channel.id in untracked:
|
||||
continue
|
||||
if channel.category_id and channel.category_id in untracked:
|
||||
continue
|
||||
|
||||
for channel in itertools.chain(guild.voice_channels, guild.stage_channels):
|
||||
if not self.is_untracked(channel):
|
||||
for member in channel.members:
|
||||
if member.bot:
|
||||
continue
|
||||
voice_members[(guild.id, member.id)] = TrackedVoiceState.from_voice_state(member.voice)
|
||||
voice_guilds.add(guild.id)
|
||||
if member.voice and not member.bot:
|
||||
state = TrackedVoiceState.from_voice_state(member.voice)
|
||||
states[(guild.id, member.id)] = state
|
||||
|
||||
logger.debug(f"Cached {len(voice_members)} members from voice channels.")
|
||||
logger.info(
|
||||
f"Saved voice snapshot with {len(states)} tracked states. Re-enabling voice event handling."
|
||||
)
|
||||
self.handle_events = True
|
||||
logger.debug("Re-enabled voice state event handling.")
|
||||
|
||||
# Iterate through members with current ongoing sessions
|
||||
# End or update sessions as needed, based on saved tracked state
|
||||
ongoing_rows = await self.data.VoiceSessionsOngoing.fetch_where(
|
||||
guildid=[guild.id for guild in self.bot.guilds]
|
||||
)
|
||||
logger.debug(
|
||||
f"Loaded {len(ongoing_rows)} ongoing sessions from data. Splitting into complete and incomplete."
|
||||
)
|
||||
complete = []
|
||||
incomplete = []
|
||||
incomplete_guildids = set()
|
||||
|
||||
# Compute time to end complete sessions
|
||||
now = utc_now()
|
||||
last_update = max((row.last_update for row in ongoing_rows), default=now)
|
||||
end_at = min(last_update + dt.timedelta(seconds=3600), now)
|
||||
|
||||
for row in ongoing_rows:
|
||||
key = (row.guildid, row.userid)
|
||||
state = voice_members.get(key, None)
|
||||
untracked = self.untracked_channels.get(row.guildid, [])
|
||||
if (
|
||||
state
|
||||
and state.channelid == row.channelid
|
||||
and state.channelid not in untracked
|
||||
and (ch := self.bot.get_channel(state.channelid)) is not None
|
||||
and (not ch.category_id or ch.category_id not in untracked)
|
||||
):
|
||||
# Mark session as ongoing
|
||||
incomplete.append((row, state))
|
||||
incomplete_guildids.add(row.guildid)
|
||||
voice_members.pop(key)
|
||||
else:
|
||||
# Mark session as complete
|
||||
complete.append((row.guildid, row.userid, end_at))
|
||||
|
||||
# Load required guild data into cache
|
||||
active_guildids = incomplete_guildids.union(voice_guilds)
|
||||
if active_guildids:
|
||||
await self.bot.core.data.Guild.fetch_where(guildid=tuple(active_guildids))
|
||||
lguilds = {guildid: await self.bot.core.lions.fetch_guild(guildid) for guildid in active_guildids}
|
||||
|
||||
# Calculate tracked_today for members with ongoing sessions
|
||||
active_members = set((row.guildid, row.userid) for row, _ in incomplete)
|
||||
active_members.update(voice_members.keys())
|
||||
if active_members:
|
||||
tracked_today_data = await self.data.VoiceSessions.multiple_voice_tracked_since(
|
||||
*((guildid, userid, lguilds[guildid].today) for guildid, userid in active_members)
|
||||
)
|
||||
else:
|
||||
tracked_today_data = []
|
||||
tracked_today = {(row['guildid'], row['userid']): row['tracked'] for row in tracked_today_data}
|
||||
|
||||
if incomplete:
|
||||
# Note that study_time_since _includes_ ongoing sessions in its calculation
|
||||
# So expiry times are "time left today until cap" or "tomorrow + cap"
|
||||
to_load = [] # (session_data, expiry_time)
|
||||
to_update = [] # (guildid, userid, update_at, stream, video, hourly_rate)
|
||||
for session_data, state in incomplete:
|
||||
# Calculate expiry times
|
||||
lguild = lguilds[session_data.guildid]
|
||||
cap = lguild.config.get('daily_voice_cap').value
|
||||
tracked = tracked_today[(session_data.guildid, session_data.userid)]
|
||||
if tracked >= cap:
|
||||
# Already over cap
|
||||
complete.append((
|
||||
session_data.guildid,
|
||||
session_data.userid,
|
||||
max(now + dt.timedelta(seconds=tracked - cap), session_data.last_update)
|
||||
))
|
||||
else:
|
||||
tomorrow = lguild.today + dt.timedelta(days=1)
|
||||
expiry = now + dt.timedelta(seconds=(cap - tracked))
|
||||
if expiry > tomorrow:
|
||||
expiry = tomorrow + dt.timedelta(seconds=cap)
|
||||
to_load.append((session_data, expiry))
|
||||
|
||||
# TODO: Probably better to do this by batch
|
||||
# Could force all bonus calculators to accept list of members
|
||||
hourly_rate = await self._calculate_rate(session_data.guildid, session_data.userid, state)
|
||||
to_update.append((
|
||||
session_data.guildid,
|
||||
session_data.userid,
|
||||
now,
|
||||
state.stream,
|
||||
state.video,
|
||||
hourly_rate
|
||||
))
|
||||
# Run the updates, note that session_data uses registry pattern so will also update
|
||||
if to_update:
|
||||
await self.data.VoiceSessionsOngoing.update_voice_sessions_at(*to_update)
|
||||
|
||||
# Load the sessions
|
||||
for data, expiry in to_load:
|
||||
VoiceSession.from_ongoing(self.bot, data, expiry)
|
||||
|
||||
logger.info(f"Resumed {len(to_load)} ongoing voice sessions.")
|
||||
|
||||
if complete:
|
||||
logger.info(f"Ending {len(complete)} out-of-date or expired study sessions.")
|
||||
|
||||
# Complete sessions just need a mass end_voice_session_at()
|
||||
await self.data.VoiceSessionsOngoing.close_voice_sessions_at(*complete)
|
||||
|
||||
# Then iterate through the saved states from tracked voice channels
|
||||
# Start sessions if they don't already exist
|
||||
if voice_members:
|
||||
expiries = {} # (guildid, memberid) -> expiry time
|
||||
to_create = [] # (guildid, userid, channelid, start_time, last_update, live_stream, live_video, rate)
|
||||
for (guildid, userid), state in voice_members.items():
|
||||
untracked = self.untracked_channels.get(guildid, [])
|
||||
channel = self.bot.get_channel(state.channelid)
|
||||
if (
|
||||
channel
|
||||
and channel.id not in untracked
|
||||
and (not channel.category_id or channel.category_id not in untracked)
|
||||
):
|
||||
# State is from member in tracked voice channel
|
||||
# Calculate expiry
|
||||
lguild = lguilds[guildid]
|
||||
cap = lguild.config.get('daily_voice_cap').value
|
||||
tracked = tracked_today[(guildid, userid)]
|
||||
if tracked < cap:
|
||||
tomorrow = lguild.today + dt.timedelta(days=1)
|
||||
expiry = now + dt.timedelta(seconds=(cap - tracked))
|
||||
if expiry > tomorrow:
|
||||
expiry = tomorrow + dt.timedelta(seconds=cap)
|
||||
expiries[(guildid, userid)] = expiry
|
||||
|
||||
hourly_rate = await self._calculate_rate(guildid, userid, state)
|
||||
to_create.append((
|
||||
guildid, userid,
|
||||
state.channelid,
|
||||
now, now,
|
||||
state.stream, state.video,
|
||||
hourly_rate
|
||||
))
|
||||
# Bulk create the ongoing sessions
|
||||
if to_create:
|
||||
# First ensure the lion members exist
|
||||
await self.bot.core.lions.fetch_members(
|
||||
*(item[:2] for item in to_create)
|
||||
# Load ongoing session data for the entire shard
|
||||
ongoing = await self.data.VoiceSessionsOngoing.fetch_where(THIS_SHARD)
|
||||
logger.info(
|
||||
f"Retrieved {len(ongoing)} ongoing voice sessions from data. Beginning reload."
|
||||
)
|
||||
|
||||
# Then ensure the TrackedChannels exist
|
||||
cids = set((item[2], item[0]) for item in to_create)
|
||||
await self.data.TrackedChannel.fetch_multiple(*cids)
|
||||
await self._load_sessions(states, ongoing)
|
||||
|
||||
# Then actually create the ongoing sessions
|
||||
rows = await self.data.VoiceSessionsOngoing.table.insert_many(
|
||||
('guildid', 'userid', 'channelid', 'start_time', 'last_update', 'live_stream',
|
||||
'live_video', 'hourly_coins'),
|
||||
*to_create
|
||||
).with_adapter(self.data.VoiceSessionsOngoing._make_rows)
|
||||
for row in rows:
|
||||
VoiceSession.from_ongoing(self.bot, row, expiries[(row.guildid, row.userid)])
|
||||
logger.info(f"Started {len(rows)} new voice sessions from voice channels!")
|
||||
self.initialised.set()
|
||||
|
||||
@LionCog.listener("on_voice_state_update")
|
||||
@@ -314,6 +442,9 @@ class VoiceTrackerCog(LionCog):
|
||||
# If tracked state did not change, ignore event
|
||||
return
|
||||
|
||||
bchannel = before.channel if before else None
|
||||
achannel = after.channel if after else None
|
||||
|
||||
# Take tracking lock
|
||||
async with self.tracking_lock:
|
||||
# Fetch tracked member session state
|
||||
@@ -334,7 +465,7 @@ class VoiceTrackerCog(LionCog):
|
||||
"Voice event does not match session information! "
|
||||
f"Member '{member.name}' <uid:{member.id}> "
|
||||
f"of guild '{member.guild.name}' <gid:{member.guild.id}> "
|
||||
f"left channel '#{before.channel.name}' <cid:{leaving}> "
|
||||
f"left channel '{bchannel}' <cid:{leaving}> "
|
||||
f"during voice session in channel <cid:{tstate.channelid}>!"
|
||||
)
|
||||
# Close (or cancel) active session
|
||||
@@ -344,16 +475,13 @@ class VoiceTrackerCog(LionCog):
|
||||
" because they left the channel."
|
||||
)
|
||||
await session.close()
|
||||
elif (
|
||||
leaving not in untracked and
|
||||
not (before.channel.category_id and before.channel.category_id in untracked)
|
||||
):
|
||||
elif not self.is_untracked(bchannel):
|
||||
# Leaving tracked channel without an active session?
|
||||
logger.warning(
|
||||
"Voice event does not match session information! "
|
||||
f"Member '{member.name}' <uid:{member.id}> "
|
||||
f"of guild '{member.guild.name}' <gid:{member.guild.id}> "
|
||||
f"left tracked channel '#{before.channel.name}' <cid:{leaving}> "
|
||||
f"left tracked channel '{bchannel}' <cid:{leaving}> "
|
||||
f"with no matching voice session!"
|
||||
)
|
||||
|
||||
@@ -365,14 +493,11 @@ class VoiceTrackerCog(LionCog):
|
||||
"Voice event does not match session information! "
|
||||
f"Member '{member.name}' <uid:{member.id}> "
|
||||
f"of guild '{member.guild.name}' <gid:{member.guild.id}> "
|
||||
f"joined channel '#{after.channel.name}' <cid:{joining}> "
|
||||
f"joined channel '{achannel}' <cid:{joining}> "
|
||||
f"during voice session in channel <cid:{tstate.channelid}>!"
|
||||
)
|
||||
await session.close()
|
||||
if (
|
||||
joining not in untracked and
|
||||
not (after.channel.category_id and after.channel.category_id in untracked)
|
||||
):
|
||||
if not self.is_untracked(achannel):
|
||||
# If the channel they are joining is tracked, schedule a session start for them
|
||||
delay, start, expiry = await self._session_boundaries_for(member.guild.id, member.id)
|
||||
hourly_rate = await self._calculate_rate(member.guild.id, member.id, astate)
|
||||
@@ -380,10 +505,27 @@ class VoiceTrackerCog(LionCog):
|
||||
logger.debug(
|
||||
f"Scheduling voice session for member `{member.name}' <uid:{member.id}> "
|
||||
f"in guild '{member.guild.name}' <gid: member.guild.id> "
|
||||
f"in channel '{after.channel.name}' <cid: {after.channel.id}>. "
|
||||
f"in channel '{achannel}' <cid: {achannel.id}>. "
|
||||
f"Session will start at {start}, expire at {expiry}, and confirm in {delay}."
|
||||
)
|
||||
await session.schedule_start(delay, start, expiry, astate, hourly_rate)
|
||||
|
||||
t = self.bot.translator.t
|
||||
lguild = await self.bot.core.lions.fetch_guild(member.guild.id)
|
||||
lguild.log_event(
|
||||
t(_p(
|
||||
'eventlog|event:voice_session_start|title',
|
||||
"Member Joined Tracked Voice Channel"
|
||||
)),
|
||||
t(_p(
|
||||
'eventlog|event:voice_session_start|desc',
|
||||
"{member} joined {channel}."
|
||||
)).format(
|
||||
member=member.mention, channel=achannel.mention,
|
||||
),
|
||||
start=discord.utils.format_dt(start, 'F'),
|
||||
expiry=discord.utils.format_dt(expiry, 'R'),
|
||||
)
|
||||
elif session.activity:
|
||||
# If the channelid did not change, the live state must have
|
||||
# Recalculate the economy rate, and update the session
|
||||
@@ -391,116 +533,24 @@ class VoiceTrackerCog(LionCog):
|
||||
hourly_rate = await self._calculate_rate(member.guild.id, member.id, astate)
|
||||
await session.update(new_state=astate, new_rate=hourly_rate)
|
||||
|
||||
@LionCog.listener("on_guild_setting_update_untracked_channels")
|
||||
async def update_untracked_channels(self, guildid, setting):
|
||||
"""
|
||||
Close sessions in untracked channels, and recalculate previously untracked sessions
|
||||
"""
|
||||
@LionCog.listener("on_guildset_untracked_channels")
|
||||
@LionCog.listener("on_guildset_hourly_reward")
|
||||
@LionCog.listener("on_guildset_hourly_live_bonus")
|
||||
@LionCog.listener("on_guildset_daily_voice_cap")
|
||||
@LionCog.listener("on_guildset_timezone")
|
||||
async def _event_refresh_guild(self, guildid: int, setting):
|
||||
if not self.handle_events:
|
||||
return
|
||||
|
||||
async with self.tracking_lock:
|
||||
lguild = await self.bot.core.lions.fetch_guild(guildid)
|
||||
guild = self.bot.get_guild(guildid)
|
||||
if not guild:
|
||||
# Left guild while waiting on lock
|
||||
return
|
||||
cap = lguild.config.get('daily_voice_cap').value
|
||||
untracked = self.untracked_channels.get(guildid, [])
|
||||
now = utc_now()
|
||||
|
||||
# Iterate through active sessions, close any that are in untracked channels
|
||||
active = VoiceSession._active_sessions_.get(guildid, {})
|
||||
for session in list(active.values()):
|
||||
if session.state.channelid in untracked:
|
||||
await session.close()
|
||||
|
||||
# Iterate through voice members, open new sessions if needed
|
||||
expiries = {}
|
||||
to_create = []
|
||||
for channel in guild.voice_channels:
|
||||
if channel.id in untracked:
|
||||
continue
|
||||
for member in channel.members:
|
||||
if self.get_session(guildid, member.id).activity:
|
||||
# Already have an active session for this member
|
||||
continue
|
||||
userid = member.id
|
||||
state = TrackedVoiceState.from_voice_state(member.voice)
|
||||
|
||||
# TODO: Take into account tracked_today time?
|
||||
# TODO: Make a per-guild refresh function to stay DRY
|
||||
tomorrow = lguild.today + dt.timedelta(days=1)
|
||||
expiry = now + dt.timedelta(seconds=cap)
|
||||
if expiry > tomorrow:
|
||||
expiry = tomorrow + dt.timedelta(seconds=cap)
|
||||
expiries[(guildid, userid)] = expiry
|
||||
|
||||
hourly_rate = await self._calculate_rate(guildid, userid, state)
|
||||
to_create.append((
|
||||
guildid, userid,
|
||||
state.channelid,
|
||||
now, now,
|
||||
state.stream, state.video,
|
||||
hourly_rate
|
||||
))
|
||||
|
||||
if to_create:
|
||||
# Ensure LionMembers exist
|
||||
await self.bot.core.lions.fetch_members(
|
||||
*(item[:2] for item in to_create)
|
||||
if guild is None:
|
||||
logger.warning(
|
||||
f"Voice tracker discarding '{setting.setting_id}' event for unknown guild <gid: {guildid}>."
|
||||
)
|
||||
|
||||
# Ensure TrackedChannels exist
|
||||
cids = set((item[2], item[0]) for item in to_create)
|
||||
await self.data.TrackedChannel.fetch_multiple(*cids)
|
||||
|
||||
# Create new sessions
|
||||
rows = await self.data.VoiceSessionsOngoing.table.insert_many(
|
||||
('guildid', 'userid', 'channelid', 'start_time', 'last_update', 'live_stream',
|
||||
'live_video', 'hourly_coins'),
|
||||
*to_create
|
||||
).with_adapter(self.data.VoiceSessionsOngoing._make_rows)
|
||||
for row in rows:
|
||||
VoiceSession.from_ongoing(self.bot, row, expiries[(row.guildid, row.userid)])
|
||||
logger.info(
|
||||
f"Started {len(rows)} new voice sessions from voice members "
|
||||
f"in previously untracked channels of guild '{guild.name}' <gid:{guildid}>."
|
||||
else:
|
||||
logger.debug(
|
||||
f"Voice tracker handling '{setting.setting_id}' event for guild <gid: {guildid}>."
|
||||
)
|
||||
|
||||
@LionCog.listener("on_guild_setting_update_hourly_reward")
|
||||
async def update_hourly_reward(self, guildid, setting):
|
||||
if not self.handle_events:
|
||||
return
|
||||
|
||||
async with self.tracking_lock:
|
||||
sessions = VoiceSession._active_sessions_.get(guildid, {})
|
||||
for session in list(sessions.values()):
|
||||
hourly_rate = await self._calculate_rate(session.guildid, session.userid, session.state)
|
||||
await session.update(new_rate=hourly_rate)
|
||||
|
||||
@LionCog.listener("on_guild_setting_update_hourly_live_bonus")
|
||||
async def update_hourly_live_bonus(self, guildid, setting):
|
||||
if not self.handle_events:
|
||||
return
|
||||
|
||||
async with self.tracking_lock:
|
||||
sessions = VoiceSession._active_sessions_.get(guildid, {})
|
||||
for session in list(sessions.values()):
|
||||
hourly_rate = await self._calculate_rate(session.guildid, session.userid, session.state)
|
||||
await session.update(new_rate=hourly_rate)
|
||||
|
||||
@LionCog.listener("on_guild_setting_update_daily_voice_cap")
|
||||
async def update_daily_voice_cap(self, guildid, setting):
|
||||
# TODO: Guild daily_voice_cap setting triggers session expiry recalculation for all sessions
|
||||
...
|
||||
|
||||
@LionCog.listener("on_guild_setting_update_timezone")
|
||||
@log_wrap(action='Voice Track')
|
||||
@log_wrap(action='Timezone Update')
|
||||
async def update_timezone(self, guildid, setting):
|
||||
# TODO: Guild timezone setting triggers studied_today cache rebuild
|
||||
logger.info("Received dispatch event for timezone change!")
|
||||
await self.refresh_guild_sessions(guild)
|
||||
|
||||
async def _calculate_rate(self, guildid, userid, state):
|
||||
"""
|
||||
@@ -522,7 +572,7 @@ class VoiceTrackerCog(LionCog):
|
||||
|
||||
return hourly_rate
|
||||
|
||||
async def _session_boundaries_for(self, guildid: int, userid: int) -> tuple[int, dt.datetime, dt.datetime]:
|
||||
async def _session_boundaries_for(self, guildid: int, userid: int) -> tuple[float, dt.datetime, dt.datetime]:
|
||||
"""
|
||||
Compute when the next session for this member should start and expire.
|
||||
|
||||
@@ -539,7 +589,7 @@ class VoiceTrackerCog(LionCog):
|
||||
"""
|
||||
lguild = await self.bot.core.lions.fetch_guild(guildid)
|
||||
now = lguild.now
|
||||
tomorrow = now + dt.timedelta(days=1)
|
||||
tomorrow = lguild.today + dt.timedelta(days=1)
|
||||
|
||||
studied_today = await self.fetch_tracked_today(guildid, userid)
|
||||
cap = lguild.config.get('daily_voice_cap').value
|
||||
@@ -551,7 +601,8 @@ class VoiceTrackerCog(LionCog):
|
||||
start_time = now
|
||||
delay = 20
|
||||
|
||||
expiry = start_time + dt.timedelta(seconds=cap)
|
||||
remaining = max(cap - studied_today, 0)
|
||||
expiry = start_time + dt.timedelta(seconds=remaining)
|
||||
if expiry >= tomorrow:
|
||||
expiry = tomorrow + dt.timedelta(seconds=cap)
|
||||
|
||||
@@ -574,61 +625,9 @@ class VoiceTrackerCog(LionCog):
|
||||
Initialise and start required new sessions from voice channel members when we join a guild.
|
||||
"""
|
||||
if not self.handle_events:
|
||||
# Initialisation will take care of it for us
|
||||
return
|
||||
|
||||
async with self.tracking_lock:
|
||||
guildid = guild.id
|
||||
lguild = await self.bot.core.lions.fetch_guild(guildid)
|
||||
cap = lguild.config.get('daily_voice_cap').value
|
||||
untracked = self.untracked_channels.get(guildid, [])
|
||||
now = utc_now()
|
||||
|
||||
expiries = {}
|
||||
to_create = []
|
||||
for channel in guild.voice_channels:
|
||||
if channel.id in untracked:
|
||||
continue
|
||||
for member in channel.members:
|
||||
userid = member.id
|
||||
state = TrackedVoiceState.from_voice_state(member.voice)
|
||||
|
||||
tomorrow = lguild.today + dt.timedelta(days=1)
|
||||
expiry = now + dt.timedelta(seconds=cap)
|
||||
if expiry > tomorrow:
|
||||
expiry = tomorrow + dt.timedelta(seconds=cap)
|
||||
expiries[(guildid, userid)] = expiry
|
||||
|
||||
hourly_rate = await self._calculate_rate(guildid, userid, state)
|
||||
to_create.append((
|
||||
guildid, userid,
|
||||
state.channelid,
|
||||
now, now,
|
||||
state.stream, state.video,
|
||||
hourly_rate
|
||||
))
|
||||
|
||||
if to_create:
|
||||
# Ensure LionMembers exist
|
||||
await self.bot.core.lions.fetch_members(
|
||||
*(item[:2] for item in to_create)
|
||||
)
|
||||
|
||||
# Ensure TrackedChannels exist
|
||||
cids = set((item[2], item[0]) for item in to_create)
|
||||
await self.data.TrackedChannel.fetch_multiple(*cids)
|
||||
|
||||
# Create new sessions
|
||||
rows = await self.data.VoiceSessionsOngoing.table.insert_many(
|
||||
('guildid', 'userid', 'channelid', 'start_time', 'last_update', 'live_stream',
|
||||
'live_video', 'hourly_coins'),
|
||||
*to_create
|
||||
).with_adapter(self.data.VoiceSessionsOngoing._make_rows)
|
||||
for row in rows:
|
||||
VoiceSession.from_ongoing(self.bot, row, expiries[(row.guildid, row.userid)])
|
||||
logger.info(
|
||||
f"Started {len(rows)} new voice sessions from voice members "
|
||||
f"in new guild '{guild.name}' <gid:{guildid}>."
|
||||
)
|
||||
await self.refresh_guild_sessions(guild)
|
||||
|
||||
@LionCog.listener("on_guild_remove")
|
||||
@log_wrap(action='Leave Guild Voice Sessions')
|
||||
@@ -645,10 +644,7 @@ class VoiceTrackerCog(LionCog):
|
||||
now = utc_now()
|
||||
to_close = [] # (guildid, userid, _at)
|
||||
for session in sessions.values():
|
||||
if session.start_task is not None:
|
||||
session.start_task.cancel()
|
||||
if session.expiry_task is not None:
|
||||
session.expiry_task.cancel()
|
||||
session.cancel()
|
||||
to_close.append((session.guildid, session.userid, now))
|
||||
if to_close:
|
||||
await self.data.VoiceSessionsOngoing.close_voice_sessions_at(*to_close)
|
||||
|
||||
@@ -7,6 +7,7 @@ from data import RowModel, Registry, Table
|
||||
from data.columns import Integer, String, Timestamp, Bool
|
||||
|
||||
from core.data import CoreData
|
||||
from utils.lib import utc_now
|
||||
|
||||
|
||||
class VoiceTrackerData(Registry):
|
||||
@@ -108,11 +109,16 @@ class VoiceTrackerData(Registry):
|
||||
video_duration = Integer()
|
||||
stream_duration = Integer()
|
||||
coins_earned = Integer()
|
||||
last_update = Integer()
|
||||
last_update = Timestamp()
|
||||
live_stream = Bool()
|
||||
live_video = Bool()
|
||||
hourly_coins = Integer()
|
||||
|
||||
@property
|
||||
def _total_coins_earned(self):
|
||||
since = (utc_now() - self.last_update).total_seconds() / 3600
|
||||
return self.coins_earned + since * self.hourly_coins
|
||||
|
||||
@classmethod
|
||||
@log_wrap(action='close_voice_session')
|
||||
async def close_study_session_at(cls, guildid: int, userid: int, _at: dt.datetime) -> int:
|
||||
@@ -154,7 +160,7 @@ class VoiceTrackerData(Registry):
|
||||
async def update_voice_session_at(
|
||||
cls, guildid: int, userid: int, _at: dt.datetime,
|
||||
stream: bool, video: bool, rate: float
|
||||
) -> int:
|
||||
):
|
||||
async with cls._connector.connection() as conn:
|
||||
async with conn.cursor() as cursor:
|
||||
await cursor.execute(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from typing import Optional
|
||||
from typing import Optional, overload, Literal
|
||||
from enum import IntEnum
|
||||
from collections import defaultdict
|
||||
import datetime as dt
|
||||
@@ -12,7 +12,9 @@ from meta import LionBot
|
||||
from data import WeakCache
|
||||
from .data import VoiceTrackerData
|
||||
|
||||
from . import logger
|
||||
from . import logger, babel
|
||||
|
||||
_p = babel._p
|
||||
|
||||
|
||||
class TrackedVoiceState:
|
||||
@@ -73,11 +75,14 @@ class VoiceSession:
|
||||
'start_task', 'expiry_task',
|
||||
'data', 'state', 'hourly_rate',
|
||||
'_tag', '_start_time',
|
||||
'lock',
|
||||
'__weakref__'
|
||||
)
|
||||
|
||||
_sessions_ = defaultdict(lambda: WeakCache(TTLCache(5000, ttl=60*60))) # Registry mapping
|
||||
_active_sessions_ = defaultdict(dict) # Maintains strong references to active sessions
|
||||
|
||||
# Maintains strong references to active sessions
|
||||
_active_sessions_: dict[int, dict[int, 'VoiceSession']] = defaultdict(dict)
|
||||
|
||||
def __init__(self, bot: LionBot, guildid: int, userid: int, data=None):
|
||||
self.bot = bot
|
||||
@@ -96,6 +101,17 @@ class VoiceSession:
|
||||
self._tag = None
|
||||
self._start_time = None
|
||||
|
||||
# Member session lock
|
||||
# Ensures state changes are atomic and serialised
|
||||
self.lock = asyncio.Lock()
|
||||
|
||||
def cancel(self):
|
||||
if self.start_task is not None:
|
||||
self.start_task.cancel()
|
||||
if self.expiry_task is not None:
|
||||
self.expiry_task.cancel()
|
||||
self._active_sessions_[self.guildid].pop(self.userid, None)
|
||||
|
||||
@property
|
||||
def tag(self) -> Optional[str]:
|
||||
if self.data:
|
||||
@@ -121,6 +137,16 @@ class VoiceSession:
|
||||
else:
|
||||
return SessionState.INACTIVE
|
||||
|
||||
@overload
|
||||
@classmethod
|
||||
def get(cls, bot: LionBot, guildid: int, userid: int, create: Literal[False]) -> Optional['VoiceSession']:
|
||||
...
|
||||
|
||||
@overload
|
||||
@classmethod
|
||||
def get(cls, bot: LionBot, guildid: int, userid: int, create: Literal[True] = True) -> 'VoiceSession':
|
||||
...
|
||||
|
||||
@classmethod
|
||||
def get(cls, bot: LionBot, guildid: int, userid: int, create=True) -> Optional['VoiceSession']:
|
||||
"""
|
||||
@@ -149,6 +175,7 @@ class VoiceSession:
|
||||
return self
|
||||
|
||||
async def set_tag(self, new_tag):
|
||||
async with self.lock:
|
||||
if self.activity is SessionState.INACTIVE:
|
||||
raise ValueError("Cannot set tag on an inactive voice session.")
|
||||
self._tag = new_tag
|
||||
@@ -167,6 +194,7 @@ class VoiceSession:
|
||||
|
||||
self.start_task = asyncio.create_task(self._start_after(delay, start_time))
|
||||
self.schedule_expiry(expire_time)
|
||||
self._active_sessions_[self.guildid][self.userid] = self
|
||||
|
||||
async def _start_after(self, delay: int, start_time: dt.datetime):
|
||||
"""
|
||||
@@ -174,10 +202,10 @@ class VoiceSession:
|
||||
|
||||
Creates the tracked_channel if required.
|
||||
"""
|
||||
self._active_sessions_[self.guildid][self.userid] = self
|
||||
await asyncio.sleep(delay)
|
||||
|
||||
logger.debug(
|
||||
async with self.lock:
|
||||
logger.info(
|
||||
f"Starting voice session for member <uid:{self.userid}> in guild <gid:{self.guildid}> "
|
||||
f"and channel <cid:{self.state.channelid}>."
|
||||
)
|
||||
@@ -217,18 +245,6 @@ class VoiceSession:
|
||||
delay = (expire_time - utc_now()).total_seconds()
|
||||
self.expiry_task = asyncio.create_task(self._expire_after(delay))
|
||||
|
||||
async def _expire_after(self, delay: int):
|
||||
"""
|
||||
Expire a session which has exceeded the daily voice cap.
|
||||
"""
|
||||
# TODO: Logging, and guild logging, and user notification (?)
|
||||
await asyncio.sleep(delay)
|
||||
logger.info(
|
||||
f"Expiring voice session for member <uid:{self.userid}> in guild <gid:{self.guildid}> "
|
||||
f"and channel <cid:{self.state.channelid}>."
|
||||
)
|
||||
await self.close()
|
||||
|
||||
async def update(self, new_state: Optional[TrackedVoiceState] = None, new_rate: Optional[int] = None):
|
||||
"""
|
||||
Update the session state with the provided voice state or hourly rate.
|
||||
@@ -254,10 +270,114 @@ class VoiceSession:
|
||||
rate=self.hourly_rate
|
||||
)
|
||||
|
||||
async def _expire_after(self, delay: int):
|
||||
"""
|
||||
Expire a session which has exceeded the daily voice cap.
|
||||
"""
|
||||
# TODO: Logging, and guild logging, and user notification (?)
|
||||
await asyncio.sleep(delay)
|
||||
logger.info(
|
||||
f"Expiring voice session for member <uid:{self.userid}> in guild <gid:{self.guildid}> "
|
||||
f"and channel <cid:{self.state.channelid}>."
|
||||
)
|
||||
async with self.lock:
|
||||
await self._close()
|
||||
|
||||
if self.activity:
|
||||
t = self.bot.translator.t
|
||||
lguild = await self.bot.core.lions.fetch_guild(self.guildid)
|
||||
if self.activity is SessionState.ONGOING and self.data is not None:
|
||||
lguild.log_event(
|
||||
t(_p(
|
||||
'eventlog|event:voice_session_expired|title',
|
||||
"Member Voice Session Expired"
|
||||
)),
|
||||
t(_p(
|
||||
'eventlog|event:voice_session_expired|desc',
|
||||
"{member}'s voice session in {channel} expired "
|
||||
"because they reached the daily voice cap."
|
||||
)).format(
|
||||
member=f"<@{self.userid}>",
|
||||
channel=f"<#{self.state.channelid}>",
|
||||
),
|
||||
start=discord.utils.format_dt(self.data.start_time),
|
||||
coins_earned=int(self.data._total_coins_earned),
|
||||
)
|
||||
|
||||
if self.start_task is not None:
|
||||
self.start_task.cancel()
|
||||
self.start_task = None
|
||||
|
||||
self.data = None
|
||||
|
||||
cog = self.bot.get_cog('VoiceTrackerCog')
|
||||
delay, start, expiry = await cog._session_boundaries_for(self.guildid, self.userid)
|
||||
hourly_rate = await cog._calculate_rate(self.guildid, self.userid, self.state)
|
||||
|
||||
self.hourly_rate = hourly_rate
|
||||
self._start_time = start
|
||||
|
||||
self.start_task = asyncio.create_task(self._start_after(delay, start))
|
||||
self.schedule_expiry(expiry)
|
||||
|
||||
async def close(self):
|
||||
"""
|
||||
Close the session, or cancel the pending session. Idempotent.
|
||||
"""
|
||||
async with self.lock:
|
||||
await self._close()
|
||||
if self.activity:
|
||||
t = self.bot.translator.t
|
||||
lguild = await self.bot.core.lions.fetch_guild(self.guildid)
|
||||
if self.activity is SessionState.ONGOING and self.data is not None:
|
||||
lguild.log_event(
|
||||
t(_p(
|
||||
'eventlog|event:voice_session_closed|title',
|
||||
"Member Voice Session Ended"
|
||||
)),
|
||||
t(_p(
|
||||
'eventlog|event:voice_session_closed|desc',
|
||||
"{member} completed their voice session in {channel}."
|
||||
)).format(
|
||||
member=f"<@{self.userid}>",
|
||||
channel=f"<#{self.state.channelid}>",
|
||||
),
|
||||
start=discord.utils.format_dt(self.data.start_time),
|
||||
coins_earned=int(self.data._total_coins_earned),
|
||||
)
|
||||
else:
|
||||
lguild.log_event(
|
||||
t(_p(
|
||||
'eventlog|event:voice_session_cancelled|title',
|
||||
"Member Voice Session Cancelled"
|
||||
)),
|
||||
t(_p(
|
||||
'eventlog|event:voice_session_cancelled|desc',
|
||||
"{member} left {channel} before their voice session started."
|
||||
)).format(
|
||||
member=f"<@{self.userid}>",
|
||||
channel=f"<#{self.state.channelid}>",
|
||||
),
|
||||
)
|
||||
|
||||
if self.start_task is not None:
|
||||
self.start_task.cancel()
|
||||
self.start_task = None
|
||||
|
||||
if self.expiry_task is not None:
|
||||
self.expiry_task.cancel()
|
||||
self.expiry_task = None
|
||||
|
||||
self.data = None
|
||||
self.state = None
|
||||
self.hourly_rate = None
|
||||
self._tag = None
|
||||
self._start_time = None
|
||||
|
||||
# Always release strong reference to session (to allow garbage collection)
|
||||
self._active_sessions_[self.guildid].pop(self.userid)
|
||||
|
||||
async def _close(self):
|
||||
if self.activity is SessionState.ONGOING:
|
||||
# End the ongoing session
|
||||
now = utc_now()
|
||||
@@ -273,18 +393,3 @@ class VoiceSession:
|
||||
asyncio.create_task(rank_cog.on_voice_session_complete(
|
||||
(self.guildid, self.userid, int((utc_now() - self.data.start_time).total_seconds()), 0)
|
||||
))
|
||||
|
||||
if self.start_task is not None:
|
||||
self.start_task.cancel()
|
||||
self.start_task = None
|
||||
|
||||
if self.expiry_task is not None:
|
||||
self.expiry_task.cancel()
|
||||
self.expiry_task = None
|
||||
|
||||
self.data = None
|
||||
self.state = None
|
||||
self.hourly_rate = None
|
||||
|
||||
# Always release strong reference to session (to allow garbage collection)
|
||||
self._active_sessions_[self.guildid].pop(self.userid)
|
||||
|
||||
@@ -34,7 +34,7 @@ _p = babel._p
|
||||
class VoiceTrackerSettings(SettingGroup):
|
||||
class UntrackedChannels(ListData, ChannelListSetting):
|
||||
setting_id = 'untracked_channels'
|
||||
_event = 'guild_setting_update_untracked_channels'
|
||||
_event = 'guildset_untracked_channels'
|
||||
_set_cmd = 'configure voice_rewards'
|
||||
|
||||
_display_name = _p('guildset:untracked_channels', "untracked_channels")
|
||||
@@ -111,7 +111,7 @@ class VoiceTrackerSettings(SettingGroup):
|
||||
|
||||
class HourlyReward(ModelData, IntegerSetting):
|
||||
setting_id = 'hourly_reward'
|
||||
_event = 'guild_setting_update_hourly_reward'
|
||||
_event = 'on_guildset_hourly_reward'
|
||||
_set_cmd = 'configure voice_rewards'
|
||||
|
||||
_display_name = _p('guildset:hourly_reward', "hourly_reward")
|
||||
@@ -191,7 +191,7 @@ class VoiceTrackerSettings(SettingGroup):
|
||||
Guild setting describing the per-hour LionCoin bonus given to "live" members during tracking.
|
||||
"""
|
||||
setting_id = 'hourly_live_bonus'
|
||||
_event = 'guild_setting_update_hourly_live_bonus'
|
||||
_event = 'on_guildset_hourly_live_bonus'
|
||||
_set_cmd = 'configure voice_rewards'
|
||||
|
||||
_display_name = _p('guildset:hourly_live_bonus', "hourly_live_bonus")
|
||||
@@ -242,7 +242,7 @@ class VoiceTrackerSettings(SettingGroup):
|
||||
|
||||
class DailyVoiceCap(ModelData, DurationSetting):
|
||||
setting_id = 'daily_voice_cap'
|
||||
_event = 'guild_setting_update_daily_voice_cap'
|
||||
_event = 'on_guildset_daily_voice_cap'
|
||||
_set_cmd = 'configure voice_rewards'
|
||||
|
||||
_display_name = _p('guildset:daily_voice_cap', "daily_voice_cap")
|
||||
@@ -457,6 +457,9 @@ class VoiceTrackerConfigUI(ConfigUI):
|
||||
@select(
|
||||
cls=ChannelSelect,
|
||||
placeholder="UNTRACKED_CHANNELS_PLACEHOLDER",
|
||||
channel_types=[
|
||||
discord.enums.ChannelType.voice, discord.enums.ChannelType.stage_voice, discord.enums.ChannelType.category
|
||||
],
|
||||
min_values=0, max_values=25
|
||||
)
|
||||
async def untracked_channels_menu(self, selection: discord.Interaction, selected):
|
||||
|
||||
@@ -20,6 +20,7 @@ class MetaUtils(LionCog):
|
||||
'cmd:page|desc',
|
||||
"Jump to a given page of the ouput of a previous command in this channel."
|
||||
),
|
||||
with_app_command=False
|
||||
)
|
||||
async def page_group(self, ctx: LionContext):
|
||||
"""
|
||||
|
||||
@@ -765,7 +765,7 @@ class Timezoned:
|
||||
Return the start of the current month in the object's timezone
|
||||
"""
|
||||
today = self.today
|
||||
return today - datetime.timedelta(days=(today.day - 1))
|
||||
return today.replace(day=1)
|
||||
|
||||
|
||||
def replace_multiple(format_string, mapping):
|
||||
|
||||
@@ -32,7 +32,7 @@ class TaskMonitor(Generic[Taskid]):
|
||||
self.executor: Optional[Callable[[Taskid], Coroutine[Any, Any, None]]] = executor
|
||||
|
||||
self._wakeup: asyncio.Event = asyncio.Event()
|
||||
self._monitor_task: Optional[self.Task] = None
|
||||
self._monitor_task: Optional[asyncio.Task] = None
|
||||
|
||||
# Task data
|
||||
self._tasklist: list[Taskid] = []
|
||||
@@ -42,6 +42,19 @@ class TaskMonitor(Generic[Taskid]):
|
||||
# And allows simpler external cancellation if required
|
||||
self._running: dict[Taskid, asyncio.Future] = {}
|
||||
|
||||
def __repr__(self):
|
||||
return (
|
||||
"<"
|
||||
f"{self.__class__.__name__}"
|
||||
f" tasklist={len(self._tasklist)}"
|
||||
f" taskmap={len(self._taskmap)}"
|
||||
f" wakeup={self._wakeup.is_set()}"
|
||||
f" bucket={self._bucket}"
|
||||
f" running={len(self._running)}"
|
||||
f" task={self._monitor_task}"
|
||||
f">"
|
||||
)
|
||||
|
||||
def set_tasks(self, *tasks: tuple[Taskid, int]) -> None:
|
||||
"""
|
||||
Similar to `schedule_tasks`, but wipe and reset the tasklist.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user